mtproto 0.0.17 → 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
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # channels.editAdmin — grant, change, or (with no rights) revoke a participant's
6
+ # admin rights in a channel or supergroup. Returns Updates.
7
+ class ChannelsEditAdmin
8
+ include Binary
9
+
10
+ CONSTRUCTOR = 0x9a98ad68
11
+ CHAT_ADMIN_RIGHTS = 0x5fb224d5
12
+ INPUT_USER = 0xf21158c6
13
+
14
+ # Flag bit per chatAdminRights right (the schema leaves bits 6 and 8 unused).
15
+ ADMIN_RIGHT_BITS = {
16
+ change_info: 0, post_messages: 1, edit_messages: 2, delete_messages: 3,
17
+ ban_users: 4, invite_users: 5, pin_messages: 7, add_admins: 9, anonymous: 10,
18
+ manage_call: 11, other: 12, manage_topics: 13, post_stories: 14,
19
+ edit_stories: 15, delete_stories: 16, manage_direct_messages: 17, manage_ranks: 18
20
+ }.freeze
21
+
22
+ # channel and user are { id:, access_hash: }. Pass any ADMIN_RIGHT_BITS key as
23
+ # true to grant that right (e.g. post_messages: true); none/all-false revokes
24
+ # the participant's admin status. rank is the optional custom admin title.
25
+ def initialize(channel:, user:, rank: '', **rights)
26
+ unknown = rights.keys - ADMIN_RIGHT_BITS.keys
27
+ raise ArgumentError, "unknown admin right(s): #{unknown.join(', ')}" unless unknown.empty?
28
+
29
+ @channel = channel
30
+ @user = user
31
+ @rank = rank
32
+ @rights = rights
33
+ end
34
+
35
+ def serialize
36
+ result = u32_b(CONSTRUCTOR)
37
+ result += u32_b(rank_flag)
38
+ result += serialize_input_channel
39
+ result += serialize_input_user
40
+ result += serialize_admin_rights
41
+ result += serialize_tl_string(@rank) unless @rank.to_s.empty?
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ # The method's own flags word: only bit 0, set when the optional rank is present.
48
+ def rank_flag
49
+ @rank.to_s.empty? ? 0 : (1 << 0)
50
+ end
51
+
52
+ def serialize_input_channel
53
+ u32_b(Constructors::INPUT_CHANNEL) + u64_b(@channel[:id]) + u64_b(@channel[:access_hash])
54
+ end
55
+
56
+ def serialize_input_user
57
+ u32_b(INPUT_USER) + u64_b(@user[:id]) + u64_b(@user[:access_hash] || 0)
58
+ end
59
+
60
+ def serialize_admin_rights
61
+ flags = ADMIN_RIGHT_BITS.sum { |right, bit| @rights[right] ? (1 << bit) : 0 }
62
+ u32_b(CHAT_ADMIN_RIGHTS) + u32_b(flags)
63
+ end
64
+
65
+ def serialize_tl_string(str)
66
+ bytes = str.to_s.b.bytes
67
+ length = bytes.length
68
+ if length <= 253
69
+ [length] + bytes + padding(length + 1)
70
+ else
71
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
72
+ end
73
+ end
74
+
75
+ def padding(current_length)
76
+ [0] * ((4 - (current_length % 4)) % 4)
77
+ end
78
+ end
79
+ end
80
+ 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
 
@@ -6,7 +6,7 @@ module MTProto
6
6
  module TL
7
7
  class Dialogs
8
8
  Dialog = Struct.new(:peer_type, :peer_id, :top_message, :unread_count, :pts, keyword_init: true)
9
- Chat = Struct.new(:id, :title, :type, :access_hash, keyword_init: true)
9
+ Chat = Struct.new(:id, :title, :type, :access_hash, :username, keyword_init: true)
10
10
 
11
11
  SCHEMA_PATH = File.expand_path('../../../../data/tl-schema.json', __dir__)
12
12
 
@@ -310,7 +310,8 @@ module MTProto
310
310
  offset += 8
311
311
  end
312
312
  title, offset = read_tl_string(data, offset)
313
- offset = skip_tl_string(data, offset) if flags.anybits?(1 << 6) # username
313
+ username = nil
314
+ username, offset = read_tl_string(data, offset) if flags.anybits?(1 << 6) # username
314
315
  offset = schema.skip(data, offset) # photo (ChatPhoto)
315
316
  offset += 4 # date
316
317
  if flags.anybits?(1 << 9) # restriction_reason Vector<RestrictionReason>
@@ -334,7 +335,7 @@ module MTProto
334
335
  offset += 8 if flags2.anybits?(1 << 18) # linked_monoforum_id (long)
335
336
 
336
337
  type = flags.anybits?(1 << 8) ? :supergroup : :channel
337
- [Chat.new(id: id, title: title, type: type, access_hash: access_hash), offset]
338
+ [Chat.new(id: id, title: title, type: type, access_hash: access_hash, username: username), offset]
338
339
  end
339
340
 
340
341
  # chatForbidden#6592a1a7 id:long title:string
@@ -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