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
MESSAGE_SERVICE =
0x7a800e0a
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).



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/mtproto/updates/event_builder.rb', line 230

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



204
205
206
207
208
# File 'lib/mtproto/updates/event_builder.rb', line 204

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.



195
196
197
198
199
200
201
202
# File 'lib/mtproto/updates/event_builder.rb', line 195

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



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

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



210
211
212
213
214
215
216
# File 'lib/mtproto/updates/event_builder.rb', line 210

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, io, action, from_id, peer_type, peer_id, id) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/mtproto/updates/event_builder.rb', line 153

def membership_event(data, io, action, from_id, peer_type, peer_id, id)
  case action
  when ACTION_CHAT_ADD_USER
    joined = 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: peer_type, peer_id: peer_id, msg_id: id)
  when ACTION_JOINED_BY_LINK
    Event.new(kind: :join, joined_user_id: from_id, peer_type: peer_type, peer_id: peer_id, msg_id: id)
  when ACTION_CHAT_DELETE_USER
    Event.new(kind: :leave, left_user_id: data[io + 4, 8].unpack1('Q<'),
      peer_type: peer_type, peer_id: peer_id, msg_id: id)
  end
end

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



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

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:) ⇒ Object

A Vector entry (a difference's new_messages) → a :message / :join / :leave Event WITHOUT pts (the protocol omits per-message pts there), or nil. channel is the envelope's kind (true for a channelDifference), since the message itself doesn't say.



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

def message_or_service(data, offset, ctor, channel:)
  if ctor == TL::Constructors::MESSAGE
    message_event(data, offset, ctor, kind: :message, channel: channel, pts: nil, pts_count: nil)
  elsif ctor == MESSAGE_SERVICE
    service_event(data, offset, channel: channel, pts: nil, pts_count: nil)
  end
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.



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/mtproto/updates/event_builder.rb', line 93

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)

  if inner_ctor == TL::Constructors::MESSAGE
    message_event(data, inner_off, inner_ctor, kind: :message, channel: channel, pts: pts, pts_count: pts_count)
  elsif inner_ctor == MESSAGE_SERVICE
    service_event(data, inner_off, channel: channel, pts: pts, pts_count: pts_count)
  end
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.



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/mtproto/updates/event_builder.rb', line 172

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.



123
124
125
126
# File 'lib/mtproto/updates/event_builder.rb', line 123

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).



220
221
222
223
224
# File 'lib/mtproto/updates/event_builder.rb', line 220

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].



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

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.



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/mtproto/updates/event_builder.rb', line 281

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.



267
268
269
270
271
272
273
274
275
276
277
# File 'lib/mtproto/updates/event_builder.rb', line 267

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, 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).



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/mtproto/updates/event_builder.rb', line 130

def service_event(data, offset, channel:, pts:, pts_count:)
  io = offset + 4
  flags = data[io, 4].unpack1('L<')
  io += 8 # flags + id
  _from_type, from_id, io = TL::Reader.parse_peer(data, io) if flags.anybits?(1 << 8) # from_id
  peer_type, peer_id, io = TL::Reader.parse_peer(data, io)
  io = TL::Reader.schema.skip(data, io) if flags.anybits?(1 << 28) # saved_peer_id
  io = TL::Reader.schema.skip(data, io) if flags.anybits?(1 << 3)  # reply_to
  io += 4 # date
  action = data[io, 4].unpack1('L<')
  id = data[offset + 8, 4].unpack1('l<')

  event = membership_event(data, io, action, from_id, peer_type, peer_id, id)
  return unless event

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