Module: MTProto::Updates::EventBuilder

Defined in:
lib/mtproto/updates/event_builder.rb

Overview

Turns ONE Telegram Update (a single entry of a Vector, or the inner update of an updateShort) into a normalized Event, or nil for an update we don't surface. Reuses the gem's TL::Message / Reader / Schema and the existing TL::Updates callback/guest-query readers. The envelope walking (which vector, which reply) lives in Parser; this module owns the per-update semantics.

Constant Summary collapse

UPDATE_NEW_MESSAGE =
0x1f2b0afd
UPDATE_NEW_CHANNEL_MESSAGE =
0x62ba04d9
UPDATE_EDIT_MESSAGE =
0xe40370a3
UPDATE_EDIT_CHANNEL_MESSAGE =
0x1b3f4df7
UPDATE_MESSAGE_REACTIONS =
0x1e297bfa
UPDATE_BOT_MESSAGE_REACTION =
0xac21d3ce
UPDATE_DELETE_CHANNEL_MESSAGES =
0xc32d5b12
UPDATE_BOT_CALLBACK_QUERY =
0xb9cfc48d
UPDATE_CHANNEL_PARTICIPANT =
0x985d3abb
UPDATE_BOT_GUEST_CHAT_QUERY =
0xcdd4093d
UPDATE_MANAGED_BOT =
0x4880ed9a
ACTION_CHAT_ADD_USER =
0x15cefd00
0x031224c3
ACTION_CHAT_DELETE_USER =
0xa43f30cc
CHANNEL_PARTICIPANT =
0x1bd54456
MEMBER_PARTICIPANTS =

channelParticipant / Self / Admin / Creator — a still-present member.

[0x1bd54456, 0xa9478a1a, 0x34c3bb53, 0x2fe601d3].freeze
REACTION_EMOJI =
0x1b2286b8
REACTION_EMPTY =
0x79f5d419
REACTION_CUSTOM_EMOJI =
0x8935fc73
REACTION_PAID =
0x523da4eb
VECTOR =
0x1cb5c415

Class Method Summary collapse

Class Method Details

.bot_reaction_event(data, offset) ⇒ Object

updateBotMessageReaction#ac21d3ce peer:Peer msg_id:int date:int actor:Peer old_reactions:Vector new_reactions:Vector qts:int — the bot-facing per-user reaction, the one that names the reactor. Diff old vs new to classify add (:reaction) vs remove (:unreaction).



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/mtproto/updates/event_builder.rb', line 218

def bot_reaction_event(data, offset)
  io = offset + 4
  peer_type, peer_id, io = TL::Reader.parse_peer(data, io)
  msg_id = data[io, 4].unpack1('l<')
  io += 4
  date = data[io, 4].unpack1('L<')
  io += 4
  actor_type, actor_id, io = TL::Reader.parse_peer(data, io)
  old_reactions, io = read_reactions_vector(data, io)
  new_reactions, io = read_reactions_vector(data, io)
  qts = data[io, 4].unpack1('l<')

  added = new_reactions.reject { |r| old_reactions.any? { |o| o[:key] == r[:key] } }
  removed = old_reactions.reject { |r| new_reactions.any? { |o| o[:key] == r[:key] } }
  kind, diff = if added.any? then [:reaction, added]
               elsif removed.any? then [:unreaction, removed]
               end
  return unless kind

  reactor = { peer_type: actor_type, peer_id: actor_id, date: date, emoji: diff.first[:emoji] }
  Event.new(kind: kind, msg_id: msg_id, qts: qts, peer_type: peer_type, peer_id: peer_id,
    user_id: actor_id, reactors: [reactor])
rescue StandardError
  nil
end

.callback_event(data, offset) ⇒ Object



192
193
194
195
196
# File 'lib/mtproto/updates/event_builder.rb', line 192

def callback_event(data, offset)
  query = TL::Updates.extract_callback_query(data, offset)
  Event.new(kind: :callback, query_id: query[:query_id], user_id: query[:user_id],
    peer_type: query[:peer_type], peer_id: query[:peer_id], msg_id: query[:msg_id], data: query[:data])
end

.classify_update(data, offset, ctor) ⇒ Object

One Vector entry at offset → its Event, or nil for an update we don't surface (the caller's Schema#skip still advances past it).



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/mtproto/updates/event_builder.rb', line 47

def classify_update(data, offset, ctor)
  case ctor
  when UPDATE_NEW_MESSAGE, UPDATE_NEW_CHANNEL_MESSAGE
    new_message(data, offset, channel: ctor == UPDATE_NEW_CHANNEL_MESSAGE)
  when UPDATE_EDIT_MESSAGE, UPDATE_EDIT_CHANNEL_MESSAGE
    edit_message(data, offset, channel: ctor == UPDATE_EDIT_CHANNEL_MESSAGE)
  when UPDATE_MESSAGE_REACTIONS
    reaction_event(data, offset)
  when UPDATE_BOT_MESSAGE_REACTION
    bot_reaction_event(data, offset)
  when UPDATE_DELETE_CHANNEL_MESSAGES
    delete_event(data, offset)
  when UPDATE_BOT_CALLBACK_QUERY
    callback_event(data, offset)
  when UPDATE_CHANNEL_PARTICIPANT
    participant_event(data, offset)
  when UPDATE_BOT_GUEST_CHAT_QUERY
    guest_query_event(data, offset)
  when UPDATE_MANAGED_BOT
    managed_bot_event(data, offset)
  end
