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.
@@ -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
- def initialize(channel:, participant:, view_messages: false, send_messages: false, until_date: 0)
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 fwd_from_type fwd_from_id].freeze
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
- is_reply = false
67
- reply_to_msg_id = nil
68
+ reply = nil
68
69
  if flags.anybits?(1 << 3) # reply_to
69
- reply_to_msg_id, is_reply = x.parse_reply_to(data, offset)
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, is_reply: is_reply,
84
- reply_to_msg_id: reply_to_msg_id, fwd_from_type: fwd_from_type, fwd_from_id: fwd_from_id)
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:, random_id: nil, reply_to: nil, reply_markup: nil, silent: false)
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
- result += serialize_tl_string(@message)
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
- u32_b(INPUT_REPLY_TO_MESSAGE) + u32_b(0) + u32_b(@reply_to)
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