telegem 3.2.4 → 3.3.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/Readme.md +1 -1
- data/bin/telegem-ssl +7 -0
- data/lib/api/client.rb +1 -10
- data/lib/api/types.rb +36 -0
- data/lib/core/bot.rb +23 -17
- data/lib/core/context.rb +9 -7
- data/lib/core/rate_limit.rb +0 -14
- data/lib/core/scene.rb +1 -1
- data/lib/markup/inline.rb +104 -0
- data/lib/markup/keyboard.rb +77 -298
- data/lib/plugins/file_extract.rb +6 -26
- data/lib/session/memory_store.rb +1 -25
- data/lib/telegem.rb +2 -1
- data/lib/webhook/server.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 156ed056e2f5201d26aaa636b1bd2fdace0499dedca7aa52cff1692546480733
|
|
4
|
+
data.tar.gz: b0fef7cc585db195c4eeef5c0d656a659b1d12f8e9ea25506745c9318bc5dca8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a99c675b08fec66843ecc4d41880a74043a0498a486db404049e63eeb523ab746f186c482ce828550e010fa8f4f4ebd1102507e73532fb681c912c48c144417e
|
|
7
|
+
data.tar.gz: 0dc7360428b2d1fcfe383941f54c0b44f75af45c35eb7cb9cfbbfee190445c095bd5fac6007cd2ec885736ec8eea38ca9e286030b9b4486cd97ac4bef353d96c
|
data/Readme.md
CHANGED
data/bin/telegem-ssl
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
# bin/telegem-ssl
|
|
3
3
|
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'yaml'
|
|
5
6
|
|
|
6
7
|
puts "🔐 Telegem SSL Setup"
|
|
7
8
|
puts "=" * 10
|
|
8
9
|
|
|
10
|
+
if File.exist?('.telegem-ssl')
|
|
11
|
+
puts "🚫 SSL config already exists at .telegem-ssl"
|
|
12
|
+
print "Overwrite? (y/n): "
|
|
13
|
+
exit unless gets.chomp.downcase == 'y'
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
# Get domain
|
|
10
17
|
domain = ARGV[0]
|
|
11
18
|
unless domain
|
data/lib/api/client.rb
CHANGED
|
@@ -18,26 +18,19 @@ module Telegem
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def call(method, params = {})
|
|
21
|
-
Async do
|
|
22
21
|
make_request(method, params)
|
|
23
|
-
end.wait
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
def call!(method, params = {}, &callback)
|
|
27
25
|
return unless callback
|
|
28
|
-
|
|
29
|
-
Async do
|
|
30
26
|
begin
|
|
31
27
|
result = make_request(method, params)
|
|
32
28
|
callback.call(result, nil)
|
|
33
29
|
rescue => error
|
|
34
30
|
callback.call(nil, error)
|
|
35
31
|
end
|
|
36
|
-
end
|
|
37
32
|
end
|
|
38
|
-
|
|
39
33
|
def upload(method, params)
|
|
40
|
-
Async do
|
|
41
34
|
url = "/bot#{@token}/#{method}"
|
|
42
35
|
|
|
43
36
|
body = Async::HTTP::Body::Multipart.new
|
|
@@ -52,11 +45,10 @@ module Telegem
|
|
|
52
45
|
|
|
53
46
|
response = @client.post(url, {}, body)
|
|
54
47
|
handle_response(response)
|
|
55
|
-
|
|
48
|
+
|
|
56
49
|
end
|
|
57
50
|
|
|
58
51
|
def download(file_id, destination_path = nil)
|
|
59
|
-
Async do
|
|
60
52
|
file_info = call('getFile', file_id: file_id)
|
|
61
53
|
return nil unless file_info && file_info['file_path']
|
|
62
54
|
|
|
@@ -76,7 +68,6 @@ module Telegem
|
|
|
76
68
|
else
|
|
77
69
|
raise NetworkError.new("Download failed: HTTP #{response.status}")
|
|
78
70
|
end
|
|
79
|
-
end.wait
|
|
80
71
|
end
|
|
81
72
|
|
|
82
73
|
def get_updates(offset: nil, timeout: 30, limit: 100, allowed_updates: nil)
|
data/lib/api/types.rb
CHANGED
|
@@ -294,6 +294,42 @@ module Telegem
|
|
|
294
294
|
u.is_a?(User) ? u : User.new(u)
|
|
295
295
|
end
|
|
296
296
|
end
|
|
297
|
+
|
|
298
|
+
wrap_media_objects
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def wrap_media_objects
|
|
302
|
+
# Media files
|
|
303
|
+
@_raw_data['document'] = BaseType.new(@_raw_data['document']) if @_raw_data['document'] && !@_raw_data['document'].is_a?(BaseType)
|
|
304
|
+
@_raw_data['audio'] = BaseType.new(@_raw_data['audio']) if @_raw_data['audio'] && !@_raw_data['audio'].is_a?(BaseType)
|
|
305
|
+
@_raw_data['video'] = BaseType.new(@_raw_data['video']) if @_raw_data['video'] && !@_raw_data['video'].is_a?(BaseType)
|
|
306
|
+
@_raw_data['voice'] = BaseType.new(@_raw_data['voice']) if @_raw_data['voice'] && !@_raw_data['voice'].is_a?(BaseType)
|
|
307
|
+
@_raw_data['video_note'] = BaseType.new(@_raw_data['video_note']) if @_raw_data['video_note'] && !@_raw_data['video_note'].is_a?(BaseType)
|
|
308
|
+
@_raw_data['sticker'] = BaseType.new(@_raw_data['sticker']) if @_raw_data['sticker'] && !@_raw_data['sticker'].is_a?(BaseType)
|
|
309
|
+
|
|
310
|
+
# Photo array
|
|
311
|
+
if @_raw_data['photo'] && @_raw_data['photo'].is_a?(Array)
|
|
312
|
+
@_raw_data['photo'] = @_raw_data['photo'].map do |p|
|
|
313
|
+
p.is_a?(BaseType) ? p : BaseType.new(p)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Contact, location, venue
|
|
318
|
+
@_raw_data['contact'] = BaseType.new(@_raw_data['contact']) if @_raw_data['contact'] && !@_raw_data['contact'].is_a?(BaseType)
|
|
319
|
+
@_raw_data['location'] = BaseType.new(@_raw_data['location']) if @_raw_data['location'] && !@_raw_data['location'].is_a?(BaseType)
|
|
320
|
+
@_raw_data['venue'] = BaseType.new(@_raw_data['venue']) if @_raw_data['venue'] && !@_raw_data['venue'].is_a?(BaseType)
|
|
321
|
+
|
|
322
|
+
# Payment & other
|
|
323
|
+
@_raw_data['invoice'] = BaseType.new(@_raw_data['invoice']) if @_raw_data['invoice'] && !@_raw_data['invoice'].is_a?(BaseType)
|
|
324
|
+
@_raw_data['successful_payment'] = BaseType.new(@_raw_data['successful_payment']) if @_raw_data['successful_payment'] && !@_raw_data['successful_payment'].is_a?(BaseType)
|
|
325
|
+
@_raw_data['reply_markup'] = BaseType.new(@_raw_data['reply_markup']) if @_raw_data['reply_markup'] && !@_raw_data['reply_markup'].is_a?(BaseType)
|
|
326
|
+
|
|
327
|
+
# Chat photo array
|
|
328
|
+
if @_raw_data['new_chat_photo'] && @_raw_data['new_chat_photo'].is_a?(Array)
|
|
329
|
+
@_raw_data['new_chat_photo'] = @_raw_data['new_chat_photo'].map do |p|
|
|
330
|
+
p.is_a?(BaseType) ? p : BaseType.new(p)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
297
333
|
end
|
|
298
334
|
end
|
|
299
335
|
|
data/lib/core/bot.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
#frozen_string_literal: true
|
|
1
2
|
require 'json'
|
|
2
|
-
|
|
3
|
+
require 'async'
|
|
3
4
|
module Telegem
|
|
4
5
|
module Core
|
|
5
6
|
class Bot
|
|
@@ -135,21 +136,29 @@ module Telegem
|
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
def set_my_profile_photo(photo, **options)
|
|
138
|
-
@api.call('setMyProfilePhoto', {
|
|
139
|
-
|
|
139
|
+
@api.call('setMyProfilePhoto', {
|
|
140
|
+
photo: photo
|
|
141
|
+
}.merge(options))
|
|
142
|
+
end
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
def remove_my_profile_photo
|
|
145
|
+
@api.call('removeMyProfilePhoto', {})
|
|
146
|
+
end
|
|
144
147
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
def get_user_profile_audios(user_id, **options)
|
|
149
|
+
result = @api.call('getUserProfileAudios', {
|
|
150
|
+
user_id: user_id
|
|
151
|
+
}.merge(options))
|
|
152
|
+
return nil unless result && result['audios']
|
|
153
|
+
Types::UserProfileAudios.new(result)
|
|
154
|
+
end
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
def create_forum_topic(chat_id, name, **options)
|
|
157
|
+
@api.call('createForumTopic', {
|
|
158
|
+
chat_id: chat_id,
|
|
159
|
+
name: name
|
|
160
|
+
}.merge(options))
|
|
161
|
+
end
|
|
153
162
|
def location(&block)
|
|
154
163
|
on(:message, location: true) do |ctx|
|
|
155
164
|
block.call(ctx)
|
|
@@ -230,12 +239,9 @@ module Telegem
|
|
|
230
239
|
|
|
231
240
|
if updates && updates.any?
|
|
232
241
|
updates.each do |update_data|
|
|
233
|
-
Async do
|
|
234
242
|
update = Types::Update.new(update_data)
|
|
235
243
|
process_update(update)
|
|
236
244
|
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
245
|
@offset = updates.last['update_id'] + 1
|
|
240
246
|
@logger.debug("Updated offset to: #{@offset}")
|
|
241
247
|
end
|
|
@@ -385,4 +391,4 @@ module Telegem
|
|
|
385
391
|
end
|
|
386
392
|
end
|
|
387
393
|
end
|
|
388
|
-
end
|
|
394
|
+
end
|
data/lib/core/context.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Telegem
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def caption_entities
|
|
74
|
-
message
|
|
74
|
+
message&.caption_entities || []
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def caption
|
|
@@ -401,9 +401,6 @@ module Telegem
|
|
|
401
401
|
def uploading_document(**options)
|
|
402
402
|
send_chat_action('upload_document', **options)
|
|
403
403
|
end
|
|
404
|
-
def scene
|
|
405
|
-
session[:telegem_scene]&.[](:id)
|
|
406
|
-
end
|
|
407
404
|
def ask(question, **options)
|
|
408
405
|
scene_data = session[:telegem_scene]
|
|
409
406
|
if scene_data
|
|
@@ -439,10 +436,15 @@ module Telegem
|
|
|
439
436
|
scene&.next_step(self, step_name)
|
|
440
437
|
end
|
|
441
438
|
def with_typing(&block)
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
thread = Thread.new do
|
|
440
|
+
while @typing_active
|
|
441
|
+
typing
|
|
442
|
+
sleep 5
|
|
443
|
+
end
|
|
444
|
+
end
|
|
444
445
|
result = block.call
|
|
445
|
-
|
|
446
|
+
@typing_active = false
|
|
447
|
+
thread.join
|
|
446
448
|
result
|
|
447
449
|
end
|
|
448
450
|
|
data/lib/core/rate_limit.rb
CHANGED
|
@@ -64,36 +64,22 @@ module Telegem
|
|
|
64
64
|
|
|
65
65
|
def increment_counters(ctx)
|
|
66
66
|
now = Time.now.to_i
|
|
67
|
-
|
|
68
|
-
|
|
69
67
|
if @options[:global]
|
|
70
68
|
key = "global"
|
|
71
|
-
cleanup_counter(:global, key, now)
|
|
72
69
|
@counters[:global].increment(key, 1, ttl: @options[:global][:per])
|
|
73
70
|
end
|
|
74
|
-
|
|
75
|
-
|
|
76
71
|
if @options[:user] && ctx.from&.id
|
|
77
72
|
key = "user:#{ctx.from.id}"
|
|
78
|
-
cleanup_counter(:user, key, now)
|
|
79
73
|
@counters[:user].increment(key, 1, ttl: @options[:user][:per])
|
|
80
74
|
end
|
|
81
75
|
|
|
82
76
|
|
|
83
77
|
if @options[:chat] && ctx.chat&.id
|
|
84
78
|
key = "chat:#{ctx.chat.id}"
|
|
85
|
-
cleanup_counter(:chat, key, now)
|
|
86
79
|
@counters[:chat].increment(key, 1, ttl: @options[:chat][:per])
|
|
87
80
|
end
|
|
88
81
|
end
|
|
89
|
-
|
|
90
|
-
def cleanup_counter(type, key, now)
|
|
91
|
-
expires = @counters[type].get_ttl(key) || now
|
|
92
|
-
@counters[type].delete(key) if now > expires
|
|
93
|
-
end
|
|
94
|
-
|
|
95
82
|
def rate_limit_response(ctx)
|
|
96
|
-
|
|
97
83
|
ctx.reply("⏳ Please wait a moment before sending another request.") rescue nil
|
|
98
84
|
nil
|
|
99
85
|
end
|
data/lib/core/scene.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Telegem
|
|
|
124
124
|
def process_response(ctx, scene_data)
|
|
125
125
|
|
|
126
126
|
current_step_name = scene_data[:step]
|
|
127
|
-
scene_data[:data][current_step_name] = ctx.message
|
|
127
|
+
scene_data[:data][current_step_name] = ctx.message
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
scene_data[:waiting_for_response] = false
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Telegem
|
|
2
|
+
module Markup
|
|
3
|
+
|
|
4
|
+
module InlineButtons
|
|
5
|
+
def callback(text, data, style: nil, icon_custom_emoji_id: nil)
|
|
6
|
+
{
|
|
7
|
+
text: text,
|
|
8
|
+
callback_data: data,
|
|
9
|
+
style: style,
|
|
10
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
11
|
+
}.compact
|
|
12
|
+
end
|
|
13
|
+
def url(text, url, style: nil, icon_custom_emoji_id: nil)
|
|
14
|
+
{
|
|
15
|
+
text: text,
|
|
16
|
+
url: url,
|
|
17
|
+
style: style,
|
|
18
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
19
|
+
}.compact
|
|
20
|
+
end
|
|
21
|
+
def switch_inline(text, query: nil, style: nil, icon_custom_emoji_id: nil)
|
|
22
|
+
{
|
|
23
|
+
text: text,
|
|
24
|
+
switch_inline_query: query,
|
|
25
|
+
style: style,
|
|
26
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
27
|
+
}.compact
|
|
28
|
+
end
|
|
29
|
+
def switch_inline_current_chat(text, query: nil, style: nil, icon_custom_emoji_id: nil)
|
|
30
|
+
{
|
|
31
|
+
text: text,
|
|
32
|
+
switch_inline_query_current_chat: query,
|
|
33
|
+
style: style,
|
|
34
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
def callback_game(text, game_short_name, style: nil, icon_custom_emoji_id: nil)
|
|
38
|
+
{
|
|
39
|
+
text: text,
|
|
40
|
+
callback_game: { short_name: game_short_name },
|
|
41
|
+
style: style,
|
|
42
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
43
|
+
}.compact
|
|
44
|
+
end
|
|
45
|
+
def pay(text, style: nil, icon_custom_emoji_id: nil)
|
|
46
|
+
{
|
|
47
|
+
text: text,
|
|
48
|
+
pay: true,
|
|
49
|
+
style: style,
|
|
50
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
51
|
+
}.compact
|
|
52
|
+
end
|
|
53
|
+
def web_app(text, url: nil, style: nil, icon_custom_emoji_id: nil)
|
|
54
|
+
{
|
|
55
|
+
text: text,
|
|
56
|
+
web_app: { url: url },
|
|
57
|
+
style: style,
|
|
58
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
59
|
+
}.compact
|
|
60
|
+
end
|
|
61
|
+
def login(text, url, style: nil, icon_custom_emoji_id: nil, **options)
|
|
62
|
+
login_url = { url: url}.merge(options)
|
|
63
|
+
{
|
|
64
|
+
text: text,
|
|
65
|
+
login_url: login_url,
|
|
66
|
+
style: style,
|
|
67
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
68
|
+
}.compact
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
class InlineBuilder
|
|
72
|
+
include InlineButtons
|
|
73
|
+
def initialize
|
|
74
|
+
@rows = []
|
|
75
|
+
end
|
|
76
|
+
def row(*buttons)
|
|
77
|
+
@rows << buttons
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
def build
|
|
81
|
+
InlineKeyboard.new(@rows)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
class InlineKeyboard
|
|
85
|
+
attr_reader :rows
|
|
86
|
+
def initialize(rows)
|
|
87
|
+
@rows = rows
|
|
88
|
+
end
|
|
89
|
+
def to_h
|
|
90
|
+
{
|
|
91
|
+
inline_keyboard: @rows
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
def to_json(*args)
|
|
95
|
+
to_h.to_json(*args)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
def self.inline(&block)
|
|
99
|
+
builder = InlineBuilder.new
|
|
100
|
+
builder.instance_eval(&block) if block_given?
|
|
101
|
+
builder.build
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/markup/keyboard.rb
CHANGED
|
@@ -1,321 +1,100 @@
|
|
|
1
1
|
module Telegem
|
|
2
2
|
module Markup
|
|
3
|
-
class Keyboard
|
|
4
|
-
attr_reader :buttons, :options
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
module ReplyButtons
|
|
5
|
+
def text(content, style: nil, icon_custom_emoji_id: nil)
|
|
6
|
+
{
|
|
7
|
+
text: content,
|
|
8
|
+
icon_custom_emoji_id: icon_custom_emoji_id,
|
|
9
|
+
style: style
|
|
10
|
+
}.compact
|
|
11
|
+
end
|
|
12
|
+
def request_contact(text, style: nil, icon_custom_emoji_id: nil)
|
|
13
|
+
{
|
|
14
|
+
text: text,
|
|
15
|
+
style: style,
|
|
16
|
+
icon_custom_emoji_id: icon_custom_emoji_id,
|
|
17
|
+
request_contact: true
|
|
18
|
+
}.compact
|
|
19
|
+
end
|
|
20
|
+
def request_location(text, style: nil, icon_custom_emoji_id: nil)
|
|
21
|
+
{
|
|
22
|
+
text: text,
|
|
23
|
+
style: style,
|
|
24
|
+
icon_custom_emoji_id: icon_custom_emoji_id,
|
|
25
|
+
request_location: true
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
def request_poll(text, poll_type: nil, style: nil, icon_custom_emoji_id: nil)
|
|
29
|
+
{
|
|
30
|
+
text: text,
|
|
31
|
+
style: style,
|
|
32
|
+
icon_custom_emoji_id: icon_custom_emoji_id,
|
|
33
|
+
request_poll: poll_type ? { type: poll_type } : {}
|
|
34
|
+
}.compact
|
|
35
|
+
end
|
|
36
|
+
def web_app(text, url: nil, style: nil, icon_custom_emoji_id: nil)
|
|
37
|
+
{
|
|
38
|
+
text: text,
|
|
39
|
+
url: url,
|
|
40
|
+
style: style,
|
|
41
|
+
icon_custom_emoji_id: icon_custom_emoji_id
|
|
42
|
+
}.compact
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
class ReplyBuilder
|
|
46
|
+
include ReplyButtons
|
|
47
|
+
def initialize
|
|
48
|
+
@rows = []
|
|
8
49
|
@options = {
|
|
9
50
|
resize_keyboard: true,
|
|
10
51
|
one_time_keyboard: false,
|
|
11
52
|
selective: false
|
|
12
|
-
}.merge(options)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.[](*rows)
|
|
16
|
-
new(rows)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.build(&block)
|
|
20
|
-
builder = Builder.new
|
|
21
|
-
builder.instance_eval(&block) if block_given?
|
|
22
|
-
builder.keyboard
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def row(*buttons)
|
|
26
|
-
@buttons << buttons.flatten
|
|
27
|
-
self
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def button(text, style: nil, icon_custom_emoji_id: nil, **options)
|
|
31
|
-
btn = {
|
|
32
|
-
text: text
|
|
33
|
-
}.merge(options)
|
|
34
|
-
btn[:style] = style if style
|
|
35
|
-
btn[:icon_custom_emoji_id] = icon_custom_emoji_id if icon_custom_emoji_id
|
|
36
|
-
if @buttons.empty? || @buttons.last.is_a?(Array)
|
|
37
|
-
@buttons << [btn]
|
|
38
|
-
else
|
|
39
|
-
@buttons.last << btn
|
|
40
|
-
end
|
|
41
|
-
self
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def request_contact(text)
|
|
45
|
-
button(text, request_contact: true)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def request_location(text)
|
|
49
|
-
button(text, request_location: true)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def request_poll(text, type = nil)
|
|
53
|
-
opts = type ? { request_poll: { type: type } } : { request_poll: {} }
|
|
54
|
-
button(text, opts)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def resize(resize = true)
|
|
58
|
-
@options[:resize_keyboard] = resize
|
|
59
|
-
self
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def one_time(one_time = true)
|
|
63
|
-
@options[:one_time_keyboard] = one_time
|
|
64
|
-
self
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def selective(selective = true)
|
|
68
|
-
@options[:selective] = selective
|
|
69
|
-
self
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def to_h
|
|
73
|
-
{
|
|
74
|
-
keyboard: @buttons.map { |row| row.is_a?(Array) ? row : [row] },
|
|
75
|
-
**@options
|
|
76
53
|
}
|
|
77
54
|
end
|
|
78
|
-
|
|
79
|
-
def to_json(*args)
|
|
80
|
-
to_h.to_json(*args)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.remove(selective: false)
|
|
84
|
-
{
|
|
85
|
-
remove_keyboard: true,
|
|
86
|
-
selective: selective
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def self.force_reply(selective: false, input_field_placeholder: nil)
|
|
91
|
-
markup = {
|
|
92
|
-
force_reply: true,
|
|
93
|
-
selective: selective
|
|
94
|
-
}
|
|
95
|
-
markup[:input_field_placeholder] = input_field_placeholder if input_field_placeholder
|
|
96
|
-
markup
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
class InlineKeyboard
|
|
101
|
-
attr_reader :buttons
|
|
102
|
-
|
|
103
|
-
def initialize(buttons = [])
|
|
104
|
-
@buttons = buttons
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def self.[](*rows)
|
|
108
|
-
new(rows)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def self.build(&block)
|
|
112
|
-
builder = InlineBuilder.new
|
|
113
|
-
builder.instance_eval(&block) if block_given?
|
|
114
|
-
builder.keyboard
|
|
115
|
-
end
|
|
116
|
-
|
|
117
55
|
def row(*buttons)
|
|
118
|
-
@
|
|
56
|
+
@rows << buttons
|
|
119
57
|
self
|
|
120
58
|
end
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
btn = {
|
|
124
|
-
text: text
|
|
125
|
-
}.merge(options)
|
|
126
|
-
btn[:style] = style if style
|
|
127
|
-
btn[:icon_custom_emoji_id] = icon_custom_emoji_id if icon_custom_emoji_id
|
|
128
|
-
if @buttons.empty? || @buttons.last.is_a?(Array)
|
|
129
|
-
@buttons << [btn]
|
|
130
|
-
else
|
|
131
|
-
@buttons.last << btn
|
|
132
|
-
end
|
|
133
|
-
self
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def url(text, url)
|
|
137
|
-
button(text, url: url)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def callback(text, data)
|
|
141
|
-
button(text, callback_data: data)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def web_app(text, url)
|
|
145
|
-
button(text, web_app: { url: url })
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def login(text, url, **options)
|
|
149
|
-
button(text, login_url: { url: url, **options })
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def switch_inline(text, query = "")
|
|
153
|
-
button(text, switch_inline_query: query)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def switch_inline_current(text, query = "")
|
|
157
|
-
button(text, switch_inline_query_current_chat: query)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def pay(text)
|
|
161
|
-
button(text, pay: true)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def to_h
|
|
165
|
-
clean_rows = @buttons.compact.map do |row|
|
|
166
|
-
row = Array(row).compact.select { |btn| is_a?(Hash) }
|
|
167
|
-
row.empty? ? nil : row
|
|
168
|
-
end.compact
|
|
169
|
-
{ inline_keyboard: clean_rows}
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def to_json(*args)
|
|
173
|
-
to_h.to_json(*args)
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
class Builder
|
|
178
|
-
attr_reader :keyboard
|
|
179
|
-
|
|
180
|
-
def initialize
|
|
181
|
-
@keyboard = Keyboard.new
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def row(*buttons, &block)
|
|
185
|
-
if block_given?
|
|
186
|
-
sub_builder = Builder.new
|
|
187
|
-
sub_builder.instance_eval(&block)
|
|
188
|
-
@keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
|
|
189
|
-
elsif buttons.any?
|
|
190
|
-
@keyboard.row(*buttons)
|
|
191
|
-
else
|
|
192
|
-
@keyboard.row
|
|
193
|
-
end
|
|
59
|
+
def resize(value = true)
|
|
60
|
+
@options[:resize_keyboard] = value
|
|
194
61
|
self
|
|
195
62
|
end
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
@keyboard.button(text, **options)
|
|
63
|
+
def one_time(value = true)
|
|
64
|
+
@options[:one_time_keyboard] = value
|
|
199
65
|
self
|
|
200
66
|
end
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@keyboard.request_contact(text)
|
|
67
|
+
def selective(value = true)
|
|
68
|
+
@options[:selective] = value
|
|
204
69
|
self
|
|
205
70
|
end
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@keyboard.request_location(text)
|
|
71
|
+
def placeholder(text)
|
|
72
|
+
@options[:input_field_placeholder] = text
|
|
209
73
|
self
|
|
210
74
|
end
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
@keyboard.request_poll(text, type)
|
|
214
|
-
self
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def method_missing(name, *args, &block)
|
|
218
|
-
if @keyboard && @keyboard.respond_to?(name)
|
|
219
|
-
@keyboard.send(name, *args, &block)
|
|
220
|
-
else
|
|
221
|
-
super
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def respond_to_missing?(name, include_private = false)
|
|
226
|
-
@keyboard && @keyboard.respond_to?(name) || super
|
|
227
|
-
end
|
|
75
|
+
def build
|
|
76
|
+
ReplyKeyboard.new(@rows, @options)
|
|
228
77
|
end
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@keyboard = InlineKeyboard.new
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def row(*buttons, &block)
|
|
238
|
-
if block_given?
|
|
239
|
-
sub_builder = InlineBuilder.new
|
|
240
|
-
sub_builder.instance_eval(&block)
|
|
241
|
-
@keyboard.row(*sub_builder.keyboard.buttons.flatten(1))
|
|
242
|
-
elsif buttons.any?
|
|
243
|
-
@keyboard.row(*buttons)
|
|
244
|
-
else
|
|
245
|
-
@keyboard.row([])
|
|
78
|
+
end
|
|
79
|
+
class ReplyKeyboard
|
|
80
|
+
def initialize(rows, options = {})
|
|
81
|
+
@rows = rows
|
|
82
|
+
@options = options
|
|
246
83
|
end
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
@keyboard.button(text, **options)
|
|
252
|
-
self
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def url(text, url)
|
|
256
|
-
@keyboard.url(text, url)
|
|
257
|
-
self
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def callback(text, data)
|
|
261
|
-
@keyboard.callback(text, data)
|
|
262
|
-
self
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def web_app(text, url)
|
|
266
|
-
@keyboard.web_app(text, url)
|
|
267
|
-
self
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def login(text, url, **options)
|
|
271
|
-
@keyboard.login(text, url, **options)
|
|
272
|
-
self
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def switch_inline(text, query = "")
|
|
276
|
-
@keyboard.switch_inline(text, query)
|
|
277
|
-
self
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def switch_inline_current(text, query = "")
|
|
281
|
-
@keyboard.switch_inline_current(text, query)
|
|
282
|
-
self
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def pay(text)
|
|
286
|
-
@keyboard.pay(text)
|
|
287
|
-
self
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def method_missing(name, *args, &block)
|
|
291
|
-
if @keyboard && @keyboard.respond_to?(name)
|
|
292
|
-
@keyboard.send(name, *args, &block)
|
|
293
|
-
else
|
|
294
|
-
super
|
|
84
|
+
def to_h
|
|
85
|
+
{
|
|
86
|
+
keyboard: @rows
|
|
87
|
+
}.merge(@options)
|
|
295
88
|
end
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def inline(&block)
|
|
309
|
-
InlineKeyboard.build(&block)
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
def remove(**options)
|
|
313
|
-
Keyboard.remove(**options)
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def force_reply(**options)
|
|
317
|
-
Keyboard.force_reply(**options)
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
end
|
|
89
|
+
def to_json(*args)
|
|
90
|
+
to_h.to_json(*args)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
def self.keyboard(&block)
|
|
94
|
+
builder = ReplyBuilder.new
|
|
95
|
+
builder.instance_eval(&block) if block_given?
|
|
96
|
+
builder.build
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
data/lib/plugins/file_extract.rb
CHANGED
|
@@ -85,28 +85,8 @@ module Telegem
|
|
|
85
85
|
success: false,
|
|
86
86
|
error: "Failed to extract PDF: #{e.message}"
|
|
87
87
|
}
|
|
88
|
-
rescue LoadError
|
|
89
|
-
{
|
|
90
|
-
success: false,
|
|
91
|
-
error: "PDF extraction requires the 'pdf-reader' gem. Please add it to your Gemfile."
|
|
92
|
-
}
|
|
93
|
-
rescue PDF::Reader::MalformedPDFError => e
|
|
94
|
-
{
|
|
95
|
-
success: false,
|
|
96
|
-
error: "Malformed PDF: #{e.message}"
|
|
97
|
-
}
|
|
98
|
-
rescue PDF::Reader::UnsupportedFeatureError => e
|
|
99
|
-
{
|
|
100
|
-
success: false,
|
|
101
|
-
error: "Unsupported PDF feature: #{e.message}"
|
|
102
|
-
}
|
|
103
|
-
rescue PDF::Reader::EncryptedPDFError => e
|
|
104
|
-
{
|
|
105
|
-
success: false,
|
|
106
|
-
error: "Encrypted PDF: #{e.message}"
|
|
107
|
-
}
|
|
108
88
|
ensure
|
|
109
|
-
cleanup if @options[:auto_delete]
|
|
89
|
+
cleanup if @options[:auto_delete]
|
|
110
90
|
end
|
|
111
91
|
end
|
|
112
92
|
def extract_json
|
|
@@ -196,8 +176,8 @@ module Telegem
|
|
|
196
176
|
def cleanup
|
|
197
177
|
@temp_file.unlink if @temp_file
|
|
198
178
|
@temp_file = nil
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/session/memory_store.rb
CHANGED
|
@@ -5,7 +5,6 @@ module Telegem
|
|
|
5
5
|
def initialize
|
|
6
6
|
@store = {}
|
|
7
7
|
@ttls = {}
|
|
8
|
-
@mutex = Mutex.new
|
|
9
8
|
@default_ttl = 300 # 5 minutes
|
|
10
9
|
@cleanup_interval = 60 # Clean expired every minute
|
|
11
10
|
@last_cleanup = Time.now
|
|
@@ -13,18 +12,15 @@ module Telegem
|
|
|
13
12
|
|
|
14
13
|
# Store with optional TTL
|
|
15
14
|
def set(key, value, ttl: nil)
|
|
16
|
-
@mutex.synchronize do
|
|
17
15
|
auto_cleanup
|
|
18
16
|
key_s = key.to_s
|
|
19
17
|
@store[key_s] = value
|
|
20
18
|
@ttls[key_s] = Time.now + (ttl || @default_ttl)
|
|
21
19
|
value
|
|
22
20
|
end
|
|
23
|
-
end
|
|
24
21
|
|
|
25
22
|
# Get value if not expired
|
|
26
23
|
def get(key)
|
|
27
|
-
@mutex.synchronize do
|
|
28
24
|
key_s = key.to_s
|
|
29
25
|
return nil unless @store.key?(key_s)
|
|
30
26
|
|
|
@@ -36,37 +32,30 @@ module Telegem
|
|
|
36
32
|
|
|
37
33
|
@store[key_s]
|
|
38
34
|
end
|
|
39
|
-
end
|
|
40
35
|
|
|
41
36
|
# Check if key exists and not expired
|
|
42
37
|
def exist?(key)
|
|
43
|
-
@mutex.synchronize do
|
|
44
38
|
key_s = key.to_s
|
|
45
39
|
return false unless @store.key?(key_s)
|
|
46
40
|
!expired?(key_s)
|
|
47
41
|
end
|
|
48
|
-
end
|
|
49
42
|
|
|
50
43
|
# Delete key
|
|
51
44
|
def delete(key)
|
|
52
|
-
@mutex.synchronize do
|
|
53
45
|
key_s = key.to_s
|
|
54
46
|
@store.delete(key_s)
|
|
55
47
|
@ttls.delete(key_s)
|
|
56
48
|
true
|
|
57
49
|
end
|
|
58
|
-
end
|
|
59
50
|
|
|
60
51
|
# Increment counter (for rate limiting)
|
|
61
52
|
def increment(key, amount = 1, ttl: nil)
|
|
62
|
-
@mutex.synchronize do
|
|
63
53
|
key_s = key.to_s
|
|
64
54
|
current = get(key_s) || 0
|
|
65
55
|
new_value = current + amount
|
|
66
56
|
set(key_s, new_value, ttl: ttl)
|
|
67
57
|
new_value
|
|
68
58
|
end
|
|
69
|
-
end
|
|
70
59
|
|
|
71
60
|
# Decrement counter
|
|
72
61
|
def decrement(key, amount = 1)
|
|
@@ -75,7 +64,6 @@ module Telegem
|
|
|
75
64
|
|
|
76
65
|
# Clear expired entries (auto-called)
|
|
77
66
|
def cleanup
|
|
78
|
-
@mutex.synchronize do
|
|
79
67
|
now = Time.now
|
|
80
68
|
@ttls.each do |key, expires|
|
|
81
69
|
if now > expires
|
|
@@ -84,25 +72,20 @@ module Telegem
|
|
|
84
72
|
end
|
|
85
73
|
end
|
|
86
74
|
@last_cleanup = now
|
|
87
|
-
end
|
|
88
|
-
end
|
|
75
|
+
end
|
|
89
76
|
|
|
90
77
|
# Clear everything
|
|
91
78
|
def clear
|
|
92
|
-
@mutex.synchronize do
|
|
93
79
|
@store.clear
|
|
94
80
|
@ttls.clear
|
|
95
81
|
@last_cleanup = Time.now
|
|
96
82
|
end
|
|
97
|
-
end
|
|
98
83
|
|
|
99
84
|
# Get all keys (non-expired)
|
|
100
85
|
def keys
|
|
101
|
-
@mutex.synchronize do
|
|
102
86
|
auto_cleanup
|
|
103
87
|
@store.keys.select { |k| !expired?(k) }
|
|
104
88
|
end
|
|
105
|
-
end
|
|
106
89
|
|
|
107
90
|
# Get size (non-expired entries)
|
|
108
91
|
def size
|
|
@@ -115,34 +98,28 @@ module Telegem
|
|
|
115
98
|
|
|
116
99
|
# Get TTL remaining in seconds
|
|
117
100
|
def ttl(key)
|
|
118
|
-
@mutex.synchronize do
|
|
119
101
|
key_s = key.to_s
|
|
120
102
|
return -1 unless @ttls[key_s]
|
|
121
103
|
|
|
122
104
|
remaining = @ttls[key_s] - Time.now
|
|
123
105
|
remaining > 0 ? remaining.ceil : -1
|
|
124
106
|
end
|
|
125
|
-
end
|
|
126
107
|
|
|
127
108
|
# Set TTL for existing key
|
|
128
109
|
def expire(key, ttl)
|
|
129
|
-
@mutex.synchronize do
|
|
130
110
|
key_s = key.to_s
|
|
131
111
|
return false unless @store.key?(key_s)
|
|
132
112
|
|
|
133
113
|
@ttls[key_s] = Time.now + ttl
|
|
134
114
|
true
|
|
135
115
|
end
|
|
136
|
-
end
|
|
137
116
|
|
|
138
117
|
# Redis-like scan for pattern matching
|
|
139
118
|
def scan(pattern = "*", count: 10)
|
|
140
|
-
@mutex.synchronize do
|
|
141
119
|
auto_cleanup
|
|
142
120
|
regex = pattern_to_regex(pattern)
|
|
143
121
|
matching_keys = @store.keys.select { |k| k.match?(regex) && !expired?(k) }
|
|
144
122
|
matching_keys.first(count)
|
|
145
|
-
end
|
|
146
123
|
end
|
|
147
124
|
|
|
148
125
|
private
|
|
@@ -158,7 +135,6 @@ module Telegem
|
|
|
158
135
|
end
|
|
159
136
|
|
|
160
137
|
def pattern_to_regex(pattern)
|
|
161
|
-
# Convert Redis-style pattern to Ruby regex
|
|
162
138
|
regex_str = pattern.gsub('*', '.*').gsub('?', '.')
|
|
163
139
|
Regexp.new("^#{regex_str}$")
|
|
164
140
|
end
|
data/lib/telegem.rb
CHANGED
|
@@ -3,7 +3,7 @@ require 'logger'
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module Telegem
|
|
6
|
-
VERSION = "3.
|
|
6
|
+
VERSION = "3.3.0".freeze
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
#
|
|
@@ -16,6 +16,7 @@ require_relative 'core/scene'
|
|
|
16
16
|
require_relative 'session/middleware'
|
|
17
17
|
require_relative 'session/memory_store'
|
|
18
18
|
require_relative 'markup/keyboard'
|
|
19
|
+
require_relative 'markup/inline'
|
|
19
20
|
|
|
20
21
|
require_relative 'plugins/file_extract'
|
|
21
22
|
require_relative 'session/scene_middleware'
|
data/lib/webhook/server.rb
CHANGED
|
@@ -120,7 +120,7 @@ module Telegem
|
|
|
120
120
|
begin
|
|
121
121
|
body = request.body.read
|
|
122
122
|
update_data = JSON.parse(body)
|
|
123
|
-
|
|
123
|
+
process_webhook_update(update_data)
|
|
124
124
|
[200, {}, ["OK"]]
|
|
125
125
|
rescue
|
|
126
126
|
[500, {}, ["Internal Server Error"]]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: telegem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- sick_phantom
|
|
@@ -156,6 +156,7 @@ files:
|
|
|
156
156
|
- lib/core/rate_limit.rb
|
|
157
157
|
- lib/core/scene.rb
|
|
158
158
|
- lib/markup/.gitkeep
|
|
159
|
+
- lib/markup/inline.rb
|
|
159
160
|
- lib/markup/keyboard.rb
|
|
160
161
|
- lib/plugins/.gitkeep
|
|
161
162
|
- lib/plugins/file_extract.rb
|
|
@@ -176,7 +177,7 @@ metadata:
|
|
|
176
177
|
bug_tracker_uri: https://gitlab.com/ruby-telegem/telegem/-/issues
|
|
177
178
|
documentation_uri: https://gitlab.com/ruby-telegem/telegem/-/tree/main/docs-src?ref_type=heads
|
|
178
179
|
rubygems_mfa_required: 'false'
|
|
179
|
-
post_install_message: "Thanks for installing Telegem 3.
|
|
180
|
+
post_install_message: "Thanks for installing Telegem 3.3.0!\n\n\U0001F4DA Documentation:
|
|
180
181
|
https://gitlab.com/ruby-telegem/telegem\n\n\U0001F510 For SSL Webhooks:\nRun: telegem-ssl
|
|
181
182
|
your-domain.com\nThis sets up Let's Encrypt certificates automatically.\n\n\U0001F916
|
|
182
183
|
Happy bot building!\n"
|
|
@@ -194,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
194
195
|
- !ruby/object:Gem::Version
|
|
195
196
|
version: '0'
|
|
196
197
|
requirements: []
|
|
197
|
-
rubygems_version:
|
|
198
|
+
rubygems_version: 4.0.6
|
|
198
199
|
specification_version: 4
|
|
199
200
|
summary: Modern, fast Telegram Bot Framework for Ruby
|
|
200
201
|
test_files: []
|