telegem 3.4.0 → 3.6.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/.anv +2 -0
- data/CHANGELOG.md +28 -2
- data/README.md +2 -0
- data/_config.yml +10 -0
- data/docs/changelog.md +134 -169
- data/docs/webhooks.md +511 -19
- data/lib/api/client.rb +176 -55
- data/lib/api/types.rb +60 -9
- data/lib/core/context.rb +56 -0
- data/lib/core/rate_limit.rb +6 -3
- data/lib/telegem.rb +1 -1
- data/lib/webhook/server.rb +36 -14
- metadata +30 -16
- data/CODE_OF_CONDUCT.md +0 -13
- data/Gemfile.lock +0 -11
data/lib/api/client.rb
CHANGED
|
@@ -1,11 +1,112 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require "async/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "logger"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "tempfile"
|
|
3
7
|
|
|
4
8
|
module Telegem
|
|
5
9
|
module API
|
|
10
|
+
class MultipartForm
|
|
11
|
+
CRLF = "\r\n"
|
|
12
|
+
|
|
13
|
+
attr_reader :content_type
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@boundary = "----telegem#{SecureRandom.hex(16)}"
|
|
17
|
+
@content_type = "multipart/form-data; boundary=#{@boundary}"
|
|
18
|
+
@parts = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add(name, value)
|
|
22
|
+
if file?(value)
|
|
23
|
+
append_file(name, value)
|
|
24
|
+
else
|
|
25
|
+
append_field(name, value.to_s)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def body
|
|
30
|
+
String.new.tap do |buffer|
|
|
31
|
+
@parts.each do |part|
|
|
32
|
+
buffer << "--#{@boundary}#{CRLF}"
|
|
33
|
+
buffer << part
|
|
34
|
+
buffer << CRLF
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
buffer << "--#{@boundary}--#{CRLF}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def append_field(name, value)
|
|
44
|
+
@parts << <<~PART.gsub("\n", CRLF)
|
|
45
|
+
Content-Disposition: form-data; name="#{name}"
|
|
46
|
+
|
|
47
|
+
#{value}
|
|
48
|
+
PART
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def append_file(name, value)
|
|
52
|
+
io =
|
|
53
|
+
case value
|
|
54
|
+
when String
|
|
55
|
+
File.open(value, "rb")
|
|
56
|
+
else
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
filename =
|
|
61
|
+
if io.respond_to?(:path) && io.path
|
|
62
|
+
File.basename(io.path)
|
|
63
|
+
else
|
|
64
|
+
"upload.bin"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
mime =
|
|
68
|
+
case File.extname(filename).downcase
|
|
69
|
+
when ".jpg", ".jpeg"
|
|
70
|
+
"image/jpeg"
|
|
71
|
+
when ".png"
|
|
72
|
+
"image/png"
|
|
73
|
+
when ".gif"
|
|
74
|
+
"image/gif"
|
|
75
|
+
when ".webp"
|
|
76
|
+
"image/webp"
|
|
77
|
+
when ".mp4"
|
|
78
|
+
"video/mp4"
|
|
79
|
+
when ".mp3"
|
|
80
|
+
"audio/mpeg"
|
|
81
|
+
when ".ogg"
|
|
82
|
+
"audio/ogg"
|
|
83
|
+
when ".pdf"
|
|
84
|
+
"application/pdf"
|
|
85
|
+
else
|
|
86
|
+
"application/octet-stream"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@parts << String.new.tap do |part|
|
|
90
|
+
part << %(Content-Disposition: form-data; name="#{name}"; filename="#{filename}") << CRLF
|
|
91
|
+
part << "Content-Type: #{mime}" << CRLF
|
|
92
|
+
part << CRLF
|
|
93
|
+
part << io.read
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
io.close if value.is_a?(String)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def file?(obj)
|
|
100
|
+
obj.is_a?(File) ||
|
|
101
|
+
obj.is_a?(Tempfile) ||
|
|
102
|
+
obj.is_a?(StringIO) ||
|
|
103
|
+
(obj.is_a?(String) && File.file?(obj))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
6
107
|
class Client
|
|
7
|
-
BASE_URL =
|
|
8
|
-
|
|
108
|
+
BASE_URL = "https://api.telegram.org"
|
|
109
|
+
|
|
9
110
|
attr_reader :token, :logger
|
|
10
111
|
|
|
11
112
|
def initialize(token, **options)
|
|
@@ -13,20 +114,21 @@ module Telegem
|
|
|
13
114
|
@logger = options[:logger] || Logger.new($stdout)
|
|
14
115
|
@timeout = options[:timeout] || 30
|
|
15
116
|
@retries = options[:retries] || 3
|
|
16
|
-
@retry_delay = options[:retry_delay] || 1
|
|
17
|
-
|
|
117
|
+
@retry_delay = options[:retry_delay] || 1
|
|
118
|
+
|
|
18
119
|
@endpoint = Async::HTTP::Endpoint.parse(BASE_URL, timeout: @timeout)
|
|
19
120
|
@client = Async::HTTP::Client.new(@endpoint)
|
|
20
121
|
end
|
|
21
|
-
|
|
122
|
+
|
|
22
123
|
def call(method, params = {})
|
|
23
124
|
with_retry do
|
|
24
125
|
make_request(method, params)
|
|
25
126
|
end
|
|
26
127
|
end
|
|
27
|
-
|
|
128
|
+
|
|
28
129
|
def call!(method, params = {}, &callback)
|
|
29
130
|
return unless callback
|
|
131
|
+
|
|
30
132
|
begin
|
|
31
133
|
result = call(method, params)
|
|
32
134
|
callback.call(result, nil)
|
|
@@ -34,38 +136,45 @@ module Telegem
|
|
|
34
136
|
callback.call(nil, error)
|
|
35
137
|
end
|
|
36
138
|
end
|
|
37
|
-
|
|
139
|
+
|
|
38
140
|
def upload(method, params)
|
|
39
141
|
with_retry do
|
|
40
142
|
url = "/bot#{@token}/#{method}"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
143
|
+
|
|
144
|
+
form = MultipartForm.new
|
|
145
|
+
|
|
44
146
|
params.each do |key, value|
|
|
45
|
-
|
|
46
|
-
body.add(key.to_s, value, filename: File.basename(value))
|
|
47
|
-
else
|
|
48
|
-
body.add(key.to_s, value.to_s)
|
|
49
|
-
end
|
|
147
|
+
form.add(key.to_s, value)
|
|
50
148
|
end
|
|
51
|
-
|
|
52
|
-
|
|
149
|
+
|
|
150
|
+
body = form.body
|
|
151
|
+
|
|
152
|
+
response = @client.post(
|
|
153
|
+
url,
|
|
154
|
+
{
|
|
155
|
+
"content-type" => form.content_type,
|
|
156
|
+
"content-length" => body.bytesize.to_s
|
|
157
|
+
},
|
|
158
|
+
body
|
|
159
|
+
)
|
|
160
|
+
|
|
53
161
|
handle_response(response)
|
|
54
162
|
end
|
|
55
163
|
end
|
|
56
|
-
|
|
164
|
+
|
|
57
165
|
def download(file_id, destination_path = nil)
|
|
58
166
|
with_retry do
|
|
59
|
-
file_info = call(
|
|
60
|
-
return nil unless file_info && file_info[
|
|
61
|
-
|
|
62
|
-
file_path = file_info[
|
|
167
|
+
file_info = call("getFile", file_id: file_id)
|
|
168
|
+
return nil unless file_info && file_info["file_path"]
|
|
169
|
+
|
|
170
|
+
file_path = file_info["file_path"]
|
|
63
171
|
download_url = "/file/bot#{@token}/#{file_path}"
|
|
64
|
-
|
|
172
|
+
|
|
65
173
|
response = @client.get(download_url)
|
|
66
|
-
|
|
174
|
+
|
|
67
175
|
if response.status == 200
|
|
68
176
|
content = response.read
|
|
177
|
+
|
|
69
178
|
if destination_path
|
|
70
179
|
File.binwrite(destination_path, content)
|
|
71
180
|
destination_path
|
|
@@ -77,78 +186,90 @@ module Telegem
|
|
|
77
186
|
end
|
|
78
187
|
end
|
|
79
188
|
end
|
|
80
|
-
|
|
189
|
+
|
|
81
190
|
def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
|
|
82
|
-
params = {
|
|
191
|
+
params = {
|
|
192
|
+
timeout: timeout,
|
|
193
|
+
limit: limit
|
|
194
|
+
}
|
|
195
|
+
|
|
83
196
|
params[:offset] = offset if offset
|
|
84
197
|
params[:allowed_updates] = allowed_updates if allowed_updates
|
|
85
|
-
|
|
198
|
+
|
|
199
|
+
call("getUpdates", params)
|
|
86
200
|
end
|
|
87
|
-
|
|
201
|
+
|
|
88
202
|
def close
|
|
89
203
|
@client.close
|
|
90
204
|
end
|
|
91
|
-
|
|
205
|
+
|
|
92
206
|
private
|
|
93
|
-
|
|
94
|
-
def with_retry
|
|
207
|
+
|
|
208
|
+
def with_retry
|
|
95
209
|
retries = 0
|
|
210
|
+
|
|
96
211
|
begin
|
|
97
|
-
|
|
212
|
+
yield
|
|
98
213
|
rescue NetworkError, Async::TimeoutError => e
|
|
99
214
|
retries += 1
|
|
215
|
+
|
|
100
216
|
if retries <= @retries
|
|
101
217
|
@logger.warn("API request failed: #{e.message}. Retry #{retries}/#{@retries}") if @logger
|
|
102
|
-
sleep
|
|
218
|
+
sleep(@retry_delay * retries)
|
|
103
219
|
retry
|
|
104
220
|
else
|
|
105
221
|
raise
|
|
106
222
|
end
|
|
107
|
-
rescue APIError
|
|
108
|
-
# Don't retry API errors (bad request, unauthorized, etc.)
|
|
223
|
+
rescue APIError
|
|
109
224
|
raise
|
|
110
225
|
end
|
|
111
226
|
end
|
|
112
|
-
|
|
227
|
+
|
|
113
228
|
def make_request(method, params)
|
|
114
229
|
url = "/bot#{@token}/#{method}"
|
|
115
|
-
|
|
116
|
-
|
|
230
|
+
|
|
231
|
+
@logger.debug("API call #{method}") if @logger
|
|
232
|
+
|
|
117
233
|
response = @client.post(
|
|
118
234
|
url,
|
|
119
|
-
{
|
|
235
|
+
{
|
|
236
|
+
"content-type" => "application/json"
|
|
237
|
+
},
|
|
120
238
|
JSON.dump(params.compact)
|
|
121
239
|
)
|
|
122
|
-
|
|
240
|
+
|
|
123
241
|
handle_response(response)
|
|
124
242
|
end
|
|
125
|
-
|
|
243
|
+
|
|
126
244
|
def handle_response(response)
|
|
127
245
|
json = JSON.parse(response.read)
|
|
128
|
-
|
|
129
|
-
if json
|
|
130
|
-
json[
|
|
246
|
+
|
|
247
|
+
if json["ok"]
|
|
248
|
+
json["result"]
|
|
131
249
|
else
|
|
132
|
-
error_msg = json
|
|
250
|
+
error_msg = json["description"] || "HTTP #{response.status}"
|
|
133
251
|
raise APIError.new(error_msg, response.status)
|
|
134
252
|
end
|
|
135
253
|
end
|
|
136
|
-
|
|
254
|
+
|
|
137
255
|
def file_object?(obj)
|
|
138
|
-
obj.is_a?(File) ||
|
|
139
|
-
|
|
256
|
+
obj.is_a?(File) ||
|
|
257
|
+
obj.is_a?(StringIO) ||
|
|
258
|
+
obj.is_a?(Tempfile) ||
|
|
259
|
+
(obj.is_a?(String) && File.file?(obj))
|
|
140
260
|
end
|
|
141
261
|
end
|
|
142
|
-
|
|
262
|
+
|
|
143
263
|
class APIError < StandardError
|
|
144
264
|
attr_reader :code
|
|
145
|
-
|
|
265
|
+
|
|
146
266
|
def initialize(message, code = nil)
|
|
147
267
|
super(message)
|
|
148
268
|
@code = code
|
|
149
269
|
end
|
|
150
270
|
end
|
|
151
|
-
|
|
152
|
-
class NetworkError < APIError
|
|
271
|
+
|
|
272
|
+
class NetworkError < APIError
|
|
273
|
+
end
|
|
153
274
|
end
|
|
154
|
-
end
|
|
275
|
+
end
|
data/lib/api/types.rb
CHANGED
|
@@ -53,10 +53,7 @@ module Telegem
|
|
|
53
53
|
elsif @_raw_data.key?(camel_key)
|
|
54
54
|
define_singleton_method(name) { @_raw_data[camel_key] }
|
|
55
55
|
else
|
|
56
|
-
define_singleton_method(name)
|
|
57
|
-
raise NoMethodError,
|
|
58
|
-
"undefined method `#{name}' for #{self.class} with keys: #{@_raw_data.keys}"
|
|
59
|
-
end
|
|
56
|
+
define_singleton_method(name) { nil }
|
|
60
57
|
end
|
|
61
58
|
|
|
62
59
|
@_accessors_defined[name] = true
|
|
@@ -91,7 +88,8 @@ module Telegem
|
|
|
91
88
|
can_join_groups can_read_all_group_messages
|
|
92
89
|
supports_inline_queries language_code
|
|
93
90
|
is_premium added_to_attachment_menu
|
|
94
|
-
can_connect_to_business can_manage_bots
|
|
91
|
+
can_connect_to_business can_manage_bots
|
|
92
|
+
supports_guest_queries].freeze
|
|
95
93
|
|
|
96
94
|
def initialize(data)
|
|
97
95
|
super(data)
|
|
@@ -191,7 +189,9 @@ module Telegem
|
|
|
191
189
|
forward_from_message_id forward_signature
|
|
192
190
|
forward_sender_name forward_date reply_to_message
|
|
193
191
|
media_group_id author_signature
|
|
194
|
-
has_protected_content managed_bot_created managed_bot
|
|
192
|
+
has_protected_content managed_bot_created managed_bot
|
|
193
|
+
guest_bot_caller_user guest_bot_caller_chat
|
|
194
|
+
guest_query_id live_photo].freeze
|
|
195
195
|
|
|
196
196
|
def initialize(data)
|
|
197
197
|
super(data)
|
|
@@ -301,6 +301,10 @@ module Telegem
|
|
|
301
301
|
wrap('voice', Voice)
|
|
302
302
|
wrap('video_note', VideoNote)
|
|
303
303
|
wrap('sticker', Sticker)
|
|
304
|
+
wrap('live_photo', LivePhoto)
|
|
305
|
+
|
|
306
|
+
wrap('guest_bot_caller_user', User)
|
|
307
|
+
wrap('guest_bot_caller_chat', Chat)
|
|
304
308
|
|
|
305
309
|
wrap('invoice', Invoice)
|
|
306
310
|
wrap('successful_payment', SuccessfulPayment)
|
|
@@ -409,7 +413,8 @@ module Telegem
|
|
|
409
413
|
edited_channel_post inline_query chosen_inline_result
|
|
410
414
|
callback_query shipping_query pre_checkout_query
|
|
411
415
|
poll poll_answer my_chat_member chat_member
|
|
412
|
-
chat_join_request managed_bot_created managed_bot
|
|
416
|
+
chat_join_request managed_bot_created managed_bot
|
|
417
|
+
guest_message].freeze
|
|
413
418
|
|
|
414
419
|
def initialize(data)
|
|
415
420
|
super(data)
|
|
@@ -438,6 +443,7 @@ module Telegem
|
|
|
438
443
|
return :chat_join_request if chat_join_request
|
|
439
444
|
return :managed_bot_created if managed_bot_created
|
|
440
445
|
return :managed_bot if managed_bot
|
|
446
|
+
return :guest_message if guest_message
|
|
441
447
|
:unknown
|
|
442
448
|
end
|
|
443
449
|
|
|
@@ -463,6 +469,8 @@ module Telegem
|
|
|
463
469
|
chat_join_request.from
|
|
464
470
|
when :managed_bot_created, :managed_bot
|
|
465
471
|
managed_bot_created&.from || managed_bot&.from
|
|
472
|
+
when :guest_message
|
|
473
|
+
guest_message.from
|
|
466
474
|
else
|
|
467
475
|
nil
|
|
468
476
|
end
|
|
@@ -497,6 +505,8 @@ module Telegem
|
|
|
497
505
|
# Bot API 9.6 - Managed Bot Support
|
|
498
506
|
wrap('managed_bot_created', ManagedBotCreated)
|
|
499
507
|
wrap('managed_bot', ManagedBotUpdated)
|
|
508
|
+
# Bot API 10.0 - Guest Message Support
|
|
509
|
+
wrap('guest_message', SentGuestMessage)
|
|
500
510
|
end
|
|
501
511
|
end
|
|
502
512
|
|
|
@@ -546,6 +556,8 @@ module Telegem
|
|
|
546
556
|
# Bot API 9.6 enhancements
|
|
547
557
|
wrap('added_by_user', User)
|
|
548
558
|
wrap('added_by_chat', Chat)
|
|
559
|
+
# Bot API 10.0 - media support
|
|
560
|
+
wrap('media', InputPollOptionMedia)
|
|
549
561
|
end
|
|
550
562
|
end
|
|
551
563
|
|
|
@@ -556,10 +568,19 @@ module Telegem
|
|
|
556
568
|
super(data)
|
|
557
569
|
wrap_array('options', PollOption)
|
|
558
570
|
wrap_array('explanation_entities', MessageEntity)
|
|
571
|
+
# Bot API 10.0 - poll media support
|
|
572
|
+
wrap('media', PollMedia)
|
|
573
|
+
wrap('explanation_media', PollMedia)
|
|
559
574
|
end
|
|
560
575
|
end
|
|
561
576
|
|
|
562
|
-
class ChatPermissions < BaseType
|
|
577
|
+
class ChatPermissions < BaseType
|
|
578
|
+
def initialize(data)
|
|
579
|
+
super(data)
|
|
580
|
+
# Bot API 10.0 - reaction support
|
|
581
|
+
define_accessor(:can_react_to_messages)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
563
584
|
class ChatPhoto < BaseType; end
|
|
564
585
|
class ChatInviteLink < BaseType; end
|
|
565
586
|
|
|
@@ -568,7 +589,13 @@ module Telegem
|
|
|
568
589
|
class ChatMemberOwner < ChatMember; end
|
|
569
590
|
class ChatMemberAdministrator < ChatMember; end
|
|
570
591
|
class ChatMemberMember < ChatMember; end
|
|
571
|
-
class ChatMemberRestricted < ChatMember
|
|
592
|
+
class ChatMemberRestricted < ChatMember
|
|
593
|
+
def initialize(data)
|
|
594
|
+
super(data)
|
|
595
|
+
# Bot API 10.0 - reaction support
|
|
596
|
+
define_accessor(:can_react_to_messages)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
572
599
|
class ChatMemberLeft < ChatMember; end
|
|
573
600
|
class ChatMemberBanned < ChatMember; end
|
|
574
601
|
|
|
@@ -699,5 +726,29 @@ module Telegem
|
|
|
699
726
|
class ManagedBotCreated < BaseType; end
|
|
700
727
|
class ManagedBotDeleted < BaseType; end
|
|
701
728
|
class ManagedBotUpdated < BaseType; end
|
|
729
|
+
|
|
730
|
+
# Bot API 10.0 - Guest Mode Support
|
|
731
|
+
class SentGuestMessage < BaseType; end
|
|
732
|
+
|
|
733
|
+
# Bot API 10.0 - Poll Media Support
|
|
734
|
+
class PollMedia < BaseType; end
|
|
735
|
+
class InputPollMedia < BaseType; end
|
|
736
|
+
class InputPollOptionMedia < BaseType; end
|
|
737
|
+
|
|
738
|
+
# Bot API 10.0 - Live Photo Support
|
|
739
|
+
class LivePhoto < BaseType
|
|
740
|
+
def initialize(data)
|
|
741
|
+
super(data)
|
|
742
|
+
wrap('photo', PhotoSize)
|
|
743
|
+
wrap('video', Video)
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
class InputMediaLivePhoto < BaseType; end
|
|
748
|
+
class PaidMediaLivePhoto < BaseType; end
|
|
749
|
+
class InputPaidMediaLivePhoto < BaseType; end
|
|
750
|
+
|
|
751
|
+
# Bot API 10.0 - Business Access Settings
|
|
752
|
+
class BotAccessSettings < BaseType; end
|
|
702
753
|
end
|
|
703
754
|
end
|
data/lib/core/context.rb
CHANGED
|
@@ -124,6 +124,62 @@ module Telegem
|
|
|
124
124
|
params = { chat_id: chat.id, text: text }.merge(options)
|
|
125
125
|
@bot.api.call('sendMessage', params)
|
|
126
126
|
end
|
|
127
|
+
|
|
128
|
+
def reply_rich(rich_message, **options)
|
|
129
|
+
return nil unless chat
|
|
130
|
+
|
|
131
|
+
params = { chat_id: chat.id, rich_message: rich_message }.merge(options)
|
|
132
|
+
@bot.api.call('sendRichMessage', params)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Draft streaming support (Bot API 10.1)
|
|
136
|
+
def start_draft(initial_text = "", **options)
|
|
137
|
+
return nil unless chat
|
|
138
|
+
|
|
139
|
+
draft_id = "draft_#{chat.id}_#{Time.now.to_i}_#{rand(1000)}"
|
|
140
|
+
session[:telegem_draft_id] = draft_id
|
|
141
|
+
|
|
142
|
+
rich_message = { blocks: [{ type: "paragraph", content: initial_text }] }
|
|
143
|
+
params = { chat_id: chat.id, draft_id: draft_id, rich_message: rich_message }.merge(options)
|
|
144
|
+
@bot.api.call('sendRichMessageDraft', params)
|
|
145
|
+
|
|
146
|
+
draft_id
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def append_to_draft(draft_id = nil, content, **options)
|
|
150
|
+
return nil unless chat
|
|
151
|
+
|
|
152
|
+
draft_id ||= session[:telegem_draft_id]
|
|
153
|
+
return nil unless draft_id
|
|
154
|
+
|
|
155
|
+
rich_message = { blocks: [{ type: "paragraph", content: content }] }
|
|
156
|
+
params = { chat_id: chat.id, draft_id: draft_id, rich_message: rich_message }.merge(options)
|
|
157
|
+
@bot.api.call('sendRichMessageDraft', params)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def publish_draft(draft_id = nil, **options)
|
|
161
|
+
return nil unless chat
|
|
162
|
+
|
|
163
|
+
draft_id ||= session[:telegem_draft_id]
|
|
164
|
+
return nil unless draft_id
|
|
165
|
+
|
|
166
|
+
params = { chat_id: chat.id, draft_id: draft_id }.merge(options)
|
|
167
|
+
result = @bot.api.call('publishRichMessageDraft', params)
|
|
168
|
+
session.delete(:telegem_draft_id)
|
|
169
|
+
result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def cancel_draft(draft_id = nil, **options)
|
|
173
|
+
return nil unless chat
|
|
174
|
+
|
|
175
|
+
draft_id ||= session[:telegem_draft_id]
|
|
176
|
+
return nil unless draft_id
|
|
177
|
+
|
|
178
|
+
params = { chat_id: chat.id, draft_id: draft_id }.merge(options)
|
|
179
|
+
result = @bot.api.call('cancelRichMessageDraft', params)
|
|
180
|
+
session.delete(:telegem_draft_id)
|
|
181
|
+
result
|
|
182
|
+
end
|
|
127
183
|
|
|
128
184
|
def edit_message_text(text, **options)
|
|
129
185
|
return nil unless message && chat
|
data/lib/core/rate_limit.rb
CHANGED
|
@@ -63,7 +63,6 @@ module Telegem
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def increment_counters(ctx)
|
|
66
|
-
now = Time.now.to_i
|
|
67
66
|
if @options[:global]
|
|
68
67
|
key = "global"
|
|
69
68
|
@counters[:global].increment(key, 1, ttl: @options[:global][:per])
|
|
@@ -80,8 +79,12 @@ module Telegem
|
|
|
80
79
|
end
|
|
81
80
|
end
|
|
82
81
|
def rate_limit_response(ctx)
|
|
83
|
-
|
|
82
|
+
begin
|
|
83
|
+
ctx.reply("⏳ Please wait a moment before sending another request.")
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
ctx.logger&.warn("Failed to send rate limit response: #{e.class}: #{e.message}")
|
|
86
|
+
end
|
|
84
87
|
nil
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
|
-
end
|
|
90
|
+
end
|
data/lib/telegem.rb
CHANGED
data/lib/webhook/server.rb
CHANGED
|
@@ -104,28 +104,38 @@ module Telegem
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def handle_request(request)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
begin
|
|
108
|
+
case request.path
|
|
109
|
+
when @secret_token, "/#{@secret_token}"
|
|
110
|
+
handle_webhook_request(request)
|
|
111
|
+
when '/health', '/healthz'
|
|
112
|
+
health_endpoint(request)
|
|
113
|
+
else
|
|
114
|
+
[404, {}, ["Not Found"]]
|
|
115
|
+
end
|
|
116
|
+
rescue => e
|
|
117
|
+
@logger.error("Request handler error: #{e.class} - #{e.message}\n#{e.backtrace.join("\n")}")
|
|
118
|
+
[500, {}, ["Internal Server Error"]]
|
|
114
119
|
end
|
|
115
120
|
end
|
|
116
121
|
|
|
117
122
|
def handle_webhook_request(request)
|
|
118
123
|
return [405, {}, ["Method Not Allowed"]] unless request.post?
|
|
124
|
+
|
|
119
125
|
received = request.headers['X-Telegram-Bot-Api-Secret-Token'] ||
|
|
120
|
-
|
|
121
|
-
return [403, {}, ["Forbidden"]] unless received
|
|
126
|
+
request.headers['x-telegram-bot-api-secret-token']
|
|
127
|
+
return [403, {}, ["Forbidden"]] unless received == @secret_token
|
|
122
128
|
|
|
123
129
|
begin
|
|
124
130
|
body = request.body.read
|
|
125
|
-
update_data = JSON.parse(body)
|
|
126
|
-
|
|
131
|
+
update_data = json_to_symbols(JSON.parse(body))
|
|
132
|
+
process_webhook_update(update_data)
|
|
127
133
|
[200, {}, ["OK"]]
|
|
128
|
-
rescue
|
|
134
|
+
rescue JSON::ParserError => e
|
|
135
|
+
@logger.error("JSON parse error: #{e}")
|
|
136
|
+
[400, {}, ["Bad Request"]]
|
|
137
|
+
rescue => e
|
|
138
|
+
@logger.error("Webhook handler error: #{e.class} - #{e.message}")
|
|
129
139
|
[500, {}, ["Internal Server Error"]]
|
|
130
140
|
end
|
|
131
141
|
end
|
|
@@ -137,11 +147,12 @@ module Telegem
|
|
|
137
147
|
end
|
|
138
148
|
|
|
139
149
|
def health_endpoint(request)
|
|
140
|
-
|
|
150
|
+
body = {
|
|
141
151
|
status: 'ok',
|
|
142
152
|
mode: @ssl_mode.to_s,
|
|
143
153
|
ssl: @ssl_mode != :none
|
|
144
|
-
}.to_json
|
|
154
|
+
}.to_json
|
|
155
|
+
[200, { 'Content-Type' => 'application/json' }, [body]]
|
|
145
156
|
end
|
|
146
157
|
|
|
147
158
|
def stop
|
|
@@ -183,6 +194,17 @@ module Telegem
|
|
|
183
194
|
def running?
|
|
184
195
|
@running
|
|
185
196
|
end
|
|
197
|
+
|
|
198
|
+
def json_to_symbols(obj)
|
|
199
|
+
case obj
|
|
200
|
+
when Hash
|
|
201
|
+
obj.transform_keys { |k| k.to_sym }.transform_values { |v| json_to_symbols(v) }
|
|
202
|
+
when Array
|
|
203
|
+
obj.map { |v| json_to_symbols(v) }
|
|
204
|
+
else
|
|
205
|
+
obj
|
|
206
|
+
end
|
|
207
|
+
end
|
|
186
208
|
end
|
|
187
209
|
end
|
|
188
210
|
end
|