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 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: []