telegem 3.2.2 → 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.
@@ -1,311 +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, **options)
31
- if @buttons.empty? || !@buttons.last.is_a?(Array)
32
- @buttons << [{ text: text }.merge(options)]
33
- else
34
- @buttons.last << { text: text }.merge(options)
35
- end
36
- self
37
- end
38
-
39
- def request_contact(text)
40
- button(text, request_contact: true)
41
- end
42
-
43
- def request_location(text)
44
- button(text, request_location: true)
45
- end
46
-
47
- def request_poll(text, type = nil)
48
- opts = type ? { request_poll: { type: type } } : { request_poll: {} }
49
- button(text, opts)
50
- end
51
-
52
- def resize(resize = true)
53
- @options[:resize_keyboard] = resize
54
- self
55
- end
56
-
57
- def one_time(one_time = true)
58
- @options[:one_time_keyboard] = one_time
59
- self
60
- end
61
-
62
- def selective(selective = true)
63
- @options[:selective] = selective
64
- self
65
- end
66
-
67
- def to_h
68
- {
69
- keyboard: @buttons.map { |row| row.is_a?(Array) ? row : [row] },
70
- **@options
71
- }
72
- end
73
-
74
- def to_json(*args)
75
- to_h.to_json(*args)
76
- end
77
-
78
- def self.remove(selective: false)
79
- {
80
- remove_keyboard: true,
81
- selective: selective
82
- }
83
- end
84
-
85
- def self.force_reply(selective: false, input_field_placeholder: nil)
86
- markup = {
87
- force_reply: true,
88
- selective: selective
89
53
  }
90
- markup[:input_field_placeholder] = input_field_placeholder if input_field_placeholder
91
- markup
92
- end
93
- end
94
-
95
- class InlineKeyboard
96
- attr_reader :buttons
97
-
98
- def initialize(buttons = [])
99
- @buttons = buttons
100
- end
101
-
102
- def self.[](*rows)
103
- new(rows)
104
- end
105
-
106
- def self.build(&block)
107
- builder = InlineBuilder.new
108
- builder.instance_eval(&block) if block_given?
109
- builder.keyboard
110
54
  end
111
-
112
55
  def row(*buttons)
113
- @buttons << buttons.flatten
114
- self
115
- end
116
-
117
- def button(text, **options)
118
- if @buttons.empty? || !@buttons.last.is_a?(Array)
119
- @buttons << [{ text: text }.merge(options)]
120
- else
121
- @buttons.last << { text: text }.merge(options)
122
- end
123
- self
124
- end
125
-
126
- def url(text, url)
127
- button(text, url: url)
128
- end
129
-
130
- def callback(text, data)
131
- button(text, callback_data: data)
132
- end
133
-
134
- def web_app(text, url)
135
- button(text, web_app: { url: url })
136
- end
137
-
138
- def login(text, url, **options)
139
- button(text, login_url: { url: url, **options })
140
- end
141
-
142
- def switch_inline(text, query = "")
143
- button(text, switch_inline_query: query)
144
- end
145
-
146
- def switch_inline_current(text, query = "")
147
- button(text, switch_inline_query_current_chat: query)
148
- end
149
-
150
- def pay(text)
151
- button(text, pay: true)
152
- end
153
-
154
- def to_h
155
- clean_rows = @buttons.compact.map do |row|
156
- row = Array(row).compact.select { |btn| is_a?(Hash) }
157
- row.empty? ? nil : row
158
- end.compact
159
- { inline_keyboard: clean_rows}
160
- end
161
-
162
- def to_json(*args)
163
- to_h.to_json(*args)
164
- end
165
- end
166
-
167
- class Builder
168
- attr_reader :keyboard
169
-
170
- def initialize
171
- @keyboard = Keyboard.new
172
- end
173
-
174
- def row(*buttons, &block)
175
- if block_given?
176
- sub_builder = Builder.new
177
- sub_builder.instance_eval(&block)
178
- @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
179
- elsif buttons.any?
180
- @keyboard.row(*buttons)
181
- else
182
- @keyboard.row
183
- end
56
+ @rows << buttons
184
57
  self
185
58
  end
186
-
187
- def button(text, **options)
188
- @keyboard.button(text, **options)
59
+ def resize(value = true)
60
+ @options[:resize_keyboard] = value
189
61
  self
190
62
  end
191
-
192
- def request_contact(text)
193
- @keyboard.request_contact(text)
63
+ def one_time(value = true)
64
+ @options[:one_time_keyboard] = value
194
65
  self
195
66
  end
196
-
197
- def request_location(text)
198
- @keyboard.request_location(text)
67
+ def selective(value = true)
68
+ @options[:selective] = value
199
69
  self
200
70
  end
201
-
202
- def request_poll(text, type = nil)
203
- @keyboard.request_poll(text, type)
71
+ def placeholder(text)
72
+ @options[:input_field_placeholder] = text
204
73
  self
205
74
  end
206
-
207
- def method_missing(name, *args, &block)
208
- if @keyboard && @keyboard.respond_to?(name)
209
- @keyboard.send(name, *args, &block)
210
- else
211
- super
212
- end
213
- end
214
-
215
- def respond_to_missing?(name, include_private = false)
216
- @keyboard && @keyboard.respond_to?(name) || super
217
- end
75
+ def build
76
+ ReplyKeyboard.new(@rows, @options)
218
77
  end
