chat_sdk-twilio 0.2.1
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/twilio/adapter.rb +125 -0
- data/lib/chat_sdk/twilio/api_client.rb +63 -0
- data/lib/chat_sdk/twilio/event_parser.rb +59 -0
- data/lib/chat_sdk/twilio/signature.rb +23 -0
- data/lib/chat_sdk/twilio.rb +25 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 21bad3d99ab33873fc54e54fc57e33b3171cfa946fc39845962b1e174b347685
|
|
4
|
+
data.tar.gz: acc910c814fb03b916e684afcf16c56d29f479569a67e9a0691f7029400c6df5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3f1ea62aed56dfcd1edc793ad86e59160dc6e045ac5cfae0d9b8938da22d49fb854573c912d927ccc5ee3d5d167c6639d80f901d3a5b98cc6aa54d852c731464
|
|
7
|
+
data.tar.gz: 7b15c533e2cb0807524d66b19ad9bcd7d17926e2571f9b8ead7cf810f4c720014733839fcaeab6dbeada15030ce674e0a1339b3219e3a81ce0bc76775bb55220
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
|
|
5
|
+
module ChatSDK
|
|
6
|
+
module Twilio
|
|
7
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
8
|
+
capabilities :direct_messages, :file_uploads
|
|
9
|
+
|
|
10
|
+
attr_reader :client
|
|
11
|
+
|
|
12
|
+
def initialize(account_sid: nil, auth_token: nil, phone_number: nil, messaging_service_sid: nil, webhook_url: nil)
|
|
13
|
+
@account_sid = account_sid || ENV["TWILIO_ACCOUNT_SID"]
|
|
14
|
+
@auth_token = auth_token || ENV["TWILIO_AUTH_TOKEN"]
|
|
15
|
+
@phone_number = phone_number || ENV["TWILIO_PHONE_NUMBER"]
|
|
16
|
+
@messaging_service_sid = messaging_service_sid || ENV["TWILIO_MESSAGING_SERVICE_SID"]
|
|
17
|
+
@webhook_url = webhook_url
|
|
18
|
+
|
|
19
|
+
raise ChatSDK::ConfigurationError, "Twilio account_sid required" unless @account_sid
|
|
20
|
+
raise ChatSDK::ConfigurationError, "Twilio auth_token required" unless @auth_token
|
|
21
|
+
raise ChatSDK::ConfigurationError, "Twilio phone_number or messaging_service_sid required" unless @phone_number || @messaging_service_sid
|
|
22
|
+
|
|
23
|
+
@client = ApiClient.new(@account_sid, @auth_token)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def name
|
|
27
|
+
:twilio
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Inbound
|
|
31
|
+
def verify_request!(rack_request)
|
|
32
|
+
signature = rack_request.get_header("HTTP_X_TWILIO_SIGNATURE")
|
|
33
|
+
|
|
34
|
+
unless signature
|
|
35
|
+
raise ChatSDK::SignatureVerificationError, "Missing Twilio signature header"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
body = rack_request.body.read
|
|
39
|
+
rack_request.body.rewind
|
|
40
|
+
|
|
41
|
+
params = Rack::Utils.parse_query(body)
|
|
42
|
+
url = @webhook_url || rack_request.url
|
|
43
|
+
|
|
44
|
+
Signature.verify!(@auth_token, url, params, signature)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ack_response(_rack_request)
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_events(rack_request)
|
|
52
|
+
body = rack_request.body.read
|
|
53
|
+
rack_request.body.rewind
|
|
54
|
+
|
|
55
|
+
params = Rack::Utils.parse_query(body)
|
|
56
|
+
EventParser.parse(params)
|
|
57
|
+
rescue ArgumentError
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Outbound
|
|
62
|
+
def post_message(channel_id:, message:, thread_id: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
63
|
+
text = prepare_message_payload(message)
|
|
64
|
+
|
|
65
|
+
result = @client.send_message(
|
|
66
|
+
to: channel_id,
|
|
67
|
+
body: text,
|
|
68
|
+
from: @phone_number,
|
|
69
|
+
messaging_service_sid: @messaging_service_sid
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parse_twilio_message(result, channel_id)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
76
|
+
raise ChatSDK::PlatformError.new(
|
|
77
|
+
"Twilio MMS requires a publicly accessible MediaUrl. Binary upload is not supported. " \
|
|
78
|
+
"Host the file at a public URL and send it as a message with the URL included.",
|
|
79
|
+
adapter_name: :twilio
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def open_dm(user_id)
|
|
84
|
+
user_id
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def mention(user_id)
|
|
88
|
+
user_id
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render(postable_message)
|
|
92
|
+
if postable_message.card?
|
|
93
|
+
@text_renderer ||= Cards::Renderer.new
|
|
94
|
+
@text_renderer.render(postable_message.card)
|
|
95
|
+
else
|
|
96
|
+
postable_message.text
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def prepare_message_payload(message)
|
|
103
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
104
|
+
msg.text || msg.card&.fallback_text || ""
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_twilio_message(data, channel_id)
|
|
108
|
+
ChatSDK::Message.new(
|
|
109
|
+
id: data["sid"],
|
|
110
|
+
text: data["body"] || "",
|
|
111
|
+
author: ChatSDK::Author.new(
|
|
112
|
+
id: data["from"] || "bot",
|
|
113
|
+
name: data["from"] || "bot",
|
|
114
|
+
platform: :twilio,
|
|
115
|
+
bot: true
|
|
116
|
+
),
|
|
117
|
+
thread_id: data["sid"],
|
|
118
|
+
channel_id: channel_id,
|
|
119
|
+
platform: :twilio,
|
|
120
|
+
raw: data
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Twilio
|
|
5
|
+
class ApiClient
|
|
6
|
+
BASE_URL = "https://api.twilio.com"
|
|
7
|
+
|
|
8
|
+
def initialize(account_sid, auth_token)
|
|
9
|
+
@account_sid = account_sid
|
|
10
|
+
@auth_token = auth_token
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def send_message(to:, body:, from: nil, messaging_service_sid: nil, media_url: nil)
|
|
14
|
+
params = {"To" => to, "Body" => body}
|
|
15
|
+
params["From"] = from if from
|
|
16
|
+
params["MessagingServiceSid"] = messaging_service_sid if messaging_service_sid
|
|
17
|
+
params["MediaUrl"] = media_url if media_url
|
|
18
|
+
|
|
19
|
+
response = connection.post(messages_path, URI.encode_www_form(params))
|
|
20
|
+
handle_response(response)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def messages_path
|
|
26
|
+
"/2010-04-01/Accounts/#{@account_sid}/Messages.json"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def connection
|
|
30
|
+
@connection ||= Faraday.new(url: BASE_URL) do |f|
|
|
31
|
+
f.request :authorization, :basic, @account_sid, @auth_token
|
|
32
|
+
f.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
33
|
+
f.response :json
|
|
34
|
+
f.adapter :net_http
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_response(response)
|
|
39
|
+
body = response.body
|
|
40
|
+
|
|
41
|
+
return body if response.success?
|
|
42
|
+
|
|
43
|
+
if response.status == 429
|
|
44
|
+
raise ChatSDK::RateLimitedError.new(
|
|
45
|
+
"Twilio API rate limited",
|
|
46
|
+
retry_after: nil,
|
|
47
|
+
status: response.status,
|
|
48
|
+
body: body,
|
|
49
|
+
adapter_name: :twilio
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
message = body.is_a?(Hash) ? body["message"] : response.status
|
|
54
|
+
raise ChatSDK::PlatformError.new(
|
|
55
|
+
"Twilio API error: #{message}",
|
|
56
|
+
status: response.status,
|
|
57
|
+
body: body,
|
|
58
|
+
adapter_name: :twilio
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Twilio
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(params)
|
|
8
|
+
return [] unless params.is_a?(Hash) && params["MessageSid"]
|
|
9
|
+
|
|
10
|
+
author = ChatSDK::Author.new(
|
|
11
|
+
id: params["From"] || "unknown",
|
|
12
|
+
name: params["From"] || "unknown",
|
|
13
|
+
platform: :twilio,
|
|
14
|
+
bot: false
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
attachments = extract_attachments(params)
|
|
18
|
+
|
|
19
|
+
text = params["Body"] || ""
|
|
20
|
+
text = "#{text}\n#{attachments.map { |a| a[:url] }.join("\n")}" if attachments.any? && !text.empty?
|
|
21
|
+
text = attachments.map { |a| a[:url] }.join("\n") if attachments.any? && (params["Body"].nil? || params["Body"].empty?)
|
|
22
|
+
|
|
23
|
+
msg = ChatSDK::Message.new(
|
|
24
|
+
id: params["MessageSid"],
|
|
25
|
+
text: text,
|
|
26
|
+
author: author,
|
|
27
|
+
thread_id: params["MessageSid"],
|
|
28
|
+
channel_id: params["To"] || "unknown",
|
|
29
|
+
platform: :twilio,
|
|
30
|
+
raw: params
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
[ChatSDK::Events::DirectMessage.new(
|
|
34
|
+
message: msg,
|
|
35
|
+
thread_id: params["MessageSid"],
|
|
36
|
+
channel_id: params["To"] || "unknown",
|
|
37
|
+
platform: :twilio,
|
|
38
|
+
adapter_name: :twilio,
|
|
39
|
+
raw: params
|
|
40
|
+
)]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_attachments(params)
|
|
46
|
+
num_media = (params["NumMedia"] || "0").to_i
|
|
47
|
+
return [] if num_media.zero?
|
|
48
|
+
|
|
49
|
+
(0...num_media).map do |i|
|
|
50
|
+
{
|
|
51
|
+
url: params["MediaUrl#{i}"],
|
|
52
|
+
content_type: params["MediaContentType#{i}"]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "rack/utils"
|
|
6
|
+
|
|
7
|
+
module ChatSDK
|
|
8
|
+
module Twilio
|
|
9
|
+
module Signature
|
|
10
|
+
def self.verify!(auth_token, url, params, signature)
|
|
11
|
+
data = url + params.sort.flatten.join
|
|
12
|
+
digest = OpenSSL::HMAC.digest("SHA1", auth_token, data)
|
|
13
|
+
expected = Base64.strict_encode64(digest)
|
|
14
|
+
|
|
15
|
+
unless Rack::Utils.secure_compare(expected, signature)
|
|
16
|
+
raise ChatSDK::SignatureVerificationError, "Invalid Twilio signature"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chat_sdk"
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "faraday/net_http"
|
|
6
|
+
require "zeitwerk"
|
|
7
|
+
|
|
8
|
+
module ChatSDK
|
|
9
|
+
module Twilio
|
|
10
|
+
class << self
|
|
11
|
+
def loader
|
|
12
|
+
@loader ||= begin
|
|
13
|
+
loader = Zeitwerk::Loader.new
|
|
14
|
+
loader.tag = "chat_sdk-twilio"
|
|
15
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK")
|
|
16
|
+
loader.push_dir("#{__dir__}/twilio", namespace: ChatSDK::Twilio)
|
|
17
|
+
loader.setup
|
|
18
|
+
loader
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ChatSDK::Twilio.loader
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-twilio
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Quentin Rousseau
|
|
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: chat_sdk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Twilio SMS/MMS adapter for the ChatSDK framework with HMAC-SHA1 signature
|
|
41
|
+
verification
|
|
42
|
+
email:
|
|
43
|
+
- quentin@rootly.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- lib/chat_sdk/twilio.rb
|
|
49
|
+
- lib/chat_sdk/twilio/adapter.rb
|
|
50
|
+
- lib/chat_sdk/twilio/api_client.rb
|
|
51
|
+
- lib/chat_sdk/twilio/event_parser.rb
|
|
52
|
+
- lib/chat_sdk/twilio/signature.rb
|
|
53
|
+
homepage: https://github.com/rootlyhq/chat-sdk
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.2.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 4.0.16
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Twilio SMS/MMS adapter for ChatSDK
|
|
75
|
+
test_files: []
|