mtproto 0.0.15 → 0.0.17

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/data/tl-schema.json +47158 -42684
  3. data/lib/mtproto/auth_key_generator.rb +2 -2
  4. data/lib/mtproto/client/api/export_authorization.rb +17 -0
  5. data/lib/mtproto/client/api/import_authorization.rb +23 -0
  6. data/lib/mtproto/client/api.rb +2 -0
  7. data/lib/mtproto/client.rb +1 -1
  8. data/lib/mtproto/file_downloader.rb +122 -0
  9. data/lib/mtproto/tl/constructor_names.rb +314 -109
  10. data/lib/mtproto/tl/constructors.rb +16 -8
  11. data/lib/mtproto/tl/objects/bot_command_scope.rb +81 -0
  12. data/lib/mtproto/tl/objects/channels_join_channel.rb +1 -1
  13. data/lib/mtproto/tl/objects/channels_update_username.rb +42 -0
  14. data/lib/mtproto/tl/objects/contacts.rb +1 -1
  15. data/lib/mtproto/tl/objects/dialogs.rb +15 -10
  16. data/lib/mtproto/tl/objects/export_authorization.rb +17 -0
  17. data/lib/mtproto/tl/objects/exported_authorization.rb +32 -0
  18. data/lib/mtproto/tl/objects/forward_messages.rb +5 -2
  19. data/lib/mtproto/tl/objects/get_bot_callback_answer.rb +67 -0
  20. data/lib/mtproto/tl/objects/get_bot_commands.rb +46 -0
  21. data/lib/mtproto/tl/objects/get_file.rb +10 -2
  22. data/lib/mtproto/tl/objects/import_authorization.rb +38 -0
  23. data/lib/mtproto/tl/objects/keyboard_button_callback.rb +50 -0
  24. data/lib/mtproto/tl/objects/message.rb +97 -21
  25. data/lib/mtproto/tl/objects/messages.rb +7 -74
  26. data/lib/mtproto/tl/objects/reply_inline_markup.rb +30 -0
  27. data/lib/mtproto/tl/objects/reset_bot_commands.rb +45 -0
  28. data/lib/mtproto/tl/objects/send_media.rb +76 -4
  29. data/lib/mtproto/tl/objects/send_message.rb +4 -2
  30. data/lib/mtproto/tl/objects/send_message_action.rb +48 -0
  31. data/lib/mtproto/tl/objects/set_bot_callback_answer.rb +54 -0
  32. data/lib/mtproto/tl/objects/set_bot_commands.rb +6 -3
  33. data/lib/mtproto/tl/objects/set_bot_guest_chat_result.rb +84 -0
  34. data/lib/mtproto/tl/objects/set_typing.rb +49 -0
  35. data/lib/mtproto/tl/objects/update_status.rb +24 -0
  36. data/lib/mtproto/tl/objects/updates.rb +117 -0
  37. data/lib/mtproto/tl/objects/updates_difference.rb +16 -119
  38. data/lib/mtproto/tl/reader.rb +188 -0
  39. data/lib/mtproto/transport/abridged_packet_codec.rb +5 -1
  40. data/lib/mtproto/unencrypted_message.rb +39 -0
  41. data/lib/mtproto/version.rb +1 -1
  42. data/lib/mtproto.rb +4 -1
  43. data/scripts/gen_constructor_names.rb +72 -0
  44. data/scripts/tl_to_json.rb +72 -0
  45. data/scripts/verify_ids.rb +33 -0
  46. metadata +25 -2
  47. data/lib/mtproto/message/message.rb +0 -85
@@ -1,37 +1,113 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../reader'
4
+
3
5
  module MTProto
4
6
  module TL
