telegem 0.2.5 → 1.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.
- checksums.yaml +4 -4
- data/.replit +13 -0
- data/Contributing.md +553 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +11 -0
- data/LICENSE +21 -0
- data/Readme.md +353 -0
- data/Test-Projects/.gitkeep +0 -0
- data/Test-Projects/bot_test1.rb +75 -0
- data/Test-Projects/pizza_test_bot_guide.md +163 -0
- data/docs/.gitkeep +0 -0
- data/docs/Api.md +419 -0
- data/docs/Cookbook.md +407 -0
- data/docs/How_to_use.md +571 -0
- data/docs/QuickStart.md +258 -0
- data/docs/Usage.md +717 -0
- data/lib/api/client.rb +89 -116
- data/lib/core/bot.rb +103 -92
- data/lib/core/composer.rb +36 -18
- data/lib/core/context.rb +180 -177
- data/lib/core/scene.rb +81 -71
- data/lib/session/memory_store.rb +1 -1
- data/lib/session/middleware.rb +20 -36
- data/lib/telegem.rb +57 -54
- data/lib/webhook/.gitkeep +0 -0
- data/lib/webhook/server.rb +193 -0
- metadata +38 -35
- data/telegem.gemspec +0 -43
- data/webhook/server.rb +0 -86
data/lib/api/client.rb
CHANGED
|
@@ -1,163 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
require '
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'logger'
|
|
5
|
-
require 'mime/types'
|
|
6
|
-
require 'securerandom'
|
|
7
|
-
require 'ostruct'
|
|
1
|
+
# lib/api/client.rb - CORRECTED VERSION
|
|
2
|
+
require 'httpx'
|
|
8
3
|
|
|
9
4
|
module Telegem
|
|
10
5
|
module API
|
|
11
6
|
class Client
|
|
12
7
|
BASE_URL = 'https://api.telegram.org'
|
|
8
|
+
|
|
9
|
+
attr_reader :token, :logger, :http
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def initialize(token, endpoint: nil, logger: nil)
|
|
11
|
+
def initialize(token, logger: nil, timeout: 30)
|
|
17
12
|
@token = token
|
|
18
13
|
@logger = logger || Logger.new($stdout)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@
|
|
14
|
+
|
|
15
|
+
# HTTPX with persistent connections and proper async support
|
|
16
|
+
@http = HTTPX.plugin(:persistent)
|
|
17
|
+
.plugin(:retries, max_retries: 3, retry_on: [Timeout::Error, HTTPX::Error])
|
|
18
|
+
.with(
|
|
19
|
+
timeout: {
|
|
20
|
+
connect_timeout: 10,
|
|
21
|
+
write_timeout: 10,
|
|
22
|
+
read_timeout: timeout,
|
|
23
|
+
keep_alive_timeout: 15
|
|
24
|
+
},
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type' => 'application/json',
|
|
27
|
+
'User-Agent' => "Telegem/#{Telegem::VERSION}"
|
|
28
|
+
}
|
|
29
|
+
)
|
|
22
30
|
end
|
|
23
31
|
|
|
32
|
+
# Main API call - returns HTTPX request (promise-like object)
|
|
24
33
|
def call(method, params = {})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
url = "#{BASE_URL}/bot#{@token}/#{method}"
|
|
35
|
+
|
|
36
|
+
@logger.debug("API Call: #{method}") if @logger
|
|
37
|
+
|
|
38
|
+
# Return the async request object directly
|
|
39
|
+
@http.post(url, json: params.compact)
|
|
40
|
+
.then(&method(:handle_response))
|
|
41
|
+
.on_error(&method(:handle_error))
|
|
30
42
|
end
|
|
31
43
|
|
|
44
|
+
# File upload method
|
|
32
45
|
def upload(method, params)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
url = "#{BASE_URL}/bot#{@token}/#{method}"
|
|
47
|
+
|
|
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]
|
|
36
54
|
end
|
|
37
55
|
end
|
|
56
|
+
|
|
57
|
+
@http.post(url, form: form)
|
|
58
|
+
.then(&method(:handle_response))
|
|
59
|
+
.on_error(&method(:handle_error))
|
|
38
60
|
end
|
|
39
61
|
|
|
40
|
-
|
|
62
|
+
# Convenience method for getUpdates with proper async handling
|
|
63
|
+
def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
|
|
41
64
|
params = { timeout: timeout, limit: limit }
|
|
42
65
|
params[:offset] = offset if offset
|
|
66
|
+
params[:allowed_updates] = allowed_updates if allowed_updates
|
|
67
|
+
|
|
43
68
|
call('getUpdates', params)
|
|
44
69
|
end
|
|
45
70
|
|
|
71
|
+
# Close connections gracefully
|
|
46
72
|
def close
|
|
47
|
-
@
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def make_request(method, params)
|
|
53
|
-
with_client do |client|
|
|
54
|
-
headers = { 'content-type' => 'application/json' }
|
|
55
|
-
body = params.to_json
|
|
56
|
-
|
|
57
|
-
response = client.post("/bot#{@token}/#{method}", headers, body)
|
|
58
|
-
handle_response(response)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def make_multipart_request(method, params)
|
|
63
|
-
with_client do |client|
|
|
64
|
-
form = build_multipart(params)
|
|
65
|
-
headers = form.headers
|
|
66
|
-
|
|
67
|
-
response = client.post("/bot#{@token}/#{method}", headers, form.body)
|
|
68
|
-
handle_response(response)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def with_client(&block)
|
|
73
|
-
@client ||= Async::HTTP::Client.new(@endpoint)
|
|
74
|
-
yield @client
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def clean_params(params)
|
|
78
|
-
params.reject { |_, v| v.nil? }
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def build_multipart(params)
|
|
82
|
-
boundary = SecureRandom.hex(16)
|
|
83
|
-
parts = []
|
|
84
|
-
|
|
85
|
-
params.each do |key, value|
|
|
86
|
-
if file?(value)
|
|
87
|
-
parts << part_from_file(key, value, boundary)
|
|
88
|
-
else
|
|
89
|
-
parts << part_from_field(key, value, boundary)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
parts << "--#{boundary}--\r\n"
|
|
94
|
-
|
|
95
|
-
body = parts.join
|
|
96
|
-
headers = {
|
|
97
|
-
'content-type' => "multipart/form-data; boundary=#{boundary}",
|
|
98
|
-
'content-length' => body.bytesize.to_s
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
OpenStruct.new(headers: headers, body: body)
|
|
73
|
+
@http.close
|
|
102
74
|
end
|
|
103
75
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
108
83
|
end
|
|
109
84
|
|
|
110
|
-
|
|
111
|
-
filename = File.basename(file.path) if file.respond_to?(:path)
|
|
112
|
-
filename ||= "file"
|
|
113
|
-
|
|
114
|
-
mime_type = MIME::Types.type_for(filename).first || 'application/octet-stream'
|
|
115
|
-
|
|
116
|
-
content = if file.is_a?(String)
|
|
117
|
-
File.read(file)
|
|
118
|
-
else
|
|
119
|
-
file.read
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
<<~PART
|
|
123
|
-
--#{boundary}\r
|
|
124
|
-
Content-Disposition: form-data; name="#{key}"; filename="#{filename}"\r
|
|
125
|
-
Content-Type: #{mime_type}\r
|
|
126
|
-
\r
|
|
127
|
-
#{content}\r
|
|
128
|
-
PART
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def part_from_field(key, value, boundary)
|
|
132
|
-
<<~PART
|
|
133
|
-
--#{boundary}\r
|
|
134
|
-
Content-Disposition: form-data; name="#{key}"\r
|
|
135
|
-
\r
|
|
136
|
-
#{value}\r
|
|
137
|
-
PART
|
|
138
|
-
end
|
|
85
|
+
private
|
|
139
86
|
|
|
140
87
|
def handle_response(response)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
88
|
+
response.raise_for_status unless response.status == 200
|
|
89
|
+
|
|
90
|
+
json = response.json
|
|
91
|
+
unless json
|
|
92
|
+
raise APIError, "Empty or invalid JSON response"
|
|
93
|
+
end
|
|
94
|
+
|
|
144
95
|
if json['ok']
|
|
145
96
|
json['result']
|
|
146
97
|
else
|
|
147
98
|
raise APIError.new(json['description'], json['error_code'])
|
|
148
99
|
end
|
|
149
|
-
|
|
150
|
-
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_error(error, request = nil)
|
|
103
|
+
case error
|
|
104
|
+
when HTTPX::TimeoutError
|
|
105
|
+
@logger.error("Telegram API timeout: #{error.message}") if @logger
|
|
106
|
+
raise NetworkError, "Request timeout: #{error.message}"
|
|
107
|
+
when HTTPX::ConnectionError
|
|
108
|
+
@logger.error("Connection error: #{error.message}") if @logger
|
|
109
|
+
raise NetworkError, "Connection failed: #{error.message}"
|
|
110
|
+
when HTTPX::HTTPError
|
|
111
|
+
@logger.error("HTTP error #{error.response.status}: #{error.message}") if @logger
|
|
112
|
+
raise APIError, "HTTP #{error.response.status}: #{error.message}"
|
|
113
|
+
else
|
|
114
|
+
@logger.error("Unexpected error: #{error.class}: #{error.message}") if @logger
|
|
115
|
+
raise APIError, error.message
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
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))
|
|
151
122
|
end
|
|
152
123
|
end
|
|
153
124
|
|
|
154
125
|
class APIError < StandardError
|
|
155
126
|
attr_reader :code
|
|
156
|
-
|
|
127
|
+
|
|
157
128
|
def initialize(message, code = nil)
|
|
158
129
|
super(message)
|
|
159
130
|
@code = code
|
|
160
131
|
end
|
|
161
132
|
end
|
|
133
|
+
|
|
134
|
+
class NetworkError < APIError; end
|
|
162
135
|
end
|
|
163
136
|
end
|
data/lib/core/bot.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Telegem
|
|
|
5
5
|
|
|
6
6
|
def initialize(token, **options)
|
|
7
7
|
@token = token
|
|
8
|
-
@api = API::Client.new(token, **options.slice(:
|
|
8
|
+
@api = API::Client.new(token, **options.slice(:logger, :timeout))
|
|
9
9
|
@handlers = {
|
|
10
10
|
message: [],
|
|
11
11
|
callback_query: [],
|
|
@@ -20,10 +20,9 @@ module Telegem
|
|
|
20
20
|
@logger = options[:logger] || Logger.new($stdout)
|
|
21
21
|
@error_handler = nil
|
|
22
22
|
@session_store = options[:session_store] || Session::MemoryStore.new
|
|
23
|
-
@
|
|
24
|
-
@semaphore = Async::Semaphore.new(@concurrency)
|
|
25
|
-
@polling_task = nil
|
|
23
|
+
@polling_thread = nil
|
|
26
24
|
@running = false
|
|
25
|
+
@offset = nil
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
def command(name, **options, &block)
|
|
@@ -61,43 +60,28 @@ module Telegem
|
|
|
61
60
|
end
|
|
62
61
|
|
|
63
62
|
def start_polling(**options)
|
|
64
|
-
return
|
|
63
|
+
return if @running
|
|
65
64
|
|
|
66
|
-
@
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
parent.async do |child|
|
|
79
|
-
@semaphore.async do
|
|
80
|
-
process_update(update)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
offset = updates.last&.update_id.to_i + 1 if updates.any?
|
|
86
|
-
end
|
|
87
|
-
rescue => e
|
|
88
|
-
handle_error(e)
|
|
89
|
-
raise
|
|
90
|
-
ensure
|
|
91
|
-
@running = false
|
|
92
|
-
end
|
|
65
|
+
@running = true
|
|
66
|
+
@polling_options = {
|
|
67
|
+
timeout: 30,
|
|
68
|
+
limit: 100,
|
|
69
|
+
allowed_updates: nil
|
|
70
|
+
}.merge(options)
|
|
71
|
+
|
|
72
|
+
@offset = nil
|
|
73
|
+
|
|
74
|
+
@polling_thread = Thread.new do
|
|
75
|
+
@logger.info "🤖 Starting Telegem bot (HTTPX async)..."
|
|
76
|
+
poll_loop
|
|
93
77
|
end
|
|
94
78
|
|
|
95
|
-
|
|
79
|
+
self
|
|
96
80
|
end
|
|
97
81
|
|
|
98
82
|
def webhook(app = nil, &block)
|
|
99
|
-
|
|
100
|
-
|
|
83
|
+
require 'telegem/webhook/server'
|
|
84
|
+
|
|
101
85
|
if block_given?
|
|
102
86
|
Webhook::Server.new(self, &block)
|
|
103
87
|
elsif app
|
|
@@ -108,42 +92,30 @@ module Telegem
|
|
|
108
92
|
end
|
|
109
93
|
|
|
110
94
|
def set_webhook(url, **options)
|
|
111
|
-
|
|
112
|
-
params = { url: url }.merge(options)
|
|
113
|
-
await @api.call('setWebhook', params)
|
|
114
|
-
end
|
|
95
|
+
@api.call('setWebhook', { url: url }.merge(options))
|
|
115
96
|
end
|
|
116
97
|
|
|
117
98
|
def delete_webhook
|
|
118
|
-
|
|
119
|
-
await @api.call('deleteWebhook', {})
|
|
120
|
-
end
|
|
99
|
+
@api.call('deleteWebhook', {})
|
|
121
100
|
end
|
|
122
101
|
|
|
123
102
|
def get_webhook_info
|
|
124
|
-
|
|
125
|
-
await @api.call('getWebhookInfo', {})
|
|
126
|
-
end
|
|
103
|
+
@api.call('getWebhookInfo', {})
|
|
127
104
|
end
|
|
128
105
|
|
|
129
106
|
def process(update_data)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await process_update(update)
|
|
133
|
-
end
|
|
107
|
+
update = Types::Update.new(update_data)
|
|
108
|
+
process_update(update)
|
|
134
109
|
end
|
|
135
110
|
|
|
136
111
|
def shutdown
|
|
137
|
-
@logger.info "Shutting down..."
|
|
112
|
+
@logger.info "🛑 Shutting down..."
|
|
138
113
|
|
|
139
|
-
if @polling_task && @polling_task.running?
|
|
140
|
-
@polling_task.stop
|
|
141
|
-
@polling_task = nil
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
@api.close
|
|
145
114
|
@running = false
|
|
146
|
-
@
|
|
115
|
+
@polling_thread&.join(5) if @polling_thread&.alive?
|
|
116
|
+
@api.close
|
|
117
|
+
|
|
118
|
+
@logger.info "✅ Bot stopped"
|
|
147
119
|
end
|
|
148
120
|
|
|
149
121
|
def running?
|
|
@@ -152,26 +124,74 @@ module Telegem
|
|
|
152
124
|
|
|
153
125
|
private
|
|
154
126
|
|
|
155
|
-
def
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
127
|
+
def poll_loop
|
|
128
|
+
while @running
|
|
129
|
+
begin
|
|
130
|
+
# Get updates - returns HTTPX request immediately
|
|
131
|
+
updates_request = fetch_updates
|
|
132
|
+
|
|
133
|
+
# Wait for this poll to complete (with timeout)
|
|
134
|
+
response = updates_request.wait(@polling_options[:timeout] + 5)
|
|
135
|
+
|
|
136
|
+
if response && response.status == 200 && response.json
|
|
137
|
+
handle_updates_response(response.json)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Small delay to prevent tight loop on errors
|
|
141
|
+
sleep 0.1 unless @offset.nil?
|
|
142
|
+
|
|
143
|
+
rescue => e
|
|
144
|
+
handle_error(e)
|
|
145
|
+
# Longer delay on error
|
|
146
|
+
sleep 5
|
|
147
|
+
end
|
|
148
|
+
end
|
|
161
149
|
end
|
|
162
150
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
def fetch_updates
|
|
152
|
+
params = {
|
|
153
|
+
timeout: @polling_options[:timeout],
|
|
154
|
+
limit: @polling_options[:limit]
|
|
155
|
+
}
|
|
156
|
+
params[:offset] = @offset if @offset
|
|
157
|
+
params[:allowed_updates] = @polling_options[:allowed_updates] if @polling_options[:allowed_updates]
|
|
158
|
+
|
|
159
|
+
@api.call('getUpdates', params)
|
|
160
|
+
end
|
|
166
161
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
def handle_updates_response(api_response)
|
|
163
|
+
return unless api_response['ok']
|
|
164
|
+
|
|
165
|
+
updates_data = api_response['result'] || []
|
|
166
|
+
|
|
167
|
+
# Process each update in its own thread for concurrency
|
|
168
|
+
updates_data.each do |update_data|
|
|
169
|
+
Thread.new do
|
|
170
|
+
begin
|
|
171
|
+
update = Types::Update.new(update_data)
|
|
172
|
+
process_update(update)
|
|
173
|
+
rescue => e
|
|
174
|
+
@logger.error("Error in update thread: #{e.message}")
|
|
170
175
|
end
|
|
171
|
-
rescue => e
|
|
172
|
-
await handle_error(e, ctx)
|
|
173
176
|
end
|
|
174
177
|
end
|
|
178
|
+
|
|
179
|
+
# Update offset for next poll
|
|
180
|
+
if updates_data.any?
|
|
181
|
+
@offset = updates_data.last['update_id'] + 1
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def process_update(update)
|
|
186
|
+
ctx = Context.new(update, self)
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
run_middleware_chain(ctx) do |context|
|
|
190
|
+
dispatch_to_handlers(context)
|
|
191
|
+
end
|
|
192
|
+
rescue => e
|
|
193
|
+
handle_error(e, ctx)
|
|
194
|
+
end
|
|
175
195
|
end
|
|
176
196
|
|
|
177
197
|
def run_middleware_chain(ctx, &final)
|
|
@@ -199,16 +219,13 @@ module Telegem
|
|
|
199
219
|
end
|
|
200
220
|
|
|
201
221
|
def dispatch_to_handlers(ctx)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
result = await(result) if result.is_a?(Async::Task)
|
|
210
|
-
break
|
|
211
|
-
end
|
|
222
|
+
update_type = detect_update_type(ctx.update)
|
|
223
|
+
handlers = @handlers[update_type] || []
|
|
224
|
+
|
|
225
|
+
handlers.each do |handler|
|
|
226
|
+
if matches_filters?(ctx, handler[:filters])
|
|
227
|
+
handler[:handler].call(ctx)
|
|
228
|
+
break # First matching handler wins
|
|
212
229
|
end
|
|
213
230
|
end
|
|
214
231
|
end
|
|
@@ -236,13 +253,7 @@ module Telegem
|
|
|
236
253
|
when :command
|
|
237
254
|
matches_command_filter(ctx, value)
|
|
238
255
|
else
|
|
239
|
-
|
|
240
|
-
result = value.call(ctx)
|
|
241
|
-
result = await(result) if result.is_a?(Async::Task)
|
|
242
|
-
result
|
|
243
|
-
else
|
|
244
|
-
ctx.update.send(key) == value
|
|
245
|
-
end
|
|
256
|
+
ctx.update.send(key) == value
|
|
246
257
|
end
|
|
247
258
|
end
|
|
248
259
|
end
|
|
@@ -269,11 +280,11 @@ module Telegem
|
|
|
269
280
|
|
|
270
281
|
def handle_error(error, ctx = nil)
|
|
271
282
|
if @error_handler
|
|
272
|
-
|
|
273
|
-
await(result) if result.is_a?(Async::Task)
|
|
283
|
+
@error_handler.call(error, ctx)
|
|
274
284
|
else
|
|
275
|
-
@logger.error("Unhandled error: #{error.class}: #{error.message}")
|
|
276
|
-
@logger.error(
|
|
285
|
+
@logger.error("❌ Unhandled error: #{error.class}: #{error.message}")
|
|
286
|
+
@logger.error("Context: #{ctx.raw_update}") if ctx
|
|
287
|
+
@logger.error(error.backtrace&.join("\n")) if error.backtrace
|
|
277
288
|
end
|
|
278
289
|
end
|
|
279
290
|
end
|
data/lib/core/composer.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# lib/core/composer.rb - HTTPX VERSION (NO ASYNC GEM)
|
|
1
2
|
module Telegem
|
|
2
3
|
module Core
|
|
3
4
|
class Composer
|
|
@@ -13,24 +14,14 @@ module Telegem
|
|
|
13
14
|
def call(ctx, &final)
|
|
14
15
|
return final.call(ctx) if @middleware.empty?
|
|
15
16
|
|
|
16
|
-
# Build
|
|
17
|
+
# Build the middleware chain
|
|
17
18
|
chain = final
|
|
18
|
-
|
|
19
|
+
|
|
20
|
+
# Reverse the middleware so last added runs last in chain
|
|
19
21
|
@middleware.reverse_each do |middleware|
|
|
20
|
-
chain =
|
|
21
|
-
if middleware.respond_to?(:call)
|
|
22
|
-
result = middleware.call(context, chain)
|
|
23
|
-
result.is_a?(Async::Task) ? result : Async::Task.new(result)
|
|
24
|
-
elsif middleware.is_a?(Class)
|
|
25
|
-
instance = middleware.new
|
|
26
|
-
result = instance.call(context, chain)
|
|
27
|
-
result.is_a?(Async::Task) ? result : Async::Task.new(result)
|
|
28
|
-
else
|
|
29
|
-
raise "Invalid middleware: #{middleware.class}"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
22
|
+
chain = create_middleware_wrapper(middleware, chain)
|
|
32
23
|
end
|
|
33
|
-
|
|
24
|
+
|
|
34
25
|
# Execute the chain
|
|
35
26
|
chain.call(ctx)
|
|
36
27
|
end
|
|
@@ -38,6 +29,33 @@ module Telegem
|
|
|
38
29
|
def empty?
|
|
39
30
|
@middleware.empty?
|
|
40
31
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def create_middleware_wrapper(middleware, next_middleware)
|
|
36
|
+
->(context) do
|
|
37
|
+
middleware_instance = instantiate_middleware(middleware)
|
|
38
|
+
|
|
39
|
+
if middleware_instance.respond_to?(:call)
|
|
40
|
+
# Call the middleware with next in chain
|
|
41
|
+
middleware_instance.call(context, next_middleware)
|
|
42
|
+
else
|
|
43
|
+
raise "Invalid middleware: #{middleware.class}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def instantiate_middleware(middleware)
|
|
49
|
+
case middleware
|
|
50
|
+
when Class
|
|
51
|
+
middleware.new
|
|
52
|
+
when Proc, Method
|
|
53
|
+
middleware
|
|
54
|
+
else
|
|
55
|
+
# Assume it's already a middleware instance
|
|
56
|
+
middleware
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|