chat_sdk 0.1.0
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 +7 -0
- data/lib/chat_sdk/adapter/base.rb +99 -0
- data/lib/chat_sdk/adapter/capabilities.rb +34 -0
- data/lib/chat_sdk/author.rb +28 -0
- data/lib/chat_sdk/cards/actions_context.rb +32 -0
- data/lib/chat_sdk/cards/builder.rb +52 -0
- data/lib/chat_sdk/cards/fields_context.rb +17 -0
- data/lib/chat_sdk/cards/node.rb +45 -0
- data/lib/chat_sdk/cards/renderer.rb +49 -0
- data/lib/chat_sdk/cards/select_context.rb +19 -0
- data/lib/chat_sdk/channel.rb +31 -0
- data/lib/chat_sdk/chat.rb +93 -0
- data/lib/chat_sdk/config.rb +41 -0
- data/lib/chat_sdk/dispatcher.rb +118 -0
- data/lib/chat_sdk/errors.rb +38 -0
- data/lib/chat_sdk/event_registry.rb +41 -0
- data/lib/chat_sdk/events/action.rb +19 -0
- data/lib/chat_sdk/events/base.rb +17 -0
- data/lib/chat_sdk/events/direct_message.rb +16 -0
- data/lib/chat_sdk/events/mention.rb +16 -0
- data/lib/chat_sdk/events/reaction.rb +27 -0
- data/lib/chat_sdk/events/slash_command.rb +18 -0
- data/lib/chat_sdk/events/subscribed_message.rb +16 -0
- data/lib/chat_sdk/log.rb +23 -0
- data/lib/chat_sdk/message.rb +30 -0
- data/lib/chat_sdk/modals/builder.rb +40 -0
- data/lib/chat_sdk/postable_message.rb +28 -0
- data/lib/chat_sdk/state/base.rb +47 -0
- data/lib/chat_sdk/state/memory.rb +105 -0
- data/lib/chat_sdk/streaming/stream.rb +71 -0
- data/lib/chat_sdk/testing/adapter_contract.rb +81 -0
- data/lib/chat_sdk/testing/fake_adapter.rb +195 -0
- data/lib/chat_sdk/testing/helpers.rb +31 -0
- data/lib/chat_sdk/testing/recorded_call.rb +22 -0
- data/lib/chat_sdk/testing/state_contract.rb +81 -0
- data/lib/chat_sdk/testing.rb +13 -0
- data/lib/chat_sdk/thread.rb +104 -0
- data/lib/chat_sdk/version.rb +5 -0
- data/lib/chat_sdk/webhook/endpoint.rb +35 -0
- data/lib/chat_sdk/webhook/router.rb +24 -0
- data/lib/chat_sdk.rb +26 -0
- metadata +97 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
class FakeAdapter < Adapter::Base
|
|
6
|
+
capabilities :edit_messages, :delete_messages, :ephemeral_messages,
|
|
7
|
+
:file_uploads, :reactions, :modals, :typing_indicator,
|
|
8
|
+
:streaming_edit, :threads, :direct_messages, :message_history
|
|
9
|
+
|
|
10
|
+
attr_reader :posted_messages, :edited_messages, :deleted_messages,
|
|
11
|
+
:ephemeral_messages_sent, :reactions_added, :reactions_removed,
|
|
12
|
+
:files_uploaded, :modals_opened, :typing_started, :dm_channels
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
reset!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
:test
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def client
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def verify_request!(rack_request)
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ack_response(rack_request)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_events(rack_request)
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def post_message(channel_id:, message:, thread_id: nil)
|
|
39
|
+
record = RecordedCall.new(:post_message, channel_id: channel_id, message: message, thread_id: thread_id)
|
|
40
|
+
@posted_messages << record
|
|
41
|
+
Message.new(
|
|
42
|
+
id: "msg_#{@posted_messages.size}",
|
|
43
|
+
text: message.is_a?(PostableMessage) ? message.text : message.to_s,
|
|
44
|
+
author: Author.new(id: "bot", name: "test-bot", platform: :test, bot: true),
|
|
45
|
+
thread_id: thread_id,
|
|
46
|
+
channel_id: channel_id,
|
|
47
|
+
platform: :test
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def edit_message(channel_id:, message_id:, message:)
|
|
52
|
+
record = RecordedCall.new(:edit_message, channel_id: channel_id, message_id: message_id, message: message)
|
|
53
|
+
@edited_messages << record
|
|
54
|
+
record
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete_message(channel_id:, message_id:)
|
|
58
|
+
record = RecordedCall.new(:delete_message, channel_id: channel_id, message_id: message_id)
|
|
59
|
+
@deleted_messages << record
|
|
60
|
+
record
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil)
|
|
64
|
+
record = RecordedCall.new(:post_ephemeral, channel_id: channel_id, user_id: user_id, message: message, thread_id: thread_id)
|
|
65
|
+
@ephemeral_messages_sent << record
|
|
66
|
+
record
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
|
|
70
|
+
record = RecordedCall.new(:upload_file, channel_id: channel_id, filename: filename, thread_id: thread_id, comment: comment)
|
|
71
|
+
@files_uploaded << record
|
|
72
|
+
record
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def add_reaction(channel_id:, message_id:, emoji:)
|
|
76
|
+
record = RecordedCall.new(:add_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
77
|
+
@reactions_added << record
|
|
78
|
+
record
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def remove_reaction(channel_id:, message_id:, emoji:)
|
|
82
|
+
record = RecordedCall.new(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
83
|
+
@reactions_removed << record
|
|
84
|
+
record
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def open_dm(user_id)
|
|
88
|
+
channel_id = "dm_#{user_id}"
|
|
89
|
+
@dm_channels << RecordedCall.new(:open_dm, user_id: user_id, channel_id: channel_id)
|
|
90
|
+
channel_id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
|
|
94
|
+
[[], nil]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def open_modal(trigger_id:, modal:)
|
|
98
|
+
record = RecordedCall.new(:open_modal, trigger_id: trigger_id, modal: modal)
|
|
99
|
+
@modals_opened << record
|
|
100
|
+
record
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def start_typing(channel_id:, thread_id: nil)
|
|
104
|
+
record = RecordedCall.new(:start_typing, channel_id: channel_id, thread_id: thread_id)
|
|
105
|
+
@typing_started << record
|
|
106
|
+
record
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def mention(user_id)
|
|
110
|
+
"<@#{user_id}>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render(postable_message)
|
|
114
|
+
Cards::Renderer.new.render(postable_message.card)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Simulation helpers
|
|
118
|
+
def simulate_mention(chat, text:, user_id: "U123", user_name: "testuser", channel_id: "C123", thread_id: nil)
|
|
119
|
+
thread_id ||= "T#{rand(1000..9999)}"
|
|
120
|
+
author = Author.new(id: user_id, name: user_name, platform: :test)
|
|
121
|
+
message = Message.new(
|
|
122
|
+
id: "evt_#{rand(10000..99999)}",
|
|
123
|
+
text: text,
|
|
124
|
+
author: author,
|
|
125
|
+
thread_id: thread_id,
|
|
126
|
+
channel_id: channel_id,
|
|
127
|
+
platform: :test
|
|
128
|
+
)
|
|
129
|
+
event = Events::Mention.new(
|
|
130
|
+
message: message,
|
|
131
|
+
thread_id: thread_id,
|
|
132
|
+
channel_id: channel_id,
|
|
133
|
+
platform: :test,
|
|
134
|
+
adapter_name: :test
|
|
135
|
+
)
|
|
136
|
+
chat.dispatch(event, adapter_name: :test)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def simulate_action(chat, action_id:, value: nil, user_id: "U123", channel_id: "C123", thread_id: "T123", trigger_id: nil)
|
|
140
|
+
user = Author.new(id: user_id, name: "testuser", platform: :test)
|
|
141
|
+
event = Events::Action.new(
|
|
142
|
+
action_id: action_id,
|
|
143
|
+
value: value,
|
|
144
|
+
user: user,
|
|
145
|
+
thread_id: thread_id,
|
|
146
|
+
channel_id: channel_id,
|
|
147
|
+
trigger_id: trigger_id,
|
|
148
|
+
platform: :test,
|
|
149
|
+
adapter_name: :test
|
|
150
|
+
)
|
|
151
|
+
chat.dispatch(event, adapter_name: :test)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def simulate_reaction(chat, emoji:, user_id: "U123", message_id: "M123", channel_id: "C123", thread_id: "T123", added: true)
|
|
155
|
+
event = Events::Reaction.new(
|
|
156
|
+
emoji: emoji,
|
|
157
|
+
user_id: user_id,
|
|
158
|
+
message_id: message_id,
|
|
159
|
+
thread_id: thread_id,
|
|
160
|
+
channel_id: channel_id,
|
|
161
|
+
added: added,
|
|
162
|
+
platform: :test,
|
|
163
|
+
adapter_name: :test
|
|
164
|
+
)
|
|
165
|
+
chat.dispatch(event, adapter_name: :test)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def simulate_slash_command(chat, command:, text: "", user_id: "U123", channel_id: "C123", trigger_id: nil)
|
|
169
|
+
event = Events::SlashCommand.new(
|
|
170
|
+
command: command,
|
|
171
|
+
text: text,
|
|
172
|
+
user_id: user_id,
|
|
173
|
+
channel_id: channel_id,
|
|
174
|
+
trigger_id: trigger_id,
|
|
175
|
+
platform: :test,
|
|
176
|
+
adapter_name: :test
|
|
177
|
+
)
|
|
178
|
+
chat.dispatch(event, adapter_name: :test)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def reset!
|
|
182
|
+
@posted_messages = []
|
|
183
|
+
@edited_messages = []
|
|
184
|
+
@deleted_messages = []
|
|
185
|
+
@ephemeral_messages_sent = []
|
|
186
|
+
@reactions_added = []
|
|
187
|
+
@reactions_removed = []
|
|
188
|
+
@files_uploaded = []
|
|
189
|
+
@modals_opened = []
|
|
190
|
+
@typing_started = []
|
|
191
|
+
@dm_channels = []
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
module Helpers
|
|
6
|
+
def build_bot(**options)
|
|
7
|
+
ChatSDK::Testing.build_bot(**options)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fake_adapter
|
|
11
|
+
@fake_adapter ||= FakeAdapter.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build_message(text: "hello", user_id: "U123", user_name: "testuser", channel_id: "C123", thread_id: "T123")
|
|
15
|
+
author = Author.new(id: user_id, name: user_name, platform: :test)
|
|
16
|
+
Message.new(
|
|
17
|
+
id: "msg_#{rand(10000..99999)}",
|
|
18
|
+
text: text,
|
|
19
|
+
author: author,
|
|
20
|
+
thread_id: thread_id,
|
|
21
|
+
channel_id: channel_id,
|
|
22
|
+
platform: :test
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_card(title: "Test Card", &block)
|
|
27
|
+
ChatSDK.card(title: title, &block)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
class RecordedCall
|
|
6
|
+
attr_reader :method_name, :arguments
|
|
7
|
+
|
|
8
|
+
def initialize(method_name, **arguments)
|
|
9
|
+
@method_name = method_name
|
|
10
|
+
@arguments = arguments
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def [](key)
|
|
14
|
+
@arguments[key]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{method: @method_name}.merge(@arguments)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
module StateContract
|
|
6
|
+
RSpec.shared_examples "a chat_sdk state adapter" do
|
|
7
|
+
describe "subscriptions" do
|
|
8
|
+
it "subscribes and checks" do
|
|
9
|
+
subject.subscribe("thread_1")
|
|
10
|
+
expect(subject.subscribed?("thread_1")).to be true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "unsubscribes" do
|
|
14
|
+
subject.subscribe("thread_1")
|
|
15
|
+
subject.unsubscribe("thread_1")
|
|
16
|
+
expect(subject.subscribed?("thread_1")).to be false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns false for unsubscribed threads" do
|
|
20
|
+
expect(subject.subscribed?("nonexistent")).to be false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "locks" do
|
|
25
|
+
it "acquires a lock" do
|
|
26
|
+
expect(subject.acquire_lock("key1", owner: "a", ttl: 10)).to be true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "prevents double acquisition" do
|
|
30
|
+
subject.acquire_lock("key1", owner: "a", ttl: 10)
|
|
31
|
+
expect(subject.acquire_lock("key1", owner: "b", ttl: 10)).to be false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "releases a lock by owner" do
|
|
35
|
+
subject.acquire_lock("key1", owner: "a", ttl: 10)
|
|
36
|
+
expect(subject.release_lock("key1", owner: "a")).to be true
|
|
37
|
+
expect(subject.acquire_lock("key1", owner: "b", ttl: 10)).to be true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "does not release lock with wrong owner" do
|
|
41
|
+
subject.acquire_lock("key1", owner: "a", ttl: 10)
|
|
42
|
+
expect(subject.release_lock("key1", owner: "b")).to be false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "force acquires a lock" do
|
|
46
|
+
subject.acquire_lock("key1", owner: "a", ttl: 10)
|
|
47
|
+
expect(subject.force_lock("key1", owner: "b", ttl: 10)).to be true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "key-value store" do
|
|
52
|
+
it "gets and sets values" do
|
|
53
|
+
subject.set("k1", {"count" => 1})
|
|
54
|
+
expect(subject.get("k1")).to eq({"count" => 1})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "returns nil for missing keys" do
|
|
58
|
+
expect(subject.get("missing")).to be_nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "deletes keys" do
|
|
62
|
+
subject.set("k1", "v1")
|
|
63
|
+
subject.delete("k1")
|
|
64
|
+
expect(subject.get("k1")).to be_nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "set_if_absent succeeds when key absent" do
|
|
68
|
+
expect(subject.set_if_absent("k1", "v1")).to be true
|
|
69
|
+
expect(subject.get("k1")).to eq("v1")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "set_if_absent fails when key present" do
|
|
73
|
+
subject.set("k1", "v1")
|
|
74
|
+
expect(subject.set_if_absent("k1", "v2")).to be false
|
|
75
|
+
expect(subject.get("k1")).to eq("v1")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
class << self
|
|
6
|
+
def build_bot(adapters: nil, state: nil, **options)
|
|
7
|
+
adapters ||= {test: FakeAdapter.new}
|
|
8
|
+
state ||= State::Memory.new
|
|
9
|
+
Chat.new(user_name: "test-bot", adapters: adapters, state: state, **options)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class Thread
|
|
5
|
+
attr_reader :id, :channel_id, :adapter, :chat
|
|
6
|
+
|
|
7
|
+
def initialize(id:, channel_id:, adapter:, chat:)
|
|
8
|
+
@id = id
|
|
9
|
+
@channel_id = channel_id
|
|
10
|
+
@adapter = adapter
|
|
11
|
+
@chat = chat
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def subscribe
|
|
15
|
+
chat.state.subscribe(state_key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unsubscribe
|
|
19
|
+
chat.state.unsubscribe(state_key)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def subscribed?
|
|
23
|
+
chat.state.subscribed?(state_key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def post(content)
|
|
27
|
+
message = PostableMessage.from(content)
|
|
28
|
+
adapter.post_message(channel_id: channel_id, message: message, thread_id: id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def post_ephemeral(content, user_id:)
|
|
32
|
+
message = PostableMessage.from(content)
|
|
33
|
+
adapter.post_ephemeral(channel_id: channel_id, user_id: user_id, message: message, thread_id: id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def post_stream(placeholder: nil, &block)
|
|
37
|
+
stream = Streaming::Stream.new(
|
|
38
|
+
adapter: adapter,
|
|
39
|
+
channel_id: channel_id,
|
|
40
|
+
thread_id: id,
|
|
41
|
+
placeholder: placeholder,
|
|
42
|
+
update_interval: chat.config.streaming_update_interval
|
|
43
|
+
)
|
|
44
|
+
stream.run(&block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def edit(message_id, content)
|
|
48
|
+
message = PostableMessage.from(content)
|
|
49
|
+
adapter.edit_message(channel_id: channel_id, message_id: message_id, message: message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def delete(message_id)
|
|
53
|
+
adapter.delete_message(channel_id: channel_id, message_id: message_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def react(message_id, emoji)
|
|
57
|
+
adapter.add_reaction(channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unreact(message_id, emoji)
|
|
61
|
+
adapter.remove_reaction(channel_id: channel_id, message_id: message_id, emoji: emoji)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def upload(io:, filename:, comment: nil)
|
|
65
|
+
adapter.upload_file(channel_id: channel_id, io: io, filename: filename, thread_id: id, comment: comment)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def messages(cursor: nil, limit: 50)
|
|
69
|
+
adapter.fetch_messages(channel_id: channel_id, thread_id: id, cursor: cursor, limit: limit)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def state
|
|
73
|
+
chat.state.get(state_key(:state))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def set_state(value)
|
|
77
|
+
chat.state.set(state_key(:state), value, ttl: 30 * 24 * 3600)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mention_user(user_id)
|
|
81
|
+
adapter.mention(user_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def open_modal(trigger_id:, modal:)
|
|
85
|
+
adapter.open_modal(trigger_id: trigger_id, modal: modal)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ==(other)
|
|
89
|
+
other.is_a?(ChatSDK::Thread) && id == other.id && channel_id == other.channel_id
|
|
90
|
+
end
|
|
91
|
+
alias_method :eql?, :==
|
|
92
|
+
|
|
93
|
+
def hash
|
|
94
|
+
[id, channel_id].hash
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def state_key(suffix = nil)
|
|
100
|
+
base = "#{adapter.name}:#{channel_id}:#{id}"
|
|
101
|
+
suffix ? "#{base}:#{suffix}" : base
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Webhook
|
|
5
|
+
class Endpoint
|
|
6
|
+
attr_reader :chat, :adapter, :adapter_name
|
|
7
|
+
|
|
8
|
+
def initialize(chat:, adapter:, adapter_name:)
|
|
9
|
+
@chat = chat
|
|
10
|
+
@adapter = adapter
|
|
11
|
+
@adapter_name = adapter_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
request = Rack::Request.new(env) if defined?(Rack)
|
|
16
|
+
|
|
17
|
+
adapter.verify_request!(request || env)
|
|
18
|
+
|
|
19
|
+
ack = adapter.ack_response(request || env)
|
|
20
|
+
return ack if ack
|
|
21
|
+
|
|
22
|
+
events = adapter.parse_events(request || env)
|
|
23
|
+
events.each { |event| chat.dispatch(event, adapter_name: adapter_name) }
|
|
24
|
+
|
|
25
|
+
[200, {"content-type" => "text/plain"}, [""]]
|
|
26
|
+
rescue ChatSDK::SignatureVerificationError => e
|
|
27
|
+
ChatSDK::Log.warn("Signature verification failed: #{e.message}")
|
|
28
|
+
[401, {"content-type" => "text/plain"}, ["Unauthorized"]]
|
|
29
|
+
rescue => e
|
|
30
|
+
ChatSDK::Log.error("Webhook error: #{e.message}")
|
|
31
|
+
[200, {"content-type" => "text/plain"}, [""]]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Webhook
|
|
5
|
+
class Router
|
|
6
|
+
def initialize(chat, adapters)
|
|
7
|
+
@chat = chat
|
|
8
|
+
@adapters = adapters
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
path = env["PATH_INFO"].to_s.split("/").last&.to_sym
|
|
13
|
+
adapter = @adapters[path]
|
|
14
|
+
|
|
15
|
+
unless adapter
|
|
16
|
+
return [404, {"content-type" => "text/plain"}, ["Unknown adapter: #{path}"]]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
endpoint = Endpoint.new(chat: @chat, adapter: adapter, adapter_name: path)
|
|
20
|
+
endpoint.call(env)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/chat_sdk.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require_relative "chat_sdk/version"
|
|
5
|
+
require_relative "chat_sdk/errors"
|
|
6
|
+
|
|
7
|
+
module ChatSDK
|
|
8
|
+
class << self
|
|
9
|
+
def loader
|
|
10
|
+
@loader ||= begin
|
|
11
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
12
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK")
|
|
13
|
+
loader.ignore("#{__dir__}/chat_sdk/version.rb")
|
|
14
|
+
loader.ignore("#{__dir__}/chat_sdk/errors.rb")
|
|
15
|
+
loader.setup
|
|
16
|
+
loader
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def card(title: nil, subtitle: nil, &block)
|
|
21
|
+
Cards::Builder.new(title: title, subtitle: subtitle, &block).build
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ChatSDK.loader
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rootly
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: zeitwerk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.6'
|
|
26
|
+
description: Platform-agnostic chat bot framework with normalized events, cards DSL,
|
|
27
|
+
streaming, and pluggable adapters
|
|
28
|
+
email:
|
|
29
|
+
- eng@rootly.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/chat_sdk.rb
|
|
35
|
+
- lib/chat_sdk/adapter/base.rb
|
|
36
|
+
- lib/chat_sdk/adapter/capabilities.rb
|
|
37
|
+
- lib/chat_sdk/author.rb
|
|
38
|
+
- lib/chat_sdk/cards/actions_context.rb
|
|
39
|
+
- lib/chat_sdk/cards/builder.rb
|
|
40
|
+
- lib/chat_sdk/cards/fields_context.rb
|
|
41
|
+
- lib/chat_sdk/cards/node.rb
|
|
42
|
+
- lib/chat_sdk/cards/renderer.rb
|
|
43
|
+
- lib/chat_sdk/cards/select_context.rb
|
|
44
|
+
- lib/chat_sdk/channel.rb
|
|
45
|
+
- lib/chat_sdk/chat.rb
|
|
46
|
+
- lib/chat_sdk/config.rb
|
|
47
|
+
- lib/chat_sdk/dispatcher.rb
|
|
48
|
+
- lib/chat_sdk/errors.rb
|
|
49
|
+
- lib/chat_sdk/event_registry.rb
|
|
50
|
+
- lib/chat_sdk/events/action.rb
|
|
51
|
+
- lib/chat_sdk/events/base.rb
|
|
52
|
+
- lib/chat_sdk/events/direct_message.rb
|
|
53
|
+
- lib/chat_sdk/events/mention.rb
|
|
54
|
+
- lib/chat_sdk/events/reaction.rb
|
|
55
|
+
- lib/chat_sdk/events/slash_command.rb
|
|
56
|
+
- lib/chat_sdk/events/subscribed_message.rb
|
|
57
|
+
- lib/chat_sdk/log.rb
|
|
58
|
+
- lib/chat_sdk/message.rb
|
|
59
|
+
- lib/chat_sdk/modals/builder.rb
|
|
60
|
+
- lib/chat_sdk/postable_message.rb
|
|
61
|
+
- lib/chat_sdk/state/base.rb
|
|
62
|
+
- lib/chat_sdk/state/memory.rb
|
|
63
|
+
- lib/chat_sdk/streaming/stream.rb
|
|
64
|
+
- lib/chat_sdk/testing.rb
|
|
65
|
+
- lib/chat_sdk/testing/adapter_contract.rb
|
|
66
|
+
- lib/chat_sdk/testing/fake_adapter.rb
|
|
67
|
+
- lib/chat_sdk/testing/helpers.rb
|
|
68
|
+
- lib/chat_sdk/testing/recorded_call.rb
|
|
69
|
+
- lib/chat_sdk/testing/state_contract.rb
|
|
70
|
+
- lib/chat_sdk/thread.rb
|
|
71
|
+
- lib/chat_sdk/version.rb
|
|
72
|
+
- lib/chat_sdk/webhook/endpoint.rb
|
|
73
|
+
- lib/chat_sdk/webhook/router.rb
|
|
74
|
+
homepage: https://github.com/rootlyhq/rootly-chat-sdk
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.2.0
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 4.0.16
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Unified Ruby SDK for building chat bots across Slack, Teams, Google Chat,
|
|
96
|
+
and more
|
|
97
|
+
test_files: []
|