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.
@@ -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