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
data/lib/grubY/raw.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module GrubY
|
|
2
|
+
module Raw
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def call(api, method, params = {})
|
|
6
|
+
api.raw(method, params)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def td_call(td_client, query, timeout: 30.0)
|
|
10
|
+
if td_client.respond_to?(:raw)
|
|
11
|
+
td_client.raw(query, timeout: timeout)
|
|
12
|
+
else
|
|
13
|
+
td_client.invoke(query, timeout: timeout)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def td_call!(td_client, query, timeout: 30.0)
|
|
18
|
+
response = td_call(td_client, query, timeout: timeout)
|
|
19
|
+
if response.is_a?(Hash) && response["@type"] == "error"
|
|
20
|
+
raise StandardError, "TDLib raw error: #{response['message']}"
|
|
21
|
+
end
|
|
22
|
+
response
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "types/base_object"
|
|
3
|
+
|
|
4
|
+
module GrubY
|
|
5
|
+
module RawTypes
|
|
6
|
+
class UpdateGroupCall < GrubY::BaseObject
|
|
7
|
+
fields :group_call
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def to_data_json(data)
|
|
13
|
+
{
|
|
14
|
+
"_" => "DataJSON",
|
|
15
|
+
"data" => data.is_a?(String) ? data : JSON.generate(data)
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def input_group_call(id:, access_hash:)
|
|
20
|
+
{
|
|
21
|
+
"_" => "InputGroupCall",
|
|
22
|
+
"id" => id.to_i,
|
|
23
|
+
"access_hash" => access_hash.to_i
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def input_peer_self
|
|
28
|
+
{
|
|
29
|
+
"_" => "InputPeerSelf"
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def join_group_call(call:, params:, muted: false, video_stopped: true, invite_hash: nil, join_as: nil)
|
|
34
|
+
{
|
|
35
|
+
"_" => "JoinGroupCall",
|
|
36
|
+
"call" => call,
|
|
37
|
+
"join_as" => join_as || input_peer_self,
|
|
38
|
+
"params" => params,
|
|
39
|
+
"muted" => !!muted,
|
|
40
|
+
"video_stopped" => !!video_stopped,
|
|
41
|
+
"invite_hash" => invite_hash
|
|
42
|
+
}.compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def leave_group_call(call:, source:)
|
|
46
|
+
{
|
|
47
|
+
"_" => "LeaveGroupCall",
|
|
48
|
+
"call" => call,
|
|
49
|
+
"source" => source.to_i
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def update_group_call(payload)
|
|
54
|
+
UpdateGroupCall.new(payload)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/grubY/retry.rb
ADDED
data/lib/grubY/server.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "sinatra/base"
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "../../config/config"
|
|
4
|
+
|
|
5
|
+
module GrubY
|
|
6
|
+
class Server < Sinatra::Base
|
|
7
|
+
def initialize(bot)
|
|
8
|
+
super()
|
|
9
|
+
@bot = bot
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
post Config::WEBHOOK_PATH do
|
|
13
|
+
data = JSON.parse(request.body.read)
|
|
14
|
+
@bot.process_update(data)
|
|
15
|
+
"ok"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module GrubY
|
|
4
|
+
class Session
|
|
5
|
+
FILE = "storage/sessions.json"
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
Dir.mkdir("storage") unless Dir.exist?("storage")
|
|
9
|
+
File.write(FILE, "{}") unless File.exist?(FILE)
|
|
10
|
+
@data = JSON.parse(File.read(FILE))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(user_id)
|
|
14
|
+
@data[user_id.to_s] ||= {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def set(user_id, value)
|
|
18
|
+
@data[user_id.to_s] = value
|
|
19
|
+
save
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def save
|
|
23
|
+
File.write(FILE, JSON.pretty_generate(@data))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
@data.dup
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "logger"
|
|
3
|
+
require "timeout"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "thread"
|
|
6
|
+
require_relative "decorators"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "group_manager"
|
|
9
|
+
require_relative "tdjson"
|
|
10
|
+
|
|
11
|
+
module GrubY
|
|
12
|
+
module TDLib
|
|
13
|
+
class Client
|
|
14
|
+
include Decorators
|
|
15
|
+
|
|
16
|
+
Handler = Struct.new(
|
|
17
|
+
:id,
|
|
18
|
+
:update_type,
|
|
19
|
+
:block,
|
|
20
|
+
:filter,
|
|
21
|
+
:position,
|
|
22
|
+
:inner_object,
|
|
23
|
+
:timeout,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
attr_reader :api_id, :api_hash, :database_directory, :files_directory, :group_manager
|
|
28
|
+
attr_reader :authorization_state, :td_options
|
|
29
|
+
|
|
30
|
+
def initialize(
|
|
31
|
+
api_id:,
|
|
32
|
+
api_hash:,
|
|
33
|
+
database_directory: "storage/tdlib",
|
|
34
|
+
files_directory: "storage/tdlib/files",
|
|
35
|
+
phone_number: nil,
|
|
36
|
+
bot_token: nil,
|
|
37
|
+
tdjson_path: nil,
|
|
38
|
+
database_encryption_key: "",
|
|
39
|
+
use_test_dc: false,
|
|
40
|
+
system_language_code: "en",
|
|
41
|
+
device_model: "GrubY",
|
|
42
|
+
system_version: RUBY_PLATFORM,
|
|
43
|
+
application_version: "0.2.0",
|
|
44
|
+
options: {},
|
|
45
|
+
workers: 4,
|
|
46
|
+
queue_size: 1_000,
|
|
47
|
+
default_handler_timeout: nil,
|
|
48
|
+
td_verbosity: 1
|
|
49
|
+
)
|
|
50
|
+
@api_id = api_id
|
|
51
|
+
@api_hash = api_hash
|
|
52
|
+
@database_directory = database_directory
|
|
53
|
+
@files_directory = files_directory
|
|
54
|
+
@phone_number = phone_number
|
|
55
|
+
@bot_token = bot_token
|
|
56
|
+
@database_encryption_key = database_encryption_key.to_s
|
|
57
|
+
@use_test_dc = use_test_dc
|
|
58
|
+
@system_language_code = system_language_code
|
|
59
|
+
@device_model = device_model
|
|
60
|
+
@system_version = system_version
|
|
61
|
+
@application_version = application_version
|
|
62
|
+
@td_options = options
|
|
63
|
+
@workers = workers
|
|
64
|
+
@queue_size = queue_size
|
|
65
|
+
@default_handler_timeout = default_handler_timeout
|
|
66
|
+
@logger = Logger.new($stdout)
|
|
67
|
+
@logger.level = Logger::INFO
|
|
68
|
+
|
|
69
|
+
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
70
|
+
@next_handler_id = 1
|
|
71
|
+
@pending = {}
|
|
72
|
+
@pending_mutex = Mutex.new
|
|
73
|
+
@sequence = 0
|
|
74
|
+
@running = false
|
|
75
|
+
@authorized = false
|
|
76
|
+
@authorization_state = nil
|
|
77
|
+
@updates_thread = nil
|
|
78
|
+
@workers_threads = []
|
|
79
|
+
@queue = Queue.new
|
|
80
|
+
@group_manager = GroupManager.new(self)
|
|
81
|
+
@local_handlers = {
|
|
82
|
+
"updateAuthorizationState" => method(:process_authorization_update),
|
|
83
|
+
"updateOption" => method(:process_update_option)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@tdjson = TdJson.new(lib_path: tdjson_path, verbosity: td_verbosity)
|
|
87
|
+
@native_client = @tdjson.create_client_id
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def start
|
|
91
|
+
return self if @running
|
|
92
|
+
|
|
93
|
+
ensure_native_client!
|
|
94
|
+
ensure_storage!
|
|
95
|
+
verify_arguments!
|
|
96
|
+
@running = true
|
|
97
|
+
set_options
|
|
98
|
+
boot_authorization_state
|
|
99
|
+
start_update_loop
|
|
100
|
+
start_workers
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
alias connect start
|
|
105
|
+
|
|
106
|
+
def stop
|
|
107
|
+
return self unless @running
|
|
108
|
+
|
|
109
|
+
@running = false
|
|
110
|
+
@updates_thread&.kill
|
|
111
|
+
@updates_thread = nil
|
|
112
|
+
@workers_threads.each(&:kill)
|
|
113
|
+
@workers_threads.clear
|
|
114
|
+
send_query("@type": "close")
|
|
115
|
+
@tdjson.destroy(@native_client) if @native_client
|
|
116
|
+
@native_client = nil
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
alias disconnect stop
|
|
121
|
+
alias terminate stop
|
|
122
|
+
alias initialize_client start
|
|
123
|
+
|
|
124
|
+
def restart
|
|
125
|
+
stop
|
|
126
|
+
start
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def run
|
|
130
|
+
start
|
|
131
|
+
idle
|
|
132
|
+
stop
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def idle
|
|
136
|
+
@idle = true
|
|
137
|
+
trap_signals
|
|
138
|
+
sleep 0.3 while @idle
|
|
139
|
+
ensure
|
|
140
|
+
@idle = false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def add_handler(
|
|
144
|
+
update_type = nil,
|
|
145
|
+
filter: nil,
|
|
146
|
+
position: nil,
|
|
147
|
+
inner_object: false,
|
|
148
|
+
timeout: nil,
|
|
149
|
+
&block
|
|
150
|
+
)
|
|
151
|
+
raise ArgumentError, "handler block is required" unless block
|
|
152
|
+
|
|
153
|
+
id = @next_handler_id
|
|
154
|
+
@next_handler_id += 1
|
|
155
|
+
type = update_type.to_s
|
|
156
|
+
handler = Handler.new(
|
|
157
|
+
id: id,
|
|
158
|
+
update_type: type,
|
|
159
|
+
block: block,
|
|
160
|
+
filter: filter,
|
|
161
|
+
position: position,
|
|
162
|
+
inner_object: inner_object,
|
|
163
|
+
timeout: timeout
|
|
164
|
+
)
|
|
165
|
+
@handlers[type] << handler
|
|
166
|
+
sort_handlers(type)
|
|
167
|
+
id
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def on(update_type = nil, **options, &block)
|
|
171
|
+
add_handler(update_type, **options, &block)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def remove_handler(handler_id = nil, &block)
|
|
175
|
+
removed = false
|
|
176
|
+
@handlers.each_key do |type|
|
|
177
|
+
before = @handlers[type].length
|
|
178
|
+
@handlers[type].delete_if do |handler|
|
|
179
|
+
(handler_id && handler.id == handler_id) || (block && handler.block == block)
|
|
180
|
+
end
|
|
181
|
+
removed ||= before != @handlers[type].length
|
|
182
|
+
end
|
|
183
|
+
removed
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def send_query(query)
|
|
187
|
+
@tdjson.send(@native_client, with_extra(query))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def execute(query)
|
|
191
|
+
@tdjson.execute(@native_client, stringify_keys(query))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def invoke(query, timeout: 30.0, poll_interval: 0.05)
|
|
195
|
+
tagged = with_extra(query)
|
|
196
|
+
tag = extra_id(tagged["@extra"])
|
|
197
|
+
@pending_mutex.synchronize { @pending[tag] = nil }
|
|
198
|
+
@tdjson.send(@native_client, tagged)
|
|
199
|
+
|
|
200
|
+
deadline = Time.now + timeout
|
|
201
|
+
while Time.now < deadline
|
|
202
|
+
consume_once(poll_interval)
|
|
203
|
+
value = @pending_mutex.synchronize { @pending[tag] }
|
|
204
|
+
return value if value
|
|
205
|
+
end
|
|
206
|
+
raise Timeout::Error, "TDLib response timeout for #{query[:@type] || query['@type']}"
|
|
207
|
+
ensure
|
|
208
|
+
@pending_mutex.synchronize { @pending.delete(tag) } if tag
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def raw(query, timeout: 30.0, poll_interval: 0.05)
|
|
212
|
+
invoke(query, timeout: timeout, poll_interval: poll_interval)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def raw!(query, timeout: 30.0, poll_interval: 0.05)
|
|
216
|
+
result = raw(query, timeout: timeout, poll_interval: poll_interval)
|
|
217
|
+
if result.is_a?(Hash) && result["@type"] == "error"
|
|
218
|
+
message = result["message"] || "unknown TDLib error"
|
|
219
|
+
raise StandardError, "TDLib raw call failed: #{message}"
|
|
220
|
+
end
|
|
221
|
+
result
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def authorized?
|
|
225
|
+
@authorized
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def check_authentication_code(code)
|
|
229
|
+
send_query("@type": "checkAuthenticationCode", code: code)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
alias sign_in check_authentication_code
|
|
233
|
+
|
|
234
|
+
def check_authentication_password(password)
|
|
235
|
+
send_query("@type": "checkAuthenticationPassword", password: password)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
alias check_password check_authentication_password
|
|
239
|
+
|
|
240
|
+
def check_authentication_bot_token(token = @bot_token)
|
|
241
|
+
send_query("@type": "checkAuthenticationBotToken", token: token)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
alias sign_in_bot check_authentication_bot_token
|
|
245
|
+
|
|
246
|
+
def send_phone_number_code(phone_number, settings: nil)
|
|
247
|
+
payload = { "@type": "setAuthenticationPhoneNumber", phone_number: phone_number }
|
|
248
|
+
payload[:settings] = settings if settings
|
|
249
|
+
send_query(payload)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def resend_phone_number_code
|
|
253
|
+
send_query("@type": "resendAuthenticationCode")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def send_recovery_code
|
|
257
|
+
send_query("@type": "requestAuthenticationPasswordRecovery")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def recover_password(recovery_code, new_password: nil, new_hint: nil)
|
|
261
|
+
send_query(
|
|
262
|
+
"@type": "recoverAuthenticationPassword",
|
|
263
|
+
recovery_code: recovery_code,
|
|
264
|
+
new_password: new_password,
|
|
265
|
+
new_hint: new_hint
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def log_out
|
|
270
|
+
send_query("@type": "logOut")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def get_active_sessions
|
|
274
|
+
invoke("@type": "getActiveSessions")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def reset_session(session_hash)
|
|
278
|
+
send_query("@type": "terminateSession", session_id: session_hash)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def reset_sessions
|
|
282
|
+
send_query("@type": "terminateAllOtherSessions")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def set_log_verbosity(level)
|
|
286
|
+
Native.td_set_log_verbosity_level(level.to_i)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
290
|
+
return super if block
|
|
291
|
+
return super unless args.empty?
|
|
292
|
+
|
|
293
|
+
type = self.class.camelize_td_type(name)
|
|
294
|
+
invoke({ "@type": type }.merge(kwargs))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
298
|
+
true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def native_client_key
|
|
302
|
+
@native_client.to_i
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def self.compose(*clients, &block)
|
|
306
|
+
clients.each(&:start)
|
|
307
|
+
block.call if block
|
|
308
|
+
clients.each(&:idle)
|
|
309
|
+
ensure
|
|
310
|
+
clients.each(&:stop)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def self.camelize_td_type(name)
|
|
314
|
+
parts = name.to_s.split("_")
|
|
315
|
+
parts.first + parts[1..].map(&:capitalize).join
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
def ensure_storage!
|
|
321
|
+
FileUtils.mkdir_p(@database_directory)
|
|
322
|
+
FileUtils.mkdir_p(@files_directory)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def ensure_native_client!
|
|
326
|
+
@native_client ||= @tdjson.create_client_id
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def verify_arguments!
|
|
330
|
+
raise TypeError, "api_id must be an Integer" unless @api_id.is_a?(Integer)
|
|
331
|
+
raise TypeError, "api_hash must be a String" unless @api_hash.is_a?(String)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def boot_authorization_state
|
|
335
|
+
send_query("@type": "getAuthorizationState")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def set_options
|
|
339
|
+
return unless @td_options.is_a?(Hash)
|
|
340
|
+
|
|
341
|
+
@td_options.each do |name, value|
|
|
342
|
+
option_payload = case value
|
|
343
|
+
when String
|
|
344
|
+
{ "@type" => "optionValueString", "value" => value }
|
|
345
|
+
when Integer
|
|
346
|
+
{ "@type" => "optionValueInteger", "value" => value }
|
|
347
|
+
when TrueClass, FalseClass
|
|
348
|
+
{ "@type" => "optionValueBoolean", "value" => value }
|
|
349
|
+
else
|
|
350
|
+
next
|
|
351
|
+
end
|
|
352
|
+
send_query("@type": "setOption", "name": name.to_s, "value": option_payload)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def start_update_loop
|
|
357
|
+
@updates_thread = Thread.new do
|
|
358
|
+
while @running
|
|
359
|
+
begin
|
|
360
|
+
consume_once(0.5)
|
|
361
|
+
rescue => e
|
|
362
|
+
warn "[TDLIB] update loop error: #{e.class}: #{e.message}"
|
|
363
|
+
sleep 0.2
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def start_workers
|
|
370
|
+
worker_count = @workers.to_i
|
|
371
|
+
return if worker_count <= 0
|
|
372
|
+
|
|
373
|
+
@workers_threads = worker_count.times.map do
|
|
374
|
+
Thread.new do
|
|
375
|
+
while @running
|
|
376
|
+
begin
|
|
377
|
+
update = @queue.pop
|
|
378
|
+
run_update(update)
|
|
379
|
+
rescue StandardError => e
|
|
380
|
+
warn "[TDLIB] worker failed: #{e.class}: #{e.message}"
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def consume_once(timeout)
|
|
388
|
+
update = @tdjson.receive(@native_client, timeout: timeout)
|
|
389
|
+
return unless update
|
|
390
|
+
handle_incoming(update)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def handle_incoming(update)
|
|
394
|
+
update_extra_id = extra_id(update["@extra"])
|
|
395
|
+
if update_extra_id && @pending_mutex.synchronize { @pending.key?(update_extra_id) }
|
|
396
|
+
@pending_mutex.synchronize { @pending[update_extra_id] = update }
|
|
397
|
+
return
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
local_handler = @local_handlers[update["@type"]]
|
|
401
|
+
local_handler.call(update) if local_handler
|
|
402
|
+
|
|
403
|
+
if @workers_threads.empty?
|
|
404
|
+
run_update(update)
|
|
405
|
+
return
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
if @queue.size > @queue_size
|
|
409
|
+
warn "[TDLIB] queue is full; dropping update #{update["@type"]}"
|
|
410
|
+
return
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
@queue << update
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def process_authorization_update(update)
|
|
417
|
+
return unless update["@type"] == "updateAuthorizationState"
|
|
418
|
+
|
|
419
|
+
state = update.dig("authorization_state", "@type")
|
|
420
|
+
@authorization_state = state
|
|
421
|
+
case state
|
|
422
|
+
when "authorizationStateWaitTdlibParameters"
|
|
423
|
+
set_options
|
|
424
|
+
send_query(
|
|
425
|
+
"@type": "setTdlibParameters",
|
|
426
|
+
parameters: {
|
|
427
|
+
"@type": "tdlibParameters",
|
|
428
|
+
use_test_dc: @use_test_dc,
|
|
429
|
+
database_directory: @database_directory,
|
|
430
|
+
files_directory: @files_directory,
|
|
431
|
+
use_file_database: true,
|
|
432
|
+
use_chat_info_database: true,
|
|
433
|
+
use_message_database: true,
|
|
434
|
+
use_secret_chats: true,
|
|
435
|
+
api_id: @api_id,
|
|
436
|
+
api_hash: @api_hash,
|
|
437
|
+
system_language_code: @system_language_code,
|
|
438
|
+
device_model: @device_model,
|
|
439
|
+
system_version: @system_version,
|
|
440
|
+
application_version: @application_version
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
when "authorizationStateWaitEncryptionKey"
|
|
444
|
+
send_query(
|
|
445
|
+
"@type": "checkDatabaseEncryptionKey",
|
|
446
|
+
encryption_key: @database_encryption_key
|
|
447
|
+
)
|
|
448
|
+
when "authorizationStateWaitPhoneNumber"
|
|
449
|
+
if @bot_token
|
|
450
|
+
check_authentication_bot_token(@bot_token)
|
|
451
|
+
elsif @phone_number
|
|
452
|
+
send_query("@type": "setAuthenticationPhoneNumber", phone_number: @phone_number)
|
|
453
|
+
end
|
|
454
|
+
when "authorizationStateWaitCode"
|
|
455
|
+
run_update("@type" => "authCodeNeeded", "hint" => "Call check_authentication_code(code)")
|
|
456
|
+
when "authorizationStateWaitPassword"
|
|
457
|
+
run_update("@type" => "authPasswordNeeded", "hint" => "Call check_authentication_password(password)")
|
|
458
|
+
when "authorizationStateWaitRegistration"
|
|
459
|
+
run_update("@type" => "authRegistrationNeeded")
|
|
460
|
+
when "authorizationStateReady"
|
|
461
|
+
@authorized = true
|
|
462
|
+
run_update("@type" => "clientReady")
|
|
463
|
+
when "authorizationStateClosed"
|
|
464
|
+
@authorized = false
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def process_update_option(update)
|
|
469
|
+
value = update["value"]
|
|
470
|
+
parsed = if value.is_a?(Hash)
|
|
471
|
+
value["value"]
|
|
472
|
+
else
|
|
473
|
+
value
|
|
474
|
+
end
|
|
475
|
+
@td_options[update["name"]] = parsed
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def run_update(update)
|
|
479
|
+
run_handlers_for("initializer", update)
|
|
480
|
+
run_handlers_for(update["@type"].to_s, update)
|
|
481
|
+
rescue StopHandlers
|
|
482
|
+
ensure
|
|
483
|
+
run_handlers_for("finalizer", update)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def run_handlers_for(type, update)
|
|
487
|
+
handlers = @handlers[type]
|
|
488
|
+
return if handlers.nil? || handlers.empty?
|
|
489
|
+
|
|
490
|
+
handlers.each do |handler|
|
|
491
|
+
payload = handler.inner_object ? extract_inner_object(update) : update
|
|
492
|
+
next unless pass_filter?(handler, payload)
|
|
493
|
+
|
|
494
|
+
call_handler(handler, payload)
|
|
495
|
+
rescue StopHandlers
|
|
496
|
+
raise
|
|
497
|
+
rescue StandardError => e
|
|
498
|
+
warn "[TDLIB] handler failed: #{e.class}: #{e.message}"
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def extract_inner_object(update)
|
|
503
|
+
return update["message"] if update["@type"] == "updateNewMessage"
|
|
504
|
+
|
|
505
|
+
update
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def pass_filter?(handler, payload)
|
|
509
|
+
return true unless handler.filter.respond_to?(:call)
|
|
510
|
+
|
|
511
|
+
handler.filter.call(payload)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def call_handler(handler, payload)
|
|
515
|
+
timeout = handler.timeout || @default_handler_timeout
|
|
516
|
+
if timeout
|
|
517
|
+
Timeout.timeout(timeout) { handler.block.call(payload) }
|
|
518
|
+
else
|
|
519
|
+
handler.block.call(payload)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def sort_handlers(type)
|
|
524
|
+
@handlers[type].sort_by! do |h|
|
|
525
|
+
[h.position.nil? ? 1 : 0, h.position || 0]
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def with_extra(query)
|
|
530
|
+
@sequence += 1
|
|
531
|
+
q = stringify_keys(query)
|
|
532
|
+
q["@extra"] ||= { "id" => "gruby-#{@sequence}" }
|
|
533
|
+
q
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def extra_id(extra)
|
|
537
|
+
return nil if extra.nil?
|
|
538
|
+
return extra["id"] if extra.is_a?(Hash)
|
|
539
|
+
|
|
540
|
+
extra
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def stringify_keys(hash)
|
|
544
|
+
hash.each_with_object({}) do |(k, v), out|
|
|
545
|
+
key = k.to_s
|
|
546
|
+
out[key] = case v
|
|
547
|
+
when Hash
|
|
548
|
+
stringify_keys(v)
|
|
549
|
+
when Array
|
|
550
|
+
v.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
|
|
551
|
+
else
|
|
552
|
+
v
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def trap_signals
|
|
558
|
+
%w[INT TERM].each do |sig|
|
|
559
|
+
Signal.trap(sig) { @idle = false }
|
|
560
|
+
rescue ArgumentError
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
end
|