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



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



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

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



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

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

.membership_event(data, io, action, from_id, peer_type, peer_id, id) ⇒ Object



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

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



102
103
104
105
106
107
108
# File 'lib/mtproto/updates/event_builder.rb', line 102

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.



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

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.



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

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.



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.



111
112
113
114
# File 'lib/mtproto/updates/event_builder.rb', line 111

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



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mtproto/updates/event_builder.rb', line 118

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