stream-chat-ruby 3.26.1 → 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 +18 -0
- data/lib/stream-chat/client.rb +54 -3
- data/lib/stream-chat/errors.rb +29 -4
- data/lib/stream-chat/version.rb +1 -1
- data/lib/stream-chat/webhook.rb +206 -0
- metadata +4 -2
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,24 @@
|
|
|
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
|
+
|
|
5
23
|
### [3.26.1](https://github.com/GetStream/stream-chat-ruby/compare/v3.25.0...v3.26.1) (2026-04-29)
|
|
6
24
|
|
|
7
25
|
|
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,6 +22,7 @@ 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
28
|
DEFAULT_BLOCKLIST = 'profanity'
|
|
@@ -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.
|
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
|
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.26.
|
|
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
|
|
@@ -113,6 +113,7 @@ extensions: []
|
|
|
113
113
|
extra_rdoc_files: []
|
|
114
114
|
files:
|
|
115
115
|
- ".gitignore"
|
|
116
|
+
- ".rspec"
|
|
116
117
|
- ".rubocop.yml"
|
|
117
118
|
- ".versionrc.js"
|
|
118
119
|
- CHANGELOG.md
|
|
@@ -136,6 +137,7 @@ files:
|
|
|
136
137
|
- lib/stream-chat/types.rb
|
|
137
138
|
- lib/stream-chat/util.rb
|
|
138
139
|
- lib/stream-chat/version.rb
|
|
140
|
+
- lib/stream-chat/webhook.rb
|
|
139
141
|
- stream-chat.gemspec
|
|
140
142
|
homepage: http://github.com/GetStream/stream-chat-ruby
|
|
141
143
|
licenses: []
|