219
-
220
- class InlineBuilder
221
- attr_reader :keyboard
222
-
223
- def initialize
224
- @keyboard = InlineKeyboard.new
225
- end
226
-
227
- def row(*buttons, &block)
228
- if block_given?
229
- sub_builder = InlineBuilder.new
230
- sub_builder.instance_eval(&block)
231
- @keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
232
- elsif buttons.any?
233
- @keyboard.row(*buttons)
234
- else
235
- @keyboard.row([])
78
+ end
79
+ class ReplyKeyboard
80
+ def initialize(rows, options = {})
81
+ @rows = rows
82
+ @options = options
236
83
  end
237
- self
238
- end
239
-
240
- def button(text, **options)
241
- @keyboard.button(text, **options)
242
- self
243
- end
244
-
245
- def url(text, url)
246
- @keyboard.url(text, url)
247
- self
248
- end
249
-
250
- def callback(text, data)
251
- @keyboard.callback(text, data)
252
- self
253
- end
254
-
255
- def web_app(text, url)
256
- @keyboard.web_app(text, url)
257
- self
258
- end
259
-
260
- def login(text, url, **options)
261
- @keyboard.login(text, url, **options)
262
- self
263
- end
264
-
265
- def switch_inline(text, query = "")
266
- @keyboard.switch_inline(text, query)
267
- self
268
- end
269
-
270
- def switch_inline_current(text, query = "")
271
- @keyboard.switch_inline_current(text, query)
272
- self
273
- end
274
-
275
- def pay(text)
276
- @keyboard.pay(text)
277
- self
278
- end
279
-
280
- def method_missing(name, *args, &block)
281
- if @keyboard && @keyboard.respond_to?(name)
282
- @keyboard.send(name, *args, &block)
283
- else
284
- super
84
+ def to_h
85
+ {
86
+ keyboard: @rows
87
+ }.merge(@options)
285
88
  end
286
- end
287
-
288
- def respond_to_missing?(name, include_private = false)
289
- @keyboard && @keyboard.respond_to?(name) || super
290
- end
291
- end
292
-
293
- class << self
294
- def keyboard(&block)
295
- Keyboard.build(&block)
296
- end
297
-
298
- def inline(&block)
299
- InlineKeyboard.build(&block)
300
- end
301
-
302
- def remove(**options)
303
- Keyboard.remove(**options)
304
- end
305
-
306
- def force_reply(**options)
307
- Keyboard.force_reply(**options)
308
- end
309
- end
310
- end
311
- 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
@@ -1,12 +1,12 @@
1
- # lib/telegem.rb - MAIN ENTRY POINT
1
+
2
2
  require 'logger'
3
3
  require 'json'
4
4
 
5
5
  module Telegem
6
- VERSION = "3.2.2".freeze
6
+ VERSION = "3.3.0".freeze
7
7
  end
8
8
 
9
- # Load core components
9
+ #
10
10
  require_relative 'api/client'
11
11
  require_relative 'api/types'
12
12
  require_relative 'core/bot'
@@ -16,17 +16,18 @@ require_relative 'core/scene'
16
16
  require_relative 'session/middleware'
17
17
  require_relative 'session/memory_store'
18
18
  require_relative 'markup/keyboard'
19
- # Webhook is loaded lazily when needed
19
+ require_relative 'markup/inline'
20
+
20
21
  require_relative 'plugins/file_extract'
21
22
  require_relative 'session/scene_middleware'
22
23
 
23
24
  module Telegem
24
- # Main entry point: Telegem.new(token)
25
+
25
26
  def self.new(token, **options)
26
27
  Core::Bot.new(token, **options)
27
28
  end
28
29
 
29
- # Shortcut for creating keyboards
30
+
30
31
  def self.keyboard(&block)
31
32
  Markup.keyboard(&block)
32
33
  end
@@ -35,28 +36,23 @@ module Telegem
35
36
  Markup.inline(&block)
36
37
  end
37
38
 
38
- # Remove keyboard markup
39
39
  def self.remove_keyboard(**options)
40
40
  Markup.remove(**options)
41
41
  end
42
42
 
43
- # Force reply markup
44
43
  def self.force_reply(**options)
45
44
  Markup.force_reply(**options)
46
45
  end
47
46
 
48
- # Current version
49
47
  def self.version
50
48
  VERSION
51
49
  end
52
-
53
- # Quick webhook setup
50
+
54
51
  def self.webhook(bot, **options)
55
52
  require_relative 'webhook/server'
56
53
  Webhook::Server.setup(bot, **options)
57
54
  end
58
55
 
59
- # Framework information
60
56
  def self.info
61
57
  <<~INFO
62
58
  🤖 Telegem #{VERSION}
@@ -77,7 +73,7 @@ module Telegem
77
73
  end
78
74
  end
79
75
 
80
- # Optional global shortcut (enabled by env var)
76
+
81
77
  if ENV['TELEGEM_GLOBAL'] == 'true'
82
78
  def Telegem(token, **options)
83
79
  ::Telegem.new(token, **options)
@@ -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"]]