gruubY 0.2.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +6 -0
  4. data/Rakefile +51 -0
  5. data/config/config.rb +5 -0
  6. data/example/bot.rb +160 -0
  7. data/example/generate_user_session.rb +81 -0
  8. data/example/ntgcalls.rb +80 -0
  9. data/example/tdlib.rb +77 -0
  10. data/example/userbot.rb +133 -0
  11. data/lib/grubY/api.rb +363 -0
  12. data/lib/grubY/async.rb +13 -0
  13. data/lib/grubY/bot.rb +90 -0
  14. data/lib/grubY/bound.rb +14 -0
  15. data/lib/grubY/client.rb +296 -0
  16. data/lib/grubY/context.rb +242 -0
  17. data/lib/grubY/dispatcher.rb +60 -0
  18. data/lib/grubY/enums.rb +62 -0
  19. data/lib/grubY/exception.rb +5 -0
  20. data/lib/grubY/file_stream.rb +17 -0
  21. data/lib/grubY/filters.rb +292 -0
  22. data/lib/grubY/group_manager.rb +213 -0
  23. data/lib/grubY/handlers.rb +170 -0
  24. data/lib/grubY/keyboard.rb +49 -0
  25. data/lib/grubY/media.rb +38 -0
  26. data/lib/grubY/middleware.rb +18 -0
  27. data/lib/grubY/ntgcalls/native.rb +100 -0
  28. data/lib/grubY/ntgcalls.rb +478 -0
  29. data/lib/grubY/plugin.rb +14 -0
  30. data/lib/grubY/raw.rb +25 -0
  31. data/lib/grubY/raw_types.rb +57 -0
  32. data/lib/grubY/retry.rb +14 -0
  33. data/lib/grubY/server.rb +18 -0
  34. data/lib/grubY/session.rb +30 -0
  35. data/lib/grubY/tdlib/client.rb +565 -0
  36. data/lib/grubY/tdlib/client_manager.rb +37 -0
  37. data/lib/grubY/tdlib/decorators.rb +17 -0
  38. data/lib/grubY/tdlib/errors.rb +6 -0
  39. data/lib/grubY/tdlib/group_manager.rb +58 -0
  40. data/lib/grubY/tdlib/native.rb +72 -0
  41. data/lib/grubY/tdlib/schema_builder.rb +237 -0
  42. data/lib/grubY/tdlib/tdjson.rb +49 -0
  43. data/lib/grubY/tdlib/user_session.rb +69 -0
  44. data/lib/grubY/types/base_object.rb +72 -0
  45. data/lib/grubY/types/bound_entities.rb +245 -0
  46. data/lib/grubY/types/chat.rb +93 -0
  47. data/lib/grubY/types/chat_info.rb +31 -0
  48. data/lib/grubY/types/extra.rb +251 -0
  49. data/lib/grubY/types/message.rb +295 -0
  50. data/lib/grubY/types/message_entity.rb +14 -0
  51. data/lib/grubY/types/registry.rb +67 -0
  52. data/lib/grubY/types/user.rb +31 -0
  53. data/lib/grubY/webapp.rb +22 -0
  54. data/lib/grubY.rb +41 -0
  55. data/plugins/logger.rb +7 -0
  56. data/plugins/media_tools.rb +13 -0
  57. metadata +110 -0