7
+ # The high-level chat-message TL structure `message#7600b9d3` (text/media/
8
+ # fwd_from/reply/sender/...). Owns the field walk for that one constructor; the
9
+ # low-level TL reads it needs (peer/string/bytes/media/document, plus the
10
+ # shared schema) live in Reader and are reused here.
11
+ #
12
+ # Reading is decoupled from alignment: `.deserialize` only READS the fields we
13
+ # surface and returns a Message (or nil for a non-`message` constructor — e.g.
14
+ # messageService/messageEmpty). Advancing past the whole object is the caller's
15
+ # job via `Schema#skip`, so any unmodelled tail never drops the message. This is
16
+ # the single parser for all three inbound paths (live `Updates` push,
17
+ # `updates.getDifference`, and `messages.getHistory`).
5
18
  class Message
6
- attr_reader :auth_key_id, :msg_id, :body
19
+ MESSAGE = Constructors::MESSAGE
20
+ MESSAGE_FWD_HEADER = 0x4e4df4bb
7
21
 
8
- def initialize(auth_key_id:, msg_id:, body:)
9
- @auth_key_id = auth_key_id
10
- @msg_id = msg_id
11
- @body = body
12
- end
22
+ 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
+
25
+ attr_reader(*FIELDS)
26
+
27
+ # `text` is the message body; a get_history-friendly alias for `message`.
28
+ alias text message
13
29
 
14
- def serialize
15
- auth_key_id_bytes = [@auth_key_id].pack('Q<')
16
- msg_id_bytes = [@msg_id].pack('Q<')
17
- body_length = [@body.bytesize].pack('L<')
30
+ def initialize(attrs)
31
+ FIELDS.each { |f| instance_variable_set("@#{f}", attrs[f]) }
32
+ end
18
33
 
19
- auth_key_id_bytes + msg_id_bytes + body_length + @body
34
+ # The legacy hash shape the Updates / UpdatesDifference consumers expect.
35
+ def to_h
36
+ FIELDS.each_with_object({}) { |f, h| h[f] = public_send(f) }
20
37
  end
21
38
 