end

.delete_event(data, offset) ⇒ Object

updateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector pts:int pts_count:int.



183
184
185
186
187
188
189
190
# File 'lib/mtproto/updates/event_builder.rb', line 183

def delete_event(data, offset)
  io = offset + 4
  channel_id = data[io, 8].unpack1('Q<')
  io += 8
  deleted_ids, io = read_int_vector(data, io)
  Event.new(kind: :delete, channel: true, peer_type: :channel, peer_id: channel_id,
    deleted_ids: deleted_ids, pts: data[io, 4].unpack1('l<'), pts_count: data[io + 4, 4].unpack1('l<'))
end

.edit_message(data, offset, channel:) ⇒ Object



98
99
100
101
102
103
104
105
# File 'lib/mtproto/updates/event_builder.rb', line 98

def edit_message(data, offset, channel:)
  inner_off = offset + 4
  inner_ctor = data[inner_off, 4].unpack1('L<')
  return unless inner_ctor == TL::Constructors::MESSAGE

  pts, pts_count = pts_after(data, inner_off)
  message_event(data, inner_off, inner_ctor, kind: :edit, channel: channel, pts: pts, pts_count: pts_count)
end

.guest_query_event(data, offset) ⇒ Object



198
199
200
201
202
203
204
# File 'lib/mtproto/updates/event_builder.rb', line 198

def guest_query_event(data, offset)
  query = TL::Updates.extract_guest_query(data, offset)
  message = query[:message]
  Event.new(kind: :guest_query, query_id: query[:query_id], qts: query[:qts], message: message,
    peer_type: message && message[:peer_type], peer_id: message && message[:peer_id],
    user_id: message && message[:from_id])
end

.managed_bot_event(data, offset) ⇒ Object

updateManagedBot#4880ed9a user_id:long bot_id:long — a bot the user just minted through this bot acting as its manager. Carries the owner (user_id) and the new bot (bot_id); the bot's access_hash rides in the batch users the parser attaches.



73
74
75
76
77
# File 'lib/mtproto/updates/event_builder.rb', line 73

def managed_bot_event(data, offset)
  Event.new(kind: :managed_bot,
            user_id: data[offset + 4, 8].unpack1('Q<'),
            bot_id: data[offset + 12, 8].unpack1('Q<'))
end

.membership_event(data, header) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mtproto/updates/event_builder.rb', line 138

def membership_event(data, header)
  io = header.action_offset
  case data[io, 4].unpack1('L<')
  when ACTION_CHAT_ADD_USER
    joined = header.from_id
    if data[io + 4, 4].unpack1('L<') == VECTOR && data[io + 8, 4].unpack1('L<').positive?
      joined = data[io + 12, 8].unpack1('Q<')
    end
    Event.new(kind: :join, joined_user_id: joined,
      peer_type: header.peer_type, peer_id: header.peer_id, msg_id: header.id)
  when ACTION_JOINED_BY_LINK
    Event.new(kind: :join, joined_user_id: header.from_id,
      peer_type: header.peer_type, peer_id: header.peer_id, msg_id: header.id)
  when ACTION_CHAT_DELETE_USER
    Event.new(kind: :leave, left_user_id: data[io + 4, 8].unpack1('Q<'),
      peer_type: header.peer_type, peer_id: header.peer_id, msg_id: header.id)
  end
end

.message_event(data, offset, constructor, kind:, channel:, pts:, pts_count:) ⇒ Object



107
108
109
110
111
112
113
# File 'lib/mtproto/updates/event_builder.rb', line 107

def message_event(data, offset, constructor, kind:, channel:, pts:, pts_count:)
  msg = TL::Message.deserialize(data, offset, constructor)
  return unless msg

  Event.new(kind: kind, channel: channel, pts: pts, pts_count: pts_count,
    peer_type: msg.peer_type, peer_id: msg.peer_id, msg_id: msg.id, message: msg.to_h)
end

.message_or_service(data, offset, ctor, channel:, pts:, pts_count:) ⇒ Object

One message — an entry of a Vector, or the one inside an updateNew(Channel)Message — → a :message / :join / :leave Event, or nil. channel is the envelope's kind (true for a channelDifference), since the message itself doesn't say.



83
84
85
86
# File 'lib/mtproto/updates/event_builder.rb', line 83

def message_or_service(data, offset, ctor, channel:, pts:, pts_count:)
  message_event(data, offset, ctor, kind: :message, channel: channel, pts: pts, pts_count: pts_count) ||
    service_event(data, offset, ctor, channel: channel, pts: pts, pts_count: pts_count)
end

.new_message(data, offset, channel:) ⇒ Object

