mtproto 0.0.19 → 0.0.20
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 +4 -4
- data/lib/mtproto/client/api/get_bot_callback_answer.rb +22 -0
- data/lib/mtproto/client/api/get_channel_difference.rb +22 -0
- data/lib/mtproto/client/api/import_bot_authorization.rb +27 -0
- data/lib/mtproto/client/api/send_message.rb +18 -3
- data/lib/mtproto/client/api.rb +21 -9
- data/lib/mtproto/client/rpc.rb +13 -1
- data/lib/mtproto/client.rb +129 -34
- data/lib/mtproto/dc.rb +31 -0
- data/lib/mtproto/markdown.rb +439 -0
- data/lib/mtproto/tl/constructors.rb +24 -0
- data/lib/mtproto/tl/message_entity.rb +154 -0
- data/lib/mtproto/tl/objects/bot_callback_answer.rb +75 -0
- data/lib/mtproto/tl/objects/channel_difference.rb +163 -0
- data/lib/mtproto/tl/objects/channels_edit_banned.rb +23 -6
- data/lib/mtproto/tl/objects/get_bot_info.rb +58 -0
- data/lib/mtproto/tl/objects/message.rb +26 -6
- data/lib/mtproto/tl/objects/messages_add_chat_user.rb +27 -0
- data/lib/mtproto/tl/objects/messages_create_chat.rb +47 -0
- data/lib/mtproto/tl/objects/reply_keyboard_markup.rb +26 -1
- data/lib/mtproto/tl/objects/send_message.rb +37 -3
- data/lib/mtproto/tl/objects/update.rb +15 -0
- data/lib/mtproto/tl/reader.rb +35 -8
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +3 -0
- metadata +12 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# messages.botCallbackAnswer — the reply to messages.getBotCallbackAnswer: the
|
|
6
|
+
# answer the pressed bot produced via setBotCallbackAnswer (or the server's
|
|
7
|
+
# empty default when the bot never answered). `message` is the toast/alert
|
|
8
|
+
# text, `alert` selects a modal over a toast, `url` (paired with `has_url`) is a
|
|
9
|
+
# game/redirect link, `native_ui` marks a Telegram-native prompt, and
|
|
10
|
+
# `cache_time` is how long the client may cache this answer.
|
|
11
|
+
class BotCallbackAnswer
|
|
12
|
+
CONSTRUCTOR = 0x36585ea4
|
|
13
|
+
|
|
14
|
+
attr_reader :alert, :has_url, :native_ui, :message, :url, :cache_time
|
|
15
|
+
|
|
16
|
+
def initialize(alert: false, has_url: false, native_ui: false, message: nil, url: nil, cache_time: 0)
|
|
17
|
+
@alert = alert
|
|
18
|
+
@has_url = has_url
|
|
19
|
+
@native_ui = native_ui
|
|
20
|
+
@message = message
|
|
21
|
+
@url = url
|
|
22
|
+
@cache_time = cache_time
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true
|
|
26
|
+
# has_url:flags.3?true native_ui:flags.4?true message:flags.0?string
|
|
27
|
+
# url:flags.2?string cache_time:int
|
|
28
|
+
def self.deserialize(data)
|
|
29
|
+
constructor = data[0, 4].unpack1('L<')
|
|
30
|
+
unless constructor == CONSTRUCTOR
|
|
31
|
+
raise "Expected botCallbackAnswer constructor, got 0x#{constructor.to_s(16)}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
flags = data[4, 4].unpack1('L<')
|
|
35
|
+
offset = 8
|
|
36
|
+
|
|
37
|
+
message = nil
|
|
38
|
+
message, offset = read_tl_string(data, offset) if flags.anybits?(1 << 0)
|
|
39
|
+
|
|
40
|
+
url = nil
|
|
41
|
+
url, offset = read_tl_string(data, offset) if flags.anybits?(1 << 2)
|
|
42
|
+
|
|
43
|
+
cache_time = data[offset, 4].unpack1('l<')
|
|
44
|
+
|
|
45
|
+
new(
|
|
46
|
+
alert: flags.anybits?(1 << 1),
|
|
47
|
+
has_url: flags.anybits?(1 << 3),
|
|
48
|
+
native_ui: flags.anybits?(1 << 4),
|
|
49
|
+
message: message,
|
|
50
|
+
url: url,
|
|
51
|
+
cache_time: cache_time
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def read_tl_string(data, offset)
|
|
59
|
+
first_byte = data.getbyte(offset)
|
|
60
|
+
if first_byte == 254
|
|
61
|
+
len = data.getbyte(offset + 1) |
|
|
62
|
+
(data.getbyte(offset + 2) << 8) |
|
|
63
|
+
(data.getbyte(offset + 3) << 16)
|
|
64
|
+
total = 4 + len
|
|
65
|
+
else
|
|
66
|
+
len = first_byte
|
|
67
|
+
total = 1 + len
|
|
68
|
+
end
|
|
69
|
+
padding = (4 - (total % 4)) % 4
|
|
70
|
+
[data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../schema'
|
|
4
|
+
require_relative 'message'
|
|
5
|
+
require_relative 'dialogs'
|
|
6
|
+
|
|
7
|
+
module MTProto
|
|
8
|
+
module TL
|
|
9
|
+
# updates.ChannelDifference — the reply to updates.getChannelDifference: a
|
|
10
|
+
# channel/supergroup's backlog since a given pts. Channels aren't pushed to a
|
|
11
|
+
# fresh session, so the client pulls this. Three variants:
|
|
12
|
+
# channelDifference — new_messages + other_updates + chats + users, with the new pts
|
|
13
|
+
# channelDifferenceEmpty — nothing new since pts (just pts/final/timeout)
|
|
14
|
+
# channelDifferenceTooLong — gap too large: a Dialog snapshot + a messages slice
|
|
15
|
+
# + chats + users; the caller resyncs pts from the dialog
|
|
16
|
+
# Mirrors UpdatesDifference: reads only the surfaced fields and advances the rest
|
|
17
|
+
# via Schema#skip (zero-drop, robust to an unmodelled tail).
|
|
18
|
+
class ChannelDifference
|
|
19
|
+
attr_reader :type, :final, :pts, :timeout, :new_messages, :chats, :users
|
|
20
|
+
|
|
21
|
+
SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
|
|
22
|
+
|
|
23
|
+
# Channel messages ride in new_messages, but message-bearing updates can also
|
|
24
|
+
# appear in other_updates — mine those too, like UpdatesDifference.
|
|
25
|
+
UPDATE_NEW_MESSAGE = 0x1f2b0afd
|
|
26
|
+
UPDATE_NEW_CHANNEL_MESSAGE = 0x62ba04d9
|
|
27
|
+
|
|
28
|
+
def initialize(type:, final: false, pts: nil, timeout: nil, new_messages: [], chats: [], users: [])
|
|
29
|
+
@type = type
|
|
30
|
+
@final = final
|
|
31
|
+
@pts = pts
|
|
32
|
+
@timeout = timeout
|
|
33
|
+
@new_messages = new_messages
|
|
34
|
+
@chats = chats
|
|
35
|
+
@users = users
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.schema
|
|
39
|
+
@schema ||= Schema.new(SCHEMA_PATH)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.deserialize(data)
|
|
43
|
+
constructor = data[0, 4].unpack1('L<')
|
|
44
|
+
|
|
45
|
+
case constructor
|
|
46
|
+
when Constructors::UPDATES_CHANNEL_DIFFERENCE_EMPTY
|
|
47
|
+
parse_empty(data)
|
|
48
|
+
when Constructors::UPDATES_CHANNEL_DIFFERENCE
|
|
49
|
+
parse_difference(data)
|
|
50
|
+
when Constructors::UPDATES_CHANNEL_DIFFERENCE_TOO_LONG
|
|
51
|
+
parse_too_long(data)
|
|
52
|
+
else
|
|
53
|
+
warn "Unknown ChannelDifference constructor: 0x#{constructor.to_s(16)}, treating as empty"
|
|
54
|
+
new(type: :empty, final: true)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int
|
|
62
|
+
def parse_empty(data)
|
|
63
|
+
flags = data[4, 4].unpack1('L<')
|
|
64
|
+
timeout = data[12, 4].unpack1('L<') if flags.anybits?(1 << 1)
|
|
65
|
+
new(type: :empty, final: flags.anybits?(1 << 0), pts: data[8, 4].unpack1('L<'), timeout: timeout)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int
|
|
69
|
+
# new_messages:Vector<Message> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User>
|
|
70
|
+
def parse_difference(data)
|
|
71
|
+
flags = data[4, 4].unpack1('L<')
|
|
72
|
+
pts = data[8, 4].unpack1('L<')
|
|
73
|
+
offset = 12
|
|
74
|
+
timeout = nil
|
|
75
|
+
if flags.anybits?(1 << 1)
|
|
76
|
+
timeout = data[offset, 4].unpack1('L<')
|
|
77
|
+
offset += 4
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
messages, offset = parse_messages_vector(data, offset)
|
|
81
|
+
other_messages, offset = parse_other_updates(data, offset)
|
|
82
|
+
chats, offset = Dialogs.chats_at(data, offset)
|
|
83
|
+
users, = Dialogs.users_at(data, offset)
|
|
84
|
+
|
|
85
|
+
new(
|
|
86
|
+
type: :difference,
|
|
87
|
+
final: flags.anybits?(1 << 0),
|
|
88
|
+
pts: pts,
|
|
89
|
+
timeout: timeout,
|
|
90
|
+
new_messages: messages + other_messages,
|
|
91
|
+
chats: chats,
|
|
92
|
+
users: users
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int
|
|
97
|
+
# dialog:Dialog messages:Vector<Message> chats:Vector<Chat> users:Vector<User>
|
|
98
|
+
# No inline pts: the fresh Dialog carries the new pts (its own flags.0). We skip
|
|
99
|
+
# the Dialog structurally and surface the messages slice + chats/users; the caller
|
|
100
|
+
# resyncs pts from the dialog before the next pull.
|
|
101
|
+
def parse_too_long(data)
|
|
102
|
+
flags = data[4, 4].unpack1('L<')
|
|
103
|
+
offset = 8
|
|
104
|
+
timeout = nil
|
|
105
|
+
if flags.anybits?(1 << 1)
|
|
106
|
+
timeout = data[offset, 4].unpack1('L<')
|
|
107
|
+
offset += 4
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
offset = schema.skip(data, offset) # dialog:Dialog
|
|
111
|
+
messages, offset = parse_messages_vector(data, offset)
|
|
112
|
+
chats, offset = Dialogs.chats_at(data, offset)
|
|
113
|
+
users, = Dialogs.users_at(data, offset)
|
|
114
|
+
|
|
115
|
+
new(
|
|
116
|
+
type: :too_long,
|
|
117
|
+
final: flags.anybits?(1 << 0),
|
|
118
|
+
timeout: timeout,
|
|
119
|
+
new_messages: messages,
|
|
120
|
+
chats: chats,
|
|
121
|
+
users: users
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_messages_vector(data, offset)
|
|
126
|
+
offset += 4 # vector constructor
|
|
127
|
+
count = data[offset, 4].unpack1('L<')
|
|
128
|
+
offset += 4
|
|
129
|
+
|
|
130
|
+
messages = []
|
|
131
|
+
count.times do
|
|
132
|
+
constructor = data[offset, 4].unpack1('L<')
|
|
133
|
+
msg = Message.deserialize(data, offset, constructor)
|
|
134
|
+
messages << msg.to_h if msg
|
|
135
|
+
offset = schema.skip(data, offset)
|
|
136
|
+
end
|
|
137
|
+
[messages, offset]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# other_updates:Vector<Update> — mine message-bearing updates
|
|
141
|
+
# (updateNewMessage/updateNewChannelMessage: the ctor, then an inner Message,
|
|
142
|
+
# then pts/pts_count), and advance the rest via schema.
|
|
143
|
+
def parse_other_updates(data, offset)
|
|
144
|
+
offset += 4 # vector constructor
|
|
145
|
+
count = data[offset, 4].unpack1('L<')
|
|
146
|
+
offset += 4
|
|
147
|
+
|
|
148
|
+
messages = []
|
|
149
|
+
count.times do
|
|
150
|
+
ctor = data[offset, 4].unpack1('L<')
|
|
151
|
+
if [UPDATE_NEW_MESSAGE, UPDATE_NEW_CHANNEL_MESSAGE].include?(ctor)
|
|
152
|
+
inner_off = offset + 4
|
|
153
|
+
msg = Message.deserialize(data, inner_off, data[inner_off, 4].unpack1('L<'))
|
|
154
|
+
messages << msg.to_h if msg
|
|
155
|
+
end
|
|
156
|
+
offset = schema.skip(data, offset)
|
|
157
|
+
end
|
|
158
|
+
[messages, offset]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -2,18 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
module MTProto
|
|
4
4
|
module TL
|
|
5
|
+
# channels.editBanned — restrict, ban, or (with no rights) lift restrictions on
|
|
6
|
+
# a participant in a supergroup. Returns Updates.
|
|
5
7
|
class ChannelsEditBanned
|
|
6
8
|
include Binary
|
|
7
9
|
|
|
8
10
|
CONSTRUCTOR = 0x96e6cd81
|
|
9
11
|
CHAT_BANNED_RIGHTS = 0x9f120418
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
# Flag bit per chatBannedRights right (the schema leaves bits 9, 11-14 and 16
|
|
14
|
+
# unused). view_messages is a full ban (can't even read); the rest restrict
|
|
15
|
+
# specific actions.
|
|
16
|
+
BANNED_RIGHT_BITS = {
|
|
17
|
+
view_messages: 0, send_messages: 1, send_media: 2, send_stickers: 3,
|
|
18
|
+
send_gifs: 4, send_games: 5, send_inline: 6, embed_links: 7, send_polls: 8,
|
|
19
|
+
change_info: 10, invite_users: 15, pin_messages: 17, manage_topics: 18,
|
|
20
|
+
send_photos: 19, send_videos: 20, send_roundvideos: 21, send_audios: 22,
|
|
21
|
+
send_voices: 23, send_docs: 24, send_plain: 25, edit_rank: 26, send_reactions: 27
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# channel is { id:, access_hash: }; participant is { type:, id:, access_hash: }.
|
|
25
|
+
# Pass any BANNED_RIGHT_BITS key as true to apply that restriction (e.g.
|
|
26
|
+
# view_messages: true bans, send_messages: true mutes); none/all-false with
|
|
27
|
+
# until_date 0 lifts. until_date is the unix expiry (0 = until lifted).
|
|
28
|
+
def initialize(channel:, participant:, until_date: 0, **rights)
|
|
29
|
+
unknown = rights.keys - BANNED_RIGHT_BITS.keys
|
|
30
|
+
raise ArgumentError, "unknown banned right(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
31
|
+
|
|
12
32
|
@channel = channel
|
|
13
33
|
@participant = participant
|
|
14
|
-
@view_messages = view_messages
|
|
15
|
-
@send_messages = send_messages
|
|
16
34
|
@until_date = until_date
|
|
35
|
+
@rights = rights
|
|
17
36
|
end
|
|
18
37
|
|
|
19
38
|
def serialize
|
|
@@ -27,9 +46,7 @@ module MTProto
|
|
|
27
46
|
private
|
|
28
47
|
|
|
29
48
|
def serialize_banned_rights
|
|
30
|
-
flags = 0
|
|
31
|
-
flags |= (1 << 0) if @view_messages
|
|
32
|
-
flags |= (1 << 1) if @send_messages
|
|
49
|
+
flags = BANNED_RIGHT_BITS.sum { |right, bit| @rights[right] ? (1 << bit) : 0 }
|
|
33
50
|
u32_b(CHAT_BANNED_RIGHTS) + u32_b(flags) + u32_b(@until_date)
|
|
34
51
|
end
|
|
35
52
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# bots.getBotInfo — read a bot's name/about/description for a language.
|
|
6
|
+
# Returns bots.botInfo. Result parsing belongs to a later layer; this object
|
|
7
|
+
# only builds the request.
|
|
8
|
+
class GetBotInfo
|
|
9
|
+
include Binary
|
|
10
|
+
|
|
11
|
+
CONSTRUCTOR = 0xdcd914fd
|
|
12
|
+
INPUT_USER = 0xf21158c6
|
|
13
|
+
INPUT_USER_SELF = 0xf7c1b13f
|
|
14
|
+
|
|
15
|
+
# bot: { id:, access_hash: } or { type: :self }; nil targets the calling bot
|
|
16
|
+
def initialize(bot: nil, lang_code: '')
|
|
17
|
+
@bot = bot
|
|
18
|
+
@lang_code = lang_code
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def serialize
|
|
22
|
+
flags = 0
|
|
23
|
+
flags |= (1 << 0) if @bot
|
|
24
|
+
|
|
25
|
+
result = u32_b(CONSTRUCTOR)
|
|
26
|
+
result += u32_b(flags)
|
|
27
|
+
result += serialize_input_user(@bot) if @bot
|
|
28
|
+
result += serialize_tl_string(@lang_code)
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def serialize_input_user(user)
|
|
35
|
+
if user[:type] == :self
|
|
36
|
+
u32_b(INPUT_USER_SELF)
|
|
37
|
+
else
|
|
38
|
+
u32_b(INPUT_USER) + u64_b(user[:id]) + u64_b(user[:access_hash] || 0)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serialize_tl_string(str)
|
|
43
|
+
bytes = str.to_s.encode('UTF-8').bytes
|
|
44
|
+
length = bytes.length
|
|
45
|
+
if length <= 253
|
|
46
|
+
[length] + bytes + padding(length + 1)
|
|
47
|
+
else
|
|
48
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def padding(current_length)
|
|
53
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
54
|
+
[0] * pad_length
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../reader'
|
|
4
|
+
require_relative '../message_entity'
|
|
4
5
|
|
|
5
6
|
module MTProto
|
|
6
7
|
module TL
|
|
@@ -20,7 +21,8 @@ module MTProto
|
|
|
20
21
|
MESSAGE_FWD_HEADER = 0x4e4df4bb
|
|
21
22
|
|
|
22
23
|
FIELDS = %i[id date message peer_type peer_id from_type from_id out media
|
|
23
|
-
document photo is_reply reply_to_msg_id
|
|
24
|
+
document photo is_reply reply_to_msg_id reply_to_peer_type reply_to_peer_id
|
|
25
|
+
is_quote quote_text quote_offset entities fwd_from_type fwd_from_id].freeze
|
|
24
26
|
|
|
25
27
|
attr_reader(*FIELDS)
|
|
26
28
|
|
|
@@ -63,10 +65,9 @@ module MTProto
|
|
|
63
65
|
offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
|
|
64
66
|
offset = x.schema.skip(data, offset) if flags2.anybits?(1 << 19) # guestchat_via_from (layer 227, Peer)
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
reply_to_msg_id = nil
|
|
68
|
+
reply = nil
|
|
68
69
|
if flags.anybits?(1 << 3) # reply_to
|
|
69
|
-
|
|
70
|
+
reply = x.parse_reply_to(data, offset)
|
|
70
71
|
offset = x.schema.skip(data, offset)
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -77,15 +78,34 @@ module MTProto
|
|
|
77
78
|
media = has_media ? x.parse_media(data, offset) : nil
|
|
78
79
|
document = has_media ? x.parse_document(data, offset) : nil
|
|
79
80
|
photo = has_media ? x.parse_photo(data, offset) : nil
|
|
81
|
+
entities = parse_entities(data, offset, flags, has_media)
|
|
80
82
|
|
|
81
83
|
new(id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id,
|
|
82
84
|
from_type: from_type, from_id: from_id, out: flags.anybits?(1 << 1),
|
|
83
|
-
media: media, document: document, photo: photo,
|
|
84
|
-
|
|
85
|
+
media: media, document: document, photo: photo,
|
|
86
|
+
is_reply: reply ? reply[:is_reply] : false, reply_to_msg_id: reply && reply[:msg_id],
|
|
87
|
+
reply_to_peer_type: reply && reply[:peer_type], reply_to_peer_id: reply && reply[:peer_id],
|
|
88
|
+
is_quote: reply ? reply[:is_quote] : false, quote_text: reply && reply[:quote_text],
|
|
89
|
+
quote_offset: reply && reply[:quote_offset], entities: entities,
|
|
90
|
+
fwd_from_type: fwd_from_type, fwd_from_id: fwd_from_id)
|
|
85
91
|
end
|
|
86
92
|
|
|
87
93
|
private
|
|
88
94
|
|
|
95
|
+
# entities (flags.7) sit past media (flags.9) and reply_markup (flags.6) in the
|
|
96
|
+
# message tail; step over those (schema-sized) then decode the
|
|
97
|
+
# Vector<MessageEntity>. Best-effort: any misread yields [] so a message is
|
|
98
|
+
# never dropped over its formatting.
|
|
99
|
+
def parse_entities(data, offset, flags, has_media)
|
|
100
|
+
offset = x.schema.skip(data, offset) if has_media
|
|
101
|
+
offset = x.schema.skip(data, offset) if flags.anybits?(1 << 6) # reply_markup
|
|
102
|
+
return [] unless flags.anybits?(1 << 7) # entities
|
|
103
|
+
|
|
104
|
+
MessageEntities.parse(data, offset).first
|
|
105
|
+
rescue StandardError
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
# The low-level TL primitives, shared from Reader.
|
|
90
110
|
def x
|
|
91
111
|
Reader
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# messages.addChatUser — add a user to a basic group chat. Returns
|
|
6
|
+
# messages.InvitedUsers.
|
|
7
|
+
class MessagesAddChatUser
|
|
8
|
+
include Binary
|
|
9
|
+
|
|
10
|
+
CONSTRUCTOR = 0xcbc6d107
|
|
11
|
+
INPUT_USER = 0xf21158c6
|
|
12
|
+
|
|
13
|
+
def initialize(chat_id:, user:, fwd_limit: 50)
|
|
14
|
+
@chat_id = chat_id
|
|
15
|
+
@user = user
|
|
16
|
+
@fwd_limit = fwd_limit
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def serialize
|
|
20
|
+
u32_b(CONSTRUCTOR) +
|
|
21
|
+
u64_b(@chat_id) +
|
|
22
|
+
u32_b(INPUT_USER) + u64_b(@user[:id]) + u64_b(@user[:access_hash] || 0) +
|
|
23
|
+
u32_b(@fwd_limit)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
# messages.createChat — create a basic group chat seeded with an initial set
|
|
6
|
+
# of users (a basic chat cannot be created empty). Returns
|
|
7
|
+
# messages.InvitedUsers wrapping the creation Updates.
|
|
8
|
+
class MessagesCreateChat
|
|
9
|
+
include Binary
|
|
10
|
+
|
|
11
|
+
CONSTRUCTOR = 0x92ceddd4
|
|
12
|
+
INPUT_USER = 0xf21158c6
|
|
13
|
+
|
|
14
|
+
def initialize(users:, title:)
|
|
15
|
+
@users = users
|
|
16
|
+
@title = title
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def serialize
|
|
20
|
+
result = u32_b(CONSTRUCTOR)
|
|
21
|
+
result += u32_b(0) # flags: no ttl_period
|
|
22
|
+
result += u32_b(Constructors::VECTOR) + u32_b(@users.length)
|
|
23
|
+
@users.each do |u|
|
|
24
|
+
result += u32_b(INPUT_USER) + u64_b(u[:id]) + u64_b(u[:access_hash] || 0)
|
|
25
|
+
end
|
|
26
|
+
result += serialize_tl_string(@title)
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def serialize_tl_string(str)
|
|
33
|
+
bytes = str.to_s.b.bytes
|
|
34
|
+
length = bytes.length
|
|
35
|
+
if length <= 253
|
|
36
|
+
[length] + bytes + padding(length + 1)
|
|
37
|
+
else
|
|
38
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def padding(current_length)
|
|
43
|
+
[0] * ((4 - (current_length % 4)) % 4)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -9,16 +9,23 @@ module MTProto
|
|
|
9
9
|
KEYBOARD_BUTTON_ROW = 0x77608b83
|
|
10
10
|
|
|
11
11
|
# buttons: a single row of button objects (each responds to #serialize)
|
|
12
|
-
def initialize(buttons:, resize: true, single_use: false
|
|
12
|
+
def initialize(buttons:, resize: true, single_use: false,
|
|
13
|
+
selective: false, persistent: false, placeholder: nil)
|
|
13
14
|
@buttons = buttons
|
|
14
15
|
@resize = resize
|
|
15
16
|
@single_use = single_use
|
|
17
|
+
@selective = selective
|
|
18
|
+
@persistent = persistent
|
|
19
|
+
@placeholder = placeholder
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def serialize
|
|
19
23
|
flags = 0
|
|
20
24
|
flags |= (1 << 0) if @resize
|
|
21
25
|
flags |= (1 << 1) if @single_use
|
|
26
|
+
flags |= (1 << 2) if @selective
|
|
27
|
+
flags |= (1 << 3) if @placeholder
|
|
28
|
+
flags |= (1 << 4) if @persistent
|
|
22
29
|
|
|
23
30
|
result = u32_b(CONSTRUCTOR)
|
|
24
31
|
result += u32_b(flags)
|
|
@@ -28,8 +35,26 @@ module MTProto
|
|
|
28
35
|
result += u32_b(Constructors::VECTOR)
|
|
29
36
|
result += u32_b(@buttons.size)
|
|
30
37
|
@buttons.each { |button| result += button.serialize }
|
|
38
|
+
result += serialize_tl_string(@placeholder) if @placeholder
|
|
31
39
|
result
|
|
32
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def serialize_tl_string(str)
|
|
45
|
+
bytes = str.encode('UTF-8').bytes
|
|
46
|
+
length = bytes.length
|
|
47
|
+
if length <= 253
|
|
48
|
+
[length] + bytes + padding(length + 1)
|
|
49
|
+
else
|
|
50
|
+
[254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def padding(current_length)
|
|
55
|
+
pad_length = (4 - (current_length % 4)) % 4
|
|
56
|
+
[0] * pad_length
|
|
57
|
+
end
|
|
33
58
|
end
|
|
34
59
|
end
|
|
35
60
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require_relative '../message_entity'
|
|
4
5
|
|
|
5
6
|
module MTProto
|
|
6
7
|
module TL
|
|
@@ -8,36 +9,69 @@ module MTProto
|
|
|
8
9
|
include Binary
|
|
9
10
|
|
|
10
11
|
INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
|
|
12
|
+
INPUT_RICH_MESSAGE_MARKDOWN = 0x004b572c
|
|
11
13
|
|
|
12
|
-
def initialize(peer:, message
|
|
14
|
+
def initialize(peer:, message: '', random_id: nil, reply_to: nil, reply_markup: nil, silent: false,
|
|
15
|
+
entities: nil, rich_markdown: nil, quote_text: nil, quote_offset: nil)
|
|
13
16
|
@peer = peer
|
|
14
17
|
@message = message
|
|
15
18
|
@random_id = random_id || SecureRandom.random_number(2**63)
|
|
16
19
|
@reply_to = reply_to
|
|
17
20
|
@reply_markup = reply_markup
|
|
18
21
|
@silent = silent
|
|
22
|
+
@entities = entities
|
|
23
|
+
@rich_markdown = rich_markdown
|
|
24
|
+
@quote_text = quote_text
|
|
25
|
+
@quote_offset = quote_offset
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
def serialize
|
|
22
29
|
flags = 0
|
|
23
30
|
flags |= (1 << 0) if @reply_to
|
|
24
31
|
flags |= (1 << 2) if @reply_markup
|
|
32
|
+
flags |= (1 << 3) if entities?
|
|
25
33
|
flags |= (1 << 5) if @silent
|
|
34
|
+
flags |= (1 << 23) if @rich_markdown # rich_message
|
|
26
35
|
|
|
27
36
|
result = u32_b(Constructors::MESSAGES_SEND_MESSAGE)
|
|
28
37
|
result += u32_b(flags)
|
|
29
38
|
result += serialize_input_peer
|
|
30
39
|
result += serialize_reply_to if @reply_to
|
|
31
|
-
|
|
40
|
+
# Server-side rich markdown carries the whole message; the text field goes
|
|
41
|
+
# empty and no entities are sent (matches Telegram Desktop's sendRichMessage).
|
|
42
|
+
result += serialize_tl_string(@rich_markdown ? '' : @message)
|
|
32
43
|
result += u64_b(@random_id)
|
|
33
44
|
result += @reply_markup.serialize if @reply_markup
|
|
45
|
+
result += MessageEntities.serialize(@entities) if entities?
|
|
46
|
+
result += serialize_rich_markdown if @rich_markdown
|
|
34
47
|
result
|
|
35
48
|
end
|
|
36
49
|
|
|
37
50
|
private
|
|
38
51
|
|
|
52
|
+
def entities?
|
|
53
|
+
@entities && !@entities.empty? && !@rich_markdown
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# inputRichMessageMarkdown#004b572c flags:# rtl:flags.0?true noautolink:flags.1?true
|
|
57
|
+
# markdown:string files:flags.2?Vector<InputRichFile>. Text-only: flags=0, no files.
|
|
58
|
+
def serialize_rich_markdown
|
|
59
|
+
u32_b(INPUT_RICH_MESSAGE_MARKDOWN) + u32_b(0) + serialize_tl_string(@rich_markdown)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# inputReplyToMessage#3bd4b7c2 flags:# reply_to_msg_id:int quote_text:flags.2?string
|
|
63
|
+
# quote_offset:flags.4?int. A quote replies to @reply_to while pinning the exact
|
|
64
|
+
# fragment (@quote_text at @quote_offset in that message); Telegram validates the
|
|
65
|
+
# text against the original at the offset.
|
|
39
66
|
def serialize_reply_to
|
|
40
|
-
|
|
67
|
+
flags = 0
|
|
68
|
+
flags |= (1 << 2) if @quote_text
|
|
69
|
+
flags |= (1 << 4) unless @quote_offset.nil?
|
|
70
|
+
|
|
71
|
+
result = u32_b(INPUT_REPLY_TO_MESSAGE) + u32_b(flags) + u32_b(@reply_to)
|
|
72
|
+
result += serialize_tl_string(@quote_text) if @quote_text
|
|
73
|
+
result += u32_b(@quote_offset) unless @quote_offset.nil?
|
|
74
|
+
result
|
|
41
75
|
end
|
|
42
76
|
|
|
43
77
|
def serialize_input_peer
|
|
@@ -23,6 +23,21 @@ module MTProto
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Whether a top-level pushed message (as delivered to Client#on_update with
|
|
27
|
+
# its outer constructor and body) carries an updateLoginToken — the signal
|
|
28
|
+
# that a QR login token was scanned and accepted. Arrives bare or, in
|
|
29
|
+
# practice, wrapped in updateShort.
|
|
30
|
+
def self.login_token?(constructor, body)
|
|
31
|
+
case constructor
|
|
32
|
+
when Constructors::UPDATE_LOGIN_TOKEN
|
|
33
|
+
true
|
|
34
|
+
when Constructors::UPDATE_SHORT
|
|
35
|
+
UpdateShort.deserialize(body).update.type == 'login_token'
|
|
36
|
+
else
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
26
41
|
class << self
|
|
27
42
|
private
|
|
28
43
|
|