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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ef2f9709d76322e1eff6824e63ba6476cac004e5c6d47b09e0b73b64b0a090a
4
+ data.tar.gz: 77b279b046e9b8a760e721d5289ac6de30a7b390771e55d79b568a5197be0550
5
+ SHA512:
6
+ metadata.gz: 9c88a5cda9410a8e150afee26b78e9974c38e803d4bcd7fbbd9ab03ce652a358e1cc9b05b64077f5ecca9c0f1573a4c407c2435c949ecc4ecff07f39e813283c
7
+ data.tar.gz: 67fd67fd3a9ffe5056d4e69a59d80e5b8e142ea455835bb96236b54692aa757bf3d11e33901913718db5fa1fa8c0673137bb6d53cd4d73c77b47503257ebd2b9
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sreyanshu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ > GrubY (Unstable)
2
+
3
+ [![Ruby](https://img.shields.io/badge/ruby-3.2.0-red.svg)](https://www.ruby-lang.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ *Maintained by [Sreyanshu](https://github.com/iSreyanshu)*
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require "rake/clean"
2
+
3
+ APP_ROOT = File.expand_path(__dir__)
4
+
5
+ CLEAN.include(*Dir["*.gem"])
6
+
7
+ desc "Build gem package"
8
+ task :build do
9
+ sh "gem build grubY.gemspec"
10
+ end
11
+
12
+ desc "Install built gem locally"
13
+ task install_local: :build do
14
+ gem_file = Dir["grubY-*.gem"].max_by { |f| File.mtime(f) }
15
+ abort "No gem package found" unless gem_file
16
+
17
+ sh "gem install #{gem_file}"
18
+ end
19
+
20
+ desc "Install Ruby dependencies"
21
+ task :bundle_install do
22
+ sh "bundle install"
23
+ end
24
+
25
+ desc "Prepare Ruby dependencies"
26
+ task setup: [:bundle_install]
27
+
28
+ namespace :ci do
29
+ desc "Minimal smoke check for gem load"
30
+ task :smoke do
31
+ ruby "-e", "require_relative 'lib/grubY'; puts 'grubY load ok'"
32
+ end
33
+ end
34
+
35
+ namespace :tdlib do
36
+ desc "Generate tdlib.json in project root (override via SRC, VERSION, COMMIT, OUT env vars)"
37
+ task :generate_json do
38
+ src = ENV.fetch("SRC", "https://raw.githubusercontent.com/tdlib/td/refs/heads/master/td/generate/scheme/td_api.tl")
39
+ version = ENV.fetch("VERSION", "")
40
+ commit = ENV.fetch("COMMIT", "")
41
+ out = ENV.fetch("OUT", File.join(APP_ROOT, "tdlib.json"))
42
+
43
+ ruby "scripts/generate-tdlib-json.rb",
44
+ "--src", src,
45
+ "--version", version,
46
+ "--commit", commit,
47
+ "--out", out
48
+ end
49
+ end
50
+
51
+ task default: :build
data/config/config.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Config
2
+ TOKEN = "7543495331:AAHLYllkmBjf0VjKy4qxLSlA7DBA45htvdQ"
3
+ WEBHOOK_PATH = "/webhook"
4
+ PORT = 4545
5
+ end
data/example/bot.rb ADDED
@@ -0,0 +1,160 @@
1
+ require_relative "../lib/grubY"
2
+ require_relative "../config/config"
3
+
4
+ client = GrubY::Client.new(Config::TOKEN)
5
+ gm = client.group_manager
6
+
7
+ client.use_plugin("../plugins/logger.rb")
8
+
9
+ HELP = <<~TXT
10
+ Commands:
11
+ /start - Boot check
12
+ /help - Show this help message
13
+ /echo <text> - Echo text
14
+ /md <text> - Markdown v2 safe send
15
+ /html <text> - HTML safe send
16
+ /poll - Send sample poll
17
+ /media - Send sample media methods
18
+ /whoami - Show user and chat info
19
+ /warn <user_ID> [reason] - Warn user
20
+ /mute <user_ID> - Mute user
21
+ /unmute <user_ID> - Unmute user
22
+ /ban <user_ID> - Ban user
23
+ /unban <user_ID> - Unban user
24
+ /pin - Pin message
25
+ /unpin - Unpin latest pinned message
26
+ /lock - Lock group
27
+ /unlock - Unlock group
28
+ /reaction - Set reaction on replied message
29
+ TXT
30
+
31
+ client.on(:message, GrubY::Filters.command("start")) do |ctx|
32
+ ctx.reply("GrubY Client started! Use /help for help message.")
33
+ end
34
+
35
+ client.on(:message, GrubY::Filters.command("help")) do |ctx|
36
+ ctx.reply(HELP)
37
+ end
38
+
39
+ client.on(:message, GrubY::Filters.command("echo")) do |ctx|
40
+ text = ctx.command_args.join(" ")
41
+ ctx.reply(text.empty? ? "Usage: /echo good morning" : text)
42
+ end
43
+
44
+ client.on(:message, GrubY::Filters.command("md")) do |ctx|
45
+ text = ctx.command_args.join(" ")
46
+ safe = GrubY::API.escape_markdown_v2(text.empty? ? "markdown_v2 demo" : text)
47
+ client.send_message(ctx.chat_id, "*Safe:* #{safe}", parse_mode: "MarkdownV2")
48
+ end
49
+
50
+ client.on(:message, GrubY::Filters.command("html")) do |ctx|
51
+ text = ctx.command_args.join(" ")
52
+ safe = GrubY::API.escape_html(text.empty? ? "<b>html</b> demo" : text)
53
+ client.send_message(ctx.chat_id, "<b>Safe:</b> #{safe}", parse_mode: "HTML")
54
+ end
55
+
56
+ client.on(:message, GrubY::Filters.command("poll")) do |ctx|
57
+ client.send_poll(
58
+ ctx.chat_id,
59
+ "Pick Stack",
60
+ [
61
+ { text: "Ruby" },
62
+ { text: "Python" },
63
+ { text: "JavaScript" }
64
+ ],
65
+ is_anonymous: false,
66
+ allows_multiple_answers: true
67
+ )
68
+ end
69
+
70
+ client.on(:message, GrubY::Filters.command("media")) do |ctx|
71
+ client.send_chat_action(ctx.chat_id, "typing")
72
+ ctx.reply("Try these API methods from code: send_photo/send_audio/send_document/send_video/send_animation/send_voice/send_video_note/send_media_group/send_paid_media")
73
+ end
74
+
75
+ client.on(:message, GrubY::Filters.command("whoami")) do |ctx|
76
+ user = ctx.user
77
+ chat = ctx.message&.chat
78
+ text = "user_id=#{user&.id} username=@#{user&.username} chat_id=#{chat&.id} chat_type=#{chat&.type}"
79
+ ctx.reply(text)
80
+ end
81
+
82
+ client.on(:message, GrubY::Filters.command("warn")) do |ctx|
83
+ user_id = ctx.command_args[0]&.to_i
84
+ reason = ctx.command_args[1..]&.join(" ")
85
+ if user_id.nil? || user_id.zero?
86
+ ctx.reply("Usage: /warn <user_ID> [reason]")
87
+ next
88
+ end
89
+
90
+ result = gm.enforce_warns(ctx.chat_id, user_id, reason: reason)
91
+ if result[:action] == :kick
92
+ ctx.reply("User #{user_id} has been kicked. Reason: Warning limit exceeded.")
93
+ else
94
+ ctx.reply("Warned #{user_id}. #{result[:warns]}/#{result[:limit]}")
95
+ end
96
+ end
97
+
98
+ client.on(:message, GrubY::Filters.command("mute")) do |ctx|
99
+ user_id = ctx.command_args[0]&.to_i
100
+ next ctx.reply("Usage: /mute <user_ID>") if user_id.nil? || user_id.zero?
101
+
102
+ gm.mute(ctx.chat_id, user_id)
103
+ ctx.reply("Muted #{user_id}")
104
+ end
105
+
106
+ client.on(:message, GrubY::Filters.command("unmute")) do |ctx|
107
+ user_id = ctx.command_args[0]&.to_i
108
+ next ctx.reply("Usage: /unmute <user_ID>") if user_id.nil? || user_id.zero?
109
+
110
+ gm.unmute(ctx.chat_id, user_id)
111
+ ctx.reply("Unmuted #{user_id}")
112
+ end
113
+
114
+ client.on(:message, GrubY::Filters.command("ban")) do |ctx|
115
+ user_id = ctx.command_args[0]&.to_i
116
+ next ctx.reply("Usage: /ban <user_ID>") if user_id.nil? || user_id.zero?
117
+
118
+ gm.ban(ctx.chat_id, user_id)
119
+ ctx.reply("Banned #{user_id}")
120
+ end
121
+
122
+ client.on(:message, GrubY::Filters.command("unban")) do |ctx|
123
+ user_id = ctx.command_args[0]&.to_i
124
+ next ctx.reply("Usage: /unban <user_ID>") if user_id.nil? || user_id.zero?
125
+
126
+ gm.unban(ctx.chat_id, user_id, only_if_banned: true)
127
+ ctx.reply("Unbanned #{user_id}")
128
+ end
129
+
130
+ client.on(:message, GrubY::Filters.command("pin")) do |ctx|
131
+ gm.pin(ctx.chat_id, ctx.message.message_id)
132
+ ctx.reply("Pinned this message.")
133
+ end
134
+
135
+ client.on(:message, GrubY::Filters.command("unpin")) do |ctx|
136
+ gm.unpin(ctx.chat_id)
137
+ ctx.reply("Unpinned latest message.")
138
+ end
139
+
140
+ client.on(:message, GrubY::Filters.command("lock")) do |ctx|
141
+ gm.lock(ctx.chat_id)
142
+ ctx.reply("Group locked!")
143
+ end
144
+
145
+ client.on(:message, GrubY::Filters.command("unlock")) do |ctx|
146
+ gm.unlock(ctx.chat_id)
147
+ ctx.reply("Group unlocked!")
148
+ end
149
+
150
+ client.on(:message, GrubY::Filters.command("reaction")) do |ctx|
151
+ message_id = ctx.message.message_id
152
+ client.send_reaction(ctx.chat_id, message_id, reaction: [{ type: "emoji", emoji: "\u{1F525}" }])
153
+ ctx.reply("Reaction sent.")
154
+ end
155
+
156
+ client.on(:message, GrubY::Filters.regex(/hello/i)) do |ctx|
157
+ ctx.reply("Hello world from GrubY!")
158
+ end
159
+
160
+ client.run
@@ -0,0 +1,81 @@
1
+ require_relative "../lib/grubY"
2
+
3
+ def prompt(label)
4
+ print label
5
+ STDIN.gets.to_s.strip
6
+ end
7
+
8
+ api_id = ENV["TD_API_ID"]&.to_i
9
+ api_hash = ENV["TD_API_HASH"].to_s
10
+ session_name = ENV.fetch("TD_SESSION_NAME", "default")
11
+
12
+ abort("Missing TD_API_ID or TD_API_HASH") if api_id.nil? || api_hash.empty?
13
+
14
+ bundle = GrubY::TDLib::UserSession.build(session_name: session_name)
15
+ session_string = GrubY::TDLib::UserSession.encode(
16
+ api_id: api_id,
17
+ api_hash: api_hash,
18
+ database_directory: bundle[:database_directory],
19
+ files_directory: bundle[:files_directory],
20
+ database_encryption_key: bundle[:database_encryption_key],
21
+ session_name: bundle[:session_name]
22
+ )
23
+
24
+ client = GrubY::TDLib::Client.new(
25
+ api_id: api_id,
26
+ api_hash: api_hash,
27
+ database_directory: bundle[:database_directory],
28
+ files_directory: bundle[:files_directory],
29
+ database_encryption_key: bundle[:database_encryption_key],
30
+ tdjson_path: ENV["TDJSON_PATH"],
31
+ td_verbosity: 2,
32
+ workers: 2
33
+ )
34
+
35
+ client.on("updateAuthorizationState") do |update|
36
+ state = update.dig("authorization_state", "@type")
37
+
38
+ case state
39
+ when "authorizationStateWaitPhoneNumber"
40
+ loop do
41
+ phone = prompt("Enter phone number (+countrycode...): ")
42
+ next if phone.empty?
43
+
44
+ client.set_authentication_phone_number(phone_number: phone)
45
+ break
46
+ end
47
+ when "authorizationStateWaitCode"
48
+ loop do
49
+ code = prompt("Enter login code: ")
50
+ next if code.empty?
51
+
52
+ client.check_authentication_code(code)
53
+ break
54
+ end
55
+ when "authorizationStateWaitPassword"
56
+ loop do
57
+ pass = prompt("Enter 2FA password: ")
58
+ next if pass.empty?
59
+
60
+ client.check_authentication_password(pass)
61
+ break
62
+ end
63
+ when "authorizationStateWaitOtherDeviceConfirmation"
64
+ link = update.dig("authorization_state", "link")
65
+ puts "Open Telegram > Settings > Devices > Link Desktop Device"
66
+ puts link.to_s
67
+ when "authorizationStateReady"
68
+ me = client.get_me
69
+ puts "Authorized: #{me['first_name']} (id=#{me['id']})"
70
+ puts
71
+ puts "TD_USER_SESSION=#{session_string}"
72
+ puts
73
+ puts "Reuse in Ruby:"
74
+ puts " cfg = GrubY::TDLib::UserSession.client_kwargs(ENV.fetch('TD_USER_SESSION'))"
75
+ puts " client = GrubY::TDLib::Client.new(**cfg)"
76
+ client.stop
77
+ end
78
+ end
79
+
80
+ client.start
81
+ sleep 0.3 while client.authorized? == false
@@ -0,0 +1,80 @@
1
+ require_relative "../lib/grubY"
2
+ require "json"
3
+
4
+ # Usage:
5
+ # TD_USER_SESSION='<base64>' \
6
+ # VC_CHAT='@YourGroupOrChannel' \
7
+ # VC_AUDIO=./song.mp3 \
8
+ # ruby example/ntgcalls.rb
9
+ #
10
+ # Optional:
11
+ # TDJSON_PATH=/path/to/libtdjson.so
12
+ # NTGCALLS_PATH=/path/to/libntgcalls.so
13
+ # VC_INVITE_HASH=xxxx
14
+
15
+ td_user_session = ENV["TD_USER_SESSION"].to_s
16
+ chat = ENV["VC_CHAT"].to_s
17
+ audio = ENV["VC_AUDIO"].to_s
18
+
19
+ abort("Missing TD_USER_SESSION") if td_user_session.empty?
20
+ abort("Missing VC_CHAT (chat ID or @username)") if chat.empty?
21
+ abort("Missing VC_AUDIO") if audio.empty?
22
+
23
+ bot = GrubY::NTgCalls::MusicBot.new(
24
+ td_user_session: td_user_session,
25
+ tdjson_path: ENV["TDJSON_PATH"],
26
+ ntgcalls_path: ENV["NTGCALLS_PATH"],
27
+ phone_number: ENV["TD_PHONE"]
28
+ )
29
+
30
+ begin
31
+ bot.start
32
+ result = bot.join_and_play(
33
+ chat: chat,
34
+ audio: audio,
35
+ invite_hash: ENV.fetch("VC_INVITE_HASH", "")
36
+ )
37
+ puts "[NTgCalls] Joined chat #{result[:chat_id]} and started playback"
38
+ puts "[NTgCalls] offer payload size=#{result[:offer].to_s.bytesize}"
39
+
40
+ puts "Commands: pause, resume, mute, unmute, time, cpu, stop, showraw, quit"
41
+ loop do
42
+ print "> "
43
+ cmd = STDIN.gets.to_s.strip.downcase
44
+
45
+ case cmd
46
+ when "pause"
47
+ bot.pause
48
+ puts "paused"
49
+ when "resume"
50
+ bot.resume
51
+ puts "resumed"
52
+ when "mute"
53
+ bot.mute
54
+ puts "muted"
55
+ when "unmute"
56
+ bot.unmute
57
+ puts "unmuted"
58
+ when "time"
59
+ puts bot.time
60
+ when "cpu"
61
+ puts bot.cpu_usage
62
+ when "stop"
63
+ bot.stop_call
64
+ puts "stopped"
65
+ when "showraw"
66
+ igc = bot.input_group_call(chat: chat)
67
+ puts "InputGroupCall: #{igc.to_json}"
68
+ puts "InputPeerSelf: #{bot.input_peer_self.to_json}"
69
+ puts "DataJSON: #{bot.data_json(result[:offer]).to_json}"
70
+ puts "JoinGroupCall: #{GrubY::RawTypes.join_group_call(call: igc, params: bot.data_json(result[:offer])).to_json}"
71
+ puts "LeaveGroupCall: #{GrubY::RawTypes.leave_group_call(call: igc, source: 0).to_json}"
72
+ when "quit", "exit"
73
+ break
74
+ else
75
+ puts "Unknown command!"
76
+ end
77
+ end
78
+ ensure
79
+ bot.stop
80
+ end
data/example/tdlib.rb ADDED
@@ -0,0 +1,77 @@
1
+ require_relative "../lib/grubY"
2
+
3
+ # Set these in your environment before running:
4
+ # TD_API_ID=12345
5
+ # TD_API_HASH=xxxxx
6
+ # TD_BOT_TOKEN=12345:abc.. (or use TD_PHONE for user auth)
7
+ # TDJSON_PATH=/path/to/libtdjson.so|dylib|dll
8
+
9
+ api_id = ENV["TD_API_ID"]&.to_i
10
+ api_hash = ENV["TD_API_HASH"]
11
+ bot_token = ENV["TD_BOT_TOKEN"]
12
+ phone = ENV["TD_PHONE"]
13
+
14
+ abort("Missing TD_API_ID or TD_API_HASH") unless api_id && api_hash
15
+
16
+ client = GrubY::TDLib::Client.new(
17
+ api_id: api_id,
18
+ api_hash: api_hash,
19
+ bot_token: bot_token,
20
+ phone_number: phone,
21
+ tdjson_path: ENV["TDJSON_PATH"],
22
+ workers: 4,
23
+ default_handler_timeout: 10
24
+ )
25
+
26
+ gm = client.group_manager
27
+
28
+ client.on("clientReady") do
29
+ puts "[TDLIB] ready"
30
+ end
31
+
32
+ client.initializer do |update|
33
+ type = update["@type"]
34
+ puts "[TDLIB] update=#{type}" if type
35
+ end
36
+
37
+ client.on("authCodeNeeded") do
38
+ print "Enter login code: "
39
+ code = STDIN.gets.to_s.strip
40
+ client.check_authentication_code(code)
41
+ end
42
+
43
+ client.on("authPasswordNeeded") do
44
+ print "Enter 2FA password: "
45
+ pass = STDIN.gets.to_s.strip
46
+ client.check_authentication_password(pass)
47
+ end
48
+
49
+ client.on_message do |message|
50
+ chat_id = message["chat_id"]
51
+ content = message.dig("content", "@type")
52
+
53
+ next unless content == "messageText"
54
+
55
+ text = message.dig("content", "text", "text").to_s
56
+ next unless text.start_with?("/")
57
+
58
+ case text
59
+ when "/ping"
60
+ client.sendMessage(
61
+ chat_id: chat_id,
62
+ input_message_content: {
63
+ "@type": "inputMessageText",
64
+ text: { "@type": "formattedText", text: "pong" }
65
+ }
66
+ )
67
+ when "/lock"
68
+ gm.set_slow_mode(chat_id: chat_id, delay: 30)
69
+ when "/title"
70
+ gm.set_chat_title(chat_id: chat_id, title: "GrubY TDLib Room")
71
+ end
72
+ end
73
+
74
+ client.finalizer do |_update|
75
+ end
76
+
77
+ client.run
@@ -0,0 +1,133 @@
1
+ require_relative "../lib/grubY"
2
+
3
+ begin
4
+ require "io/console"
5
+ rescue LoadError
6
+ end
7
+
8
+ begin
9
+ require "rqrcode"
10
+ rescue LoadError
11
+ RQRCode = nil
12
+ end
13
+
14
+ def clear_print
15
+ print "\e[2J\e[f"
16
+ end
17
+
18
+ def prompt(label)
19
+ print label
20
+ STDIN.gets.to_s.strip
21
+ end
22
+
23
+ api_id = ENV["TD_API_ID"]&.to_i
24
+ api_hash = ENV["TD_API_HASH"]
25
+
26
+ abort("Missing TD_API_ID or TD_API_HASH") if api_id.nil? || api_hash.to_s.empty?
27
+
28
+ client = GrubY::TDLib::Client.new(
29
+ api_id: api_id,
30
+ api_hash: api_hash,
31
+ database_directory: "storage/tdlib-userbot",
32
+ files_directory: "storage/tdlib-userbot/files",
33
+ tdjson_path: ENV["TDJSON_PATH"],
34
+ td_verbosity: 2,
35
+ options: {
36
+ "ignore_background_updates" => false
37
+ },
38
+ workers: 4,
39
+ default_handler_timeout: 15
40
+ )
41
+
42
+ client.on("updateAuthorizationState") do |update|
43
+ state = update.dig("authorization_state", "@type")
44
+
45
+ case state
46
+ when "authorizationStateWaitPhoneNumber"
47
+ loop do
48
+ input = prompt('Enter phone number or "qr": ')
49
+ next if input.empty?
50
+
51
+ if input.downcase == "qr"
52
+ res = client.request_qr_code_authentication
53
+ if res["@type"] == "error"
54
+ puts "Error: #{res['message']}"
55
+ next
56
+ end
57
+ else
58
+ res = client.set_authentication_phone_number(phone_number: input)
59
+ if res["@type"] == "error"
60
+ puts "Error: #{res['message']}"
61
+ next
62
+ end
63
+ end
64
+
65
+ break
66
+ end
67
+ when "authorizationStateWaitOtherDeviceConfirmation"
68
+ link = update.dig("authorization_state", "link")
69
+ clear_print
70
+ puts "Scan QR from Telegram mobile app -> Settings -> Devices -> Link Desktop Device"
71
+ puts
72
+ puts link
73
+
74
+ if RQRCode
75
+ puts
76
+ qr = RQRCode::QRCode.new(link)
77
+ qr.modules.each do |row|
78
+ puts row.map { |m| m ? "##" : " " }.join
79
+ end
80
+ else
81
+ puts "\nInstall `rqrcode` gem for terminal QR rendering."
82
+ end
83
+ when "authorizationStateWaitCode"
84
+ loop do
85
+ code = prompt("Enter login code: ")
86
+ next if code.empty?
87
+
88
+ res = client.check_authentication_code(code)
89
+ if res["@type"] == "error"
90
+ puts "Error: #{res['message']}"
91
+ next
92
+ end
93
+ break
94
+ end
95
+ when "authorizationStateWaitPassword"
96
+ clear_print
97
+ hint = update.dig("authorization_state", "password_hint")
98
+ loop do
99
+ pass = prompt("Enter 2FA password (hint: #{hint}): ")
100
+ next if pass.empty?
101
+
102
+ res = client.check_authentication_password(pass)
103
+ if res["@type"] == "error"
104
+ puts "Error: #{res['message']}"
105
+ next
106
+ end
107
+ break
108
+ end
109
+ when "authorizationStateReady"
110
+ me = client.get_me
111
+ clear_print
112
+ puts "Logged in as #{me['first_name']} (ID: #{me['id']})"
113
+ end
114
+ end
115
+
116
+ client.on_message do |message|
117
+ next unless message.dig("content", "@type") == "messageText"
118
+ next unless message["is_outgoing"]
119
+
120
+ text = message.dig("content", "text", "text").to_s
121
+ next unless text == "!hi"
122
+
123
+ client.edit_message_text(
124
+ chat_id: message["chat_id"],
125
+ message_id: message["id"],
126
+ input_message_content: {
127
+ "@type" => "inputMessageText",
128
+ "text" => { "@type" => "formattedText", "text" => "Hey, This is from GrubY TDLib Userbot!" }
129
+ }
130
+ )
131
+ end
132
+
133
+ client.run