Module: StreamChat::Webhook
- Defined in:
- lib/getstream_ruby/generated/webhook.rb
Defined Under Namespace
Classes: InvalidWebhookError, UnknownEvent
Constant Summary collapse
- EVENT_TYPE_WILDCARD =
Webhook event type constants
'*'- EVENT_TYPE_APPEAL_ACCEPTED =
'appeal.accepted'- EVENT_TYPE_APPEAL_CREATED =
'appeal.created'- EVENT_TYPE_APPEAL_REJECTED =
'appeal.rejected'- EVENT_TYPE_CALL_ACCEPTED =
'call.accepted'- EVENT_TYPE_CALL_BLOCKED_USER =
'call.blocked_user'- EVENT_TYPE_CALL_CLOSED_CAPTION =
'call.closed_caption'- EVENT_TYPE_CALL_CLOSED_CAPTIONS_FAILED =
'call.closed_captions_failed'- EVENT_TYPE_CALL_CLOSED_CAPTIONS_STARTED =
'call.closed_captions_started'- EVENT_TYPE_CALL_CLOSED_CAPTIONS_STOPPED =
'call.closed_captions_stopped'- EVENT_TYPE_CALL_CREATED =
'call.created'- EVENT_TYPE_CALL_DELETED =
'call.deleted'- EVENT_TYPE_CALL_DTMF =
'call.dtmf'- EVENT_TYPE_CALL_ENDED =
'call.ended'- EVENT_TYPE_CALL_FRAME_RECORDING_FAILED =
'call.frame_recording_failed'- EVENT_TYPE_CALL_FRAME_RECORDING_READY =
'call.frame_recording_ready'- EVENT_TYPE_CALL_FRAME_RECORDING_STARTED =
'call.frame_recording_started'- EVENT_TYPE_CALL_FRAME_RECORDING_STOPPED =
'call.frame_recording_stopped'- EVENT_TYPE_CALL_HLS_BROADCASTING_FAILED =
'call.hls_broadcasting_failed'- EVENT_TYPE_CALL_HLS_BROADCASTING_STARTED =
'call.hls_broadcasting_started'- EVENT_TYPE_CALL_HLS_BROADCASTING_STOPPED =
'call.hls_broadcasting_stopped'- EVENT_TYPE_CALL_KICKED_USER =
'call.kicked_user'- EVENT_TYPE_CALL_LIVE_STARTED =
'call.live_started'- EVENT_TYPE_CALL_MEMBER_ADDED =
'call.member_added'- EVENT_TYPE_CALL_MEMBER_REMOVED =
'call.member_removed'- EVENT_TYPE_CALL_MEMBER_UPDATED =
'call.member_updated'- EVENT_TYPE_CALL_MEMBER_UPDATED_PERMISSION =
'call.member_updated_permission'- EVENT_TYPE_CALL_MISSED =
'call.missed'- EVENT_TYPE_CALL_MODERATION_BLUR =
'call.moderation_blur'- EVENT_TYPE_CALL_MODERATION_WARNING =
'call.moderation_warning'- EVENT_TYPE_CALL_NOTIFICATION =
'call.notification'- EVENT_TYPE_CALL_PERMISSION_REQUEST =
'call.permission_request'- EVENT_TYPE_CALL_PERMISSIONS_UPDATED =
'call.permissions_updated'- EVENT_TYPE_CALL_REACTION_NEW =
'call.reaction_new'- EVENT_TYPE_CALL_RECORDING_FAILED =
'call.recording_failed'- EVENT_TYPE_CALL_RECORDING_READY =
'call.recording_ready'- EVENT_TYPE_CALL_RECORDING_STARTED =
'call.recording_started'- EVENT_TYPE_CALL_RECORDING_STOPPED =
'call.recording_stopped'- EVENT_TYPE_CALL_REJECTED =
'call.rejected'- EVENT_TYPE_CALL_RING =
'call.ring'- EVENT_TYPE_CALL_RTMP_BROADCAST_FAILED =
'call.rtmp_broadcast_failed'- EVENT_TYPE_CALL_RTMP_BROADCAST_STARTED =
'call.rtmp_broadcast_started'- EVENT_TYPE_CALL_RTMP_BROADCAST_STOPPED =
'call.rtmp_broadcast_stopped'- EVENT_TYPE_CALL_SESSION_ENDED =
'call.session_ended'- EVENT_TYPE_CALL_SESSION_PARTICIPANT_COUNT_UPDATED =
'call.session_participant_count_updated'- EVENT_TYPE_CALL_SESSION_PARTICIPANT_JOINED =
'call.session_participant_joined'- EVENT_TYPE_CALL_SESSION_PARTICIPANT_LEFT =
'call.session_participant_left'- EVENT_TYPE_CALL_SESSION_STARTED =
'call.session_started'- EVENT_TYPE_CALL_STATS_REPORT_READY =
'call.stats_report_ready'- EVENT_TYPE_CALL_TRANSCRIPTION_FAILED =
'call.transcription_failed'- EVENT_TYPE_CALL_TRANSCRIPTION_READY =
'call.transcription_ready'- EVENT_TYPE_CALL_TRANSCRIPTION_STARTED =
'call.transcription_started'- EVENT_TYPE_CALL_TRANSCRIPTION_STOPPED =
'call.transcription_stopped'- EVENT_TYPE_CALL_UNBLOCKED_USER =
'call.unblocked_user'- EVENT_TYPE_CALL_UPDATED =
'call.updated'- EVENT_TYPE_CALL_USER_FEEDBACK_SUBMITTED =
'call.user_feedback_submitted'- EVENT_TYPE_CALL_USER_MUTED =
'call.user_muted'- EVENT_TYPE_CAMPAIGN_COMPLETED =
'campaign.completed'- EVENT_TYPE_CAMPAIGN_STARTED =
'campaign.started'- EVENT_TYPE_CHANNEL_CREATED =
'channel.created'- EVENT_TYPE_CHANNEL_DELETED =
'channel.deleted'- EVENT_TYPE_CHANNEL_FROZEN =
'channel.frozen'- EVENT_TYPE_CHANNEL_HIDDEN =
'channel.hidden'- EVENT_TYPE_CHANNEL_MAX_STREAK_CHANGED =
'channel.max_streak_changed'- EVENT_TYPE_CHANNEL_MUTED =
'channel.muted'- EVENT_TYPE_CHANNEL_TRUNCATED =
'channel.truncated'- EVENT_TYPE_CHANNEL_UNFROZEN =
'channel.unfrozen'- EVENT_TYPE_CHANNEL_UNMUTED =
'channel.unmuted'- EVENT_TYPE_CHANNEL_UPDATED =
'channel.updated'- EVENT_TYPE_CHANNEL_VISIBLE =
'channel.visible'- EVENT_TYPE_CHANNEL_BATCH_UPDATE_COMPLETED =
'channel_batch_update.completed'- EVENT_TYPE_CHANNEL_BATCH_UPDATE_STARTED =
'channel_batch_update.started'- EVENT_TYPE_CUSTOM =
'custom'- EVENT_TYPE_EXPORT_BULK_IMAGE_MODERATION_ERROR =
'export.bulk_image_moderation.error'- EVENT_TYPE_EXPORT_BULK_IMAGE_MODERATION_SUCCESS =
'export.bulk_image_moderation.success'- EVENT_TYPE_EXPORT_CHANNELS_ERROR =
'export.channels.error'- EVENT_TYPE_EXPORT_CHANNELS_SUCCESS =
'export.channels.success'- EVENT_TYPE_EXPORT_MODERATION_LOGS_ERROR =
'export.moderation_logs.error'- EVENT_TYPE_EXPORT_MODERATION_LOGS_SUCCESS =
'export.moderation_logs.success'- EVENT_TYPE_EXPORT_USERS_ERROR =
'export.users.error'- EVENT_TYPE_EXPORT_USERS_SUCCESS =
'export.users.success'- EVENT_TYPE_FEEDS_ACTIVITY_ADDED =
'feeds.activity.added'- EVENT_TYPE_FEEDS_ACTIVITY_DELETED =
'feeds.activity.deleted'- EVENT_TYPE_FEEDS_ACTIVITY_FEEDBACK =
'feeds.activity.feedback'- EVENT_TYPE_FEEDS_ACTIVITY_MARKED =
'feeds.activity.marked'- EVENT_TYPE_FEEDS_ACTIVITY_PINNED =
'feeds.activity.pinned'- EVENT_TYPE_FEEDS_ACTIVITY_REACTION_ADDED =
'feeds.activity.reaction.added'- EVENT_TYPE_FEEDS_ACTIVITY_REACTION_DELETED =
'feeds.activity.reaction.deleted'- EVENT_TYPE_FEEDS_ACTIVITY_REACTION_UPDATED =
'feeds.activity.reaction.updated'- EVENT_TYPE_FEEDS_ACTIVITY_REMOVED_FROM_FEED =
'feeds.activity.removed_from_feed'- EVENT_TYPE_FEEDS_ACTIVITY_RESTORED =
'feeds.activity.restored'- EVENT_TYPE_FEEDS_ACTIVITY_UNPINNED =
'feeds.activity.unpinned'- EVENT_TYPE_FEEDS_ACTIVITY_UPDATED =
'feeds.activity.updated'- EVENT_TYPE_FEEDS_BOOKMARK_ADDED =
'feeds.bookmark.added'- EVENT_TYPE_FEEDS_BOOKMARK_DELETED =
'feeds.bookmark.deleted'- EVENT_TYPE_FEEDS_BOOKMARK_UPDATED =
'feeds.bookmark.updated'- EVENT_TYPE_FEEDS_BOOKMARK_FOLDER_DELETED =
'feeds.bookmark_folder.deleted'- EVENT_TYPE_FEEDS_BOOKMARK_FOLDER_UPDATED =
'feeds.bookmark_folder.updated'- EVENT_TYPE_FEEDS_COMMENT_ADDED =
'feeds.comment.added'- EVENT_TYPE_FEEDS_COMMENT_DELETED =
'feeds.comment.deleted'- EVENT_TYPE_FEEDS_COMMENT_REACTION_ADDED =
'feeds.comment.reaction.added'- EVENT_TYPE_FEEDS_COMMENT_REACTION_DELETED =
'feeds.comment.reaction.deleted'- EVENT_TYPE_FEEDS_COMMENT_REACTION_UPDATED =
'feeds.comment.reaction.updated'- EVENT_TYPE_FEEDS_COMMENT_RESTORED =
'feeds.comment.restored'- EVENT_TYPE_FEEDS_COMMENT_UPDATED =
'feeds.comment.updated'- EVENT_TYPE_FEEDS_FEED_CREATED =
'feeds.feed.created'- EVENT_TYPE_FEEDS_FEED_DELETED =
'feeds.feed.deleted'- EVENT_TYPE_FEEDS_FEED_UPDATED =
'feeds.feed.updated'- EVENT_TYPE_FEEDS_FEED_GROUP_CHANGED =
'feeds.feed_group.changed'- EVENT_TYPE_FEEDS_FEED_GROUP_DELETED =
'feeds.feed_group.deleted'- EVENT_TYPE_FEEDS_FEED_GROUP_RESTORED =
'feeds.feed_group.restored'- EVENT_TYPE_FEEDS_FEED_MEMBER_ADDED =
'feeds.feed_member.added'- EVENT_TYPE_FEEDS_FEED_MEMBER_REMOVED =
'feeds.feed_member.removed'- EVENT_TYPE_FEEDS_FEED_MEMBER_UPDATED =
'feeds.feed_member.updated'- EVENT_TYPE_FEEDS_FOLLOW_CREATED =
'feeds.follow.created'- EVENT_TYPE_FEEDS_FOLLOW_DELETED =
'feeds.follow.deleted'- EVENT_TYPE_FEEDS_FOLLOW_UPDATED =
'feeds.follow.updated'- EVENT_TYPE_FEEDS_NOTIFICATION_FEED_UPDATED =
'feeds.notification_feed.updated'- EVENT_TYPE_FEEDS_STORIES_FEED_UPDATED =
'feeds.stories_feed.updated'- EVENT_TYPE_FLAG_UPDATED =
'flag.updated'- EVENT_TYPE_INGRESS_ERROR =
'ingress.error'- EVENT_TYPE_INGRESS_STARTED =
'ingress.started'- EVENT_TYPE_INGRESS_STOPPED =
'ingress.stopped'- EVENT_TYPE_MEMBER_ADDED =
'member.added'- EVENT_TYPE_MEMBER_REMOVED =
'member.removed'- EVENT_TYPE_MEMBER_UPDATED =
'member.updated'- EVENT_TYPE_MESSAGE_DELETED =
'message.deleted'- EVENT_TYPE_MESSAGE_FLAGGED =
'message.flagged'- EVENT_TYPE_MESSAGE_NEW =
'message.new'- EVENT_TYPE_MESSAGE_PENDING =
'message.pending'- EVENT_TYPE_MESSAGE_READ =
'message.read'- EVENT_TYPE_MESSAGE_UNBLOCKED =
'message.unblocked'- EVENT_TYPE_MESSAGE_UNDELETED =
'message.undeleted'- EVENT_TYPE_MESSAGE_UPDATED =
'message.updated'- EVENT_TYPE_MODERATION_CUSTOM_ACTION =
'moderation.custom_action'- EVENT_TYPE_MODERATION_FLAGGED =
'moderation.flagged'- EVENT_TYPE_MODERATION_MARK_REVIEWED =
'moderation.mark_reviewed'- EVENT_TYPE_MODERATION_CHECK_COMPLETED =
'moderation_check.completed'- EVENT_TYPE_MODERATION_RULE_TRIGGERED =
'moderation_rule.triggered'- EVENT_TYPE_NOTIFICATION_MARK_UNREAD =
'notification.mark_unread'- EVENT_TYPE_NOTIFICATION_REMINDER_DUE =
'notification.reminder_due'- EVENT_TYPE_NOTIFICATION_THREAD_MESSAGE_NEW =
'notification.thread_message_new'- EVENT_TYPE_REACTION_DELETED =
'reaction.deleted'- EVENT_TYPE_REACTION_NEW =
'reaction.new'- EVENT_TYPE_REACTION_UPDATED =
'reaction.updated'- EVENT_TYPE_REMINDER_CREATED =
'reminder.created'- EVENT_TYPE_REMINDER_DELETED =
'reminder.deleted'- EVENT_TYPE_REMINDER_UPDATED =
'reminder.updated'- EVENT_TYPE_REVIEW_QUEUE_ITEM_NEW =
'review_queue_item.new'- EVENT_TYPE_REVIEW_QUEUE_ITEM_UPDATED =
'review_queue_item.updated'- EVENT_TYPE_THREAD_UPDATED =
'thread.updated'- EVENT_TYPE_USER_BANNED =
'user.banned'- EVENT_TYPE_USER_DEACTIVATED =
'user.deactivated'- EVENT_TYPE_USER_DELETED =
'user.deleted'- EVENT_TYPE_USER_FLAGGED =
'user.flagged'- EVENT_TYPE_USER_MESSAGES_DELETED =
'user.messages.deleted'- EVENT_TYPE_USER_MUTED =
'user.muted'- EVENT_TYPE_USER_REACTIVATED =
'user.reactivated'- EVENT_TYPE_USER_UNBANNED =
'user.unbanned'- EVENT_TYPE_USER_UNMUTED =
'user.unmuted'- EVENT_TYPE_USER_UNREAD_MESSAGE_REMINDER =
'user.unread_message_reminder'- EVENT_TYPE_USER_UPDATED =
'user.updated'- EVENT_TYPE_USER_GROUP_CREATED =
'user_group.created'- EVENT_TYPE_USER_GROUP_DELETED =
'user_group.deleted'- EVENT_TYPE_USER_GROUP_MEMBER_ADDED =
'user_group.member_added'- EVENT_TYPE_USER_GROUP_MEMBER_REMOVED =
'user_group.member_removed'- EVENT_TYPE_USER_GROUP_UPDATED =
'user_group.updated'
Class Method Summary collapse
-
.decode_sns_payload(notification_body) ⇒ String
Decode an SNS notification body.
-
.decode_sqs_payload(message_body) ⇒ String
Decode an SQS Message Body: try base64 first, fall back to raw bytes if base64 fails, then gunzip if gzip-prefixed.
-
.get_event_type(raw_event) ⇒ String?
Extract the event type from a raw webhook payload.
-
.gunzip_payload(body) ⇒ String
Decompress the body if it is gzip-prefixed (first two bytes 0x1F 0x8B), else return the body bytes unchanged.
-
.parse_event(payload) ⇒ Object
Parse a webhook payload and return the typed event for known discriminators or UnknownEvent for well-formed-but-unknown ones.
-
.parse_sns(notification_body) ⇒ Object
SNS composite: parse SNS envelope -> base64-decode -> gunzip -> parse.
-
.parse_sqs(message_body) ⇒ Object
SQS composite: base64-decode -> gunzip (if gzip-prefixed) -> parse.
-
.parse_webhook_event(raw_event) ⇒ Object
Deserialize a raw webhook payload into a typed event object.
-
.verify_and_parse_webhook(body, signature, secret) ⇒ Object
HTTP composite: gunzip (if gzip-prefixed) -> verify HMAC-SHA256 -> parse.
-
.verify_signature(body, signature, secret) ⇒ Boolean
Verify the HMAC-SHA256 signature of a webhook payload.
Class Method Details
.decode_sns_payload(notification_body) ⇒ String
Decode an SNS notification body. Accepts either:
-
a full SNS HTTP notification envelope JSON (“Type”:“Notification”,“Message”:“<base64>”,…), or
-
a pre-extracted Message string (forwarded-through-SQS path).
The inner payload is then base64-decoded and gunzipped via decode_sqs_payload.
866 867 868 869 870 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 866 def self.decode_sns_payload(notification_body) raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: notification_body must be a String" unless notification_body.is_a?(String) decode_sqs_payload(unwrap_sns_notification_body(notification_body)) end |
.decode_sqs_payload(message_body) ⇒ String
Decode an SQS Message Body: try base64 first, fall back to raw bytes if base64 fails, then gunzip if gzip-prefixed.
Wire format (per CHA-3071): SQS bodies are raw JSON when enable_hook_payload_compression is off (today’s default for all existing apps), and base64(gzip(json)) when it’s on. This helper handles both: raw JSON starts with ‘which is not valid base64, so the base64 decode fails and we fall through to raw bytes, then {gunzip_payload‘s magic-byte detection decides whether to decompress.
parse_sqs sits on top of this and works transparently for both wire formats — no caller code change, no flag, no header.
825 826 827 828 829 830 831 832 833 834 835 836 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 825 def self.decode_sqs_payload() raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: message_body must be a String" unless .is_a?(String) decoded = begin Base64.strict_decode64() rescue ArgumentError # Not base64 — treat input as raw bytes (uncompressed wire format). .dup.force_encoding(Encoding::ASCII_8BIT) end gunzip_payload(decoded) end |
.get_event_type(raw_event) ⇒ String?
Extract the event type from a raw webhook payload.
389 390 391 392 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 389 def self.get_event_type(raw_event) data = parse_to_hash(raw_event) data&.fetch('type', nil) end |
.gunzip_payload(body) ⇒ String
Decompress the body if it is gzip-prefixed (first two bytes 0x1F 0x8B), else return the body bytes unchanged.
Magic-byte detection is reliable for Stream payloads because Stream webhook bodies are always JSON, and JSON cannot start with 0x1F.
798 799 800 801 802 803 804 805 806 807 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 798 def self.gunzip_payload(body) raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: body must be a String" unless body.is_a?(String) bytes = body.b return bytes if bytes.bytesize < 2 || bytes.byteslice(0, 2) != GZIP_MAGIC Zlib.gunzip(bytes) rescue Zlib::Error => e raise InvalidWebhookError, "#{InvalidWebhookError::GZIP_FAILED}: #{e.}" end |
.parse_event(payload) ⇒ Object
Parse a webhook payload and return the typed event for known discriminators or UnknownEvent for well-formed-but-unknown ones.
Distinct from parse_webhook_event: parse_event returns an UnknownEvent on unrecognized discriminators (forward-compat); parse_webhook_event throws.
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 882 def self.parse_event(payload) raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: payload must be a String" unless payload.is_a?(String) raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: payload must not be empty" if payload.empty? data = JSON.parse(payload) raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: webhook payload must be a JSON object" unless data.is_a?(Hash) event_type = data['type'] unless event_type.is_a?(String) && !event_type.empty? raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: webhook payload missing 'type' string field" end event_class = event_class_for_type(event_type) return build_unknown_event(event_type, data) if event_class.nil? begin event_class.new(data) rescue StandardError => e raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: failed to deserialize event: #{e.}" end rescue JSON::ParserError => e raise InvalidWebhookError, "#{InvalidWebhookError::INVALID_JSON}: failed to parse webhook payload: #{e.}" end |
.parse_sns(notification_body) ⇒ Object
SNS composite: parse SNS envelope -> base64-decode -> gunzip -> parse. Same no-signature posture as parse_sqs.
961 962 963 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 961 def self.parse_sns(notification_body) parse_event(decode_sns_payload(notification_body)) end |
.parse_sqs(message_body) ⇒ Object
SQS composite: base64-decode -> gunzip (if gzip-prefixed) -> parse.
The backend emits no signature attribute on SQS messages today; this helper therefore performs no signature verification. If a signed variant is added later, it will be a separate function rather than retrofitting this signature.
951 952 953 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 951 def self.parse_sqs() parse_event(decode_sqs_payload()) end |
.parse_webhook_event(raw_event) ⇒ Object
Deserialize a raw webhook payload into a typed event object.
Throws on unknown event types. For forward-compatible parsing that returns an UnknownEvent on unrecognized discriminators, use parse_event.
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 402 def self.parse_webhook_event(raw_event) data = parse_to_hash(raw_event) raise ArgumentError, 'Invalid webhook payload' unless data event_type = data['type'] raise ArgumentError, "Webhook payload missing 'type' field" unless event_type event_class = event_class_for_type(event_type) raise ArgumentError, "Unknown webhook event type: #{event_type}" unless event_class event_class.new(data) rescue JSON::ParserError => e raise ArgumentError, "Invalid JSON in webhook payload: #{e.}" rescue StandardError => e raise ArgumentError, "Failed to deserialize webhook event: #{e.}" end |
.verify_and_parse_webhook(body, signature, secret) ⇒ Object
HTTP composite: gunzip (if gzip-prefixed) -> verify HMAC-SHA256 -> parse.
The signature header is X-Signature. The signature is HMAC-SHA256 of the uncompressed JSON body, hex-encoded. Magic-byte detection means callers can pass either the raw HTTP body or already-decompressed bytes; both work.
935 936 937 938 939 940 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 935 def self.verify_and_parse_webhook(body, signature, secret) payload = gunzip_payload(body) raise InvalidWebhookError, InvalidWebhookError::SIGNATURE_MISMATCH unless verify_signature(payload, signature, secret) parse_event(payload) end |
.verify_signature(body, signature, secret) ⇒ Boolean
Verify the HMAC-SHA256 signature of a webhook payload.
779 780 781 782 783 784 785 786 |
# File 'lib/getstream_ruby/generated/webhook.rb', line 779 def self.verify_signature(body, signature, secret) return false if signature.nil? || signature.bytesize != 64 expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body) OpenSSL.fixed_length_secure_compare(signature, expected) rescue ArgumentError false end |