data/lib/grubY/api.rb ADDED
@@ -0,0 +1,363 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+ require_relative "retry"
5
+
6
+ module GrubY
7
+ class API
8
+ class Error < StandardError; end
9
+ METHOD_ALIASES = {
10
+ "set_bot_commands" => "setMyCommands",
11
+ "get_bot_commands" => "getMyCommands",
12
+ "delete_bot_commands" => "deleteMyCommands",
13
+ "set_bot_name" => "setMyName",
14
+ "get_bot_name" => "getMyName",
15
+ "set_bot_info_description" => "setMyDescription",
16
+ "get_bot_info_description" => "getMyDescription",
17
+ "set_bot_info_short_description" => "setMyShortDescription",
18
+ "get_bot_info_short_description" => "getMyShortDescription",
19
+ "set_bot_default_privileges" => "setMyDefaultAdministratorRights",
20
+ "get_bot_default_privileges" => "getMyDefaultAdministratorRights",
21
+ "get_chat_members_count" => "getChatMemberCount",
22
+ "get_chat_photos" => "getUserProfilePhotos",
23
+ "get_chat_audios" => "getUserProfileAudios",
24
+ "set_emoji_status" => "setUserEmojiStatus",
25
+ "get_managed_bot_token" => "getManagedBotToken",
26
+ "replace_managed_bot_token" => "replaceManagedBotToken",
27
+ "ban_chat_member" => "banChatMember",
28
+ "unban_chat_member" => "unbanChatMember",
29
+ "restrict_chat_member" => "restrictChatMember",
30
+ "promote_chat_member" => "promoteChatMember",
31
+ "set_administrator_title" => "setChatAdministratorCustomTitle",
32
+ "set_chat_member_tag" => "setChatMemberTag",
33
+ "set_chat_menu_button" => "setChatMenuButton",
34
+ "get_chat_menu_button" => "getChatMenuButton",
35
+ "send_reaction" => "setMessageReaction",
36
+ "set_message_reaction" => "setMessageReaction",
37
+ "export_chat_invite_link" => "exportChatInviteLink",
38
+ "create_chat_invite_link" => "createChatInviteLink",
39
+ "edit_chat_invite_link" => "editChatInviteLink",
40
+ "revoke_chat_invite_link" => "revokeChatInviteLink",
41
+ "approve_chat_join_request" => "approveChatJoinRequest",
42
+ "decline_chat_join_request" => "declineChatJoinRequest",
43
+ "send_message_draft" => "sendMessageDraft",
44
+ "send_paid_reaction" => "sendPaidReaction",
45
+ "get_user_chat_boosts" => "getUserChatBoosts",
46
+ "get_business_connection" => "getBusinessConnection",
47
+ "forward_media_group" => "forwardMessages",
48
+ "copy_media_group" => "copyMessages"
49
+ }.freeze
50
+
51
+ def initialize(token)
52
+ @token = token
53
+ @base = URI("https://api.telegram.org/bot#{token}/")
54
+ end
55
+
56
+ def request(method, params = {})
57
+ payload = raw(method, params)
58
+ payload["result"]
59
+ end
60
+
61
+ def raw(method, params = {})
62
+ uri = @base + method.to_s
63
+ response = Retry.call { Net::HTTP.post_form(uri, serialize_params(params)) }
64
+ parse_response(response)
65
+ end
66
+
67
+ def get_updates(offset = nil, timeout: 30, **opts)
68
+ params = { timeout: timeout }.merge(opts)
69
+ params[:offset] = offset if offset
70
+ request("getUpdates", params)
71
+ end
72
+
73
+ def get_file(file_id)
74
+ request("getFile", { file_id: file_id })
75
+ end
76
+
77
+ def download_file(file_path, save_as)
78
+ url = URI("https://api.telegram.org/file/bot#{@token}/#{file_path}")
79
+ File.open(save_as, "wb") do |file|
80
+ file.write(Net::HTTP.get(url))
81
+ end
82
+ save_as
83
+ end
84
+
85
+ def send_message(chat_id, text, **opts)
86
+ request("sendMessage", { chat_id: chat_id, text: text }.merge(opts))
87
+ end
88
+
89
+ def forward_message(chat_id, from_chat_id, message_id, **opts)
90
+ request("forwardMessage", {
91
+ chat_id: chat_id,
92
+ from_chat_id: from_chat_id,
93
+ message_id: message_id
94
+ }.merge(opts))
95
+ end
96
+
97
+ def forward_messages(chat_id, from_chat_id, message_ids, **opts)
98
+ request("forwardMessages", {
99
+ chat_id: chat_id,
100
+ from_chat_id: from_chat_id,
101
+ message_ids: message_ids
102
+ }.merge(opts))
103
+ end
104
+
105
+ def forward_media_group(chat_id, from_chat_id, message_ids, **opts)
106
+ forward_messages(chat_id, from_chat_id, message_ids, **opts)
107
+ end
108
+
109
+ def copy_message(chat_id, from_chat_id, message_id, **opts)
110
+ request("copyMessage", {
111
+ chat_id: chat_id,
112
+ from_chat_id: from_chat_id,
113
+ message_id: message_id
114
+ }.merge(opts))
115
+ end
116
+
117
+ def copy_messages(chat_id, from_chat_id, message_ids, **opts)
118
+ request("copyMessages", {
119
+ chat_id: chat_id,
120
+ from_chat_id: from_chat_id,
121
+ message_ids: message_ids
122
+ }.merge(opts))
123
+ end
124
+
125
+ def copy_media_group(chat_id, from_chat_id, message_ids, **opts)
126
+ copy_messages(chat_id, from_chat_id, message_ids, **opts)
127
+ end
128
+
129
+ def send_photo(chat_id, photo, **opts)
130
+ request("sendPhoto", { chat_id: chat_id, photo: photo }.merge(opts))
131
+ end
132
+
133
+ def send_audio(chat_id, audio, **opts)
134
+ request("sendAudio", { chat_id: chat_id, audio: audio }.merge(opts))
135
+ end
136
+
137
+ def send_document(chat_id, document, **opts)
138
+ request("sendDocument", { chat_id: chat_id, document: document }.merge(opts))
139
+ end
140
+
141
+ def send_video(chat_id, video, **opts)
142
+ request("sendVideo", { chat_id: chat_id, video: video }.merge(opts))
143
+ end
144
+
145
+ def send_animation(chat_id, animation, **opts)
146
+ request("sendAnimation", { chat_id: chat_id, animation: animation }.merge(opts))
147
+ end
148
+
149
+ def send_voice(chat_id, voice, **opts)
150
+ request("sendVoice", { chat_id: chat_id, voice: voice }.merge(opts))
151
+ end
152
+
153
+ def send_video_note(chat_id, video_note, **opts)
154
+ request("sendVideoNote", { chat_id: chat_id, video_note: video_note }.merge(opts))
155
+ end
156
+
157
+ def send_sticker(chat_id, sticker, **opts)
158
+ request("sendSticker", { chat_id: chat_id, sticker: sticker }.merge(opts))
159
+ end
160
+
161
+ def send_cached_media(chat_id, file_id, **opts)
162
+ request("sendDocument", { chat_id: chat_id, document: file_id }.merge(opts))
163
+ end
164
+
165
+ def send_screenshot_notification(chat_id, **opts)
166
+ request("sendScreenshotNotification", { chat_id: chat_id }.merge(opts))
167
+ end
168
+
169
+ def send_paid_media(chat_id, star_count, media, **opts)
170
+ request("sendPaidMedia", {
171
+ chat_id: chat_id,
172
+ star_count: star_count,
173
+ media: media
174
+ }.merge(opts))
175
+ end
176
+
177
+ def send_media_group(chat_id, media, **opts)
178
+ request("sendMediaGroup", { chat_id: chat_id, media: media }.merge(opts))
179
+ end
180
+
181
+ def send_location(chat_id, latitude, longitude, **opts)
182
+ request("sendLocation", {
183
+ chat_id: chat_id,
184
+ latitude: latitude,
185
+ longitude: longitude
186
+ }.merge(opts))
187
+ end
188
+
189
+ def send_venue(chat_id, latitude, longitude, title, address, **opts)
190
+ request("sendVenue", {
191
+ chat_id: chat_id,
192
+ latitude: latitude,
193
+ longitude: longitude,
194
+ title: title,
195
+ address: address
196
+ }.merge(opts))
197
+ end
198
+
199
+ def send_contact(chat_id, phone_number, first_name, **opts)
200
+ request("sendContact", {
201
+ chat_id: chat_id,
202
+ phone_number: phone_number,
203
+ first_name: first_name
204
+ }.merge(opts))
205
+ end
206
+
207
+ def send_poll(chat_id, question, options, **opts)
208
+ request("sendPoll", {
209
+ chat_id: chat_id,
210
+ question: question,
211
+ options: options
212
+ }.merge(opts))
213
+ end
214
+
215
+ def send_checklist(chat_id, checklist, business_connection_id:, **opts)
216
+ request("sendChecklist", {
217
+ business_connection_id: business_connection_id,
218
+ chat_id: chat_id,
219
+ checklist: checklist
220
+ }.merge(opts))
221
+ end
222
+
223
+ def get_web_app_link_url(**opts)
224
+ request("getWebAppLinkUrl", opts)
225
+ end
226
+
227
+ def get_web_app_url(**opts)
228
+ request("getWebAppUrl", opts)
229
+ end
230
+
231
+ def open_web_app(**opts)
232
+ request("openWebApp", opts)
233
+ end
234
+
235
+ def send_dice(chat_id, **opts)
236
+ request("sendDice", { chat_id: chat_id }.merge(opts))
237
+ end
238
+
239
+ def send_message_draft(chat_id, draft_id, text, **opts)
240
+ request("sendMessageDraft", {
241
+ chat_id: chat_id,
242
+ draft_id: draft_id,
243
+ text: text
244
+ }.merge(opts))
245
+ end
246
+
247
+ def send_chat_action(chat_id, action, **opts)
248
+ request("sendChatAction", { chat_id: chat_id, action: action }.merge(opts))
249
+ end
250
+
251
+ def set_message_reaction(chat_id, message_id, **opts)
252
+ request("setMessageReaction", {
253
+ chat_id: chat_id,
254
+ message_id: message_id
255
+ }.merge(opts))
256
+ end
257
+
258
+ def send_reaction(chat_id, message_id, **opts)
259
+ set_message_reaction(chat_id, message_id, **opts)
260
+ end
261
+
262
+ def answer_callback_query(callback_query_id, **opts)
263
+ request("answerCallbackQuery", {
264
+ callback_query_id: callback_query_id
265
+ }.merge(opts))
266
+ end
267
+
268
+ def answer_callback(callback_query_id, **opts)
269
+ answer_callback_query(callback_query_id, **opts)
270
+ end
271
+
272
+ def edit_message_text(chat_id, message_id, text, **opts)
273
+ request("editMessageText", {
274
+ chat_id: chat_id,
275
+ message_id: message_id,
276
+ text: text
277
+ }.merge(opts))
278
+ end
279
+
280
+ def delete_message(chat_id, message_id)
281
+ request("deleteMessage", {
282
+ chat_id: chat_id,
283
+ message_id: message_id
284
+ })
285
+ end
286
+
287
+ def delete_messages(chat_id, message_ids, **opts)
288
+ request("deleteMessages", {
289
+ chat_id: chat_id,
290
+ message_ids: message_ids
291
+ }.merge(opts))
292
+ end
293
+
294
+ def method_missing(name, *args, **kwargs, &block)
295
+ return super if block
296
+
297
+ params = {}
298
+ if args.length == 1 && args.first.is_a?(Hash)
299
+ params.merge!(args.first)
300
+ elsif !args.empty?
301
+ return super
302
+ end
303
+ params.merge!(kwargs) unless kwargs.empty?
304
+
305
+ request(self.class.camelize_api_method(name), params)
306
+ end
307
+
308
+ def respond_to_missing?(_name, _include_private = false)
309
+ true
310
+ end
311
+
312
+ def self.escape_markdown_v2(text)
313
+ text.to_s.gsub(/([_\*\[\]\(\)~`>#+\-=|{}.!\\])/, '\\\\\1')
314
+ end
315
+
316
+ def self.escape_html(text)
317
+ text.to_s
318
+ .gsub("&", "&amp;")
319
+ .gsub("<", "&lt;")
320
+ .gsub(">", "&gt;")
321
+ .gsub('"', "&quot;")
322
+ end
323
+
324
+ def self.camelize_api_method(name)
325
+ alias_name = METHOD_ALIASES[name.to_s]
326
+ return alias_name if alias_name
327
+
328
+ parts = name.to_s.split("_")
329
+ return name.to_s if parts.empty?
330
+
331
+ parts.first + parts[1..].map(&:capitalize).join
332
+ end
333
+
334
+ private
335
+
336
+ def serialize_params(params)
337
+ params.each_with_object({}) do |(key, value), out|
338
+ next if value.nil?
339
+
340
+ out[key.to_s] = serialize_value(value)
341
+ end
342
+ end
343
+
344
+ def serialize_value(value)
345
+ case value
346
+ when Hash, Array
347
+ JSON.generate(value)
348
+ else
349
+ value
350
+ end
351
+ end
352
+
353
+ def parse_response(response)
354
+ payload = JSON.parse(response.body)
355
+ return payload if payload["ok"]
356
+
357
+ message = payload["description"] || "Unknown Telegram API error"
358
+ raise Error, message
359
+ rescue JSON::ParserError
360
+ raise Error, "Invalid Telegram API response (HTTP #{response.code})"
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,13 @@
1
+ module GrubY
2
+ class Async
3
+ def self.run(&block)
4
+ Thread.new do
5
+ begin
6
+ block.call
7
+ rescue => e
8
+ puts "[ERROR] #{e}"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/grubY/bot.rb ADDED
@@ -0,0 +1,90 @@
1
+ require_relative "api"
2
+ require_relative "dispatcher"
3
+ require_relative "middleware"
4
+ require_relative "context"
5
+ require_relative "plugin"
6
+ require_relative "async"
7
+ require_relative "session"
8
+
9
+ module GrubY
10
+ class Bot
11
+ attr_reader :api, :session
12
+
13
+ def initialize(token, session: Session.new)
14
+ @api = API.new(token)
15
+ @dispatcher = Dispatcher.new
16
+ @middleware = Middleware.new
17
+ @session = session
18
+ end
19
+
20
+ def on(event, filter=nil, group: 0, &block)
21
+ @dispatcher.on(event, filter, group: group, &block)
22
+ end
23
+
24
+ def remove_handler(event, handler_id = nil, &block)
25
+ @dispatcher.off(event, handler_id, &block)
26
+ end
27
+
28
+ def use(&block)
29
+ @middleware.use(&block)
30
+ end
31
+
32
+ def use_plugin(path)
33
+ Plugin.load(self, path)
34
+ end
35
+
36
+ def process_update(update)
37
+ Async.run do
38
+ ctx = Context.new(update, @api, @session)
39
+
40
+ @middleware.run(ctx) do
41
+ @dispatcher.trigger(:raw_update, ctx)
42
+ dispatch_update_events(update, ctx)
43
+ end
44
+ end
45
+ rescue => e
46
+ error_ctx = Context.new({ "error" => e }, @api, @session)
47
+ @dispatcher.trigger(:error, error_ctx)
48
+ warn "[BOT] process_update error: #{e.class}: #{e.message}"
49
+ end
50
+
51
+ def emit(event, payload = {})
52
+ ctx = Context.new(payload, @api, @session)
53
+ @dispatcher.trigger(event, ctx)
54
+ end
55
+
56
+ private
57
+
58
+ def dispatch_update_events(update, ctx)
59
+ mapping = {
60
+ "message" => :message,
61
+ "edited_message" => :edited_message,
62
+ "business_message" => :business_message,
63
+ "edited_business_message" => :edited_business_message,
64
+ "deleted_messages" => :deleted_messages,
65
+ "deleted_business_messages" => :deleted_business_messages,
66
+ "message_reaction_count" => :message_reaction_count,
67
+ "message_reaction" => :message_reaction,
68
+ "business_connection" => :business_connection,
69
+ "story" => :story,
70
+ "callback_query" => :callback_query,
71
+ "chat_boost" => :chat_boost,
72
+ "chat_join_request" => :chat_join_request,
73
+ "chat_member_updated" => :chat_member_updated,
74
+ "chosen_inline_result" => :chosen_inline_result,
75
+ "inline_query" => :inline_query,
76
+ "poll" => :poll,
77
+ "pre_checkout_query" => :pre_checkout_query,
78
+ "purchased_paid_media" => :purchased_paid_media,
79
+ "shipping_query" => :shipping_query,
80
+ "user_status" => :user_status,
81
+ "managed_bot_updated" => :managed_bot
82
+ }
83
+
84
+ mapping.each do |key, event|
85
+ @dispatcher.trigger(event, ctx) if update.key?(key)
86
+ end
87
+ @dispatcher.trigger(:callback, ctx) if update.key?("callback_query")
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ module GrubY
2
+ module Bound
3
+ module_function
4
+
5
+ def bind(ctx)
6
+ {
7
+ send: ->(text) { ctx.reply(text) },
8
+ raw: ->(method, params = {}) { ctx.raw(method, params) },
9
+ inline_keyboard: ->(rows) { GrubY::Keyboard.inline(rows) },
10
+ webapp_button: ->(text, url) { GrubY::WebApp.inline_button(text: text, url: url) }
11
+ }
12
+ end
13
+ end
14
+ end