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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +6 -0
- data/Rakefile +51 -0
- data/config/config.rb +5 -0
- data/example/bot.rb +160 -0
- data/example/generate_user_session.rb +81 -0
- data/example/ntgcalls.rb +80 -0
- data/example/tdlib.rb +77 -0
- data/example/userbot.rb +133 -0
- data/lib/grubY/api.rb +363 -0
- data/lib/grubY/async.rb +13 -0
- data/lib/grubY/bot.rb +90 -0
- data/lib/grubY/bound.rb +14 -0
- data/lib/grubY/client.rb +296 -0
- data/lib/grubY/context.rb +242 -0
- data/lib/grubY/dispatcher.rb +60 -0
- data/lib/grubY/enums.rb +62 -0
- data/lib/grubY/exception.rb +5 -0
- data/lib/grubY/file_stream.rb +17 -0
- data/lib/grubY/filters.rb +292 -0
- data/lib/grubY/group_manager.rb +213 -0
- data/lib/grubY/handlers.rb +170 -0
- data/lib/grubY/keyboard.rb +49 -0
- data/lib/grubY/media.rb +38 -0
- data/lib/grubY/middleware.rb +18 -0
- data/lib/grubY/ntgcalls/native.rb +100 -0
- data/lib/grubY/ntgcalls.rb +478 -0
- data/lib/grubY/plugin.rb +14 -0
- data/lib/grubY/raw.rb +25 -0
- data/lib/grubY/raw_types.rb +57 -0
- data/lib/grubY/retry.rb +14 -0
- data/lib/grubY/server.rb +18 -0
- data/lib/grubY/session.rb +30 -0
- data/lib/grubY/tdlib/client.rb +565 -0
- data/lib/grubY/tdlib/client_manager.rb +37 -0
- data/lib/grubY/tdlib/decorators.rb +17 -0
- data/lib/grubY/tdlib/errors.rb +6 -0
- data/lib/grubY/tdlib/group_manager.rb +58 -0
- data/lib/grubY/tdlib/native.rb +72 -0
- data/lib/grubY/tdlib/schema_builder.rb +237 -0
- data/lib/grubY/tdlib/tdjson.rb +49 -0
- data/lib/grubY/tdlib/user_session.rb +69 -0
- data/lib/grubY/types/base_object.rb +72 -0
- data/lib/grubY/types/bound_entities.rb +245 -0
- data/lib/grubY/types/chat.rb +93 -0
- data/lib/grubY/types/chat_info.rb +31 -0
- data/lib/grubY/types/extra.rb +251 -0
- data/lib/grubY/types/message.rb +295 -0
- data/lib/grubY/types/message_entity.rb +14 -0
- data/lib/grubY/types/registry.rb +67 -0
- data/lib/grubY/types/user.rb +31 -0
- data/lib/grubY/webapp.rb +22 -0
- data/lib/grubY.rb +41 -0
- data/plugins/logger.rb +7 -0
- data/plugins/media_tools.rb +13 -0
- 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
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](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
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
|
data/example/ntgcalls.rb
ADDED
|
@@ -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
|
data/example/userbot.rb
ADDED
|
@@ -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
|