Module: StreamChat::Webhook

Extended by:
T::Sig
Defined in:
lib/stream-chat/webhook.rb

Overview

Stateless helpers implementing the cross-SDK webhook contract documented at https://getstream.io/chat/docs/node/webhooks_overview/.

The composite functions (verify_and_parse_webhook, parse_sqs, parse_sns) are the recommended entry points. The primitives they compose (gunzip_payload, decode_sqs_payload, decode_sns_payload, verify_signature, parse_event) are exposed so callers can build custom flows or run individual steps in isolation.

The Ruby SDK currently returns the parsed JSON as a Hash; typed event classes will land in a future release.

Constant Summary collapse

GZIP_MAGIC =
T.let("\x1f\x8b".b.freeze, String)

Class Method Summary collapse

Class Method Details

.constant_time_equal?(left, right) ⇒ Boolean

Returns:

  • (Boolean)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/stream-chat/webhook.rb', line 190

def self.constant_time_equal?(left, right)
  a = left.b
  b = right.b
  return false unless a.bytesize == b.bytesize

  if OpenSSL.respond_to?(:fixed_length_secure_compare)
    OpenSSL.fixed_length_secure_compare(a, b)
  else
    a_bytes = a.bytes
    b_bytes = b.bytes
    diff = 0
    a_bytes.each_with_index { |byte, i| diff |= byte ^ b_bytes[i] }
    diff.zero?
  end
end

.decode_sns_payload(notification_body) ⇒ Object



85
86
87
88
# File 'lib/stream-chat/webhook.rb', line 85

def self.decode_sns_payload(notification_body)
  inner = extract_sns_message(notification_body)
  decode_sqs_payload(inner.nil? ? notification_body : inner)
end

.decode_sqs_payload(body) ⇒ Object



68
69
70
71
72
73
74
75
76
# File 'lib/stream-chat/webhook.rb', line 68

def self.decode_sqs_payload(body)
  decoded =
    begin
      Base64.strict_decode64(body)
    rescue ArgumentError
      raise InvalidWebhookError, InvalidWebhookError::INVALID_BASE64
    end
  gunzip_payload(decoded)
end

.gunzip_payload(body) ⇒ Object



53
54
55
56
57
58
59
60
61
62
# File 'lib/stream-chat/webhook.rb', line 53

def self.gunzip_payload(body)
  raw = normalize_body(body)
  return raw unless raw.start_with?(GZIP_MAGIC)

  begin
    Zlib::GzipReader.new(StringIO.new(raw)).read.force_encoding(Encoding::ASCII_8BIT)
  rescue Zlib::Error
    raise InvalidWebhookError, InvalidWebhookError::GZIP_FAILED
  end
end

.normalize_body(body) ⇒ Object



35
36
37
38
39
40
41
42
43
# File 'lib/stream-chat/webhook.rb', line 35

def self.normalize_body(body)
  raw =
    if body.is_a?(Array)
      body.pack('C*')
    else
      String.new(body)
    end
  raw.force_encoding(Encoding::ASCII_8BIT)
end

.parse_event(payload) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/stream-chat/webhook.rb', line 133

def self.parse_event(payload)
  result =
    begin
      JSON.parse(payload)
    rescue JSON::ParserError
      raise InvalidWebhookError, InvalidWebhookError::INVALID_JSON
    end
  raise InvalidWebhookError, InvalidWebhookError::INVALID_JSON unless result.is_a?(Hash)

  result
end

.parse_sns(message) ⇒ Object



181
182
183
# File 'lib/stream-chat/webhook.rb', line 181

def self.parse_sns(message)
  parse_event(decode_sns_payload(message))
end

.parse_sqs(message_body) ⇒ Object



175
176
177
# File 'lib/stream-chat/webhook.rb', line 175

def self.parse_sqs(message_body)
  parse_event(decode_sqs_payload(message_body))
end

.verify_and_parse_webhook(body, signature, secret) ⇒ Object



168
169
170
# File 'lib/stream-chat/webhook.rb', line 168

def self.verify_and_parse_webhook(body, signature, secret)
  verify_and_parse_internal(gunzip_payload(body), signature, secret)
end

.verify_signature(body, signature, secret) ⇒ Object



120
121
122
123
124
# File 'lib/stream-chat/webhook.rb', line 120

def self.verify_signature(body, signature, secret)
  raw = normalize_body(body)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw)
  constant_time_equal?(expected, signature)
end