Skip to content

service events

a service event is a telegram update where the payload arrives as a message but carries a special field — new_chat_members, pinned_message, video_chat_started, etc. — that signals a group lifecycle action rather than a user-authored message. puregram promotes these to their own update kinds, dispatched as dedicated tg.on<Kind> handlers just like any other update

ts
tg.onNewChatMembers((update) => {
  const names = update.newChatMembers?.map(u => u.firstName).join(', ')
  return update.send(`welcome, ${names}!`)
})

tg.onLeftChatMember((update) => {
  return update.send(`${update.leftChatMember?.firstName} left`)
})

how service events are dispatched

when a message payload arrives from telegram, the update builder checks whether the payload contains a service-event field. if it does, the payload is instantiated as the service event's class instead of MessageUpdate. the full field-check order follows SERVICE_EVENT_ORDER from @puregram/api

the check is done on message, edited_message, channel_post, and edited_channel_post payloads — the four kinds that telegram reuses for service signals

they share MessageShared

service event classes extend MessageShared — the same base as MessageUpdate. that means all the message helpers (send, reply, delete, pin, from, chat, id) are available on every service event update, without having to cast

all service events

kindclasstelegram fielddescription
new_chat_membersNewChatMembersUpdatenew_chat_membersusers joined the group
left_chat_memberLeftChatMemberUpdateleft_chat_memberuser left or was removed
new_chat_titleNewChatTitleUpdatenew_chat_titlegroup title was changed
new_chat_photoNewChatPhotoUpdatenew_chat_photogroup photo was changed
delete_chat_photoDeleteChatPhotoUpdatedelete_chat_photogroup photo was deleted
group_chat_createdGroupChatCreatedUpdategroup_chat_createdgroup was created
pinned_messagePinnedMessageUpdatepinned_messagea message was pinned
invoiceInvoiceUpdateinvoiceinvoice for a payment
successful_paymentSuccessfulPaymentUpdatesuccessful_paymentpayment completed
users_sharedUsersSharedUpdateusers_sharedusers shared via a keyboard button
chat_sharedChatSharedUpdatechat_sharedchat shared via a keyboard button
web_app_dataWebAppDataUpdateweb_app_datadata sent from a web app
video_chat_scheduledVideoChatScheduledUpdatevideo_chat_scheduledvideo chat was scheduled
video_chat_startedVideoChatStartedUpdatevideo_chat_startedvideo chat started
video_chat_endedVideoChatEndedUpdatevideo_chat_endedvideo chat ended
video_chat_participants_invitedVideoChatParticipantsInvitedUpdatevideo_chat_participants_invitedparticipants were invited
forum_topic_createdForumTopicCreatedUpdateforum_topic_createdforum topic was created
forum_topic_editedForumTopicEditedUpdateforum_topic_editedforum topic was edited
forum_topic_closedForumTopicClosedUpdateforum_topic_closedforum topic was closed
forum_topic_reopenedForumTopicReopenedUpdateforum_topic_reopenedforum topic was reopened
general_forum_topic_hiddenGeneralForumTopicHiddenUpdategeneral_forum_topic_hiddengeneral topic hidden
general_forum_topic_unhiddenGeneralForumTopicUnhiddenUpdategeneral_forum_topic_unhiddengeneral topic unhidden
giveaway_createdGiveawayCreatedUpdategiveaway_createdgiveaway started
giveaway_completedGiveawayCompletedUpdategiveaway_completedgiveaway ended
giveaway_winnersGiveawayWinnersUpdategiveaway_winnersgiveaway winners announced
boost_addedBoostAddedUpdateboost_addeduser boosted the chat
message_auto_delete_timer_changedMessageAutoDeleteTimerChangedUpdatemessage_auto_delete_timer_changedauto-delete timer changed
migrate_to_chat_idMigrateToChatIdUpdatemigrate_to_chat_idgroup migrated to supergroup
migrate_from_chat_idMigrateFromChatIdUpdatemigrate_from_chat_idsupergroup migrated from group
passport_dataPassportDataUpdatepassport_datatelegram passport data
proximity_alert_triggeredProximityAlertTriggeredUpdateproximity_alert_triggeredproximity alert triggered
write_access_allowedWriteAccessAllowedUpdatewrite_access_allowedbot was given write access

registering handlers

use the generated tg.on<Kind> dispatcher for each event. the kind string is the same as the field name, camelCased into on + PascalCase:

ts
tg.onPinnedMessage(async (update) => {
  // update.pinnedMessage is the pinned TelegramMessage
  console.log('pinned:', update.pinnedMessage?.id)
})

tg.onVideoChatStarted(async (update) => {
  return update.send('video chat started — jump in!')
})

tg.onSuccessfulPayment(async (update) => {
  const payment = update.successfulPayment
  console.log('payment received:', payment?.totalAmount, payment?.currency)
})

you can also use tg.on('kind_name', handler) with the snake_case kind string:

ts
tg.on('new_chat_members', async (update) => {
  // update is typed as NewChatMembersUpdate
})

membership events

new_chat_members and left_chat_member are the two most common service events:

ts
tg.onNewChatMembers(async (update) => {
  for (const member of update.newChatMembers ?? []) {
    await update.send(`welcome, ${member.firstName}!`)
  }
})

tg.onLeftChatMember(async (update) => {
  const user = update.leftChatMember
  if (!user?.isBot) {
    await update.send(`goodbye, ${user?.firstName}`)
  }
})

pinned message

ts
tg.onPinnedMessage(async (update) => {
  const pinned = update.pinnedMessage
  await update.send(
    `message ${pinned?.id} was pinned`,
    { reply_parameters: { message_id: pinned?.id ?? 0 } }
  )
})

forum topic events

ts
tg.onForumTopicCreated(async (update) => {
  const topic = update.forumTopicCreated
  await update.send(`new topic created: ${topic?.name}`)
})

tg.onForumTopicClosed(async (update) => {
  await update.send('this topic was closed')
})

filtering service events

filters work on service events exactly as they do on regular messages — the MessageShared base means all the same accessors are available. use tg.on<Kind>(filter, handler):

ts
import { filters } from 'puregram'

// only handle joins in supergroups
tg.onNewChatMembers(filters.chat.supergroup, async (update) => {
  const member = update.newChatMembers?.[0]
  await update.send(`welcome to the supergroup, ${member?.firstName}!`)
})

or combine with tg.onUpdate for cross-kind filtering:

ts
import { filters } from 'puregram'

// catch any service event in a specific chat
tg.onUpdate(
  filters.kindIn(['new_chat_members', 'left_chat_member', 'new_chat_title']).and(
    filters.chatId(ADMIN_CHAT_ID)
  ),
  async (update) => {
    console.log(`[admin] service event: ${update.kind}`)
    await update.send(`service event: ${update.kind}`)
  }
)

why not just use tg.onMessage?

service event kinds are not dispatched as message — they get their own kind string after the field check. a tg.onMessage(...) handler will never see a new_chat_members update. always use tg.onNewChatMembers(...) (or tg.on('new_chat_members', ...)) to handle membership changes

see also

  • updates — the Update class, kind discriminant, and class-per-kind model
  • dispatch & filters — registering handlers and composing filters