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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
require_relative "tdjson"
|
|
3
|
+
|
|
4
|
+
module GrubY
|
|
5
|
+
module TDLib
|
|
6
|
+
class ClientManager
|
|
7
|
+
attr_reader :tdjson
|
|
8
|
+
|
|
9
|
+
def initialize(lib_path: nil, verbosity: 1)
|
|
10
|
+
@tdjson = TdJson.new(lib_path: lib_path, verbosity: verbosity)
|
|
11
|
+
@clients = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_client(client)
|
|
16
|
+
key = client.native_client_key
|
|
17
|
+
@mutex.synchronize { @clients[key] = client }
|
|
18
|
+
client
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove_client(client)
|
|
22
|
+
key = client.native_client_key
|
|
23
|
+
@mutex.synchronize { @clients.delete(key) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def each_client(&block)
|
|
27
|
+
snapshot = @mutex.synchronize { @clients.values.dup }
|
|
28
|
+
snapshot.each(&block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def close
|
|
32
|
+
each_client(&:stop)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module GrubY
|
|
2
|
+
module TDLib
|
|
3
|
+
module Decorators
|
|
4
|
+
def initializer(filter: nil, position: nil, timeout: nil, &block)
|
|
5
|
+
add_handler("initializer", filter: filter, position: position, timeout: timeout, &block)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def finalizer(filter: nil, position: nil, timeout: nil, &block)
|
|
9
|
+
add_handler("finalizer", filter: filter, position: position, timeout: timeout, &block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def on_message(filter: nil, position: nil, timeout: nil, &block)
|
|
13
|
+
add_handler("updateNewMessage", filter: filter, position: position, timeout: timeout, inner_object: true, &block)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module GrubY
|
|
2
|
+
module TDLib
|
|
3
|
+
class GroupManager
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def ban_chat_member(chat_id:, member_id:, banned_until_date: 0, revoke_messages: true)
|
|
9
|
+
@client.banChatMember(
|
|
10
|
+
chat_id: chat_id,
|
|
11
|
+
member_id: member_id,
|
|
12
|
+
banned_until_date: banned_until_date,
|
|
13
|
+
revoke_messages: revoke_messages
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unban_chat_member(chat_id:, member_id:)
|
|
18
|
+
@client.unbanChatMember(chat_id: chat_id, member_id: member_id)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_chat_title(chat_id:, title:)
|
|
22
|
+
@client.setChatTitle(chat_id: chat_id, title: title)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_chat_description(chat_id:, description:)
|
|
26
|
+
@client.setChatDescription(chat_id: chat_id, description: description)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_slow_mode(chat_id:, delay:)
|
|
30
|
+
@client.setChatSlowModeDelay(chat_id: chat_id, slow_mode_delay: delay)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pin_chat_message(chat_id:, message_id:, disable_notification: false)
|
|
34
|
+
@client.pinChatMessage(
|
|
35
|
+
chat_id: chat_id,
|
|
36
|
+
message_id: message_id,
|
|
37
|
+
disable_notification: disable_notification
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def unpin_chat_message(chat_id:, message_id: nil)
|
|
42
|
+
@client.unpinChatMessage(chat_id: chat_id, message_id: message_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def leave_chat(chat_id:)
|
|
46
|
+
@client.leaveChat(chat_id: chat_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_chat_member(chat_id:, member_id:)
|
|
50
|
+
@client.getChatMember(chat_id: chat_id, member_id: member_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_chat(chat_id:)
|
|
54
|
+
@client.getChat(chat_id: chat_id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "ffi"
|
|
2
|
+
|
|
3
|
+
module GrubY
|
|
4
|
+
module TDLib
|
|
5
|
+
module Native
|
|
6
|
+
extend FFI::Library
|
|
7
|
+
|
|
8
|
+
class LibraryNotFoundError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def load!(path = nil)
|
|
12
|
+
return if @loaded
|
|
13
|
+
|
|
14
|
+
ffi_lib(resolve_library_path(path))
|
|
15
|
+
attach_function :td_json_client_create, [], :pointer
|
|
16
|
+
attach_function :td_json_client_send, [:pointer, :string], :void
|
|
17
|
+
attach_function :td_json_client_receive, [:pointer, :double], :string
|
|
18
|
+
attach_function :td_json_client_execute, [:pointer, :string], :string
|
|
19
|
+
attach_function :td_json_client_destroy, [:pointer], :void
|
|
20
|
+
attach_function :td_set_log_verbosity_level, [:int], :void
|
|
21
|
+
@loaded = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def resolve_library_path(custom_path)
|
|
27
|
+
return custom_path if custom_path && File.exist?(custom_path)
|
|
28
|
+
|
|
29
|
+
env_path = ENV["TDJSON_PATH"]
|
|
30
|
+
return env_path if env_path && File.exist?(env_path)
|
|
31
|
+
|
|
32
|
+
candidates = default_candidates
|
|
33
|
+
found = candidates.find { |path| File.exist?(path) }
|
|
34
|
+
return found if found
|
|
35
|
+
|
|
36
|
+
raise LibraryNotFoundError,
|
|
37
|
+
"TDLib (tdjson) not found. Set TDJSON_PATH or pass tdjson_path."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def default_candidates
|
|
41
|
+
if Gem.win_platform?
|
|
42
|
+
[
|
|
43
|
+
"tdjson.dll",
|
|
44
|
+
"vendor/tdlib/windows/tdjson.dll",
|
|
45
|
+
"vendor/tdlib/win/tdjson.dll",
|
|
46
|
+
"vendor/tdlib/tdjson.dll",
|
|
47
|
+
"C:/tdlib/bin/tdjson.dll",
|
|
48
|
+
"C:/Program Files/TDLib/bin/tdjson.dll"
|
|
49
|
+
]
|
|
50
|
+
elsif RUBY_PLATFORM.include?("darwin")
|
|
51
|
+
[
|
|
52
|
+
"libtdjson.dylib",
|
|
53
|
+
"vendor/tdlib/macos/libtdjson.dylib",
|
|
54
|
+
"vendor/tdlib/darwin/libtdjson.dylib",
|
|
55
|
+
"vendor/tdlib/libtdjson.dylib",
|
|
56
|
+
"/usr/local/lib/libtdjson.dylib",
|
|
57
|
+
"/opt/homebrew/lib/libtdjson.dylib"
|
|
58
|
+
]
|
|
59
|
+
else
|
|
60
|
+
[
|
|
61
|
+
"libtdjson.so",
|
|
62
|
+
"vendor/tdlib/linux/libtdjson.so",
|
|
63
|
+
"vendor/tdlib/libtdjson.so",
|
|
64
|
+
"/usr/lib/libtdjson.so",
|
|
65
|
+
"/usr/local/lib/libtdjson.so"
|
|
66
|
+
]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module GrubY
|
|
8
|
+
module TDLib
|
|
9
|
+
class SchemaBuilder
|
|
10
|
+
TL_DEF_REGEX = /^(?<name>\w+)(?:#[0-9a-f]+)?\s+(?<params>.*)=\s+(?<type>\w+);$/
|
|
11
|
+
PARAM_DETAIL_REGEX = /(?<name>\w+):(?<type>[^\s]+)/
|
|
12
|
+
ROW_REGEX = /<tr>\s*<td>(.*?)<\/td>\s*<td>(.*?)<\/td>\s*<td>(.*?)<\/td>\s*<td>(.*?)<\/td>\s*<\/tr>/m
|
|
13
|
+
TAG_REGEX = /<[^>]*>/
|
|
14
|
+
|
|
15
|
+
def initialize(src:, version: "", commit: "", out: "tdlib.json")
|
|
16
|
+
@src = src
|
|
17
|
+
@version = version
|
|
18
|
+
@commit = commit
|
|
19
|
+
@out = out
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
data = if @src.start_with?("http://", "https://")
|
|
24
|
+
fetch_and_parse_tl(@src)
|
|
25
|
+
else
|
|
26
|
+
parse_tl_from_file(@src)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
data["version"] = @version
|
|
30
|
+
data["commit"] = @commit
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
data["options"] = fetch_options
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
warn "Warning: failed to fetch options: #{e.message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
save_json(data, @out)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_tl_from_file(path)
|
|
44
|
+
File.open(path, "r:utf-8") do |file|
|
|
45
|
+
parse_tl_from_reader(file)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_and_parse_tl(url)
|
|
50
|
+
uri = URI.parse(url)
|
|
51
|
+
request = Net::HTTP::Get.new(uri)
|
|
52
|
+
request["User-Agent"] = "gruby-tdlib-schema-builder/1.0"
|
|
53
|
+
|
|
54
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 30, open_timeout: 30) do |http|
|
|
55
|
+
http.request(request)
|
|
56
|
+
end
|
|
57
|
+
raise "failed to fetch #{url}: status #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
58
|
+
|
|
59
|
+
parse_tl_from_reader(StringIO.new(response.body))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_tl_from_reader(reader)
|
|
63
|
+
data = {
|
|
64
|
+
"name" => "Auto-generated JSON TDLib API",
|
|
65
|
+
"version" => "",
|
|
66
|
+
"commit" => "",
|
|
67
|
+
"classes" => {},
|
|
68
|
+
"types" => {},
|
|
69
|
+
"updates" => {},
|
|
70
|
+
"functions" => {},
|
|
71
|
+
"options" => {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
current_description = ""
|
|
75
|
+
current_params = {}
|
|
76
|
+
is_functions_section = false
|
|
77
|
+
start = false
|
|
78
|
+
|
|
79
|
+
reader.each_line do |raw_line|
|
|
80
|
+
line = raw_line.strip
|
|
81
|
+
|
|
82
|
+
if line.include?("---functions---")
|
|
83
|
+
is_functions_section = true
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if line.start_with?("//")
|
|
88
|
+
start = true
|
|
89
|
+
if line.start_with?("//@")
|
|
90
|
+
parse_doc_tags(data, line.sub("//@", ""), current_params) do |description|
|
|
91
|
+
current_description = [current_description, description].reject(&:empty?).join(" ").strip
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
next if line.empty? || !start
|
|
98
|
+
|
|
99
|
+
match = TL_DEF_REGEX.match(line)
|
|
100
|
+
next unless match
|
|
101
|
+
|
|
102
|
+
name = match[:name]
|
|
103
|
+
params_str = match[:params]
|
|
104
|
+
result_type = match[:type]
|
|
105
|
+
|
|
106
|
+
type_def = {
|
|
107
|
+
"description" => current_description,
|
|
108
|
+
"args" => {},
|
|
109
|
+
"type" => result_type
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
params_str.scan(PARAM_DETAIL_REGEX) do |p_name, p_type|
|
|
113
|
+
p_desc = current_params[p_name] || ""
|
|
114
|
+
type_def["args"][p_name] = {
|
|
115
|
+
"description" => p_desc,
|
|
116
|
+
"is_optional" => optional_arg?(p_desc, p_type),
|
|
117
|
+
"type" => p_type
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if is_functions_section
|
|
122
|
+
data["functions"][name] = type_def
|
|
123
|
+
(data["classes"][result_type] ||= default_class)["functions"] << name
|
|
124
|
+
elsif name.start_with?("update")
|
|
125
|
+
data["updates"][name] = type_def
|
|
126
|
+
(data["classes"][result_type] ||= default_class)["types"] << name
|
|
127
|
+
else
|
|
128
|
+
data["types"][name] = type_def
|
|
129
|
+
(data["classes"][result_type] ||= default_class)["types"] << name
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
current_description = ""
|
|
133
|
+
current_params = {}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
data
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def parse_doc_tags(data, tag_line, current_params)
|
|
140
|
+
parts = (" " + tag_line).split(" @")
|
|
141
|
+
current_class = nil
|
|
142
|
+
|
|
143
|
+
parts.each do |part|
|
|
144
|
+
part = part.strip
|
|
145
|
+
next if part.empty?
|
|
146
|
+
|
|
147
|
+
tag_name, tag_text = part.split(" ", 2)
|
|
148
|
+
tag_text = (tag_text || "").strip
|
|
149
|
+
|
|
150
|
+
case tag_name
|
|
151
|
+
when "class"
|
|
152
|
+
current_class = tag_text
|
|
153
|
+
when "description"
|
|
154
|
+
if current_class
|
|
155
|
+
data["classes"][current_class] = {
|
|
156
|
+
"description" => tag_text,
|
|
157
|
+
"types" => [],
|
|
158
|
+
"functions" => []
|
|
159
|
+
}
|
|
160
|
+
current_class = nil
|
|
161
|
+
else
|
|
162
|
+
yield(tag_text)
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
clean_name = tag_name.sub(/^param_/, "")
|
|
166
|
+
current_params[clean_name] = [current_params[clean_name], tag_text].compact.join(" ").strip
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def default_class
|
|
172
|
+
{ "description" => "", "types" => [], "functions" => [] }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def optional_arg?(description, type)
|
|
176
|
+
text = description.to_s.downcase
|
|
177
|
+
text.include?("may be null") ||
|
|
178
|
+
text.include?("pass null") ||
|
|
179
|
+
text.include?("may be empty") ||
|
|
180
|
+
description.to_s.include?("If non-empty,") ||
|
|
181
|
+
type.to_s.include?("?")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fetch_options
|
|
185
|
+
url = URI.parse("https://core.telegram.org/tdlib/options")
|
|
186
|
+
request = Net::HTTP::Get.new(url)
|
|
187
|
+
request["User-Agent"] = "gruby-tdlib-schema-builder/1.0"
|
|
188
|
+
|
|
189
|
+
response = Net::HTTP.start(url.host, url.port, use_ssl: true, read_timeout: 30, open_timeout: 30) do |http|
|
|
190
|
+
http.request(request)
|
|
191
|
+
end
|
|
192
|
+
raise "failed to fetch options: status #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
193
|
+
|
|
194
|
+
content = response.body
|
|
195
|
+
marker_index = content.index("list-of-options-supported-by-tdlib")
|
|
196
|
+
raise "could not find options list in HTML" unless marker_index
|
|
197
|
+
|
|
198
|
+
options = {}
|
|
199
|
+
content[marker_index..].scan(ROW_REGEX) do |name, type_name, writable, description|
|
|
200
|
+
clean_name = clean_html(name)
|
|
201
|
+
next if clean_name.empty? || clean_name.casecmp("name").zero?
|
|
202
|
+
|
|
203
|
+
options[clean_name] = {
|
|
204
|
+
"type" => map_type(clean_html(type_name)),
|
|
205
|
+
"writable" => clean_html(writable).casecmp("yes").zero?,
|
|
206
|
+
"description" => clean_html(description)
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
raise "no options were parsed from HTML" if options.empty?
|
|
211
|
+
|
|
212
|
+
options
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def clean_html(text)
|
|
216
|
+
CGI.unescapeHTML(text.gsub(TAG_REGEX, "").strip)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def map_type(type_name)
|
|
220
|
+
case type_name
|
|
221
|
+
when "Integer"
|
|
222
|
+
"int64"
|
|
223
|
+
when "Boolean"
|
|
224
|
+
"Bool"
|
|
225
|
+
when "String"
|
|
226
|
+
"string"
|
|
227
|
+
else
|
|
228
|
+
type_name
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def save_json(data, path)
|
|
233
|
+
File.write(path, JSON.pretty_generate(data) + "\n")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "native"
|
|
3
|
+
|
|
4
|
+
module GrubY
|
|
5
|
+
module TDLib
|
|
6
|
+
class TdJson
|
|
7
|
+
attr_reader :version
|
|
8
|
+
|
|
9
|
+
def initialize(lib_path: nil, verbosity: 1)
|
|
10
|
+
Native.load!(lib_path)
|
|
11
|
+
Native.td_set_log_verbosity_level(verbosity.to_i)
|
|
12
|
+
@version = fetch_version
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create_client_id
|
|
16
|
+
Native.td_json_client_create
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def send(client_id, request)
|
|
20
|
+
Native.td_json_client_send(client_id, JSON.generate(request))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def receive(client_id, timeout:)
|
|
24
|
+
raw = Native.td_json_client_receive(client_id, timeout.to_f)
|
|
25
|
+
raw ? JSON.parse(raw) : nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute(client_id, request)
|
|
29
|
+
raw = Native.td_json_client_execute(client_id, JSON.generate(request))
|
|
30
|
+
raw ? JSON.parse(raw) : nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy(client_id)
|
|
34
|
+
Native.td_json_client_destroy(client_id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def fetch_version
|
|
40
|
+
temp = create_client_id
|
|
41
|
+
response = execute(temp, { "@type" => "getOption", "name" => "version" })
|
|
42
|
+
destroy(temp)
|
|
43
|
+
response&.dig("value", "value")
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "json"
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module GrubY
|
|
6
|
+
module TDLib
|
|
7
|
+
module UserSession
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
VERSION = 1
|
|
11
|
+
|
|
12
|
+
def encode(
|
|
13
|
+
api_id:,
|
|
14
|
+
api_hash:,
|
|
15
|
+
database_directory:,
|
|
16
|
+
files_directory:,
|
|
17
|
+
database_encryption_key: "",
|
|
18
|
+
session_name: nil
|
|
19
|
+
)
|
|
20
|
+
payload = {
|
|
21
|
+
"v" => VERSION,
|
|
22
|
+
"api_id" => api_id.to_i,
|
|
23
|
+
"api_hash" => api_hash.to_s,
|
|
24
|
+
"database_directory" => database_directory.to_s,
|
|
25
|
+
"files_directory" => files_directory.to_s,
|
|
26
|
+
"database_encryption_key" => database_encryption_key.to_s,
|
|
27
|
+
"session_name" => session_name.to_s
|
|
28
|
+
}
|
|
29
|
+
Base64.urlsafe_encode64(payload.to_json)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def decode(session_string)
|
|
33
|
+
raise ArgumentError, "session_string is empty" if session_string.to_s.strip.empty?
|
|
34
|
+
|
|
35
|
+
payload = JSON.parse(Base64.urlsafe_decode64(session_string.to_s.strip))
|
|
36
|
+
unless payload.is_a?(Hash) && payload["v"].to_i == VERSION
|
|
37
|
+
raise ArgumentError, "invalid session string version"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
payload
|
|
41
|
+
rescue ArgumentError, JSON::ParserError => e
|
|
42
|
+
raise ArgumentError, "invalid session string: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build(session_name: "default", root: "storage/tdlib-sessions")
|
|
46
|
+
normalized = session_name.to_s.strip
|
|
47
|
+
normalized = "default" if normalized.empty?
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
session_name: normalized,
|
|
51
|
+
database_directory: File.join(root, normalized, "db"),
|
|
52
|
+
files_directory: File.join(root, normalized, "files"),
|
|
53
|
+
database_encryption_key: SecureRandom.hex(24)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def client_kwargs(session_string)
|
|
58
|
+
payload = decode(session_string)
|
|
59
|
+
{
|
|
60
|
+
api_id: payload.fetch("api_id").to_i,
|
|
61
|
+
api_hash: payload.fetch("api_hash").to_s,
|
|
62
|
+
database_directory: payload.fetch("database_directory").to_s,
|
|
63
|
+
files_directory: payload.fetch("files_directory").to_s,
|
|
64
|
+
database_encryption_key: payload.fetch("database_encryption_key").to_s
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module GrubY
|
|
2
|
+
class BaseObject
|
|
3
|
+
class << self
|
|
4
|
+
def fields(*names)
|
|
5
|
+
@field_names ||= []
|
|
6
|
+
@field_names.concat(names.map(&:to_sym))
|
|
7
|
+
attr_reader(*names)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def field_names
|
|
11
|
+
@field_names || []
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(data = {}, api: nil, client: nil)
|
|
16
|
+
@data = normalize_hash(data)
|
|
17
|
+
@api = api
|
|
18
|
+
@client = client
|
|
19
|
+
self.class.field_names.each do |field|
|
|
20
|
+
instance_variable_set("@#{field}", @data[field.to_s])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :api, :client
|
|
25
|
+
|
|
26
|
+
def [](key)
|
|
27
|
+
@data[key.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def dig(*keys)
|
|
31
|
+
normalized = keys.map(&:to_s)
|
|
32
|
+
@data.dig(*normalized)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
@data.dup
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def method_missing(name, *args, &block)
|
|
40
|
+
return super unless args.empty? && block.nil?
|
|
41
|
+
return @data[name.to_s] if @data.key?(name.to_s)
|
|
42
|
+
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def respond_to_missing?(name, include_private = false)
|
|
47
|
+
@data.key?(name.to_s) || super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def call_api(method, params = {})
|
|
53
|
+
raise ArgumentError, "API is not bound to this object" unless @api
|
|
54
|
+
|
|
55
|
+
@api.request(method, params)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call_raw_api(method, params = {})
|
|
59
|
+
raise ArgumentError, "API is not bound to this object" unless @api
|
|
60
|
+
|
|
61
|
+
@api.raw(method, params)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_hash(data)
|
|
65
|
+
return {} unless data.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
data.each_with_object({}) do |(k, v), out|
|
|
68
|
+
out[k.to_s] = v
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|