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,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class ConfigurationError < Error; end
|
|
6
|
+
|
|
7
|
+
class NotSupportedError < Error
|
|
8
|
+
attr_reader :capability, :adapter_name
|
|
9
|
+
def initialize(capability, adapter_name)
|
|
10
|
+
@capability = capability
|
|
11
|
+
@adapter_name = adapter_name
|
|
12
|
+
super("#{adapter_name} adapter does not support #{capability}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class LockConflictError < Error; end
|
|
17
|
+
class SignatureVerificationError < Error; end
|
|
18
|
+
|
|
19
|
+
class PlatformError < Error
|
|
20
|
+
attr_reader :status, :body, :adapter_name
|
|
21
|
+
def initialize(message, status: nil, body: nil, adapter_name: nil)
|
|
22
|
+
@status = status
|
|
23
|
+
@body = body
|
|
24
|
+
@adapter_name = adapter_name
|
|
25
|
+
super(message)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class RateLimitedError < PlatformError
|
|
30
|
+
attr_reader :retry_after
|
|
31
|
+
def initialize(message, retry_after: nil, **kwargs)
|
|
32
|
+
@retry_after = retry_after
|
|
33
|
+
super(message, **kwargs)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class DuplicateEventError < Error; end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class EventRegistry
|
|
5
|
+
Handler = Struct.new(:type, :matcher, :block)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@handlers = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(type, matcher: nil, &block)
|
|
12
|
+
@handlers << Handler.new(type: type, matcher: matcher, block: block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def handlers_for(event)
|
|
16
|
+
@handlers.select { |h| matches?(h, event) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def matches?(handler, event)
|
|
22
|
+
return false unless handler.type == event.type
|
|
23
|
+
return true if handler.matcher.nil?
|
|
24
|
+
|
|
25
|
+
case handler.matcher
|
|
26
|
+
when Regexp
|
|
27
|
+
event.respond_to?(:message) && handler.matcher.match?(event.message&.text)
|
|
28
|
+
when String
|
|
29
|
+
case event.type
|
|
30
|
+
when :action then event.action_id == handler.matcher
|
|
31
|
+
when :slash_command then event.command == handler.matcher
|
|
32
|
+
else false
|
|
33
|
+
end
|
|
34
|
+
when Array
|
|
35
|
+
event.type == :reaction && handler.matcher.include?(event.emoji)
|
|
36
|
+
else
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Action < Base
|
|
6
|
+
attr_reader :action_id, :value, :user, :thread_id, :channel_id, :trigger_id
|
|
7
|
+
|
|
8
|
+
def initialize(action_id:, thread_id:, channel_id:, value: nil, user: nil, trigger_id: nil, **kwargs)
|
|
9
|
+
super(type: :action, **kwargs)
|
|
10
|
+
@action_id = action_id
|
|
11
|
+
@value = value
|
|
12
|
+
@user = user
|
|
13
|
+
@thread_id = thread_id
|
|
14
|
+
@channel_id = channel_id
|
|
15
|
+
@trigger_id = trigger_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :type, :platform, :adapter_name, :raw, :timestamp
|
|
7
|
+
|
|
8
|
+
def initialize(type:, platform:, adapter_name:, raw: nil, timestamp: nil)
|
|
9
|
+
@type = type
|
|
10
|
+
@platform = platform
|
|
11
|
+
@adapter_name = adapter_name
|
|
12
|
+
@raw = raw
|
|
13
|
+
@timestamp = timestamp || Time.now
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class DirectMessage < Base
|
|
6
|
+
attr_reader :message, :thread_id, :channel_id
|
|
7
|
+
|
|
8
|
+
def initialize(message:, thread_id:, channel_id:, **kwargs)
|
|
9
|
+
super(type: :direct_message, **kwargs)
|
|
10
|
+
@message = message
|
|
11
|
+
@thread_id = thread_id
|
|
12
|
+
@channel_id = channel_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Mention < Base
|
|
6
|
+
attr_reader :message, :thread_id, :channel_id
|
|
7
|
+
|
|
8
|
+
def initialize(message:, thread_id:, channel_id:, **kwargs)
|
|
9
|
+
super(type: :mention, **kwargs)
|
|
10
|
+
@message = message
|
|
11
|
+
@thread_id = thread_id
|
|
12
|
+
@channel_id = channel_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class Reaction < Base
|
|
6
|
+
attr_reader :emoji, :user_id, :message_id, :thread_id, :channel_id, :added
|
|
7
|
+
|
|
8
|
+
def initialize(emoji:, user_id:, message_id:, thread_id:, channel_id:, added: true, **kwargs)
|
|
9
|
+
super(type: :reaction, **kwargs)
|
|
10
|
+
@emoji = emoji
|
|
11
|
+
@user_id = user_id
|
|
12
|
+
@message_id = message_id
|
|
13
|
+
@thread_id = thread_id
|
|
14
|
+
@channel_id = channel_id
|
|
15
|
+
@added = added
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def added?
|
|
19
|
+
@added
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def removed?
|
|
23
|
+
!@added
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class SlashCommand < Base
|
|
6
|
+
attr_reader :command, :text, :user_id, :channel_id, :trigger_id
|
|
7
|
+
|
|
8
|
+
def initialize(command:, user_id:, channel_id:, text: "", trigger_id: nil, **kwargs)
|
|
9
|
+
super(type: :slash_command, **kwargs)
|
|
10
|
+
@command = command
|
|
11
|
+
@text = text
|
|
12
|
+
@user_id = user_id
|
|
13
|
+
@channel_id = channel_id
|
|
14
|
+
@trigger_id = trigger_id
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Events
|
|
5
|
+
class SubscribedMessage < Base
|
|
6
|
+
attr_reader :message, :thread_id, :channel_id
|
|
7
|
+
|
|
8
|
+
def initialize(message:, thread_id:, channel_id:, **kwargs)
|
|
9
|
+
super(type: :subscribed_message, **kwargs)
|
|
10
|
+
@message = message
|
|
11
|
+
@thread_id = thread_id
|
|
12
|
+
@channel_id = channel_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/chat_sdk/log.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module ChatSDK
|
|
6
|
+
module Log
|
|
7
|
+
class << self
|
|
8
|
+
attr_writer :logger
|
|
9
|
+
|
|
10
|
+
def logger
|
|
11
|
+
@logger ||= ::Logger.new($stdout, progname: "ChatSDK")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
%i[debug info warn error fatal].each do |level|
|
|
15
|
+
define_method(level) { |msg = nil, &block| logger.send(level, msg, &block) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def level=(level)
|
|
19
|
+
logger.level = level
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class Message
|
|
5
|
+
attr_reader :id, :text, :author, :thread_id, :channel_id,
|
|
6
|
+
:platform, :attachments, :raw, :timestamp
|
|
7
|
+
|
|
8
|
+
def initialize(id:, text:, author:, thread_id:, channel_id:, platform:,
|
|
9
|
+
attachments: [], raw: nil, timestamp: nil)
|
|
10
|
+
@id = id
|
|
11
|
+
@text = text
|
|
12
|
+
@author = author
|
|
13
|
+
@thread_id = thread_id
|
|
14
|
+
@channel_id = channel_id
|
|
15
|
+
@platform = platform
|
|
16
|
+
@attachments = attachments
|
|
17
|
+
@raw = raw
|
|
18
|
+
@timestamp = timestamp
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ==(other)
|
|
22
|
+
other.is_a?(Message) && id == other.id && platform == other.platform
|
|
23
|
+
end
|
|
24
|
+
alias_method :eql?, :==
|
|
25
|
+
|
|
26
|
+
def hash
|
|
27
|
+
[id, platform].hash
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Modals
|
|
5
|
+
class Builder
|
|
6
|
+
def initialize(title:, submit_label: nil, callback_id: nil, &block)
|
|
7
|
+
@title = title
|
|
8
|
+
@submit_label = submit_label
|
|
9
|
+
@callback_id = callback_id
|
|
10
|
+
@children = []
|
|
11
|
+
instance_eval(&block) if block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build
|
|
15
|
+
attrs = {title: @title}
|
|
16
|
+
attrs[:submit_label] = @submit_label if @submit_label
|
|
17
|
+
attrs[:callback_id] = @callback_id if @callback_id
|
|
18
|
+
ChatSDK::Cards::Node.new(:modal, attributes: attrs, children: @children)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def text_input(id:, label:, placeholder: nil, multiline: false, optional: false)
|
|
22
|
+
attrs = {id: id, label: label, input_type: :text, multiline: multiline, optional: optional}
|
|
23
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
24
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def select_input(id:, label:, placeholder: nil, optional: false, &block)
|
|
28
|
+
ctx = ChatSDK::Cards::SelectContext.new
|
|
29
|
+
ctx.instance_eval(&block)
|
|
30
|
+
attrs = {id: id, label: label, input_type: :select, optional: optional}
|
|
31
|
+
attrs[:placeholder] = placeholder if placeholder
|
|
32
|
+
@children << ChatSDK::Cards::Node.new(:input, attributes: attrs, children: ctx.nodes)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def static_text(content)
|
|
36
|
+
@children << ChatSDK::Cards::Node.new(:text, attributes: {content: content})
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
class PostableMessage
|
|
5
|
+
attr_reader :text, :card, :attachments, :metadata
|
|
6
|
+
|
|
7
|
+
def initialize(text: nil, card: nil, attachments: [], metadata: {})
|
|
8
|
+
raise ArgumentError, "text or card required" if text.nil? && card.nil?
|
|
9
|
+
@text = text
|
|
10
|
+
@card = card
|
|
11
|
+
@attachments = attachments
|
|
12
|
+
@metadata = metadata
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def card?
|
|
16
|
+
!@card.nil?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from(content)
|
|
20
|
+
case content
|
|
21
|
+
when PostableMessage then content
|
|
22
|
+
when String then new(text: content)
|
|
23
|
+
when Cards::Node then new(card: content, text: content.fallback_text)
|
|
24
|
+
else raise ArgumentError, "cannot convert #{content.class} to PostableMessage"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module State
|
|
5
|
+
class Base
|
|
6
|
+
def subscribe(thread_id)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def unsubscribe(thread_id)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def subscribed?(thread_id)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def acquire_lock(key, owner:, ttl:)
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def release_lock(key, owner:)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def force_lock(key, owner:, ttl:)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(key)
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def set(key, value, ttl: nil)
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delete(key)
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_if_absent(key, value, ttl: nil)
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module State
|
|
5
|
+
class Memory < Base
|
|
6
|
+
def initialize
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
@store = {}
|
|
9
|
+
@expirations = {}
|
|
10
|
+
@subscriptions = Set.new
|
|
11
|
+
@locks = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def subscribe(thread_id)
|
|
15
|
+
@mutex.synchronize { @subscriptions.add(thread_id) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unsubscribe(thread_id)
|
|
19
|
+
@mutex.synchronize { @subscriptions.delete(thread_id) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def subscribed?(thread_id)
|
|
23
|
+
@mutex.synchronize { @subscriptions.include?(thread_id) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def acquire_lock(key, owner:, ttl:)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
expire_if_needed(key)
|
|
29
|
+
return false if @locks.key?(key)
|
|
30
|
+
@locks[key] = {owner: owner, expires_at: Time.now.to_f + ttl}
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def release_lock(key, owner:)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
return false unless @locks[key] && @locks[key][:owner] == owner
|
|
38
|
+
@locks.delete(key)
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def force_lock(key, owner:, ttl:)
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@locks[key] = {owner: owner, expires_at: Time.now.to_f + ttl}
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(key)
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
expire_if_needed(key)
|
|
53
|
+
@store[key]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set(key, value, ttl: nil)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
@store[key] = value
|
|
60
|
+
@expirations[key] = Time.now.to_f + ttl if ttl
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete(key)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@store.delete(key)
|
|
68
|
+
@expirations.delete(key)
|
|
69
|
+
@locks.delete(key)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def set_if_absent(key, value, ttl: nil)
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
expire_if_needed(key)
|
|
76
|
+
return false if @store.key?(key)
|
|
77
|
+
@store[key] = value
|
|
78
|
+
@expirations[key] = Time.now.to_f + ttl if ttl
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@store.clear
|
|
86
|
+
@expirations.clear
|
|
87
|
+
@subscriptions.clear
|
|
88
|
+
@locks.clear
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def expire_if_needed(key)
|
|
95
|
+
if @expirations[key] && @expirations[key] < Time.now.to_f
|
|
96
|
+
@store.delete(key)
|
|
97
|
+
@expirations.delete(key)
|
|
98
|
+
end
|
|
99
|
+
if @locks[key] && @locks[key][:expires_at] < Time.now.to_f
|
|
100
|
+
@locks.delete(key)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Streaming
|
|
5
|
+
class Stream
|
|
6
|
+
attr_reader :adapter, :channel_id, :thread_id, :update_interval
|
|
7
|
+
|
|
8
|
+
def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5)
|
|
9
|
+
@adapter = adapter
|
|
10
|
+
@channel_id = channel_id
|
|
11
|
+
@thread_id = thread_id
|
|
12
|
+
@placeholder = placeholder
|
|
13
|
+
@update_interval = update_interval
|
|
14
|
+
@buffer = +""
|
|
15
|
+
@message_id = nil
|
|
16
|
+
@last_flush_at = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <<(chunk)
|
|
20
|
+
@buffer << chunk.to_s
|
|
21
|
+
maybe_flush
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(&block)
|
|
26
|
+
post_placeholder if @placeholder
|
|
27
|
+
yield self
|
|
28
|
+
flush
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def post_placeholder
|
|
34
|
+
result = adapter.post_message(
|
|
35
|
+
channel_id: channel_id,
|
|
36
|
+
message: PostableMessage.new(text: @placeholder),
|
|
37
|
+
thread_id: thread_id
|
|
38
|
+
)
|
|
39
|
+
@message_id = result.id if result.respond_to?(:id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def maybe_flush
|
|
43
|
+
return unless @message_id
|
|
44
|
+
return if @last_flush_at && (Time.now.to_f - @last_flush_at) < update_interval
|
|
45
|
+
|
|
46
|
+
flush
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def flush
|
|
50
|
+
return if @buffer.empty?
|
|
51
|
+
|
|
52
|
+
if @message_id && adapter.supports?(:edit_messages)
|
|
53
|
+
adapter.edit_message(
|
|
54
|
+
channel_id: channel_id,
|
|
55
|
+
message_id: @message_id,
|
|
56
|
+
message: PostableMessage.new(text: @buffer.dup)
|
|
57
|
+
)
|
|
58
|
+
elsif !@message_id
|
|
59
|
+
result = adapter.post_message(
|
|
60
|
+
channel_id: channel_id,
|
|
61
|
+
message: PostableMessage.new(text: @buffer.dup),
|
|
62
|
+
thread_id: thread_id
|
|
63
|
+
)
|
|
64
|
+
@message_id = result.id if result.respond_to?(:id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@last_flush_at = Time.now.to_f
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Testing
|
|
5
|
+
module AdapterContract
|
|
6
|
+
RSpec.shared_examples "a chat_sdk platform adapter" do
|
|
7
|
+
it "responds to #name with a symbol" do
|
|
8
|
+
expect(subject.name).to be_a(Symbol)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "responds to #client" do
|
|
12
|
+
expect(subject).to respond_to(:client)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "responds to #verify_request!" do
|
|
16
|
+
expect(subject).to respond_to(:verify_request!)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "responds to #parse_events" do
|
|
20
|
+
expect(subject).to respond_to(:parse_events)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "responds to #post_message" do
|
|
24
|
+
expect(subject).to respond_to(:post_message)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "responds to #mention" do
|
|
28
|
+
expect(subject).to respond_to(:mention)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "responds to #render" do
|
|
32
|
+
expect(subject).to respond_to(:render)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "responds to #supports?" do
|
|
36
|
+
expect(subject).to respond_to(:supports?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "raises NotSupportedError for undeclared capabilities" do
|
|
40
|
+
undeclared = Adapter::Capabilities::KNOWN - subject.class.declared_capabilities
|
|
41
|
+
undeclared.each do |cap|
|
|
42
|
+
method_for_cap = capability_method(cap)
|
|
43
|
+
next unless method_for_cap && subject.respond_to?(method_for_cap)
|
|
44
|
+
expect { subject.send(method_for_cap, **capability_args(cap)) }.to raise_error(NotSupportedError)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def capability_method(cap)
|
|
51
|
+
{
|
|
52
|
+
edit_messages: :edit_message,
|
|
53
|
+
delete_messages: :delete_message,
|
|
54
|
+
ephemeral_messages: :post_ephemeral,
|
|
55
|
+
file_uploads: :upload_file,
|
|
56
|
+
reactions: :add_reaction,
|
|
57
|
+
modals: :open_modal,
|
|
58
|
+
typing_indicator: :start_typing,
|
|
59
|
+
direct_messages: :open_dm,
|
|
60
|
+
message_history: :fetch_messages
|
|
61
|
+
}[cap]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def capability_args(cap)
|
|
65
|
+
case cap
|
|
66
|
+
when :edit_messages then {channel_id: "C1", message_id: "M1", message: PostableMessage.new(text: "t")}
|
|
67
|
+
when :delete_messages then {channel_id: "C1", message_id: "M1"}
|
|
68
|
+
when :ephemeral_messages then {channel_id: "C1", user_id: "U1", message: PostableMessage.new(text: "t")}
|
|
69
|
+
when :file_uploads then {channel_id: "C1", io: StringIO.new(""), filename: "f.txt"}
|
|
70
|
+
when :reactions then {channel_id: "C1", message_id: "M1", emoji: "thumbsup"}
|
|
71
|
+
when :modals then {trigger_id: "T1", modal: Cards::Node.new(:modal)}
|
|
72
|
+
when :typing_indicator then {channel_id: "C1"}
|
|
73
|
+
when :direct_messages then {user_id: "U1"}
|
|
74
|
+
when :message_history then {channel_id: "C1"}
|
|
75
|
+
else {}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|