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.
- checksums.yaml +4 -4
- data/docs/Api.md +146 -354
- data/docs/Cookbook(copy_paste).md +644 -0
- data/docs/Getting_started.md +348 -0
- data/docs/webhook_setup.md +199 -0
- data/lib/api/client.rb +109 -52
- data/lib/api/types.rb +283 -67
- data/lib/core/bot.rb +91 -56
- data/lib/core/context.rb +96 -110
- data/lib/markup/.gitkeep +0 -0
- data/lib/markup/keyboard.rb +53 -38
- data/lib/telegem.rb +15 -5
- data/lib/webhook/server.rb +246 -112
- metadata +108 -17
- data/docs/Cookbook.md +0 -407
- data/docs/SETTING_WEBHOOK.md +0 -367
- data/docs/UNDERSTANDING-WEBHOOK-n-POLLING.md +0 -241
data/lib/api/client.rb
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
# lib/api/client.rb -
|
|
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,
|
|
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
|
|
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(:
|
|
41
|
-
.on_error(&method(:
|
|
46
|
+
.then(&method(:handle_response_async))
|
|
47
|
+
.on_error(&method(:handle_error_async))
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
url = "#{BASE_URL}/bot#{@token}/#{method}"
|
|
50
|
+
def call!(method, params = {}, timeout: nil)
|
|
51
|
+
timeout ||= @http.options.timeout[:read_timeout]
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(:
|
|
59
|
-
.on_error(&method(:
|
|
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
|
|
96
|
+
def handle_response_async(response)
|
|
88
97
|
response.raise_for_status unless response.status == 200
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
121
|
+
response.raise_for_status
|
|
99
122
|
end
|
|
100
123
|
end
|
|
101
124
|
|
|
102
|
-
def
|
|
125
|
+
def handle_error_async(error)
|
|
103
126
|
case error
|
|
104
127
|
when HTTPX::TimeoutError
|
|
105
|
-
@logger.error("
|
|
106
|
-
raise NetworkError, "
|
|
128
|
+
@logger.error("⏰ Timeout: #{error.message}") if @logger
|
|
129
|
+
raise NetworkError, "Timeout: #{error.message}"
|
|
107
130
|
when HTTPX::ConnectionError
|
|
108
|
-
@logger.error("Connection
|
|
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
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
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
|