telegem 3.1.3 → 3.2.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: 9ef63701f663694078c74cc777b733f9d7fd8c20f9d8309e12fdcbea6e979911
4
- data.tar.gz: 29a744115f81510dfd0f2f082f7990cb67e63fafb976e69efda80caca9215de7
3
+ metadata.gz: 3e8ecb3c22ffef3476d429d9631a12b953f15c0b3b0b3d41970af0539b8627fd
4
+ data.tar.gz: ee9b68ccc7f97d6a5916e99d6d0d57ce6e5f7d311493ed6265f0bfd81393f140
5
5
  SHA512:
6
- metadata.gz: b2e45b5fad65c6637eee3f982832eb4d227904e85e0efa7bf40315e69e69267d867aa8b6e2ea9e5eef00f5274c07697dee42b213bd75286887e2e21845ac9c25
7
- data.tar.gz: 6e34ea4d896573e99ce30ecccb3fad123e43e3f60517c8aa3a5a779515428105dfc64dcf762cdb640878725ecbda8beba82d46f5aebaa104acbc8c5211c5622d
6
+ metadata.gz: 32f1f42950ab56a207c62c00f90104275cb6e9f68726d2650b244b01443a88e654f3b78af837753096a6fcfc6264cb0ab85064ec14cc44032b8ee01b46ab4e24
7
+ data.tar.gz: '0498f7e735278a6cb66b4a33c770534f92d0638fd196bda2aae5fbf547901b7b86a65d02b183d058b6d6791d35de09d540b4498fa24a51319c84c03a2a9ab731'
data/Readme.md CHANGED
@@ -1,4 +1,4 @@
1
- Telegem 🤖⚡
1
+ ![Telegem Logo](https://gitlab.com/ruby-telegem/telegem/-/raw/main/assets/logo.png)
2
2
 
3
3
  Modern, blazing-fast async Telegram Bot API for Ruby - Inspired by Telegraf, built for performance.
4
4
 
@@ -17,7 +17,7 @@ Blazing-fast, modern Telegram Bot framework for Ruby. Inspired by Telegraf.js, b
17
17
 
18
18
  ✨ Features
19
19
 
20
- - ⚡ True httpx(Async) I/O - Built on async gem, not blocking threads
20
+ - ⚡ True Async I/O - Built on async gem, not blocking threads
21
21
  - 🎯 Telegraf-style DSL - Familiar API for JavaScript developers
22
22
  - 🔌 Middleware System - Compose behavior like Express.js
23
23
  - 🧙 Scene System - Multi-step conversations (wizards/forms)
data/assets/.gitkeep ADDED
File without changes
data/assets/logo.png ADDED
Binary file
data/lib/api/client.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'httpx'
1
+ require 'async/http'
2
2
  require 'json'
3
3
 
4
4
  module Telegem
@@ -6,112 +6,121 @@ module Telegem
6
6
  class Client
7
7
  BASE_URL = 'https://api.telegram.org'
8
8
 
9
- attr_reader :token, :logger, :http
9
+ attr_reader :token, :logger
10
10
 
11
11
  def initialize(token, **options)
12
12
  @token = token
13
- @mutex = Mutex.new
14
13
  @logger = options[:logger] || Logger.new($stdout)
15
- timeout = options[:timeout] || 30
14
+ @timeout = options[:timeout] || 30
16
15
 
17
- @http = HTTPX.plugin(:callbacks).with(
18
- timeout: {
19
- request_timeout: timeout,
20
- connect_timeout: 10,
21
- write_timeout: 10,
22
- read_timeout: timeout
23
- },
24
- headers: {
25
- 'Content-Type' => 'application/json',
26
- 'User-Agent' => "Telegem/#{Telegem::VERSION}"
27
- }
28
- )
16
+ @endpoint = Async::HTTP::Endpoint.parse(BASE_URL)
17
+ @client = Async::HTTP::Client.new(@endpoint)
29
18
  end
30
19
 
31
- def call(method, params = {})
32
- url = "#{BASE_URL}/bot#{@token}/#{method}"
33
- @logger.debug("Api call #{method}") if @logger
34
- response = @http.post(url, json: params.compact)
35
- json = response.json
36
- if json && json['ok']
37
- json['result']
38
- else
39
- raise APIError.new(json ? json['description'] : "Api Error")
40
- end
41
- end
20
+ def call(method, params = {})
21
+ Async do
22
+ make_request(method, params)
23
+ end.wait
24
+ end
42
25
 
43
26
  def call!(method, params = {}, &callback)
44
- url = "#{BASE_URL}/bot#{@token}/#{method}"
45
27
  return unless callback
46
28
 
47
- @http.post(url, json: params.compact) do |response|
48
- begin
49
- if response.status == 200
50
- json = response.json
51
- if json && json['ok']
52
- @logger.debug("#{json}") if @logger
53
- callback.call(json['result'], nil)
54
- else
55
- error_msg = json ? json['description'] : "NO JSON Response"
56
- error_code = json['error_code'] if json
57
- callback.call(nil, APIError.new("API ERROR #{error_msg}", error_code))
58
- end
59
- else
60
- callback.call(nil, NetworkError.new("HTTP #{response.status}"))
61
- end
62
- rescue JSON::ParserError
63
- callback.call(nil, NetworkError.new("Invalid Json response"))
64
- rescue => e
65
- callback.call(nil, e)
66
- end
67
- end
68
- end
29
+ Async do
30
+ begin
31
+ result = make_request(method, params)
32
+ callback.call(result, nil)
33
+ rescue => error
34
+ callback.call(nil, error)
35
+ end
36
+ end
37
+ end
69
38
 
70
39
  def upload(method, params)
71
- url = "#{BASE_URL}/bot#{@token}/#{method}"
72
- response = @http.post(url, form: params)
73
- response.json
40
+ Async do
41
+ url = "/bot#{@token}/#{method}"
42
+
43
+ body = Async::HTTP::Body::Multipart.new
44
+
45
+ params.each do |key, value|
46
+ if file_object?(value)
47
+ body.add(key.to_s, value, filename: File.basename(value))
48
+ else
49
+ body.add(key.to_s, value.to_s)
50
+ end
51
+ end
52
+
53
+ response = @client.post(url, {}, body)
54
+ handle_response(response)
55
+ end.wait
56
+ end
57
+
58
+ def download(file_id, destination_path = nil)
59
+ Async do
60
+ file_info = call('getFile', file_id: file_id)
61
+ return nil unless file_info && file_info['file_path']
62
+
63
+ file_path = file_info['file_path']
64
+ download_url = "/file/bot#{@token}/#{file_path}"
65
+
66
+ response = @client.get(download_url)
67
+
68
+ if response.status == 200
69
+ content = response.read
70
+ if destination_path
71
+ File.binwrite(destination_path, content)
72
+ destination_path
73
+ else
74
+ content
75
+ end
76
+ else
77
+ raise NetworkError.new("Download failed: HTTP #{response.status}")
78
+ end
79
+ end.wait
74
80
  end
75
81
 
76
- def download(file_id, destination_path = nil)
77
- file_info = call('getFile', file_id: file_id)
78
- return nil unless file_info && file_info['file_path']
79
- file_path = file_info['file_path']
80
- download_url = "#{BASE_URL}/file/bot#{@token}/#{file_path}"
81
- @logger.debug("downloading.. #{download_url}") if @logger
82
- response = @http.get(download_url)
83
- if response.status == 200
84
- if destination_path
85
- File.binwrite(destination_path, response.body.to_s)
86
- @logger.debug("saved to #{destination_path}") if @logger
87
- destination_path
88
- else
89
- response.body.to_s
90
- end
91
- else
92
- raise NetworkError.new("Download failed : #{response.status}")
93
- end
94
- end
95
-
96
82
  def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
97
83
  params = { timeout: timeout, limit: limit }
98
84
  params[:offset] = offset if offset
99
85
  params[:allowed_updates] = allowed_updates if allowed_updates
100
86
  call('getUpdates', params)
101
87
  end
102
-
88
+
103
89
  def close
104
- @http.close
90
+ @client.close
105
91
  end
106
-
92
+
107
93
  private
108
-
94
+
95
+ def make_request(method, params)
96
+ url = "/bot#{@token}/#{method}"
97
+ @logger.debug("Api call #{method}") if @logger
98
+
99
+ response = @client.post(
100
+ url,
101
+ { 'content-type' => 'application/json' },
102
+ JSON.dump(params.compact)
103
+ )
104
+
105
+ handle_response(response)
106
+ end
107
+
108
+ def handle_response(response)
109
+ json = JSON.parse(response.read)
110
+
111
+ if json && json['ok']
112
+ json['result']
113
+ else
114
+ raise APIError.new(json ? json['description'] : "Api Error")
115
+ end
116
+ end
117
+
109
118
  def file_object?(obj)
110
119
  obj.is_a?(File) || obj.is_a?(StringIO) || obj.is_a?(Tempfile) ||
111
120
  (obj.is_a?(String) && File.exist?(obj))
112
121
  end
113
122
  end
114
-
123
+
115
124
  class APIError < StandardError
116
125
  attr_reader :code
117
126
 
@@ -120,7 +129,7 @@ module Telegem
120
129
  @code = code
121
130
  end
122
131
  end
123
-
132
+
124
133
  class NetworkError < APIError; end
125
134
  end
126
- end
135
+ end
data/lib/core/bot.rb CHANGED
@@ -1,6 +1,4 @@
1
- require 'concurrent'
2
- require 'logger'
3
- require 'async'
1
+ require 'json'
4
2
 
5
3
  module Telegem
6
4
  module Core
@@ -39,13 +37,13 @@ module Telegem
39
37
  @polling_options = options.slice(:timeout, :limit, :allowed_updates) || {}
40
38
  end
41
39
 
42
- def start_polling(**options)
43
- @running = true
44
- @polling_options = options
45
- Async do
46
- poll_loop # Now runs in Async context
40
+ def start_polling(**options)
41
+ @running = true
42
+ @polling_options = options
43
+ Async do
44
+ poll_loop
45
+ end
47
46
  end
48
- end
49
47
 
50
48
  def shutdown
51
49
  return unless @running
@@ -54,6 +52,7 @@ module Telegem
54
52
  @running = false
55
53
  sleep 0.1
56
54
  end
55
+
57
56
  def running?
58
57
  @running
59
58
  end
@@ -78,7 +77,7 @@ module Telegem
78
77
  def contact(**options, &block)
79
78
  on(:message, contact: true) do |ctx|
80
79
  block.call(ctx)
81
- end
80
+ end
82
81
  end
83
82
 
84
83
  def poll_answer(&block)
@@ -87,10 +86,10 @@ module Telegem
87
86
  end
88
87
  end
89
88
 
90
- def pre_checkout_query(&block)
91
- on(:pre_checkout_query) do |ctx|
92
- block.call(ctx)
93
- end
89
+ def pre_checkout_query(&block)
90
+ on(:pre_checkout_query) do |ctx|
91
+ block.call(ctx)
92
+ end
94
93
  end
95
94
 
96
95
  def shipping_query(&block)
@@ -100,34 +99,35 @@ module Telegem
100
99
  end
101
100
 
102
101
  def chat_join_request(&block)
103
- on(:chat_join_request) do |ctx|
104
- block.call(ctx)
105
- end
106
- end
102
+ on(:chat_join_request) do |ctx|
103
+ block.call(ctx)
104
+ end
105
+ end
107
106
 
108
- def chat_boost(&block)
109
- on(:chat_boost) do |ctx|
110
- block.call(ctx)
111
- end
112
- end
107
+ def chat_boost(&block)
108
+ on(:chat_boost) do |ctx|
109
+ block.call(ctx)
110
+ end
111
+ end
113
112
 
114
- def removed_chat_boost(&block)
115
- on(:removed_chat_boost) do |ctx|
116
- block.call(ctx)
117
- end
118
- end
113
+ def removed_chat_boost(&block)
114
+ on(:removed_chat_boost) do |ctx|
115
+ block.call(ctx)
116
+ end
117
+ end
119
118
 
120
- def message_reaction(&block)
121
- on(:message_reaction) do |ctx|
122
- block.call(ctx)
123
- end
124
- end
119
+ def message_reaction(&block)
120
+ on(:message_reaction) do |ctx|
121
+ block.call(ctx)
122
+ end
123
+ end
125
124
 
126
- def message_reaction_count(&block)
127
- on(:message_reaction_count) do |ctx|
128
- block.call(ctx)
129
- end
130
- end
125
+ def message_reaction_count(&block)
126
+ on(:message_reaction_count) do |ctx|
127
+ block.call(ctx)
128
+ end
129
+ end
130
+
131
131
  def web_app_data(&block)
132
132
  on(:message, web_app_data: true) do |ctx|
133
133
  block.call(ctx)
@@ -170,15 +170,27 @@ end
170
170
  end
171
171
 
172
172
  def set_webhook(url, **options, &callback)
173
- @api.call!('setWebhook', { url: url }.merge(options), &callback)
173
+ if callback
174
+ @api.call!('setWebhook', { url: url }.merge(options), &callback)
175
+ else
176
+ @api.call('setWebhook', { url: url }.merge(options))
177
+ end
174
178
  end
175
179
 
176
180
  def delete_webhook(&callback)
177
- @api.call!('deleteWebhook', {}, &callback)
181
+ if callback
182
+ @api.call!('deleteWebhook', {}, &callback)
183
+ else
184
+ @api.call('deleteWebhook', {})
185
+ end
178
186
  end
179
187
 
180
188
  def get_webhook_info(&callback)
181
- @api.call!('getWebhookInfo', {}, &callback)
189
+ if callback
190
+ @api.call!('getWebhookInfo', {}, &callback)
191
+ else
192
+ @api.call('getWebhookInfo', {})
193
+ end
182
194
  end
183
195
 
184
196
  def process(update_data)
@@ -188,88 +200,50 @@ end
188
200
 
189
201
  private
190
202
 
191
- def poll_loop
192
- while @running
193
- begin
194
- Async do |task|
195
- fetch_updates do |result, error|
196
- if error
197
- @logger.error "Polling error #{error.message}"
198
- task.sleep(0.2)
199
- elsif result && result['ok']
200
- handle_updates_response(result)
201
- end
202
- end
203
- end.wait
204
- rescue => e
205
- @logger.error "poll loop error #{e.message}"
206
- end
207
- sleep 0.5
208
- end
209
- end
210
-
211
- def fetch_updates(&completion_callback)
212
- params = {
213
- timeout: @polling_options[:timeout] || 30,
214
- limit: @polling_options[:limit] || 100
215
- }
216
- params[:offset] = @offset if @offset
217
- params[:allowed_updates] = @polling_options[:allowed_updates] if @polling_options[:allowed_updates]
218
-
219
- @logger.debug "Fetching updates with offset: #{@offset}"
220
-
221
- @api.call!('getUpdates', params) do |updates_array, error|
222
- if error
223
- @logger.error "Polling error: #{error.message}"
224
- completion_callback.call(nil, error) if completion_callback
225
- else
226
-
227
- # Success
228
- if updates_array && updates_array.is_a?(Array)
229
- result = { 'ok' => true, 'result' => updates_array }
230
- completion_callback.call(result, nil) if completion_callback
231
- else
232
- completion_callback.call(nil, nil) if completion_callback
203
+ def poll_loop
204
+ while @running
205
+ begin
206
+ updates = @api.get_updates(
207
+ offset: @offset,
208
+ timeout: @polling_options[:timeout] || 30,
209
+ limit: @polling_options[:limit] || 100,
210
+ allowed_updates: @polling_options[:allowed_updates]
211
+ )
212
+
213
+ if updates && updates.any?
214
+ updates.each do |update_data|
215
+ Async do
216
+ update = Types::Update.new(update_data)
217
+ process_update(update)
218
+ end
219
+ end
220
+
221
+ @offset = updates.last['update_id'] + 1
222
+ @logger.debug("Updated offset to: #{@offset}")
223
+ end
224
+ rescue => e
225
+ @logger.error("Poll loop error: #{e.message}")
226
+ sleep 1
233
227
  end
234
- end
235
- end
228
+ end
236
229
  end
237
-
238
-
239
230
 
240
- def handle_updates_response(api_response)
241
- if api_response['ok']
242
- updates = api_response['result'] || []
243
- updates.each do |data|
244
- Async do |task|
245
- update_object = Types::Update.new(data)
246
- process_update(update_object)
247
- end
248
- end
249
- if updates.any?
250
- @offset = updates.last['update_id'] + 1
251
- @logger.debug "Updated offset to; #{@offset}"
252
- end
253
- end
254
- end
255
-
256
231
  def process_update(update)
257
232
  if update.message&.text && @logger
258
- user = update.message.from
259
- cmd = update.message.text.split.first
260
- @logger.info("#{cmd} - #{user.username}")
233
+ user = update.message.from
234
+ cmd = update.message.text.split.first
235
+ @logger.info("#{cmd} - #{user.username}")
261
236
  end
262
-
263
- ctx = Context.new(update, self)
264
-
265
- begin
266
- run_middleware_chain(ctx) do |context|
267
- dispatch_to_handlers(context)
268
- end
269
- rescue => e
270
- handle_error(e, ctx)
271
- end
272
237
 
238
+ ctx = Context.new(update, self)
239
+
240
+ begin
241
+ run_middleware_chain(ctx) do |context|
242
+ dispatch_to_handlers(context)
243
+ end
244
+ rescue => e
245
+ handle_error(e, ctx)
246
+ end
273
247
  end
274
248
 
275
249
  def run_middleware_chain(ctx, &final)
@@ -290,19 +264,20 @@ end
290
264
  end
291
265
 
292
266
  unless @middleware.any? { |m, _, _| m.to_s =~ /Scene/ }
293
- begin
294
- require_relative '../session/scene_middleware'
295
- chain.use(Telegem::Scene::Middleware.new)
296
- rescue LoadError => e
297
- @logger.debug("Scene middleware not available: #{e.message}") if @logger
298
- end
299
- end
300
- unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
301
- chain.use(Session::Middleware.new(@session_store))
302
- end
303
-
304
- chain
305
- end
267
+ begin
268
+ require_relative '../session/scene_middleware'
269
+ chain.use(Telegem::Scene::Middleware.new)
270
+ rescue LoadError => e
271
+ @logger.debug("Scene middleware not available: #{e.message}") if @logger
272
+ end
273
+ end
274
+
275
+ unless @middleware.any? { |m, _, _| m.is_a?(Session::Middleware) }
276
+ chain.use(Session::Middleware.new(@session_store))
277
+ end
278
+
279
+ chain
280
+ end
306
281
 
307
282
  def dispatch_to_handlers(ctx)
308
283
  update_type = detect_update_type(ctx.update)
@@ -347,16 +322,16 @@ end
347
322
  when :location
348
323
  ctx.message&.location != nil
349
324
  when :contact
350
- ctx.message&.contact != nil
325
+ ctx.message&.contact != nil
351
326
  when :web_app_data
352
327
  ctx.message&.web_app_data != nil
353
328
  else
354
- if ctx.update.respond_to?(key)
355
- ctx.update.send(key) == value
356
- else
357
- false
358
- end
359
- end
329
+ if ctx.update.respond_to?(key)
330
+ ctx.update.send(key) == value
331
+ else
332
+ false
333
+ end
334
+ end
360
335
  end
361
336
  end
362
337
 
@@ -392,4 +367,4 @@ end
392
367
  end
393
368
  end
394
369
  end
395
- end
370
+ 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.1.3".freeze
6
+ VERSION = "3.2.0".freeze
7
7
  end
8
8
 
9
9
  # Load core components
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.1.3
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sick_phantom
@@ -118,7 +118,6 @@ executables:
118
118
  extensions: []
119
119
  extra_rdoc_files: []
120
120
  files:
121
- - ".replit"
122
121
  - CHANGELOG
123
122
  - CODE_OF_CONDUCT.md
124
123
  - Contributing.md
@@ -127,6 +126,8 @@ files:
127
126
  - LICENSE
128
127
  - Readme.md
129
128
  - Starts_HallofFame.md
129
+ - assets/.gitkeep
130
+ - assets/logo.png
130
131
  - bin/.gitkeep
131
132
  - bin/telegem-ssl
132
133
  - examples/.gitkeep
@@ -158,7 +159,7 @@ metadata:
158
159
  bug_tracker_uri: https://gitlab.com/ruby-telegem/telegem/-/issues
159
160
  documentation_uri: https://gitlab.com/ruby-telegem/telegem/-/tree/main/docs-src?ref_type=heads
160
161
  rubygems_mfa_required: 'false'
161
- post_install_message: "Thanks for installing Telegem 3.1.3!\n\n\U0001F4DA Documentation:
162
+ post_install_message: "Thanks for installing Telegem 3.2.0!\n\n\U0001F4DA Documentation:
162
163
  https://gitlab.com/ruby-telegem/telegem\n\n\U0001F510 For SSL Webhooks:\nRun: telegem-ssl
163
164
  your-domain.com\nThis sets up Let's Encrypt certificates automatically.\n\n\U0001F916
164
165
  Happy bot building!\n"
data/.replit DELETED
@@ -1,13 +0,0 @@
1
- run = "bundle exec ruby main.rb"
2
- hidden = [".bundle"]
3
- entrypoint = "main.rb"
4
- modules = ["ruby-3.2"]
5
-
6
- [nix]
7
- channel = "stable-24_05"
8
-
9
- [gitHubImport]
10
- requiredFiles = [".replit", "replit.nix"]
11
-
12
- [agent]
13
- expertMode = true