telegem 1.0.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/api/client.rb CHANGED
@@ -1,20 +1,20 @@
1
- # lib/api/client.rb - CORRECTED VERSION
1
+ # lib/api/client.rb - V2.0.0 (ASYNC-FIRST, NON-BLOCKING)
2
2
  require 'httpx'
3
+ require 'json'
3
4
 
4
5
  module Telegem
5
6
  module API
6
7
  class Client
7
8
  BASE_URL = 'https://api.telegram.org'
8
9
 
9
- attr_reader :token, :logger, :http
10
+ attr_reader :token, :logger, :http, :connection_pool
10
11
 
11
- def initialize(token, logger: nil, timeout: 30)
12
+ def initialize(token, logger: nil, timeout: 30, pool_size: 10)
12
13
  @token = token
13
14
  @logger = logger || Logger.new($stdout)
14
15
 
15
- # HTTPX with persistent connections and proper async support
16
+ # HTTPX with proper async configuration
16
17
  @http = HTTPX.plugin(:persistent)
17
- .plugin(:retries, max_retries: 3)
18
18
  .with(
19
19
  timeout: {
20
20
  connect_timeout: 10,
@@ -24,39 +24,69 @@ module Telegem
24
24
  },
25
25
  headers: {
26
26
  'Content-Type' => 'application/json',
27
- 'User-Agent' => "Telegem/#{Telegem::VERSION}"
28
- }
27
+ 'User-Agent' => "Telegem/#{Telegem::VERSION} (Ruby #{RUBY_VERSION}; #{RUBY_PLATFORM})"
28
+ },
29
+ max_requests: pool_size # Connection pool
29
30
  )
31
+
32
+ # Add retry plugin if available
33
+ if HTTPX.plugins.key?(:retries)
34
+ @http = @http.plugin(:retries, max_retries: 3, retry_on: [500, 502, 503, 504])
35
+ end
36
+
37
+ # Ensure proper cleanup
38
+ ObjectSpace.define_finalizer(self, proc { close })
30
39
  end
31
40
 
32
- # Main API call - returns HTTPX request (promise-like object)
41
+ # PRIMARY API: Async call - returns HTTPX request (promise-like)
33
42
  def call(method, params = {})
34
43
  url = "#{BASE_URL}/bot#{@token}/#{method}"
35
44
 
36
- @logger.debug("API Call: #{method}") if @logger
45
+ @logger.debug("🚀 Async API: #{method}") if @logger
37
46
 
38
- # Return the async request object directly
47
+ # Pure async - returns immediately
39
48
  @http.post(url, json: params.compact)
40
- .then(&method(:handle_response))
41
- .on_error(&method(:handle_error))
49
+ .then(&method(:handle_response_async))
50
+ .on_error(&method(:handle_error_async))
42
51
  end
43
52
 
44
- # File upload method
45
- def upload(method, params)
46
- url = "#{BASE_URL}/bot#{@token}/#{method}"
53
+ # SYNC WRAPPER: For when you absolutely need blocking
54
+ def call!(method, params = {}, timeout: nil)
55
+ timeout ||= @http.options.timeout[:read_timeout]
47
56
 
48
- # Convert params to multipart form data
49
- form = params.map do |key, value|
50
- if file_object?(value)
51
- [key.to_s, HTTPX::FormData::File.new(value)]
52
- else
53
- [key.to_s, value.to_s]
57
+ # Start async request
58
+ request = call(method, params)
59
+
60
+ # Wait with timeout
61
+ begin
62
+ wait_result = request.wait(timeout)
63
+
64
+ if request.error
65
+ # Error already logged by handle_error_async
66
+ raise APIError, request.error.message
67
+ elsif !wait_result
68
+ raise NetworkError, "Request timeout after #{timeout}s"
54
69
  end
70
+
71
+ # Return the actual result (not the request object)
72
+ request.instance_variable_get(:@result) || request.response
73
+ rescue Timeout::Error
74
+ raise NetworkError, "Request timeout after #{timeout}s"
55
75
  end
76
+ end
77
+
78
+ # File upload - also async
79
+ def upload(method, params)
80
+ url = "#{BASE_URL}/bot#{@token}/#{method}"
81
+
82
+ # Build multipart form
83
+ form = build_multipart_form(params)
84
+
85
+ @logger.debug("📤 Async Upload: #{method}") if @logger
56
86
 
57
87
  @http.post(url, form: form)
58
- .then(&method(:handle_response))
59
- .on_error(&method(:handle_error))
88
+ .then(&method(:handle_response_async))
89
+ .on_error(&method(:handle_error_async))
60
90
  end
61
91
 
62
92
  # Convenience method for getUpdates with proper async handling
