telegem 1.0.6 → 2.0.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.
data/lib/api/client.rb CHANGED
@@ -1,20 +1,21 @@
1
- # lib/api/client.rb - CORRECTED VERSION
1
+ # lib/api/client.rb - V2.0.0 (FIXED)
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, **options)
12
13
  @token = token
13
- @logger = logger || Logger.new($stdout)
14
+ @logger = options[:logger] || Logger.new($stdout)
15
+ timeout = options[:timeout] || 30
16
+ pool_size = options[:pool_size] || 10
14
17
 
15
- # HTTPX with persistent connections and proper async support
16
18
  @http = HTTPX.plugin(:persistent)
17
- .plugin(:retries, max_retries: 3)
18
19
  .with(
19
20
  timeout: {
20
21
  connect_timeout: 10,
@@ -24,42 +25,60 @@ module Telegem
24
25
  },
25
26
  headers: {
26
27
  'Content-Type' => 'application/json',
27
- 'User-Agent' => "Telegem/#{Telegem::VERSION}"
28
- }
28
+ 'User-Agent' => "Telegem/#{Telegem::VERSION} (Ruby #{RUBY_VERSION}; #{RUBY_PLATFORM})"
29
+ },
30
+ max_requests: pool_size
29
31
  )
32
+
33
+ if HTTPX.plugins.key?(:retries)
34
+ @http = @http.plugin(:retries, max_retries: 3, retry_on: [500, 502, 503, 504])
35
+ end
36
+
37
+ ObjectSpace.define_finalizer(self, proc { close })
30
38
  end
31
39
 
32
- # Main API call - returns HTTPX request (promise-like object)
33
40
  def call(method, params = {})
34
41
  url = "#{BASE_URL}/bot#{@token}/#{method}"
35
42
 
36
- @logger.debug("API Call: #{method}") if @logger
43
+ @logger.debug("🚀 Async API: #{method}") if @logger
37
44
 
38
- # Return the async request object directly
39
45
  @http.post(url, json: params.compact)
40
- .then(&method(:handle_response))
41
- .on_error(&method(:handle_error))
46
+ .then(&method(:handle_response_async))
47
+ .on_error(&method(:handle_error_async))
42
48
  end
43
49
 
44
- # File upload method
45
- def upload(method, params)
46
- url = "#{BASE_URL}/bot#{@token}/#{method}"
50
+ def call!(method, params = {}, timeout: nil)
51
+ timeout ||= @http.options.timeout[:read_timeout]
47
52
 
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]
53
+ request = call(method, params)
54
+
55
+ begin
56
+ wait_result = request.wait(timeout)
57
+
58
+ if request.error
59
+ raise APIError, request.error.message
60
+ elsif !wait_result
61
+ raise NetworkError, "Request timeout after #{timeout}s"
54
62
  end
63
+
64
+ request.instance_variable_get(:@result) || request.response
65
+ rescue Timeout::Error
66
+ raise NetworkError, "Request timeout after #{timeout}s"
55
67
  end
68
+ end
69
+
70
+ def upload(method, params)
71
+ url = "#{BASE_URL}/bot#{@token}/#{method}"
72
+
73
+ form = build_multipart_form(params)
74
+
75
+ @logger.debug("📤 Async Upload: #{method}") if @logger
56
76
 
57
77
  @http.post(url, form: form)
58
- .then(&method(:handle_response))
59
- .on_error(&method(:handle_error))
78
+ .then(&method(:handle_response_async))
79
+ .on_error(&method(:handle_error_async))
60
80
  end
61
81
 
62
- # Convenience method for getUpdates with proper async handling
63
82
  def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
64
83
  params = { timeout: timeout, limit: limit }
65
84
  params[:offset] = offset if offset
@@ -68,57 +87,86 @@ module Telegem
68
87
  call('getUpdates', params)
69
88
  end
70
89
 
71
- # Close connections gracefully
72
90
  def close
73
91
  @http.close
74
92
  end
75
93
 
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
94
  private
86
95
 
87
- def handle_response(response)
96
+ def handle_response_async(response)
88
97
  response.raise_for_status unless response.status == 200
89
98
 
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']
99
+ case response.status
100
+ when 429
101
+ retry_after = response.headers['retry-after']&.to_i || 1
102
+ raise RateLimitError.new("Rate limited", retry_after)
103
+ when 200
104
+ begin
105
+ json = response.json
106
+ rescue JSON::ParserError => e
107
+ raise APIError, "Invalid JSON: #{e.message}"
108
+ end
109
+
110
+ unless json
111
+ raise APIError, "Empty response"
112
+ end
113
+
114
+ if json['ok']
115
+ response.request.instance_variable_set(:@result, json['result'])
116
+ json['result']
117
+ else
118
+ raise APIError.new(json['description'], json['error_code'])
119
+ end
97
120
  else
98
- raise APIError.new(json['description'], json['error_code'])
121
+ response.raise_for_status
99
122
  end
100
123
  end
101
124
 
102
- def handle_error(error, request = nil)
125
+ def handle_error_async(error)
103
126
  case error
104
127
  when HTTPX::TimeoutError
105
- @logger.error("Telegram API timeout: #{error.message}") if @logger
106
- raise NetworkError, "Request timeout: #{error.message}"
128
+ @logger.error(" Timeout: #{error.message}") if @logger
129
+ raise NetworkError, "Timeout: #{error.message}"
107
130
  when HTTPX::ConnectionError
108
- @logger.error("Connection error: #{error.message}") if @logger
131
+ @logger.error("🔌 Connection: #{error.message}") if @logger
109
132
  raise NetworkError, "Connection failed: #{error.message}"
110
133
  when HTTPX::HTTPError
111
- @logger.error("HTTP error #{error.response.status}: #{error.message}") if @logger
134
+ @logger.error("🌐 HTTP #{error.response.status}: #{error.message}") if @logger
112
135
  raise APIError, "HTTP #{error.response.status}: #{error.message}"
136
+ when RateLimitError
137
+ @logger.error("🚦 Rate limit: retry after #{error.retry_after}s") if @logger
138
+ raise error
113
139
  else
114
- @logger.error("Unexpected error: #{error.class}: #{error.message}") if @logger
140
+ @logger.error("💥 Unexpected: #{error.class}: #{error.message}") if @logger
115
141
  raise APIError, error.message
116
142
  end
117
143
  end
118
144
 
145
+ def build_multipart_form(params)
146
+ params.map do |key, value|
147
+ if file_object?(value)
148
+ [key.to_s, HTTPX::FormData::File.new(value)]
149
+ else
150
+ [key.to_s, value.to_s]
151
+ end
152
+ end
153
+ end
154
+
119
155
  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))
156
+ case obj
157
+ when File, StringIO, Tempfile
158
+ true
159
+ when Pathname
160
+ obj.exist? && obj.readable?
161
+ when String
162
+ if obj.start_with?('http://', 'https://', 'ftp://')
163
+ false
164
+ else
165
+ File.exist?(obj) && File.readable?(obj)
166
+ end
167
+ else
168
+ false
169
+ end
122
170
  end
123
171
  end
124
172
 
@@ -132,5 +180,14 @@ module Telegem
132
180
  end
133
181
 
134
182
  class NetworkError < APIError; end
183
+
184
+ class RateLimitError < APIError
185
+ attr_reader :retry_after
186
+
187
+ def initialize(message, retry_after = 1)
188
+ super(message)
189
+ @retry_after = retry_after
190
+ end
191
+ end
135
192
  end
136
193
  end