telegem 0.1.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 +7 -0
- data/lib/api/client.rb +156 -0
- data/lib/api/types.rb +190 -0
- data/lib/core/bot.rb +275 -0
- data/lib/core/composer.rb +40 -0
- data/lib/core/context.rb +395 -0
- data/lib/core/scene.rb +81 -0
- data/lib/markup/keyboard.rb +294 -0
- data/lib/session/memory_store.rb +49 -0
- data/lib/session/middleware.rb +53 -0
- data/telegem.rb +55 -0
- data/version.rb +4 -0
- data/webhook/server.rb +86 -0
- metadata +123 -0
data/lib/core/context.rb
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
module Telegem
|
|
2
|
+
module Core
|
|
3
|
+
class Context
|
|
4
|
+
attr_accessor :update, :bot, :state, :match, :session, :scene
|
|
5
|
+
|
|
6
|
+
def initialize(update, bot)
|
|
7
|
+
@update = update
|
|
8
|
+
@bot = bot
|
|
9
|
+
@state = {}
|
|
10
|
+
@session = {}
|
|
11
|
+
@match = nil
|
|
12
|
+
@scene = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Message shortcuts
|
|
16
|
+
def message
|
|
17
|
+
@update.message
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def callback_query
|
|
21
|
+
@update.callback_query
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inline_query
|
|
25
|
+
@update.inline_query
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Entity shortcuts
|
|
29
|
+
def from
|
|
30
|
+
message&.from || callback_query&.from || inline_query&.from
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def chat
|
|
34
|
+
message&.chat || callback_query&.message&.chat
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def data
|
|
38
|
+
callback_query&.data
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def query
|
|
42
|
+
inline_query&.query
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Action methods
|
|
46
|
+
def reply(text, **options)
|
|
47
|
+
Async do
|
|
48
|
+
params = { chat_id: chat.id, text: text }.merge(options)
|
|
49
|
+
await @bot.api.call('sendMessage', params)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def edit_message_text(text, **options)
|
|
54
|
+
return Async::Task.new(nil) unless message
|
|
55
|
+
|
|
56
|
+
Async do
|
|
57
|
+
params = {
|
|
58
|
+
chat_id: chat.id,
|
|
59
|
+
message_id: message.message_id,
|
|
60
|
+
text: text
|
|
61
|
+
}.merge(options)
|
|
62
|
+
|
|
63
|
+
await @bot.api.call('editMessageText', params)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def delete_message(message_id = nil)
|
|
68
|
+
mid = message_id || message&.message_id
|
|
69
|
+
return Async::Task.new(nil) unless mid && chat
|
|
70
|
+
|
|
71
|
+
Async do
|
|
72
|
+
await @bot.api.call('deleteMessage', chat_id: chat.id, message_id: mid)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def answer_callback_query(text: nil, show_alert: false, **options)
|
|
77
|
+
return Async::Task.new(nil) unless callback_query
|
|
78
|
+
|
|
79
|
+
Async do
|
|
80
|
+
params = {
|
|
81
|
+
callback_query_id: callback_query.id,
|
|
82
|
+
show_alert: show_alert
|
|
83
|
+
}.merge(options)
|
|
84
|
+
|
|
85
|
+
params[:text] = text if text
|
|
86
|
+
await @bot.api.call('answerCallbackQuery', params)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def answer_inline_query(results, **options)
|
|
91
|
+
return Async::Task.new(nil) unless inline_query
|
|
92
|
+
|
|
93
|
+
Async do
|
|
94
|
+
params = {
|
|
95
|
+
inline_query_id: inline_query.id,
|
|
96
|
+
results: results.to_json
|
|
97
|
+
}.merge(options)
|
|
98
|
+
|
|
99
|
+
await @bot.api.call('answerInlineQuery', params)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Media methods
|
|
104
|
+
def photo(photo, caption: nil, **options)
|
|
105
|
+
Async do
|
|
106
|
+
params = { chat_id: chat.id, caption: caption }.merge(options)
|
|
107
|
+
|
|
108
|
+
if file_object?(photo)
|
|
109
|
+
await @bot.api.upload('sendPhoto', params.merge(photo: photo))
|
|
110
|
+
else
|
|
111
|
+
await @bot.api.call('sendPhoto', params.merge(photo: photo))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def document(document, caption: nil, **options)
|
|
117
|
+
Async do
|
|
118
|
+
params = { chat_id: chat.id, caption: caption }.merge(options)
|
|
119
|
+
|
|
120
|
+
if file_object?(document)
|
|
121
|
+
await @bot.api.upload('sendDocument', params.merge(document: document))
|
|
122
|
+
else
|
|
123
|
+
await @bot.api.call('sendDocument', params.merge(document: document))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def audio(audio, caption: nil, **options)
|
|
129
|
+
Async do
|
|
130
|
+
params = { chat_id: chat.id, caption: caption }.merge(options)
|
|
131
|
+
|
|
132
|
+
if file_object?(audio)
|
|
133
|
+
await @bot.api.upload('sendAudio', params.merge(audio: audio))
|
|
134
|
+
else
|
|
135
|
+
await @bot.api.call('sendAudio', params.merge(audio: audio))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def video(video, caption: nil, **options)
|
|
141
|
+
Async do
|
|
142
|
+
params = { chat_id: chat.id, caption: caption }.merge(options)
|
|
143
|
+
|
|
144
|
+
if file_object?(video)
|
|
145
|
+
await @bot.api.upload('sendVideo', params.merge(video: video))
|
|
146
|
+
else
|
|
147
|
+
await @bot.api.call('sendVideo', params.merge(video: video))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def voice(voice, caption: nil, **options)
|
|
153
|
+
Async do
|
|
154
|
+
params = { chat_id: chat.id, caption: caption }.merge(options)
|
|
155
|
+
|
|
156
|
+
if file_object?(voice)
|
|
157
|
+
await @bot.api.upload('sendVoice', params.merge(voice: voice))
|
|
158
|
+
else
|
|
159
|
+
await @bot.api.call('sendVoice', params.merge(voice: voice))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def sticker(sticker, **options)
|
|
165
|
+
Async do
|
|
166
|
+
params = { chat_id: chat.id, sticker: sticker }.merge(options)
|
|
167
|
+
await @bot.api.call('sendSticker', params)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def location(latitude, longitude, **options)
|
|
172
|
+
Async do
|
|
173
|
+
params = {
|
|
174
|
+
chat_id: chat.id,
|
|
175
|
+
latitude: latitude,
|
|
176
|
+
longitude: longitude
|
|
177
|
+
}.merge(options)
|
|
178
|
+
|
|
179
|
+
await @bot.api.call('sendLocation', params)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def send_chat_action(action, **options)
|
|
184
|
+
Async do
|
|
185
|
+
params = { chat_id: chat.id, action: action }.merge(options)
|
|
186
|
+
await @bot.api.call('sendChatAction', params)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def forward_message(from_chat_id, message_id, **options)
|
|
191
|
+
Async do
|
|
192
|
+
params = {
|
|
193
|
+
chat_id: chat.id,
|
|
194
|
+
from_chat_id: from_chat_id,
|
|
195
|
+
message_id: message_id
|
|
196
|
+
}.merge(options)
|
|
197
|
+
|
|
198
|
+
await @bot.api.call('forwardMessage', params)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def pin_message(message_id, **options)
|
|
203
|
+
Async do
|
|
204
|
+
params = { chat_id: chat.id, message_id: message_id }.merge(options)
|
|
205
|
+
await @bot.api.call('pinChatMessage', params)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def unpin_message(**options)
|
|
210
|
+
Async do
|
|
211
|
+
params = { chat_id: chat.id }.merge(options)
|
|
212
|
+
await @bot.api.call('unpinChatMessage', params)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def kick_chat_member(user_id, **options)
|
|
217
|
+
Async do
|
|
218
|
+
params = { chat_id: chat.id, user_id: user_id }.merge(options)
|
|
219
|
+
await @bot.api.call('kickChatMember', params)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def ban_chat_member(user_id, **options)
|
|
224
|
+
Async do
|
|
225
|
+
params = { chat_id: chat.id, user_id: user_id }.merge(options)
|
|
226
|
+
await @bot.api.call('banChatMember', params)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def unban_chat_member(user_id, **options)
|
|
231
|
+
Async do
|
|
232
|
+
params = { chat_id: chat.id, user_id: user_id }.merge(options)
|
|
233
|
+
await @bot.api.call('unbanChatMember', params)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def get_chat_administrators(**options)
|
|
238
|
+
Async do
|
|
239
|
+
params = { chat_id: chat.id }.merge(options)
|
|
240
|
+
await @bot.api.call('getChatAdministrators', params)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def get_chat_members_count(**options)
|
|
245
|
+
Async do
|
|
246
|
+
params = { chat_id: chat.id }.merge(options)
|
|
247
|
+
await @bot.api.call('getChatMembersCount', params)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def get_chat(**options)
|
|
252
|
+
Async do
|
|
253
|
+
params = { chat_id: chat.id }.merge(options)
|
|
254
|
+
await @bot.api.call('getChat', params)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Keyboard helpers
|
|
259
|
+
def keyboard(&block)
|
|
260
|
+
require 'lib/markup/keyboard'
|
|
261
|
+
Telegem::Markup.keyboard(&block)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def inline_keyboard(&block)
|
|
265
|
+
require 'telegem/markup/keyboard'
|
|
266
|
+
Telegem::Markup.inline(&block)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def reply_with_keyboard(text, keyboard_markup, **options)
|
|
270
|
+
Async do
|
|
271
|
+
reply_markup = keyboard_markup.is_a?(Hash) ? keyboard_markup : keyboard_markup.to_h
|
|
272
|
+
await reply(text, reply_markup: reply_markup, **options)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def reply_with_inline_keyboard(text, inline_markup, **options)
|
|
277
|
+
Async do
|
|
278
|
+
reply_markup = inline_markup.is_a?(Hash) ? inline_markup : inline_markup.to_h
|
|
279
|
+
await reply(text, reply_markup: reply_markup, **options)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def remove_keyboard(text = nil, **options)
|
|
284
|
+
Async do
|
|
285
|
+
reply_markup = Telegem::Markup.remove(**options.slice(:selective))
|
|
286
|
+
if text
|
|
287
|
+
await reply(text, reply_markup: reply_markup, **options.except(:selective))
|
|
288
|
+
else
|
|
289
|
+
reply_markup
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def edit_message_reply_markup(reply_markup, **options)
|
|
295
|
+
return Async::Task.new(nil) unless message
|
|
296
|
+
|
|
297
|
+
Async do
|
|
298
|
+
params = {
|
|
299
|
+
chat_id: chat.id,
|
|
300
|
+
message_id: message.message_id,
|
|
301
|
+
reply_markup: reply_markup
|
|
302
|
+
}.merge(options)
|
|
303
|
+
|
|
304
|
+
await @bot.api.call('editMessageReplyMarkup', params)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Chat action shortcuts
|
|
309
|
+
def typing(**options)
|
|
310
|
+
send_chat_action('typing', **options)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def uploading_photo(**options)
|
|
314
|
+
send_chat_action('upload_photo', **options)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def uploading_video(**options)
|
|
318
|
+
send_chat_action('upload_video', **options)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def uploading_audio(**options)
|
|
322
|
+
send_chat_action('upload_audio', **options)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def uploading_document(**options)
|
|
326
|
+
send_chat_action('upload_document', **options)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def with_typing(&block)
|
|
330
|
+
Async do
|
|
331
|
+
await typing
|
|
332
|
+
result = block.call
|
|
333
|
+
result.is_a?(Async::Task) ? await(result) : result
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Command detection
|
|
338
|
+
def command?
|
|
339
|
+
message&.command? || false
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def command_args
|
|
343
|
+
message&.command_args if command?
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Scene management
|
|
347
|
+
def enter_scene(scene_name, **options)
|
|
348
|
+
Async do
|
|
349
|
+
@scene = scene_name
|
|
350
|
+
if @bot.scenes[scene_name]
|
|
351
|
+
await @bot.scenes[scene_name].enter(self, **options)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def leave_scene(**options)
|
|
357
|
+
Async do
|
|
358
|
+
return unless @scene
|
|
359
|
+
if @bot.scenes[@scene]
|
|
360
|
+
await @bot.scenes[@scene].leave(self, **options)
|
|
361
|
+
end
|
|
362
|
+
@scene = nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def current_scene
|
|
367
|
+
@bot.scenes[@scene] if @scene
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Utilities
|
|
371
|
+
def logger
|
|
372
|
+
@bot.logger
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def raw_update
|
|
376
|
+
@update._raw_data
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def api
|
|
380
|
+
@bot.api
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def user_id
|
|
384
|
+
from&.id
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
389
|
+
def file_object?(obj)
|
|
390
|
+
obj.is_a?(File) || obj.is_a?(StringIO) || obj.is_a?(Tempfile) ||
|
|
391
|
+
(obj.is_a?(String) && File.exist?(obj))
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
data/lib/core/scene.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Telegem
|
|
2
|
+
module Core
|
|
3
|
+
class Scene
|
|
4
|
+
attr_reader :id, :steps, :enter_callbacks, :leave_callbacks
|
|
5
|
+
|
|
6
|
+
def initialize(id, default_step: :start, &block)
|
|
7
|
+
@id = id
|
|
8
|
+
@steps = {}
|
|
9
|
+
@enter_callbacks = []
|
|
10
|
+
@leave_callbacks = []
|
|
11
|
+
@default_step = default_step
|
|
12
|
+
instance_eval(&block) if block_given?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def step(name, &action)
|
|
16
|
+
@steps[name.to_sym] = action
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_enter(&callback)
|
|
21
|
+
@enter_callbacks << callback
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_leave(&callback)
|
|
26
|
+
@leave_callbacks << callback
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enter(ctx, step_name = nil)
|
|
31
|
+
Async do
|
|
32
|
+
step_name ||= @default_step
|
|
33
|
+
ctx.scene = { id: @id, step: step_name.to_sym }
|
|
34
|
+
@enter_callbacks.each { |cb| await(cb.call(ctx)) }
|
|
35
|
+
await process_step(ctx, step_name)
|
|
36
|
+
rescue => e
|
|
37
|
+
ctx.logger.error("Error entering scene #{@id}: #{e.message}")
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def leave(ctx)
|
|
43
|
+
Async do
|
|
44
|
+
@leave_callbacks.each { |cb| await(cb.call(ctx)) }
|
|
45
|
+
ctx.scene = nil
|
|
46
|
+
rescue => e
|
|
47
|
+
ctx.logger.error("Error leaving scene #{@id}: #{e.message}")
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def process_step(ctx, step_name)
|
|
53
|
+
Async do
|
|
54
|
+
step = @steps[step_name.to_sym]
|
|
55
|
+
raise "Unknown step #{step_name} in scene #{@id}" unless step
|
|
56
|
+
|
|
57
|
+
result = step.call(ctx)
|
|
58
|
+
result = await(result) if result.is_a?(Async::Task)
|
|
59
|
+
ctx.scene[:step] = step_name.to_sym
|
|
60
|
+
result
|
|
61
|
+
rescue => e
|
|
62
|
+
ctx.logger.error("Error processing step #{step_name} in scene #{@id}: #{e.message}")
|
|
63
|
+
raise
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def current_step(ctx)
|
|
68
|
+
ctx.scene[:step] if ctx.scene && ctx.scene[:id] == @id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reset(ctx)
|
|
72
|
+
ctx.scene = { id: @id, step: @default_step }
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_s
|
|
77
|
+
"#<Scene #{@id}>"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|