stream-chat-ruby 3.25.0 → 3.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/CHANGELOG.md +30 -0
- data/README.md +6 -0
- data/lib/stream-chat/client.rb +60 -9
- data/lib/stream-chat/errors.rb +29 -4
- data/lib/stream-chat/version.rb +1 -1
- data/lib/stream-chat/webhook.rb +206 -0
- data/stream-chat.gemspec +2 -2
- metadata +12 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '019bf5c538cd43fe7a5faef6b980bf00818756ec5cc470d0e61f8e5d610bbf4e'
|
|
4
|
+
data.tar.gz: 389642e627468500058a1aa007dae4a9b2bda9bcda2e73e5fd30218730e74ce3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2cbc7e4c1f481fefea8d06ee3a7e4e8c3c0c8e26a068b0754574588c0d65ce262886ffe3ebcee86196085df7d520ad4b1f6234c8b87351ba57d61e8d29132e2
|
|
7
|
+
data.tar.gz: 92abc3b3c173377ac3d5bc19957221f055fdfb15eabc38d8a64ef302be883b3441db8bbf22da87b937dbbb88f726344430bc5e6a8b2376f9a0f1c90734ff7d0a
|
data/.rspec
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [3.26.2](https://github.com/GetStream/stream-chat-ruby/compare/v3.26.1...v3.26.2) (2026-06-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **webhooks:** verify_and_parse_* API for compressed payloads (CHA-3071) ([#211](https://github.com/GetStream/stream-chat-ruby/issues/211)) ([dd76c8f](https://github.com/GetStream/stream-chat-ruby/commit/dd76c8f39d0fae98c3d2c331635e9369b40e3696))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* guard StreamAPIException against empty and non-integer error bodies ([#217](https://github.com/GetStream/stream-chat-ruby/issues/217)) ([415f6c0](https://github.com/GetStream/stream-chat-ruby/commit/415f6c0bd7b51b87631c5a1e8f821e50a5def2ec))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Other
|
|
19
|
+
|
|
20
|
+
* before-message-send hook attempt timeout (CHA-3267) ([#216](https://github.com/GetStream/stream-chat-ruby/issues/216)) ([e721776](https://github.com/GetStream/stream-chat-ruby/commit/e721776376c064b434485bd4cb4188762f9f3c2b)), closes [GetStream/chat#13541](https://github.com/GetStream/chat/issues/13541)
|
|
21
|
+
* **spec:** clean fellowship sediment + retry commands eventually ([#212](https://github.com/GetStream/stream-chat-ruby/issues/212)) ([f3c8f5b](https://github.com/GetStream/stream-chat-ruby/commit/f3c8f5be841f95c2e0e399d3d180d7565d80fd6a))
|
|
22
|
+
|
|
23
|
+
### [3.26.1](https://github.com/GetStream/stream-chat-ruby/compare/v3.25.0...v3.26.1) (2026-04-29)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* add delete_reactions support for ban user ([#206](https://github.com/GetStream/stream-chat-ruby/issues/206)) ([c876050](https://github.com/GetStream/stream-chat-ruby/commit/c876050936b3f4727002707e94db872c91a6114c))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Other
|
|
32
|
+
|
|
33
|
+
* relax jwt dependency to allow v3 ([#208](https://github.com/GetStream/stream-chat-ruby/issues/208)) ([8305806](https://github.com/GetStream/stream-chat-ruby/commit/8305806127859540bbcd936ccda19ac9c13b9897)), closes [#12716](https://github.com/GetStream/stream-chat-ruby/issues/12716)
|
|
34
|
+
|
|
5
35
|
## [3.25.0](https://github.com/GetStream/stream-chat-ruby/compare/v3.24.0...v3.25.0) (2026-03-05)
|
|
6
36
|
|
|
7
37
|
|
data/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
> **Note:** [`getstream-ruby`](https://github.com/GetStream/getstream-ruby) is the new full-product SDK for Stream, covering Chat, Video, Moderation, and Feeds with long-term support.
|
|
2
|
+
>
|
|
3
|
+
> **`stream-chat-ruby` is now in maintenance mode.** It will continue to receive critical fixes and requested features, so it is safe to keep using. However, we encourage existing users to migrate when convenient and strongly recommend that new projects start with `getstream-ruby`.
|
|
4
|
+
>
|
|
5
|
+
> Check out the [Migration Guide](https://github.com/GetStream/getstream-ruby/blob/570aaf9b73b27fe01a4abbc1f7c6b639595c304e/docs/migration-from-stream-chat-ruby/README.md) for side-by-side examples covering common use cases.
|
|
6
|
+
|
|
1
7
|
# Official Ruby SDK for [Stream Chat](https://getstream.io/chat/)
|
|
2
8
|
|
|
3
9
|
[](https://github.com/GetStream/stream-chat-ruby/actions) [](http://badge.fury.io/rb/stream-chat-ruby)
|
data/lib/stream-chat/client.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require 'base64'
|
|
4
5
|
require 'open-uri'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
require 'zlib'
|
|
5
9
|
require 'faraday'
|
|
6
10
|
require 'faraday/multipart'
|
|
7
11
|
require 'faraday/net_http_persistent'
|
|
@@ -18,9 +22,10 @@ require 'stream-chat/util'
|
|
|
18
22
|
require 'stream-chat/types'
|
|
19
23
|
require 'stream-chat/moderation'
|
|
20
24
|
require 'stream-chat/channel_batch_updater'
|
|
25
|
+
require 'stream-chat/webhook'
|
|
21
26
|
|
|
22
27
|
module StreamChat
|
|
23
|
-
DEFAULT_BLOCKLIST = '
|
|
28
|
+
DEFAULT_BLOCKLIST = 'profanity'
|
|
24
29
|
SOFT_DELETE = 'soft'
|
|
25
30
|
HARD_DELETE = 'hard'
|
|
26
31
|
|
|
@@ -698,11 +703,57 @@ module StreamChat
|
|
|
698
703
|
get('rate_limits', params: params)
|
|
699
704
|
end
|
|
700
705
|
|
|
701
|
-
# Verify the signature
|
|
706
|
+
# Verify the signature on a webhook event using the client's API secret.
|
|
707
|
+
#
|
|
708
|
+
# Backward-compatible boolean helper. New integrations should call
|
|
709
|
+
# {#verify_and_parse_webhook} (or the SQS / SNS variants), which also handle
|
|
710
|
+
# gzip payload compression and return the parsed event.
|
|
702
711
|
sig { params(request_body: String, x_signature: String).returns(T::Boolean) }
|
|
703
712
|
def verify_webhook(request_body, x_signature)
|
|
704
|
-
|
|
705
|
-
|
|
713
|
+
StreamChat::Webhook.verify_signature(request_body, x_signature, @api_secret)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Verify and parse an HTTP webhook event.
|
|
717
|
+
#
|
|
718
|
+
# Decompresses `body` when gzipped (detected from the body bytes), verifies
|
|
719
|
+
# the `X-Signature` header against the client's API secret, and returns the
|
|
720
|
+
# parsed event as a `Hash`. Typed event classes are planned for a future
|
|
721
|
+
# release.
|
|
722
|
+
#
|
|
723
|
+
# @param body [String, Array<Integer>] Raw HTTP request body bytes Stream
|
|
724
|
+
# signed.
|
|
725
|
+
# @param signature [String] Value of the `X-Signature` header.
|
|
726
|
+
# @return [Hash] The parsed event.
|
|
727
|
+
# @raise [InvalidWebhookError] When the signature does not match or the
|
|
728
|
+
# gzip envelope is malformed.
|
|
729
|
+
sig do
|
|
730
|
+
params(
|
|
731
|
+
body: T.any(String, T::Array[Integer]),
|
|
732
|
+
signature: String
|
|
733
|
+
).returns(T::Hash[String, T.untyped])
|
|
734
|
+
end
|
|
735
|
+
def verify_and_parse_webhook(body, signature)
|
|
736
|
+
StreamChat::Webhook.verify_and_parse_webhook(body, signature, @api_secret)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Parse an SQS-delivered webhook event (decode only; Stream does not HMAC-sign SQS bodies).
|
|
740
|
+
#
|
|
741
|
+
# @param message_body [String] SQS message `Body` string.
|
|
742
|
+
# @return [Hash] The parsed event.
|
|
743
|
+
# @raise [InvalidWebhookError] on malformed base64 or gzip envelope
|
|
744
|
+
sig { params(message_body: String).returns(T::Hash[String, T.untyped]) }
|
|
745
|
+
def parse_sqs(message_body)
|
|
746
|
+
StreamChat::Webhook.parse_sqs(message_body)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Parse an SNS-delivered webhook event (unwraps envelope JSON when needed).
|
|
750
|
+
#
|
|
751
|
+
# @param message [String] Raw SNS POST body or pre-extracted Message string.
|
|
752
|
+
# @return [Hash] The parsed event.
|
|
753
|
+
# @raise [InvalidWebhookError] on decode failures
|
|
754
|
+
sig { params(message: String).returns(T::Hash[String, T.untyped]) }
|
|
755
|
+
def parse_sns(message)
|
|
756
|
+
StreamChat::Webhook.parse_sns(message)
|
|
706
757
|
end
|
|
707
758
|
|
|
708
759
|
# Allows you to send custom events to a connected user.
|
|
@@ -739,7 +790,7 @@ module StreamChat
|
|
|
739
790
|
# Returns all blocklists.
|
|
740
791
|
#
|
|
741
792
|
# A Block List is a list of words that you can use to moderate chat messages. Stream Chat
|
|
742
|
-
# comes with a built-in Block List called
|
|
793
|
+
# comes with a built-in Block List called profanity which contains over a thousand
|
|
743
794
|
# of the most common profane words.
|
|
744
795
|
# You can manage your own block lists via the Stream dashboard or APIs to a manage
|
|
745
796
|
# blocklists and configure your channel types to use them.
|
|
@@ -751,7 +802,7 @@ module StreamChat
|
|
|
751
802
|
# Returns a blocklist.
|
|
752
803
|
#
|
|
753
804
|
# A Block List is a list of words that you can use to moderate chat messages. Stream Chat
|
|
754
|
-
# comes with a built-in Block List called
|
|
805
|
+
# comes with a built-in Block List called profanity which contains over a thousand
|
|
755
806
|
# of the most common profane words.
|
|
756
807
|
# You can manage your own block lists via the Stream dashboard or APIs to a manage
|
|
757
808
|
# blocklists and configure your channel types to use them.
|
|
@@ -763,7 +814,7 @@ module StreamChat
|
|
|
763
814
|
# Creates a blocklist.
|
|
764
815
|
#
|
|
765
816
|
# A Block List is a list of words that you can use to moderate chat messages. Stream Chat
|
|
766
|
-
# comes with a built-in Block List called
|
|
817
|
+
# comes with a built-in Block List called profanity which contains over a thousand
|
|
767
818
|
# of the most common profane words.
|
|
768
819
|
# You can manage your own block lists via the Stream dashboard or APIs to a manage
|
|
769
820
|
# blocklists and configure your channel types to use them.
|
|
@@ -775,7 +826,7 @@ module StreamChat
|
|
|
775
826
|
# Updates a blocklist.
|
|
776
827
|
#
|
|
777
828
|
# A Block List is a list of words that you can use to moderate chat messages. Stream Chat
|
|
778
|
-
# comes with a built-in Block List called
|
|
829
|
+
# comes with a built-in Block List called profanity which contains over a thousand
|
|
779
830
|
# of the most common profane words.
|
|
780
831
|
# You can manage your own block lists via the Stream dashboard or APIs to a manage
|
|
781
832
|
# blocklists and configure your channel types to use them.
|
|
@@ -787,7 +838,7 @@ module StreamChat
|
|
|
787
838
|
# Deletes a blocklist.
|
|
788
839
|
#
|
|
789
840
|
# A Block List is a list of words that you can use to moderate chat messages. Stream Chat
|
|
790
|
-
# comes with a built-in Block List called
|
|
841
|
+
# comes with a built-in Block List called profanity which contains over a thousand
|
|
791
842
|
# of the most common profane words.
|
|
792
843
|
# You can manage your own block lists via the Stream dashboard or APIs to a manage
|
|
793
844
|
# blocklists and configure your channel types to use them.
|
data/lib/stream-chat/errors.rb
CHANGED
|
@@ -21,14 +21,28 @@ module StreamChat
|
|
|
21
21
|
def initialize(response)
|
|
22
22
|
super()
|
|
23
23
|
@response = response
|
|
24
|
+
|
|
25
|
+
# Seed defaults first so the typed readers never return nil. A 5xx can
|
|
26
|
+
# arrive with an empty or non-JSON body (e.g. a load balancer emitting a
|
|
27
|
+
# bare 503), and an error envelope may omit "code"/"message" or send a
|
|
28
|
+
# non-integer "code". Without these defaults, reading error_code on such
|
|
29
|
+
# an exception raised a Sorbet TypeError that masked the real HTTP error.
|
|
30
|
+
@json_response = T.let(false, T::Boolean)
|
|
31
|
+
@error_code = T.let(-1, Integer)
|
|
32
|
+
@error_message = T.let('unknown', String)
|
|
33
|
+
|
|
24
34
|
begin
|
|
25
35
|
parsed_response = JSON.parse(response.body)
|
|
26
|
-
@json_response = T.let(true, T::Boolean)
|
|
27
|
-
@error_code = T.let(parsed_response.fetch('code', 'unknown'), Integer)
|
|
28
|
-
@error_message = T.let(parsed_response.fetch('message', 'unknown'), String)
|
|
29
36
|
rescue JSON::ParserError
|
|
30
|
-
|
|
37
|
+
return
|
|
31
38
|
end
|
|
39
|
+
return unless parsed_response.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
@json_response = true
|
|
42
|
+
code = parsed_response['code']
|
|
43
|
+
@error_code = code if code.is_a?(Integer)
|
|
44
|
+
msg = parsed_response['message']
|
|
45
|
+
@error_message = msg if msg.is_a?(String)
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
sig { returns(String) }
|
|
@@ -47,4 +61,15 @@ module StreamChat
|
|
|
47
61
|
end
|
|
48
62
|
|
|
49
63
|
class StreamChannelException < StandardError; end
|
|
64
|
+
|
|
65
|
+
# Raised by webhook verify/parse helpers when the HMAC does not match or a
|
|
66
|
+
# gzip/base64/JSON envelope cannot be decoded. The message identifies which
|
|
67
|
+
# failure mode fired; the class-level constants below are the canonical
|
|
68
|
+
# strings for callers that prefer exact-match filtering over substring.
|
|
69
|
+
class InvalidWebhookError < StandardError
|
|
70
|
+
SIGNATURE_MISMATCH = 'signature mismatch'
|
|
71
|
+
INVALID_BASE64 = 'invalid base64 encoding'
|
|
72
|
+
GZIP_FAILED = 'gzip decompression failed'
|
|
73
|
+
INVALID_JSON = 'invalid JSON payload'
|
|
74
|
+
end
|
|
50
75
|
end
|
data/lib/stream-chat/version.rb
CHANGED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'sorbet-runtime'
|
|
8
|
+
require 'stringio'
|
|
9
|
+
require 'zlib'
|
|
10
|
+
|
|
11
|
+
require 'stream-chat/errors'
|
|
12
|
+
|
|
13
|
+
module StreamChat
|
|
14
|
+
# Stateless helpers implementing the cross-SDK webhook contract documented at
|
|
15
|
+
# https://getstream.io/chat/docs/node/webhooks_overview/.
|
|
16
|
+
#
|
|
17
|
+
# The composite functions (`verify_and_parse_webhook`, `parse_sqs`,
|
|
18
|
+
# `parse_sns`) are the recommended entry points. The primitives
|
|
19
|
+
# they compose (`gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload`,
|
|
20
|
+
# `verify_signature`, `parse_event`) are exposed so callers can build custom
|
|
21
|
+
# flows or run individual steps in isolation.
|
|
22
|
+
#
|
|
23
|
+
# The Ruby SDK currently returns the parsed JSON as a `Hash`; typed event
|
|
24
|
+
# classes will land in a future release.
|
|
25
|
+
module Webhook # rubocop:disable Metrics/ModuleLength
|
|
26
|
+
extend T::Sig
|
|
27
|
+
|
|
28
|
+
GZIP_MAGIC = T.let("\x1f\x8b".b.freeze, String)
|
|
29
|
+
|
|
30
|
+
# Coerces the webhook body into a binary `String` regardless of whether
|
|
31
|
+
# the caller hands us a `String` (HTTP `request.raw_post`) or an array of
|
|
32
|
+
# bytes (which is what some Ruby SQS clients yield when the message body
|
|
33
|
+
# is binary safe).
|
|
34
|
+
sig { params(body: T.any(String, T::Array[Integer])).returns(String) }
|
|
35
|
+
def self.normalize_body(body)
|
|
36
|
+
raw =
|
|
37
|
+
if body.is_a?(Array)
|
|
38
|
+
body.pack('C*')
|
|
39
|
+
else
|
|
40
|
+
String.new(body)
|
|
41
|
+
end
|
|
42
|
+
raw.force_encoding(Encoding::ASCII_8BIT)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns `body` unchanged unless it starts with the gzip magic
|
|
46
|
+
# (`1f 8b`, per RFC 1952), in which case the gzip stream is inflated and
|
|
47
|
+
# the decompressed bytes are returned.
|
|
48
|
+
#
|
|
49
|
+
# Magic-byte detection (rather than relying on a header) keeps the same
|
|
50
|
+
# handler correct when middleware - Rack, Rails - auto-decompresses the
|
|
51
|
+
# request before your code sees it.
|
|
52
|
+
sig { params(body: T.any(String, T::Array[Integer])).returns(String) }
|
|
53
|
+
def self.gunzip_payload(body)
|
|
54
|
+
raw = normalize_body(body)
|
|
55
|
+
return raw unless raw.start_with?(GZIP_MAGIC)
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
Zlib::GzipReader.new(StringIO.new(raw)).read.force_encoding(Encoding::ASCII_8BIT)
|
|
59
|
+
rescue Zlib::Error
|
|
60
|
+
raise InvalidWebhookError, InvalidWebhookError::GZIP_FAILED
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reverses the SQS firehose envelope: the message `Body` is base64-decoded
|
|
65
|
+
# and, when the result begins with the gzip magic, gzip-decompressed. The
|
|
66
|
+
# same call works whether or not Stream is currently compressing payloads.
|
|
67
|
+
sig { params(body: String).returns(String) }
|
|
68
|
+
def self.decode_sqs_payload(body)
|
|
69
|
+
decoded =
|
|
70
|
+
begin
|
|
71
|
+
Base64.strict_decode64(body)
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
raise InvalidWebhookError, InvalidWebhookError::INVALID_BASE64
|
|
74
|
+
end
|
|
75
|
+
gunzip_payload(decoded)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Reverses an SNS HTTP notification envelope. When `notification_body` is a
|
|
79
|
+
# JSON envelope (`{"Type":"Notification","Message":"..."}`), the inner
|
|
80
|
+
# `Message` field is extracted and run through the SQS pipeline
|
|
81
|
+
# (base64-decode, then gzip-if-magic). When the input is not a JSON
|
|
82
|
+
# envelope it is treated as the already-extracted `Message` string, so
|
|
83
|
+
# call sites that pre-unwrap continue to work.
|
|
84
|
+
sig { params(notification_body: String).returns(String) }
|
|
85
|
+
def self.decode_sns_payload(notification_body)
|
|
86
|
+
inner = extract_sns_message(notification_body)
|
|
87
|
+
decode_sqs_payload(inner.nil? ? notification_body : inner)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { params(notification_body: String).returns(T.nilable(String)) }
|
|
91
|
+
def self.extract_sns_message(notification_body)
|
|
92
|
+
trimmed = notification_body.to_s.lstrip
|
|
93
|
+
return nil unless trimmed.start_with?('{')
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
parsed = JSON.parse(trimmed)
|
|
97
|
+
rescue JSON::ParserError
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
return nil unless parsed.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
message = parsed['Message']
|
|
103
|
+
message.is_a?(String) ? message : nil
|
|
104
|
+
end
|
|
105
|
+
private_class_method :extract_sns_message
|
|
106
|
+
|
|
107
|
+
# Constant-time HMAC-SHA256 verification of `signature` against the digest
|
|
108
|
+
# of `body` keyed by `secret`.
|
|
109
|
+
#
|
|
110
|
+
# The signature is always computed over the **uncompressed** JSON bytes,
|
|
111
|
+
# so callers that decoded a gzipped or base64-wrapped payload must pass
|
|
112
|
+
# the inflated bytes here.
|
|
113
|
+
sig do
|
|
114
|
+
params(
|
|
115
|
+
body: T.any(String, T::Array[Integer]),
|
|
116
|
+
signature: String,
|
|
117
|
+
secret: String
|
|
118
|
+
).returns(T::Boolean)
|
|
119
|
+
end
|
|
120
|
+
def self.verify_signature(body, signature, secret)
|
|
121
|
+
raw = normalize_body(body)
|
|
122
|
+
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw)
|
|
123
|
+
constant_time_equal?(expected, signature)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Parse a JSON-encoded webhook event into a `Hash`.
|
|
127
|
+
#
|
|
128
|
+
# The Ruby SDK currently returns the parsed JSON as a `Hash`; typed event
|
|
129
|
+
# classes will land in a future release. The function name matches the
|
|
130
|
+
# documented primitive so callers can swap in a typed parser later without
|
|
131
|
+
# changing call sites.
|
|
132
|
+
sig { params(payload: String).returns(T::Hash[String, T.untyped]) }
|
|
133
|
+
def self.parse_event(payload)
|
|
134
|
+
result =
|
|
135
|
+
begin
|
|
136
|
+
JSON.parse(payload)
|
|
137
|
+
rescue JSON::ParserError
|
|
138
|
+
raise InvalidWebhookError, InvalidWebhookError::INVALID_JSON
|
|
139
|
+
end
|
|
140
|
+
raise InvalidWebhookError, InvalidWebhookError::INVALID_JSON unless result.is_a?(Hash)
|
|
141
|
+
|
|
142
|
+
result
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
sig do
|
|
146
|
+
params(
|
|
147
|
+
payload: String,
|
|
148
|
+
signature: String,
|
|
149
|
+
secret: String
|
|
150
|
+
).returns(T::Hash[String, T.untyped])
|
|
151
|
+
end
|
|
152
|
+
def self.verify_and_parse_internal(payload, signature, secret)
|
|
153
|
+
raise InvalidWebhookError, InvalidWebhookError::SIGNATURE_MISMATCH unless verify_signature(payload, signature, secret)
|
|
154
|
+
|
|
155
|
+
parse_event(payload)
|
|
156
|
+
end
|
|
157
|
+
private_class_method :verify_and_parse_internal
|
|
158
|
+
|
|
159
|
+
# Decompress `body` when gzipped, verify the HMAC `signature`, and return
|
|
160
|
+
# the parsed event.
|
|
161
|
+
sig do
|
|
162
|
+
params(
|
|
163
|
+
body: T.any(String, T::Array[Integer]),
|
|
164
|
+
signature: String,
|
|
165
|
+
secret: String
|
|
166
|
+
).returns(T::Hash[String, T.untyped])
|
|
167
|
+
end
|
|
168
|
+
def self.verify_and_parse_webhook(body, signature, secret)
|
|
169
|
+
verify_and_parse_internal(gunzip_payload(body), signature, secret)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Decode the SQS `Body` (base64, then gzip-if-magic) and return the parsed
|
|
173
|
+
# event JSON. Stream does not ship an application-level signature on SQS payloads.
|
|
174
|
+
sig { params(message_body: String).returns(T::Hash[String, T.untyped]) }
|
|
175
|
+
def self.parse_sqs(message_body)
|
|
176
|
+
parse_event(decode_sqs_payload(message_body))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Decode SNS (unwrap envelope when needed) and return the parsed event JSON.
|
|
180
|
+
sig { params(message: String).returns(T::Hash[String, T.untyped]) }
|
|
181
|
+
def self.parse_sns(message)
|
|
182
|
+
parse_event(decode_sns_payload(message))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Timing-safe equality check used to compare the locally computed HMAC
|
|
186
|
+
# with the `X-Signature` header. Prefers the OpenSSL primitive when the
|
|
187
|
+
# Ruby build exposes it, otherwise falls back to a manual byte XOR loop
|
|
188
|
+
# that does not short-circuit on the first mismatch.
|
|
189
|
+
sig { params(left: String, right: String).returns(T::Boolean) }
|
|
190
|
+
def self.constant_time_equal?(left, right)
|
|
191
|
+
a = left.b
|
|
192
|
+
b = right.b
|
|
193
|
+
return false unless a.bytesize == b.bytesize
|
|
194
|
+
|
|
195
|
+
if OpenSSL.respond_to?(:fixed_length_secure_compare)
|
|
196
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
197
|
+
else
|
|
198
|
+
a_bytes = a.bytes
|
|
199
|
+
b_bytes = b.bytes
|
|
200
|
+
diff = 0
|
|
201
|
+
a_bytes.each_with_index { |byte, i| diff |= byte ^ b_bytes[i] }
|
|
202
|
+
diff.zero?
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/stream-chat.gemspec
CHANGED
|
@@ -14,7 +14,7 @@ Gem::Specification.new do |gem|
|
|
|
14
14
|
gem.homepage = 'http://github.com/GetStream/stream-chat-ruby'
|
|
15
15
|
gem.authors = ['getstream.io']
|
|
16
16
|
gem.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
17
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|sorbet|spec|\.github|scripts|assets)/}) }
|
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|sorbet|spec|\.github|scripts|assets|docs)/}) }
|
|
18
18
|
end
|
|
19
19
|
gem.required_ruby_version = '>=3.0.0'
|
|
20
20
|
gem.metadata = {
|
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |gem|
|
|
|
29
29
|
gem.add_dependency 'faraday', '~> 2.12'
|
|
30
30
|
gem.add_dependency 'faraday-multipart', '~> 1.1'
|
|
31
31
|
gem.add_dependency 'faraday-net_http_persistent', '~> 2.3'
|
|
32
|
-
gem.add_dependency 'jwt', '
|
|
32
|
+
gem.add_dependency 'jwt', '>= 2.10', '< 4'
|
|
33
33
|
gem.add_dependency 'net-http-persistent', '~> 4.0'
|
|
34
34
|
gem.add_dependency 'sorbet-runtime', '>= 0.5.11820', '< 1'
|
|
35
35
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stream-chat-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.26.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- getstream.io
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -56,16 +56,22 @@ dependencies:
|
|
|
56
56
|
name: jwt
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '2.10'
|
|
62
|
+
- - "<"
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '4'
|
|
62
65
|
type: :runtime
|
|
63
66
|
prerelease: false
|
|
64
67
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
68
|
requirements:
|
|
66
|
-
- - "
|
|
69
|
+
- - ">="
|
|
67
70
|
- !ruby/object:Gem::Version
|
|
68
71
|
version: '2.10'
|
|
72
|
+
- - "<"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '4'
|
|
69
75
|
- !ruby/object:Gem::Dependency
|
|
70
76
|
name: net-http-persistent
|
|
71
77
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +113,7 @@ extensions: []
|
|
|
107
113
|
extra_rdoc_files: []
|
|
108
114
|
files:
|
|
109
115
|
- ".gitignore"
|
|
116
|
+
- ".rspec"
|
|
110
117
|
- ".rubocop.yml"
|
|
111
118
|
- ".versionrc.js"
|
|
112
119
|
- CHANGELOG.md
|
|
@@ -130,6 +137,7 @@ files:
|
|
|
130
137
|
- lib/stream-chat/types.rb
|
|
131
138
|
- lib/stream-chat/util.rb
|
|
132
139
|
- lib/stream-chat/version.rb
|
|
140
|
+
- lib/stream-chat/webhook.rb
|
|
133
141
|
- stream-chat.gemspec
|
|
134
142
|
homepage: http://github.com/GetStream/stream-chat-ruby
|
|
135
143
|
licenses: []
|