updateNew(Channel)Message: the update tag, an inner Message (plain message, or a messageService that may be a join/leave), then pts:int pts_count:int.



90
91
92
93
94
95
96
# File 'lib/mtproto/updates/event_builder.rb', line 90

def new_message(data, offset, channel:)
  inner_off = offset + 4
  inner_ctor = data[inner_off, 4].unpack1('L<')
  pts, pts_count = pts_after(data, inner_off)

  message_or_service(data, inner_off, inner_ctor, channel: channel, pts: pts, pts_count: pts_count)
end

.participant_event(data, offset) ⇒ Object

updateChannelParticipant#985d3abb: a member joining a supergroup arrives here (not as a service message). Surface only a real join — a new_participant that is present AND was not present before.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/mtproto/updates/event_builder.rb', line 160

def participant_event(data, offset)
  io = offset + 4
  flags = data[io, 4].unpack1('L<')
  channel_id = data[io + 4, 8].unpack1('Q<')
  user_id = data[io + 24, 8].unpack1('Q<')
  io += 32

  return unless flags.anybits?(1 << 1) # no new_participant → not a join

  if flags.anybits?(1 << 0) # prev_participant
    return if MEMBER_PARTICIPANTS.include?(data[io, 4].unpack1('L<'))

    io = TL::Reader.schema.skip(data, io)
  end
  return unless data[io, 4].unpack1('L<') == CHANNEL_PARTICIPANT

  Event.new(kind: :join, channel: true, joined_user_id: user_id, peer_type: :channel, peer_id: channel_id)
rescue StandardError
  nil
end

.pts_after(data, msg_off) ⇒ Object

pts:int pts_count:int sit right after the inner Message; skip it to read them.



116
117
118
119
# File 'lib/mtproto/updates/event_builder.rb', line 116

def pts_after(data, msg_off)
  after = TL::Reader.schema.skip(data, msg_off)
  [data[after, 4].unpack1('l<'), data[after + 4, 4].unpack1('l<')]
end

.reaction_event(data, offset) ⇒ Object

updateMessageReactions# → { peer, msg_id } (the message whose reactions changed; the recent reactors are read from the message tail elsewhere).



208
209
210
211
212
# File 'lib/mtproto/updates/event_builder.rb', line 208

def reaction_event(data, offset)
  io = offset + 8 # constructor + flags
  peer_type, peer_id, io = TL::Reader.parse_peer(data, io)
  Event.new(kind: :reaction, peer_type: peer_type, peer_id: peer_id, msg_id: data[io, 4].unpack1('l<'))
end

.read_int_vector(data, offset) ⇒ Object

Vector at offset → [[int, …], offset_past].



245
246
247
248
249
250
251
# File 'lib/mtproto/updates/event_builder.rb', line 245

def read_int_vector(data, offset)
  offset += 4 # vector constructor
  count = data[offset, 4].unpack1('L<')
  offset += 4
  ids = Array.new(count) { |i| data[offset + (i * 4), 4].unpack1('l<') }
  [ids, offset + (count * 4)]
end

.read_reaction(data, offset) ⇒ Object

One Reaction at offset → [key, emoji_or_nil, offset_past]. An unknown reaction constructor is stepped over via the schema so the vector stays aligned.



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/mtproto/updates/event_builder.rb', line 269

def read_reaction(data, offset)
  case data[offset, 4].unpack1('L<')
  when REACTION_EMOJI
    emoji, io = TL::Reader.read_tl_string(data, offset + 4)
    [emoji, emoji, io]
  when REACTION_CUSTOM_EMOJI
    ["custom:#{data[offset + 4, 8].unpack1('Q<')}", nil, offset + 12]
  when REACTION_PAID
    ['paid', nil, offset + 4]
  when REACTION_EMPTY
    ['empty', nil, offset + 4]
  else
    [nil, nil, TL::Reader.schema.skip(data, offset)]
  end
end

.read_reactions_vector(data, offset) ⇒ Object

Vector at offset → [ [{ key:, emoji: }, …], offset_past ]. key is a stable identity for the add/remove diff; emoji is set for plain reactions.



255
256
257
258
259
260
261
262
263
264
265
# File 'lib/mtproto/updates/event_builder.rb', line 255

def read_reactions_vector(data, offset)
  offset += 4 # vector constructor
  count = data[offset, 4].unpack1('L<')
  offset += 4
  items = []
  count.times do
    key, emoji, offset = read_reaction(data, offset)
    items << { key: key, emoji: emoji }
  end
  [items, offset]
end

.service_event(data, offset, ctor, channel:, pts:, pts_count:) ⇒ Object

messageService carrying a membership action → a :join / :leave Event, else nil. peer_id is the service message's own chat (where the event happened).



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/mtproto/updates/event_builder.rb', line 123

def service_event(data, offset, ctor, channel:, pts:, pts_count:)
  header = TL::MessageService.deserialize(data, offset, ctor)
  return unless header

  event = membership_event(data, header)
  return unless event

  event.channel = channel
  event.pts = pts
  event.pts_count = pts_count
  event
rescue StandardError
  nil
end