@@ -73,55 +103,90 @@ module Telegem
73
103
  @http.close
74
104
  end
75
105
 
76
- # Synchronous version (for convenience in non-async contexts)
77
- def call!(method, params = {})
78
- request = call(method, params)
79
- request.wait # Wait for completion
80
- handle_response(request)
81
- rescue => e
82
- handle_error(e, request)
83
- end
84
-
85
106
  private
86
107
 
87
- def handle_response(response)
108
+ # Async response handler - stores result on request object
109
+ def handle_response_async(response)
88
110
  response.raise_for_status unless response.status == 200
89
111
 
90
- json = response.json
91
- unless json
92
- raise APIError, "Empty or invalid JSON response"
93
- end
94
-
95
- if json['ok']
96
- json['result']
112
+ case response.status
113
+ when 429 # Rate limit
114
+ retry_after = response.headers['retry-after']&.to_i || 1
115
+ raise RateLimitError.new("Rate limited", retry_after)
116
+ when 200
117
+ begin
118
+ json = response.json
119
+ rescue JSON::ParserError => e
120
+ raise APIError, "Invalid JSON: #{e.message}"
121
+ end
122
+
123
+ unless json
124
+ raise APIError, "Empty response"
125
+ end
126
+
127
+ if json['ok']
128
+ # Store result on request for sync access
129
+ response.request.instance_variable_set(:@result, json['result'])
130
+ json['result']
131
+ else
132
+ raise APIError.new(json['description'], json['error_code'])
133
+ end
97
134
  else
98
- raise APIError.new(json['description'], json['error_code'])
135
+ response.raise_for_status
99
136
  end
100
137
  end
101
138
 
102
- def handle_error(error, request = nil)
139
+ # Async error handler
140
+ def handle_error_async(error)
103
141
  case error
104
142
  when HTTPX::TimeoutError
105
- @logger.error("Telegram API timeout: #{error.message}") if @logger
106
- raise NetworkError, "Request timeout: #{error.message}"
143
+ @logger.error(" Timeout: #{error.message}") if @logger
144
+ raise NetworkError, "Timeout: #{error.message}"
107
145
  when HTTPX::ConnectionError
108
- @logger.error("Connection error: #{error.message}") if @logger
146
+ @logger.error("🔌 Connection: #{error.message}") if @logger
109
147
  raise NetworkError, "Connection failed: #{error.message}"
110
148
  when HTTPX::HTTPError
111
- @logger.error("HTTP error #{error.response.status}: #{error.message}") if @logger
149
+ @logger.error("🌐 HTTP #{error.response.status}: #{error.message}") if @logger
112
150
  raise APIError, "HTTP #{error.response.status}: #{error.message}"
151
+ when RateLimitError
152
+ @logger.error("🚦 Rate limit: retry after #{error.retry_after}s") if @logger
153
+ raise error # Re-raise for user handling
113
154
  else
114
- @logger.error("Unexpected error: #{error.class}: #{error.message}") if @logger
155
+ @logger.error("💥 Unexpected: #{error.class}: #{error.message}") if @logger
115
156
  raise APIError, error.message
116
157
  end
117
158
  end
118
159
 
160
+ def build_multipart_form(params)
161
+ params.map do |key, value|
162
+ if file_object?(value)
163
+ [key.to_s, HTTPX::FormData::File.new(value)]
164
+ else
165
+ [key.to_s, value.to_s]
166
+ end
167
+ end
168
+ end
169
+
119
170
  def file_object?(obj)
120
- obj.is_a?(File) || obj.is_a?(StringIO) || obj.is_a?(Tempfile) ||
121
- (obj.is_a?(String) && File.exist?(obj))
171
+ case obj
172
+ when File, StringIO, Tempfile
173
+ true
174
+ when Pathname
175
+ obj.exist? && obj.readable?
176
+ when String
177
+ # Check if it's a local file (not URL)
178
+ if obj.start_with?('http://', 'https://', 'ftp://')
179
+ false # Telegram handles URLs directly
180
+ else
181
+ File.exist?(obj) && File.readable?(obj)
182
+ end
183
+ else
184
+ false
185
+ end
122
186
  end
123
187
  end
124
188
 
189
+ # Custom Errors
125
190
  class APIError < StandardError
126
191
  attr_reader :code
127
192
 
@@ -132,5 +197,14 @@ module Telegem
132
197
  end
133
198
 
134
199
  class NetworkError < APIError; end
200
+
201
+ class RateLimitError < APIError
202
+ attr_reader :retry_after
203
+
204
+ def initialize(message, retry_after = 1)
205
+ super(message)
206
+ @retry_after = retry_after
207
+ end
208
+ end
135
209
  end
136
210
  end