22
- def self.deserialize(data)
23
- if data.bytesize < 20
24
- raise(ArgumentError,
25
- 'Invalid MTProto message: expected at least 20 bytes, ' \
26
- "got #{data.bytesize} bytes (hex: #{data.unpack1('H*')})")
39
+ class << self
40
+ def deserialize(data, offset, constructor)
41
+ return unless constructor == MESSAGE
42
+
43
+ offset += 4 # constructor
44
+ flags = data[offset, 4].unpack1('L<')
45
+ offset += 4
46
+ flags2 = data[offset, 4].unpack1('L<')
47
+ offset += 4
48
+ id = data[offset, 4].unpack1('l<')
49
+ offset += 4
50
+
51
+ from_type = from_id = nil
52
+ from_type, from_id, offset = x.parse_peer(data, offset) if flags.anybits?(1 << 8) # from_id (Peer)
53
+ offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
54
+ _, offset = x.read_tl_string(data, offset) if flags2.anybits?(1 << 12) # from_rank (layer 227)
55
+ peer_type, peer_id, offset = x.parse_peer(data, offset)
56
+ offset = x.schema.skip(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
57
+ fwd_from_type = fwd_from_id = nil
58
+ if flags.anybits?(1 << 2) # fwd_from
59
+ fwd_from_type, fwd_from_id = parse_fwd_origin(data, offset)
60
+ offset = x.schema.skip(data, offset)
61
+ end
62
+ offset += 8 if flags.anybits?(1 << 11) # via_bot_id
63
+ offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
64
+ offset = x.schema.skip(data, offset) if flags2.anybits?(1 << 19) # guestchat_via_from (layer 227, Peer)
65
+
66
+ is_reply = false
67
+ reply_to_msg_id = nil
68
+ if flags.anybits?(1 << 3) # reply_to
69
+ reply_to_msg_id, is_reply = x.parse_reply_to(data, offset)
70
+ offset = x.schema.skip(data, offset)
71
+ end
72
+
73
+ date = data[offset, 4].unpack1('L<')
74
+ offset += 4
75
+ message_text, offset = x.read_tl_string(data, offset)
76
+ has_media = flags.anybits?(1 << 9)
77
+ media = has_media ? x.parse_media(data, offset) : nil
78
+ document = has_media ? x.parse_document(data, offset) : nil
79
+ photo = has_media ? x.parse_photo(data, offset) : nil
80
+
81
+ new(id: id, date: date, message: message_text, peer_type: peer_type, peer_id: peer_id,
82
+ 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)
27
85
  end
28
86
 
29
- auth_key_id = data[0, 8].unpack1('Q<')
30
- msg_id = data[8, 8].unpack1('Q<')
31
- body_length = data[16, 4].unpack1('L<')
32
- body = data[20, body_length]
87
+ private
88
+
89
+ # The low-level TL primitives, shared from Reader.
90
+ def x
91
+ Reader
92
+ end
33
93
 
34
- new(auth_key_id: auth_key_id, msg_id: msg_id, body: body)
94
+ # messageFwdHeader#4e4df4bb flags:# imported:flags.7?true from_id:flags.0?Peer
95
+ # ... — from_id is the forward origin (a peerChannel for a forwarded channel
96
+ # post). imported (flags.7) is a flag-only bool, so from_id sits right after
97
+ # the flags. => [type, id] or [nil, nil].
98
+ def parse_fwd_origin(data, offset)
99
+ return [nil, nil] unless data[offset, 4].unpack1('L<') == MESSAGE_FWD_HEADER
100
+
101
+ io = offset + 4
102
+ flags = data[io, 4].unpack1('L<')
103
+ io += 4
104
+ return [nil, nil] unless flags.anybits?(1 << 0) # from_id present
105
+
106
+ type, id, = x.parse_peer(data, io)
107
+ [type, id]
108
+ rescue StandardError
109
+ [nil, nil]
110
+ end
35
111
  end
36
112
  end
37
113
  end
@@ -1,20 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../schema'
4
+ require_relative 'message'
4
5
 
5
6
  module MTProto
6
7
  module TL
8
+ # A messages.messages / messagesSlice / channelMessages container (e.g. the
9
+ # messages.getHistory reply). Its messages are TL::Message instances.
7
10
  class Messages
8
- Message = Struct.new(:id, :from_id, :date, :text, keyword_init: true)
9
-
10
- MESSAGES_MESSAGES = 0x8c718e87
11
- MESSAGES_SLICE = 0x762b263d
11
+ MESSAGES_MESSAGES = 0x1d73e7ea
12
+ MESSAGES_SLICE = 0x5f206716
12
13
  MESSAGES_CHANNEL = 0xc776ba4e
13
14
 
14
- MESSAGE = Constructors::MESSAGE
15
- MESSAGE_SERVICE = 0x7a800e0a
16
- MESSAGE_EMPTY = 0x90a6ca84
17
-
18
15
  VALID_CONSTRUCTORS = [MESSAGES_MESSAGES, MESSAGES_SLICE, MESSAGES_CHANNEL].freeze
19
16
 
20
17
  attr_reader :messages, :count
@@ -86,76 +83,12 @@ module MTProto
86
83
  messages = []
87
84
  count.times do
88
85
  constructor = data[offset, 4].unpack1('L<')
89
-
90
- if constructor == MESSAGE
91
- msg, = parse_message(data, offset)
92
- messages << msg if msg
93
- end
94
-
86
+ msg = Message.deserialize(data, offset, constructor)
87
+ messages << msg if msg
95
88
  offset = schema.skip(data, offset)
96
89
  end
97
90
  [messages, offset]
98
91
  end
99
-
100
- # message#9815cec8 flags:# flags2:# id:int
101
- # from_id:flags.8?Peer ... date:int message:string ...
102
- def parse_message(data, offset)
103
- offset += 4 # constructor
104
- flags = data[offset, 4].unpack1('L<')
105
- offset += 4
106
- flags2 = data[offset, 4].unpack1('L<')
107
- offset += 4
108
- id = data[offset, 4].unpack1('l<')
109
- offset += 4
110
-
111
- from_id = nil
112
- _, from_id, offset = parse_peer(data, offset) if flags.anybits?(1 << 8)
113
- offset += 4 if flags.anybits?(1 << 29) # from_boosts_applied
114
- _, _, offset = parse_peer(data, offset) # peer_id (skip)
115
- _, _, offset = parse_peer(data, offset) if flags.anybits?(1 << 28) # saved_peer_id
116
- offset = schema.skip(data, offset) if flags.anybits?(1 << 2) # fwd_from
117
- offset += 8 if flags.anybits?(1 << 11) # via_bot_id
118
- offset += 8 if flags2.anybits?(1 << 0) # via_business_bot_id
119
- offset = schema.skip(data, offset) if flags.anybits?(1 << 3) # reply_to
120
-
121
- date = data[offset, 4].unpack1('L<')
122
- offset += 4
123
-
124
- text, offset = read_tl_string(data, offset)
125
-
126
- msg = Message.new(id: id, from_id: from_id, date: date, text: text)
127
- [msg, offset]
128
- end
129
-
130
- def parse_peer(data, offset)
131
- constructor = data[offset, 4].unpack1('L<')
132
- offset += 4
133
- case constructor
134
- when Constructors::PEER_USER
135
- [:user, data[offset, 8].unpack1('Q<'), offset + 8]
136
- when Constructors::PEER_CHAT
137
- [:chat, data[offset, 8].unpack1('Q<'), offset + 8]
138
- when Constructors::PEER_CHANNEL
139
- [:channel, data[offset, 8].unpack1('Q<'), offset + 8]
140
- else
141
- raise "Unknown peer constructor: 0x#{constructor.to_s(16)}"
142
- end
143
- end
144
-
145
- def read_tl_string(data, offset)
146
- first_byte = data.getbyte(offset)
147
- if first_byte == 254
148
- len = data.getbyte(offset + 1) |
149
- (data.getbyte(offset + 2) << 8) |
150
- (data.getbyte(offset + 3) << 16)
151
- total = 4 + len
152
- else
153
- len = first_byte
154
- total = 1 + len
155
- end
156
- padding = (4 - (total % 4)) % 4
157
- [data[offset + (total - len), len].force_encoding('UTF-8'), offset + total + padding]
158
- end
159
92
  end
160
93
  end
161
94
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ class ReplyInlineMarkup
6
+ include Binary
7
+
8
+ CONSTRUCTOR = 0x48a30254
9
+ KEYBOARD_BUTTON_ROW = 0x77608b83
10
+
11
+ # rows: array of rows; each row is an array of button objects (each responds to #serialize).
12
+ def initialize(rows:)
13
+ @rows = rows
14
+ end
15
+
16
+ def serialize
17
+ result = u32_b(CONSTRUCTOR)
18
+ result += u32_b(Constructors::VECTOR)
19
+ result += u32_b(@rows.size)
20
+ @rows.each do |row|
21
+ result += u32_b(KEYBOARD_BUTTON_ROW)
22
+ result += u32_b(Constructors::VECTOR)
23
+ result += u32_b(row.size)
24
+ row.each { |button| result += button.serialize }
25
+ end
26
+ result
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bot_command_scope'
4
+
5
+ module MTProto
6
+ module TL
7
+ # bots.resetBotCommands — clear the bot's commands for a given scope/language.
8
+ # Returns Bool.
9
+ class ResetBotCommands
10
+ include Binary
11
+
12
+ CONSTRUCTOR = 0x3d8de0f9
13
+
14
+ # scope: BotCommandScope hash (see BotCommandScope); defaults to default scope.
15
+ def initialize(scope: { type: :default }, lang_code: '')
16
+ @scope = scope
17
+ @lang_code = lang_code
18
+ end
19
+
20
+ def serialize
21
+ result = u32_b(CONSTRUCTOR)
22
+ result += BotCommandScope.serialize(@scope)
23
+ result += serialize_tl_string(@lang_code)
24
+ result
25
+ end
26
+
27
+ private
28
+
29
+ def serialize_tl_string(str)
30
+ bytes = str.to_s.encode('UTF-8').bytes
31
+ length = bytes.length
32
+ if length <= 253
33
+ [length] + bytes + padding(length + 1)
34
+ else
35
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
36
+ end
37
+ end
38
+
39
+ def padding(current_length)
40
+ pad_length = (4 - (current_length % 4)) % 4
41
+ [0] * pad_length
42
+ end
43
+ end
44
+ end
45
+ end
@@ -7,19 +7,25 @@ module MTProto
7
7
  class SendMedia
8
8
  include Binary
9
9
 
10
- INPUT_REPLY_TO_MESSAGE = 0x869fbe10
10
+ INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
11
+ INPUT_MEDIA_PHOTO = 0xe3af4434
12
+ INPUT_MEDIA_DOCUMENT = 0xa8763ab5
13
+ INPUT_PHOTO = 0x3bb3b94a
14
+ INPUT_DOCUMENT = 0x1abfb575
11
15
 
12
- def initialize(peer:, media:, message: '', random_id: nil, reply_to: nil)
16
+ def initialize(peer:, media:, message: '', random_id: nil, reply_to: nil, silent: false)
13
17
  @peer = peer
14
18
  @media = media
15
19
  @message = message
16
20
  @random_id = random_id || SecureRandom.random_number(2**63)
17
21
  @reply_to = reply_to
22
+ @silent = silent
18
23
  end
19
24
 
20
25
  def serialize
21
26
  flags = 0
22
27
  flags |= (1 << 0) if @reply_to
28
+ flags |= (1 << 5) if @silent
23
29
 
24
30
  result = u32_b(Constructors::MESSAGES_SEND_MEDIA)
25
31
  result += u32_b(flags)
@@ -56,11 +62,27 @@ module MTProto
56
62
  case @media[:type]
57
63
  when :uploaded_photo
58
64
  serialize_input_media_uploaded_photo(@media[:file])
65
+ when :uploaded_document
66
+ serialize_input_media_uploaded_document(@media)
67
+ when :photo
68
+ serialize_existing_media(INPUT_MEDIA_PHOTO, INPUT_PHOTO, @media)
69
+ when :document
70
+ serialize_existing_media(INPUT_MEDIA_DOCUMENT, INPUT_DOCUMENT, @media)
59
71
  else
60
72
  raise "Unknown media type: #{@media[:type]}"
61
73
  end
62
74
  end
63
75
 
76
+ # Re-send media that already lives on Telegram, by reference — inputMediaPhoto/
77
+ # inputMediaDocument (flags=0) wrapping inputPhoto/inputDocument(id, access_hash,
78
+ # file_reference). Used to re-post a guest-mention's photo/video, where the bot
79
+ # cannot forward from a chat it is not a member of.
80
+ def serialize_existing_media(media_ctor, input_ctor, media)
81
+ u32_b(media_ctor) + u32_b(0) +
82
+ u32_b(input_ctor) + u64_b(media[:id]) + u64_b(media[:access_hash]) +
83
+ serialize_bytes(media[:file_reference])
84
+ end
85
+
64
86
  def serialize_input_media_uploaded_photo(file)
65
87
  result = u32_b(Constructors::INPUT_MEDIA_UPLOADED_PHOTO)
66
88
  result += u32_b(0) # flags
@@ -68,6 +90,48 @@ module MTProto
68
90
  result
69
91
  end
70
92
 
93
+ # inputMediaUploadedDocument with flags=0 (no thumb/stickers/ttl/video
94
+ # fields): file, mime_type, then the document-attributes vector. A generic
95
+ # file carries documentAttributeFilename; audio adds documentAttributeAudio.
96
+ def serialize_input_media_uploaded_document(media)
97
+ u32_b(Constructors::INPUT_MEDIA_UPLOADED_DOCUMENT) +
98
+ u32_b(0) +
99
+ serialize_input_file(media[:file]) +
100
+ serialize_tl_string(media[:mime_type].to_s) +
101
+ serialize_document_attributes(media[:attributes] || [])
102
+ end
103
+
104
+ def serialize_document_attributes(attributes)
105
+ result = u32_b(Constructors::VECTOR) + u32_b(attributes.length)
106
+ attributes.each { |attribute| result += serialize_document_attribute(attribute) }
107
+ result
108
+ end
109
+
110
+ def serialize_document_attribute(attribute)
111
+ case attribute[:type]
112
+ when :filename
113
+ u32_b(Constructors::DOCUMENT_ATTRIBUTE_FILENAME) +
114
+ serialize_tl_string(attribute[:file_name].to_s)
115
+ when :audio
116
+ serialize_document_attribute_audio(attribute)
117
+ else
118
+ raise "Unknown document attribute: #{attribute[:type]}"
119
+ end
120
+ end
121
+
122
+ # documentAttributeAudio: voice=flags.10, title=flags.0, performer=flags.1;
123
+ # title is written before performer on the wire.
124
+ def serialize_document_attribute_audio(attribute)
125
+ flags = 0
126
+ flags |= (1 << 0) if attribute[:title]
127
+ flags |= (1 << 1) if attribute[:performer]
128
+ flags |= (1 << 10) if attribute[:voice]
129
+ result = u32_b(Constructors::DOCUMENT_ATTRIBUTE_AUDIO) + u32_b(flags) + u32_b(attribute[:duration].to_i)
130
+ result += serialize_tl_string(attribute[:title]) if attribute[:title]
131
+ result += serialize_tl_string(attribute[:performer]) if attribute[:performer]
132
+ result
133
+ end
134
+
71
135
  def serialize_input_file(file)
72
136
  u32_b(Constructors::INPUT_FILE) +
73
137
  u64_b(file[:id]) +
@@ -77,9 +141,17 @@ module MTProto
77
141
  end
78
142
 
79
143
  def serialize_tl_string(str)
80
- bytes = str.encode('UTF-8').bytes
81
- length = bytes.length
144
+ serialize_byte_array(str.encode('UTF-8').bytes)
145
+ end
82
146
 
147
+ # TL bytes for a raw binary string (e.g. a file_reference) — NOT re-encoded as
148
+ # UTF-8, which would corrupt arbitrary bytes.
149
+ def serialize_bytes(raw)
150
+ serialize_byte_array(raw.to_s.b.bytes)
151
+ end
152
+
153
+ def serialize_byte_array(bytes)
154
+ length = bytes.length
83
155
  if length <= 253
84
156
  [length] + bytes + padding(length + 1)
85
157
  else
@@ -7,20 +7,22 @@ module MTProto
7
7
  class SendMessage
8
8
  include Binary
9
9
 
10
- INPUT_REPLY_TO_MESSAGE = 0x869fbe10
10
+ INPUT_REPLY_TO_MESSAGE = 0x3bd4b7c2
11
11
 
12
- def initialize(peer:, message:, random_id: nil, reply_to: nil, reply_markup: nil)
12
+ def initialize(peer:, message:, random_id: nil, reply_to: nil, reply_markup: nil, silent: false)
13
13
  @peer = peer
14
14
  @message = message
15
15
  @random_id = random_id || SecureRandom.random_number(2**63)
16
16
  @reply_to = reply_to
17
17
  @reply_markup = reply_markup
18
+ @silent = silent
18
19
  end
19
20
 
20
21
  def serialize
21
22
  flags = 0
22
23
  flags |= (1 << 0) if @reply_to
23
24
  flags |= (1 << 2) if @reply_markup
25
+ flags |= (1 << 5) if @silent
24
26
 
25
27
  result = u32_b(Constructors::MESSAGES_SEND_MESSAGE)
26
28
  result += u32_b(flags)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # A SendMessageAction — the payload of messages.setTyping. Every variant is
6
+ # just its own constructor id; the upload/import variants additionally carry
7
+ # a progress:int (0-100). One class covers them all.
8
+ class SendMessageAction
9
+ include Binary
10
+
11
+ CONSTRUCTORS = {
12
+ typing: 0x16bf744e,
13
+ cancel: 0xfd5ec8f5,
14
+ record_audio: 0xd52f73f7,
15
+ upload_audio: 0xf351d7ab,
16
+ record_video: 0xa187d66f,
17
+ upload_video: 0xe9763aec,
18
+ record_round: 0x88f27fbc,
19
+ upload_round: 0x243e1c66,
20
+ upload_photo: 0xd1d34a26,
21
+ upload_document: 0xaa0cd9e4,
22
+ geo_location: 0x176f8ba1,
23
+ choose_contact: 0x628cbc6f,
24
+ choose_sticker: 0xb05ac6b1,
25
+ game_play: 0xdd6a8f48,
26
+ history_import: 0xdbda9246
27
+ }.freeze
28
+
29
+ WITH_PROGRESS = %i[upload_audio upload_video upload_round upload_photo upload_document history_import].freeze
30
+
31
+ def self.types
32
+ CONSTRUCTORS.keys
33
+ end
34
+
35
+ def initialize(type, progress: 0)
36
+ @type = type
37
+ @progress = progress
38
+ end
39
+
40
+ def serialize
41
+ id = CONSTRUCTORS.fetch(@type) { raise ArgumentError, "unknown SendMessageAction: #{@type}" }
42
+ result = u32_b(id)
43
+ result += u32_b(@progress) if WITH_PROGRESS.include?(@type)
44
+ result
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MTProto
4
+ module TL
5
+ # messages.setBotCallbackAnswer — the bot's reaction to a press. With no
6
+ # message it just dismisses the button's spinner; with a message it shows a
7
+ # toast (alert=false) or a modal alert (alert=true).
8
+ class SetBotCallbackAnswer
9
+ include Binary
10
+
11
+ CONSTRUCTOR = 0xd58f130a
12
+
13
+ def initialize(query_id:, message: nil, alert: false, url: nil, cache_time: 0)
14
+ @query_id = query_id
15
+ @message = message
16
+ @alert = alert
17
+ @url = url
18
+ @cache_time = cache_time
19
+ end
20
+
21
+ def serialize
22
+ flags = 0
23
+ flags |= (1 << 0) if @message
24
+ flags |= (1 << 1) if @alert
25
+ flags |= (1 << 2) if @url
26
+
27
+ result = u32_b(CONSTRUCTOR)
28
+ result += u32_b(flags)
29
+ result += u64_b(@query_id)
30
+ result += serialize_tl_string(@message) if @message
31
+ result += serialize_tl_string(@url) if @url
32
+ result += u32_b(@cache_time)
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def serialize_tl_string(str)
39
+ bytes = str.to_s.encode('UTF-8').bytes
40
+ length = bytes.length
41
+ if length <= 253
42
+ [length] + bytes + padding(length + 1)
43
+ else
44
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
45
+ end
46
+ end
47
+
48
+ def padding(current_length)
49
+ pad_length = (4 - (current_length % 4)) % 4
50
+ [0] * pad_length
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,23 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'bot_command_scope'
4
+
3
5
  module MTProto
4
6
  module TL
5
7
  class SetBotCommands
6
8
  include Binary
7
9
 
8
10
  CONSTRUCTOR = 0x0517165a
9
- BOT_COMMAND_SCOPE_DEFAULT = 0x2f6cb2ab
10
11
  BOT_COMMAND = 0xc27ac8c7
11
12
 
12
13
  # commands: array of { command:, description: }
13
- def initialize(commands:, lang_code: '')
14
+ # scope: BotCommandScope hash (see BotCommandScope); defaults to default scope.
15
+ def initialize(commands:, scope: { type: :default }, lang_code: '')
14
16
  @commands = commands
17
+ @scope = scope
15
18
  @lang_code = lang_code
16
19
  end
17
20
 
18
21
  def serialize
19
22
  result = u32_b(CONSTRUCTOR)
20
- result += u32_b(BOT_COMMAND_SCOPE_DEFAULT)
23
+ result += BotCommandScope.serialize(@scope)
21
24
  result += serialize_tl_string(@lang_code)
22
25
  result += u32_b(Constructors::VECTOR)
23
26
  result += u32_b(@commands.size)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module MTProto
6
+ module TL
7
+ # messages.setBotGuestChatResult#b8f106e3 query_id:long result:InputBotInlineResult
8
+ # = InputBotInlineMessageID
9
+ # How a guest-mode bot answers an updateBotGuestChatQuery: the reply text is wrapped
10
+ # in inputBotInlineResult(type "article") -> inputBotInlineMessageText, and the server
11
+ # posts that message into the guest chat. Text-only for now (no media / buttons).
12
+ class SetBotGuestChatResult
13
+ include Binary
14
+
15
+ CONSTRUCTOR = 0xb8f106e3
16
+ INPUT_BOT_INLINE_RESULT = 0x88bf9319 # inputBotInlineResult
17
+ INPUT_BOT_INLINE_MESSAGE_TEXT = 0x3dcd7a87 # inputBotInlineMessageText
18
+
19
+ def initialize(query_id:, message:, result_id: nil, result_type: 'article', result_title: nil)
20
+ @query_id = query_id
21
+ @message = message
22
+ @result_id = result_id || SecureRandom.hex(8)
23
+ @result_type = result_type
24
+ # The "article" result type requires a non-empty title (the server rejects an
25
+ # empty one with ARTICLE_TITLE_EMPTY); the actual reply shown is send_message.
26
+ @result_title = result_title || message_title
27
+ end
28
+
29
+ def serialize
30
+ result = u32_b(CONSTRUCTOR)
31
+ result += u64_b(@query_id)
32
+ result += serialize_result
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ # inputBotInlineResult#88bf9319 flags:# id:string type:string
39
+ # title:flags.1?string description:flags.2? ... send_message:InputBotInlineMessage
40
+ # flags = 1<<1: id/type + the required title, then send_message.
41
+ def serialize_result
42
+ result = u32_b(INPUT_BOT_INLINE_RESULT)
43
+ result += u32_b(1 << 1) # title present
44
+ result += serialize_tl_string(@result_id)
45
+ result += serialize_tl_string(@result_type)
46
+ result += serialize_tl_string(@result_title)
47
+ result += serialize_send_message
48
+ result
49
+ end
50
+
51
+ def message_title
52
+ title = @message.to_s.strip
53
+ title = 'Ответ' if title.empty?
54
+ title[0, 64]
55
+ end
56
+
57
+ # inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true
58
+ # invert_media:flags.3?true message:string entities:flags.1? reply_markup:flags.2?
59
+ # flags = 0: a plain text message.
60
+ def serialize_send_message
61
+ result = u32_b(INPUT_BOT_INLINE_MESSAGE_TEXT)
62
+ result += u32_b(0)
63
+ result += serialize_tl_string(@message)
64
+ result
65
+ end
66
+
67
+ def serialize_tl_string(str)
68
+ bytes = str.encode('UTF-8').bytes
69
+ length = bytes.length
70
+
71
+ if length <= 253
72
+ [length] + bytes + padding(length + 1)
73
+ else
74
+ [254] + u32_b(length)[0, 3] + bytes + padding(length + 4)
75
+ end
76
+ end
77
+
78
+ def padding(current_length)
79
+ pad_length = (4 - (current_length % 4)) % 4
80
+ [0] * pad_length
81
+ end
82
+ end
83
+ end
84
+ end