stealth-smooch 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6e3657bee135d475584a394c1a2248f356d0e9173a49f6e941f1b162b92b2069
4
+ data.tar.gz: d19f031c8632ae4b3db4bfc9797f303e084b7bc7fe6d5bb7847d945102409180
5
+ SHA512:
6
+ metadata.gz: fef040a938acf8c730b36351812fbcbbc799406ca81e79a2f3e3360ff641f00fefb37004d8ef3f3ecb2187ed116cdb1f8b2a6a6da064a166d11516961ab42193
7
+ data.tar.gz: 61f7ebbb9e1eb16de0fa5234bafea9111cee6cc76f19efe12711d0c380f8c19c443001d52900aa8b26f9f1176e6c57040bfd269f3a40c684f19299a363fdb67c
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ # please add general patterns to your global ignore list
2
+ # see https://github.com/github/gitignore#readme
3
+ .DS_STORE
4
+ *.swp
5
+ *.rbc
6
+ *.sass-cache
7
+ /pkg
8
+ /doc/api
9
+ /coverage
10
+ .yardoc
11
+ doc/
12
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,133 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ stealth-smooch (0.9.0)
5
+ jwt (~> 2.1)
6
+ smooch-api (~> 4.0)
7
+ stealth (< 2.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (5.2.1)
13
+ actionview (= 5.2.1)
14
+ activesupport (= 5.2.1)
15
+ rack (~> 2.0)
16
+ rack-test (>= 0.6.3)
17
+ rails-dom-testing (~> 2.0)
18
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
19
+ actionview (5.2.1)
20
+ activesupport (= 5.2.1)
21
+ builder (~> 3.1)
22
+ erubi (~> 1.4)
23
+ rails-dom-testing (~> 2.0)
24
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
25
+ activemodel (5.2.1)
26
+ activesupport (= 5.2.1)
27
+ activerecord (5.2.1)
28
+ activemodel (= 5.2.1)
29
+ activesupport (= 5.2.1)
30
+ arel (>= 9.0)
31
+ activesupport (5.2.1)
32
+ concurrent-ruby (~> 1.0, >= 1.0.2)
33
+ i18n (>= 0.7, < 2)
34
+ minitest (~> 5.1)
35
+ tzinfo (~> 1.1)
36
+ arel (9.0.0)
37
+ builder (3.2.3)
38
+ concurrent-ruby (1.1.3)
39
+ connection_pool (2.2.2)
40
+ crass (1.0.4)
41
+ diff-lcs (1.3)
42
+ erubi (1.7.1)
43
+ ethon (0.11.0)
44
+ ffi (>= 1.3.0)
45
+ ffi (1.9.25)
46
+ i18n (1.1.1)
47
+ concurrent-ruby (~> 1.0)
48
+ json (1.8.6)
49
+ jwt (2.1.0)
50
+ loofah (2.2.3)
51
+ crass (~> 1.0.2)
52
+ nokogiri (>= 1.5.9)
53
+ method_source (0.9.2)
54
+ mini_portile2 (2.3.0)
55
+ minitest (5.11.3)
56
+ multi_json (1.13.1)
57
+ mustermann (1.0.3)
58
+ nokogiri (1.8.5)
59
+ mini_portile2 (~> 2.3.0)
60
+ puma (3.12.0)
61
+ rack (2.0.6)
62
+ rack-protection (2.0.4)
63
+ rack
64
+ rack-test (1.1.0)
65
+ rack (>= 1.0, < 3)
66
+ rails-dom-testing (2.0.3)
67
+ activesupport (>= 4.2.0)
68
+ nokogiri (>= 1.6)
69
+ rails-html-sanitizer (1.0.4)
70
+ loofah (~> 2.2, >= 2.2.2)
71
+ railties (5.2.1)
72
+ actionpack (= 5.2.1)
73
+ activesupport (= 5.2.1)
74
+ method_source
75
+ rake (>= 0.8.7)
76
+ thor (>= 0.19.0, < 2.0)
77
+ rake (12.3.1)
78
+ redis (4.0.3)
79
+ rspec (3.8.0)
80
+ rspec-core (~> 3.8.0)
81
+ rspec-expectations (~> 3.8.0)
82
+ rspec-mocks (~> 3.8.0)
83
+ rspec-core (3.8.0)
84
+ rspec-support (~> 3.8.0)
85
+ rspec-expectations (3.8.1)
86
+ diff-lcs (>= 1.2.0, < 2.0)
87
+ rspec-support (~> 3.8.0)
88
+ rspec-mocks (3.8.0)
89
+ diff-lcs (>= 1.2.0, < 2.0)
90
+ rspec-support (~> 3.8.0)
91
+ rspec-support (3.8.0)
92
+ rspec_junit_formatter (0.4.1)
93
+ rspec-core (>= 2, < 4, != 2.12.0)
94
+ sidekiq (5.2.3)
95
+ connection_pool (~> 2.2, >= 2.2.2)
96
+ rack-protection (>= 1.5.0)
97
+ redis (>= 3.3.5, < 5)
98
+ sinatra (2.0.4)
99
+ mustermann (~> 1.0)
100
+ rack (~> 2.0)
101
+ rack-protection (= 2.0.4)
102
+ tilt (~> 2.0)
103
+ smooch-api (4.0)
104
+ json (~> 1.8, >= 1.8.3)
105
+ typhoeus (~> 1.0, >= 1.0.1)
106
+ stealth (1.0.4)
107
+ activerecord (~> 5.2)
108
+ activesupport (~> 5.2)
109
+ multi_json (~> 1.12)
110
+ puma (~> 3.10)
111
+ railties (~> 5.2)
112
+ sidekiq (~> 5.0)
113
+ sinatra (~> 2.0)
114
+ thor (~> 0.20)
115
+ thor (0.20.3)
116
+ thread_safe (0.3.6)
117
+ tilt (2.0.8)
118
+ typhoeus (1.3.1)
119
+ ethon (>= 0.9.0)
120
+ tzinfo (1.2.5)
121
+ thread_safe (~> 0.1)
122
+
123
+ PLATFORMS
124
+ ruby
125
+
126
+ DEPENDENCIES
127
+ rack-test (~> 1.1)
128
+ rspec (~> 3.6)
129
+ rspec_junit_formatter (~> 0.3)
130
+ stealth-smooch!
131
+
132
+ BUNDLED WITH
133
+ 1.16.6
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2018 Mauricio Gomes, Black Ops Bureau
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # Stealth Smooch
2
+
3
+ This integration adds support for [Smooch](https://smooch.io) powered bots within [Stealth](https://github.com/hellostealth/stealth). It can be used as a drop-in replacement for `stealth-facebook` with the exception of some specialized quick reply buttons (such as Email & Phone).
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/stealth-smooch.svg)](https://badge.fury.io/rb/stealth-smooch)
6
+
7
+ ## Create Your Smooch App
8
+
9
+ Via the Smooch interface, create a new Smooch app for your Stealth bot. Once you do, you'll be given your `SMOOCH_APP_ID`.
10
+
11
+ Follow the instructions on the [smooch-api Ruby](https://github.com/smooch/smooch-ruby) page to generate your secret keys. This will get you your
12
+ `SMOOCH_KEY_ID` and `SMOOCH_SECRET`.
13
+
14
+ The last thing you will need is the `SMOOCH_JWT_TOKEN` which can be generated using this gem. After you have set the above creds to your `services.yml` file, from your bot's console, run:
15
+
16
+ ```ruby
17
+ Stealth::Services::Smooch::Client.generate_jwt_token
18
+ ```
19
+
20
+ It will output the JWT token based on the `app_id`, `key_id`, and `secret` from your `services.yml` file.
21
+
22
+ ## Configure the Integration
23
+
24
+ ```yaml
25
+ default: &default
26
+ smooch:
27
+ app_id: <%= ENV['SMOOCH_APP_ID'] %>
28
+ key_id: <%= ENV['SMOOCH_KEY_ID'] %>
29
+ secret: <%= ENV['SMOOCH_SECRET'] %>
30
+ jwt_token: <%= ENV['SMOOCH_JWT_TOKEN'] %>
31
+ setup:
32
+ persistent_menu:
33
+ - type: 'url'
34
+ url: 'https://mywebsite.com'
35
+ text: 'About Us'
36
+ - type: 'payload'
37
+ payload: 'contact_support'
38
+ text: 'Contact Support'
39
+
40
+ production:
41
+ <<: *default
42
+
43
+ development:
44
+ <<: *default
45
+
46
+ test:
47
+ <<: *default
48
+ ```
49
+
50
+ Additionally, you will need to create an initializer called `smooch.rb` in `config/initializers`:
51
+
52
+ ```ruby
53
+ SmoochApi.configure do |config|
54
+ config.api_key['Authorization'] = Stealth.config.smooch.jwt_token
55
+ config.api_key_prefix['Authorization'] = 'Bearer'
56
+ end
57
+ ```
58
+
59
+ As with all Stealth integrations, integrations can be specified by environment.
60
+
61
+ These are the supported setup options:
62
+
63
+ ### persistent_menu
64
+
65
+ The persistent menu is not supported by all integrations. For a complete list, please check out the [Smooch Pesistent Menu Docs](https://docs.smooch.io/rest/#persistent-menus).
66
+
67
+ Setting the persistent menu is identical to creating buttons in text replies. Please see those docs for more info.
68
+
69
+ ### Webhooks
70
+
71
+ In order for your bot to receive messages from the Smooch app, we'll need to register our webhooks.
72
+
73
+ Set `SMOOCH_ENDPOINT` to the endpoint that will be receiving the hooks. It's configured as an ENV variable so you can specify different endpoints for each of your environments.
74
+
75
+ After you have set `SMOOCH_ENDPOINT`, running setup below will register your webhooks.
76
+
77
+ ### Running Setup
78
+
79
+ This will set the persistent menu (if available) and register your webhooks.
80
+
81
+ ```
82
+ stealth setup smooch
83
+ ```
84
+
85
+ ## Replies
86
+
87
+ Here are the supported replies for the Smooch integration:
88
+
89
+ ### text
90
+
91
+ These are standard text replies.
92
+
93
+ ```yaml
94
+ - reply_type: text
95
+ text: Hello World!
96
+ ```
97
+
98
+ Text replies can also include suggestions, which will be rendered as quick replies:
99
+
100
+ ```yaml
101
+ - reply_type: text
102
+ text: What is your favorite color?
103
+ suggestions:
104
+ - text: Blue
105
+ - text: Red
106
+ ```
107
+
108
+ Although not as common, text replies can also include buttons:
109
+
110
+ ```yaml
111
+ - reply_type: text
112
+ text: Would you like to give us a call?
113
+ buttons:
114
+ - type: payload
115
+ text: 'Yes'
116
+ payload: 'Yes'
117
+ - type: payload
118
+ text: 'No'
119
+ payload: 'No'
120
+ ```
121
+
122
+ ### suggestions
123
+
124
+ Though suggestions are not a reply type on their own, they are frequently used to optimize the accuracy and speed of your bot. In the `text` reply type above, we used simple labels for our suggestions. Smooch supports a few special types of quick replies, however.
125
+
126
+ #### Location
127
+
128
+ You can ask a user for their location:
129
+
130
+ ```yaml
131
+ - reply_type: text
132
+ text: "Where are you located?"
133
+ suggestions:
134
+ - type: location
135
+ ```
136
+
137
+ If the user chooses to share their location, the `lat` and `lng` will be available via `current_message.location`:
138
+
139
+ ```ruby
140
+ current_message.location[:lat]
141
+ current_message.location[:lng]
142
+ ```
143
+
144
+ #### Images
145
+
146
+ While images are not a special quick reply type, you can include and `image_url` for a quick reply as way of adding an icon to a quick reply button:
147
+
148
+ ```yaml
149
+ - reply_type: text
150
+ text: "What is your favorite color?"
151
+ suggestions:
152
+ - text: Red
153
+ image_url: "http://example.com/img/red.png"
154
+ - text: Blue
155
+ image_url: "http://example.com/img/blue.png"
156
+ ```
157
+
158
+ More info [here](https://docs.smooch.io/rest/#reply).
159
+
160
+ ### buttons
161
+
162
+ As with `suggestions`, `buttons` are not a reply type of their own but are used to make your bot more efficient. Smooch supports a few button types and these are the ones currently supported by this integration:
163
+
164
+ #### payload
165
+
166
+ This is the most common button type. When a user presses a button that is `payload` type, that payload string will be sent to your bot. For example:
167
+
168
+ ```yaml
169
+ - reply_type: text
170
+ text: Please press the button below
171
+ buttons:
172
+ - type: payload
173
+ text: 'Press me!'
174
+ payload: 'button pressed'
175
+
176
+ ```
177
+
178
+ When a user presses the button labeled "Press me!", the payload `button pressed` will be accessible in bot via `current_message.payload`.
179
+
180
+ #### url
181
+
182
+ The `url` button is useful when sharing a link to a website. By default, it will open up within Facebook Messenger.
183
+
184
+ ```yaml
185
+ - reply_type: text
186
+ text: Find out more via our website
187
+ buttons:
188
+ - type: url
189
+ text: 'Visit website'
190
+ url: 'https://example.org'
191
+
192
+ ```
193
+
194
+ ### Delay
195
+
196
+ Delays are a very important part of bot design. They introduce a pause between text replies to give the user a chance to read each reply. With this integration, in addition to introducing a delay, we will also send a typing indicator to the user to indicate another reply is forthcoming. To insert a delay in your bot:
197
+
198
+ ```yaml
199
+ - reply_type: delay
200
+ duration: 2
201
+ ```
202
+
203
+ This will add a `2` second delay (with typing indicator). The `duration` can be specified as any floating point value, in seconds.
204
+
205
+ ### Cards
206
+
207
+ Smooch distinguishes between a single card and a carousel of cards. This integration does not, however. You can send a single card the same way you would send 10 cards (the current maximum).
208
+
209
+ ```yaml
210
+ - reply_type: cards
211
+ elements:
212
+ - title: My App
213
+ subtitle: Download our app below or visit our website for more info.
214
+ image_url: "https://my-app.com/app-image.png"
215
+ buttons:
216
+ - type: url
217
+ url: "https://my-app.com"
218
+ text: 'View'
219
+ webview_height: 'tall'
220
+ - type: url
221
+ url: "https://itunes.apple.com/us/app/my-app"
222
+ text: 'Download iOS App'
223
+ ```
224
+
225
+ The above is a single card with two buttons. If you want to include more cards, though, you would just need to specify another listing under the `elements` heading.
226
+
227
+ More info about Smooch cards [here](https://docs.smooch.io/rest/#carousel).
228
+
229
+ ### List
230
+
231
+ A Smooch list is useful for displaying things like a news feed. You can find more info about Smooch lists [here](https://docs.smooch.io/rest/#list).
232
+
233
+ To generate a list:
234
+
235
+ ```yaml
236
+ - reply_type: list
237
+ buttons:
238
+ - type: payload
239
+ text: View More
240
+ payload: view_more
241
+ elements:
242
+ - title: Your Daily News Update
243
+ subtitle: The following stories have been curated just for you.
244
+ image_url: "https://picsum.photos/320/240"
245
+ buttons:
246
+ - type: url
247
+ url: "https://news-articles.com/199"
248
+ text: 'View'
249
+ - title: Breakthrough in AI
250
+ subtitle: Major breakthrough in the AI space.
251
+ image_url: "https://picsum.photos/320/320"
252
+ buttons:
253
+ - type: url
254
+ url: "https://news-articles.com/201"
255
+ text: 'View'
256
+ ```
257
+
258
+ The list itself supports having a single button that will be rendered on the bottom of the list. Each individual list item supports having one button as well. List items should have between 2-4 elements.
259
+
260
+ More info about Smooch lists [here](https://docs.smooch.io/rest/#list).
261
+
262
+ ### Images
263
+
264
+ To send an image:
265
+
266
+ ```yaml
267
+ - reply_type: image
268
+ image_url: 'https://example.org/image.png'
269
+ ```
270
+
271
+ The `image_url` should be set to URL where the image has been uploaded.
272
+
273
+ Image replies support buttons and suggestions like text replies.
274
+
275
+ ### Files
276
+
277
+ To send a file:
278
+
279
+ ```yaml
280
+ - reply_type: file
281
+ file_url: 'https://example.org/some.pdf'
282
+ ```
283
+
284
+ The `file_url` should be set to URL where the file has been uploaded.
285
+
286
+ File replies support buttons and suggestions like text replies.
287
+
288
+ ### Video
289
+
290
+ To send a video:
291
+
292
+ ```yaml
293
+ - reply_type: video
294
+ video_url: 'https://example.org/cool_video.mp4'
295
+ ```
296
+
297
+ The `video_url` should be set to URL where the video has been uploaded.
298
+
299
+ Video replies support buttons and suggestions like text replies.
300
+
301
+ ### Audio
302
+
303
+ To send an audio clip:
304
+
305
+ ```yaml
306
+ - reply_type: audio
307
+ audio_url: 'https://example.org/podcast.mp3'
308
+ ```
309
+
310
+ The `audio_url` should be set to URL where the video has been uploaded.
311
+
312
+ Audio replies support buttons and suggestions like text replies.
313
+
314
+ ## Development
315
+
316
+ When adding features to this library, you might find it helpful to get a full printout of the HTTP requests and responses from Smooch.
317
+
318
+ In order to configure your bot to show the debug output, modify your `smooch.rb` initializer like so:
319
+
320
+ ```ruby
321
+ class SmoochLogger
322
+
323
+ def self.debug(msg)
324
+ Stealth::Logger.l(topic: 'smooch', message: msg)
325
+ end
326
+
327
+ end
328
+
329
+ SmoochApi.configure do |config|
330
+ config.logger = SmoochLogger
331
+ config.debugging = true
332
+ config.api_key['Authorization'] = Stealth.config.smooch.jwt_token
333
+ config.api_key_prefix['Authorization'] = 'Bearer'
334
+ end
335
+ ```
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -0,0 +1,2 @@
1
+ require 'smooch-api'
2
+ require 'stealth/smooch'
@@ -0,0 +1,82 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'stealth/services/smooch/message_handler'
5
+ require 'stealth/services/smooch/reply_handler'
6
+ require 'stealth/services/smooch/setup'
7
+
8
+ module Stealth
9
+ module Services
10
+ module Smooch
11
+
12
+ class Client < Stealth::Services::BaseClient
13
+
14
+ attr_reader :reply
15
+
16
+ def initialize(reply:)
17
+ @reply = reply
18
+ @smooch = SmoochApi::ConversationApi.new
19
+ end
20
+
21
+ def transmit
22
+ begin
23
+ response = @smooch.send(
24
+ reply[:reply_type],
25
+ Stealth.config.smooch.app_id,
26
+ reply[:recipient_id],
27
+ reply[:message]
28
+ )
29
+ rescue SmoochApi::ApiError => e
30
+ msg = Stealth::Logger.colorize('[Error]', color: :red) + " #{e.code}: #{e.response_body}"
31
+ Stealth::Logger.l(topic: 'smooch', message: msg)
32
+ raise Stealth::Errors::ServiceError
33
+ end
34
+
35
+ if response.present?
36
+ Stealth::Logger.l(topic: "smooch", message: "Message #{response.message._id} successfully sent.")
37
+ end
38
+ end
39
+
40
+ def self.generate_jwt_token
41
+ payload = { scope: 'app' }
42
+ jwtHeader = { kid: Stealth.config.smooch.key_id }
43
+ token = JWT.encode(payload, Stealth.config.smooch.secret, 'HS256', jwtHeader)
44
+
45
+ puts "#{Stealth::Logger.colorize('[JWT Token]', color: :green)} Your Smooch token is below. Please set the value `jwt_token` to the token in services.yml."
46
+ puts token
47
+ end
48
+
49
+ def self.register_webhooks(endpoint:)
50
+ smooch_webhook_api = SmoochApi::WebhookApi.new
51
+ webhook_create_body = SmoochApi::WebhookCreate.new(
52
+ target: endpoint,
53
+ triggers: ['message:appUser', 'postback']
54
+ )
55
+
56
+ response = smooch_webhook_api.create_webhook(
57
+ Stealth.config.smooch.app_id,
58
+ webhook_create_body
59
+ )
60
+
61
+ puts "#{Stealth::Logger.colorize('[Web Hooks]', color: :green)} Your Smooch webhooks have been registered to: #{endpoint}"
62
+ end
63
+
64
+ def self.set_persistent_menu(menu)
65
+ smooch_api = SmoochApi::IntegrationApi.new
66
+ response = smooch_api.list_integrations(Stealth.config.smooch.app_id)
67
+ response.integrations.each do |integration|
68
+ begin
69
+ smooch_api.update_integration_menu(Stealth.config.smooch.app_id, integration._id, menu)
70
+ puts "#{Stealth::Logger.colorize('[Persistent Menu]', color: :green)} set for #{integration.type} integration."
71
+ rescue SmoochApi::ApiError
72
+ # Not all integrations support the persistent menu
73
+ puts "#{Stealth::Logger.colorize('[Persistent Menu]', color: :red)} Skipping #{integration.type} integration. Persistent Menu is not supported."
74
+ next
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,56 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Services
6
+ module Smooch
7
+
8
+ class MessageEvent
9
+
10
+ attr_reader :service_message, :params
11
+
12
+ def initialize(service_message:, params:)
13
+ @service_message = service_message
14
+ @params = params
15
+ end
16
+
17
+ def process
18
+ fetch_message
19
+ fetch_location
20
+ fetch_attachments
21
+ end
22
+
23
+ private
24
+
25
+ def fetch_message
26
+ service_message.message = params['text']
27
+ end
28
+
29
+ def fetch_location
30
+ if params['type'] == 'location'
31
+ service_message.location = {
32
+ lat: params['coordinates']['lat'],
33
+ lng: params['coordinates']['lng']
34
+ }
35
+ end
36
+ end
37
+
38
+ def fetch_attachments
39
+ if params['type'] == 'image'
40
+ service_message.attachments << {
41
+ type: 'image',
42
+ url: params['mediaUrl']
43
+ }
44
+ elsif params['type'] == 'file'
45
+ service_message.attachments << {
46
+ type: params['mediaType'],
47
+ url: params['mediaUrl']
48
+ }
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Services
6
+ module Smooch
7
+
8
+ class PostbackEvent
9
+
10
+ attr_reader :service_message, :params
11
+
12
+ def initialize(service_message:, params:)
13
+ @service_message = service_message
14
+ @params = params
15
+ end
16
+
17
+ def process
18
+ fetch_payload
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_payload
24
+ service_message.payload = params['action']['payload']
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'stealth/services/smooch/events/message_event'
5
+ require 'stealth/services/smooch/events/postback_event'
6
+
7
+ module Stealth
8
+ module Services
9
+ module Smooch
10
+
11
+ class MessageHandler < Stealth::Services::BaseMessageHandler
12
+
13
+ attr_reader :service_message, :params, :headers,
14
+ :smooch_response, :smooch_message
15
+
16
+ def initialize(params:, headers:)
17
+ @params = params
18
+ @headers = headers
19
+ end
20
+
21
+ def coordinate
22
+ # Queue the request processing so we can respond quickly to Smooch
23
+ # and also keep track of this message
24
+ Stealth::Services::HandleMessageJob.perform_async('smooch', params, {})
25
+
26
+ # Relay our acceptance
27
+ [200, 'OK']
28
+ end
29
+
30
+ def process
31
+ @service_message = ServiceMessage.new(service: 'smooch')
32
+ @smooch_response = params
33
+ @smooch_message = @smooch_response['messages'].first
34
+ service_message.sender_id = get_sender_id
35
+ service_message.timestamp = get_timestamp
36
+
37
+ process_smooch_event
38
+
39
+ service_message
40
+ end
41
+
42
+ private
43
+
44
+ def get_sender_id
45
+ smooch_response['appUser']['_id']
46
+ end
47
+
48
+ def get_timestamp
49
+ Time.at(smooch_message['received']).to_datetime
50
+ end
51
+
52
+ def process_smooch_event
53
+ if smooch_response['trigger'] == 'message:appUser'
54
+ message_event = Stealth::Services::Smooch::MessageEvent.new(
55
+ service_message: service_message,
56
+ params: smooch_message
57
+ )
58
+ elsif smooch_response['trigger'] == 'postback'
59
+ message_event = Stealth::Services::Smooch::PostbackEvent.new(
60
+ service_message: service_message,
61
+ params: smooch_response['postbacks'].first
62
+ )
63
+ end
64
+
65
+ message_event.process
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,319 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Services
6
+ module Smooch
7
+
8
+ class ReplyHandler < Stealth::Services::BaseReplyHandler
9
+
10
+ attr_reader :recipient_id, :reply
11
+
12
+ def initialize(recipient_id: nil, reply: nil)
13
+ @recipient_id = recipient_id
14
+ @reply = reply
15
+ end
16
+
17
+ def text
18
+ message = SmoochApi::MessagePost.new(
19
+ role: 'appMaker',
20
+ type: 'text',
21
+ text: reply['text']
22
+ )
23
+
24
+ if reply['suggestions'].present?
25
+ smooch_suggestions = generate_suggestions(suggestions: reply['suggestions'])
26
+ message.actions = smooch_suggestions
27
+ end
28
+
29
+ if reply['buttons'].present?
30
+ smooch_buttons = generate_buttons(buttons: reply['buttons'])
31
+ message.actions = smooch_buttons
32
+ end
33
+
34
+ message_template(action: 'post_message', message: message)
35
+ end
36
+
37
+ def image
38
+ check_if_arguments_are_valid!(
39
+ suggestions: reply['suggestions'],
40
+ buttons: reply['buttons']
41
+ )
42
+
43
+ message = SmoochApi::MessagePost.new(
44
+ role: 'appMaker',
45
+ type: 'image',
46
+ mediaUrl: reply['image_url']
47
+ )
48
+
49
+ if reply['suggestions'].present?
50
+ smooch_suggestions = generate_suggestions(suggestions: reply['suggestions'])
51
+ message.actions = smooch_suggestions
52
+ end
53
+
54
+ message_template(action: 'post_message', message: message)
55
+ end
56
+
57
+ def audio
58
+ check_if_arguments_are_valid!(
59
+ suggestions: reply['suggestions'],
60
+ buttons: reply['buttons']
61
+ )
62
+
63
+ message = SmoochApi::MessagePost.new(
64
+ role: 'appMaker',
65
+ type: 'file',
66
+ text: reply['text'],
67
+ mediaUrl: reply['audio_url']
68
+ )
69
+
70
+ if reply['suggestions'].present?
71
+ smooch_suggestions = generate_suggestions(suggestions: reply['suggestions'])
72
+ message.actions = smooch_suggestions
73
+ end
74
+
75
+ message_template(action: 'post_message', message: message)
76
+ end
77
+
78
+ def video
79
+ check_if_arguments_are_valid!(
80
+ suggestions: reply['suggestions'],
81
+ buttons: reply['buttons']
82
+ )
83
+
84
+ message = SmoochApi::MessagePost.new(
85
+ role: 'appMaker',
86
+ type: 'file',
87
+ text: reply['text'],
88
+ mediaUrl: reply['video_url']
89
+ )
90
+
91
+ if reply['suggestions'].present?
92
+ smooch_suggestions = generate_suggestions(suggestions: reply['suggestions'])
93
+ message.actions = smooch_suggestions
94
+ end
95
+
96
+ message_template(action: 'post_message', message: message)
97
+ end
98
+
99
+ def file
100
+ check_if_arguments_are_valid!(
101
+ suggestions: reply['suggestions'],
102
+ buttons: reply['buttons']
103
+ )
104
+
105
+ message = SmoochApi::MessagePost.new(
106
+ role: 'appMaker',
107
+ type: 'file',
108
+ text: reply['text'],
109
+ mediaUrl: reply['file_url']
110
+ )
111
+
112
+ if reply['suggestions'].present?
113
+ smooch_suggestions = generate_suggestions(suggestions: reply['suggestions'])
114
+ message.actions = smooch_suggestions
115
+ end
116
+
117
+ message_template(action: 'post_message', message: message)
118
+ end
119
+
120
+ def cards
121
+ message = SmoochApi::MessagePost.new(
122
+ role: 'appMaker',
123
+ type: 'carousel'
124
+ )
125
+
126
+ smooch_items = generate_card_items(elements: reply["elements"])
127
+ message.items = smooch_items
128
+
129
+ message_template(action: 'post_message', message: message)
130
+ end
131
+
132
+ def list
133
+ message = SmoochApi::MessagePost.new(
134
+ role: 'appMaker',
135
+ type: 'list'
136
+ )
137
+
138
+ smooch_items = generate_list_items(elements: reply["elements"])
139
+ message.items = smooch_items
140
+
141
+ if reply['buttons'].present?
142
+ if reply["buttons"].size > 1
143
+ raise(ArgumentError, "Smooch lists support a single button attached to the list itsef.")
144
+ end
145
+
146
+ smooch_buttons = generate_buttons(buttons: reply['buttons'])
147
+ message.actions = smooch_buttons
148
+ end
149
+
150
+ message_template(action: 'post_message', message: message)
151
+ end
152
+
153
+ def enable_typing_indicator
154
+ message = SmoochApi::TypingActivityTrigger.new(
155
+ role: 'appMaker',
156
+ type: 'typing:start'
157
+ )
158
+
159
+ message_template(action: 'trigger_typing_activity', message: message)
160
+ end
161
+
162
+ def disable_typing_indicator
163
+ message = SmoochApi::TypingActivityTrigger.new(
164
+ role: 'appMaker',
165
+ type: 'typing:stop'
166
+ )
167
+
168
+ message_template(action: 'trigger_typing_activity', message: message)
169
+ end
170
+
171
+ def delay
172
+ enable_typing_indicator
173
+ end
174
+
175
+ def persistent_menu
176
+ smooch_menu = SmoochApi::Menu.new
177
+
178
+ smooch_menu_items = generate_buttons(buttons: Stealth.config.smooch.setup.persistent_menu)
179
+ smooch_menu.items = smooch_menu_items
180
+
181
+ smooch_menu
182
+ end
183
+
184
+ private
185
+
186
+ def message_template(action:, message:)
187
+ {
188
+ recipient_id: recipient_id,
189
+ reply_type: action,
190
+ message: message
191
+ }
192
+ end
193
+
194
+ def generate_card_items(elements:)
195
+ if elements.size > 10
196
+ raise(ArgumentError, "Smooch cards can have at most 10 cards.")
197
+ end
198
+
199
+ smooch_items = elements.collect do |element|
200
+ smooch_item = item_template(element_type: 'card', element: element)
201
+ end
202
+
203
+ smooch_items
204
+ end
205
+
206
+ def generate_list_items(elements:)
207
+ if elements.size < 2 || elements.size > 4
208
+ raise(ArgumentError, "Smooch lists must have 2-4 elements.")
209
+ end
210
+
211
+ smooch_items = elements.collect do |element|
212
+ smooch_item = item_template(element_type: 'list', element: element)
213
+ end
214
+
215
+ smooch_items
216
+ end
217
+
218
+ def item_template(element_type:, element:)
219
+ unless element["title"].present?
220
+ raise(ArgumentError, "Smooch card and list elements must have a 'title' attribute.")
221
+ end
222
+
223
+ smooch_item = SmoochApi::MessageItem.new
224
+
225
+ smooch_item.title = element['title']
226
+
227
+ if element["subtitle"].present?
228
+ smooch_item.description = element["subtitle"]
229
+ end
230
+
231
+ if element["image_url"].present?
232
+ smooch_item.media_url = element["image_url"]
233
+ end
234
+
235
+ if element["buttons"].present?
236
+ if element_type == 'card' && element["buttons"].size > 3
237
+ raise(ArgumentError, "Smooch card elements only support 3 buttons.")
238
+ end
239
+
240
+ if element_type == 'list' && element["buttons"].size > 1
241
+ raise(ArgumentError, "Smooch list elements only support 1 button.")
242
+ end
243
+
244
+ smooch_buttons = generate_buttons(buttons: element['buttons'])
245
+ smooch_item.actions = smooch_buttons
246
+ end
247
+
248
+ smooch_item
249
+ end
250
+
251
+ def generate_suggestions(suggestions:)
252
+ quick_replies = suggestions.collect do |suggestion|
253
+ quick_reply = SmoochApi::Action.new(type: 'reply')
254
+
255
+ case suggestion["type"]
256
+ when 'location'
257
+ quick_reply.type = 'locationRequest'
258
+ quick_reply.text = suggestion["text"]
259
+ when 'phone'
260
+ quick_reply.text = suggestion["text"]
261
+ when 'email'
262
+ quick_reply.text = suggestion["text"]
263
+ else
264
+ quick_reply.text = suggestion["text"]
265
+
266
+ if suggestion["payload"].present?
267
+ quick_reply.payload = suggestion["payload"]
268
+ else
269
+ quick_reply.payload = suggestion["text"]
270
+ end
271
+
272
+ if suggestion["image_url"].present?
273
+ quick_reply.icon_url = suggestion["image_url"]
274
+ end
275
+ end
276
+
277
+ quick_reply
278
+ end
279
+
280
+ quick_replies
281
+ end
282
+
283
+ def generate_buttons(buttons:)
284
+ smooch_buttons = buttons.collect do |button|
285
+ case button['type']
286
+ when 'url'
287
+ smooch_button = SmoochApi::Action.new(type: 'webview')
288
+ smooch_button.uri = smooch_button.fallback = button["url"]
289
+ smooch_button.text = smooch_button.fallback = button["text"]
290
+
291
+ if button["webview_height"].present?
292
+ smooch_button.size = button["webview_height"]
293
+ end
294
+
295
+ when 'payload'
296
+ smooch_button = SmoochApi::Action.new(type: 'postback')
297
+ smooch_button.payload = button["payload"]
298
+ smooch_button.text = button["text"]
299
+
300
+ else
301
+ raise(Stealth::Errors::ServiceImpaired, "Sorry, we don't yet support #{button["type"]} buttons yet!")
302
+ end
303
+
304
+ smooch_button
305
+ end
306
+
307
+ smooch_buttons
308
+ end
309
+
310
+ def check_if_arguments_are_valid!(suggestions:, buttons:)
311
+ if suggestions.present? && buttons.present?
312
+ raise(ArgumentError, "A reply cannot have buttons and suggestions!")
313
+ end
314
+ end
315
+ end
316
+
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'stealth/services/smooch/client'
5
+
6
+ module Stealth
7
+ module Services
8
+ module Smooch
9
+
10
+ class Setup
11
+
12
+ class << self
13
+ def trigger
14
+ SmoochApi.configure do |config|
15
+ config.api_key['Authorization'] = Stealth.config.smooch.jwt_token
16
+ config.api_key_prefix['Authorization'] = 'Bearer'
17
+ end
18
+
19
+ reply_handler = Stealth::Services::Smooch::ReplyHandler.new
20
+ menu = reply_handler.persistent_menu
21
+ Stealth::Services::Smooch::Client.set_persistent_menu(menu)
22
+
23
+ if ENV['SMOOCH_ENDPOINT'].present?
24
+ Stealth::Services::Smooch::Client.register_webhooks(endpoint: ENV['SMOOCH_ENDPOINT'])
25
+ else
26
+ puts '[ERROR] Please set SMOOCH_ENDPOINT to the endpoint that will receive Smooch webhooks.'
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ module Services
6
+ module Smooch
7
+ module Version
8
+ def self.version
9
+ File.read(File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'VERSION')).strip
10
+ end
11
+ end
12
+
13
+ VERSION = Version.version
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ require 'stealth/services/smooch/version'
2
+ require 'stealth/services/smooch/client'
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+
5
+ require 'stealth'
6
+ require 'stealth-smooch'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
+
12
+ RSpec.configure do |config|
13
+
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+
5
+ describe "Stealth::Services::Smooch::Version" do
6
+
7
+ let(:version_in_file) { File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).strip }
8
+
9
+ it "should return the current gem version" do
10
+ expect(Stealth::Services::Smooch::Version.version).to eq version_in_file
11
+ end
12
+
13
+ it "should return the current gem version via a constant" do
14
+ expect(Stealth::Services::Smooch::VERSION).to eq version_in_file
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ version = File.read(File.join(File.dirname(__FILE__), 'VERSION')).strip
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'stealth-smooch'
7
+ s.summary = 'Stealth Smooch driver'
8
+ s.description = 'Smooch driver for Stealth.'
9
+ s.homepage = 'https://github.com/hellostealth/stealth-smooch'
10
+ s.licenses = ['MIT']
11
+ s.version = version
12
+ s.author = 'Mauricio Gomes'
13
+ s.email = 'mauricio@edge14.com'
14
+
15
+ s.add_dependency 'stealth', '< 2.0'
16
+ s.add_dependency 'jwt', '~> 2.1'
17
+ s.add_dependency 'smooch-api', '~> 4.0'
18
+
19
+ s.add_development_dependency 'rspec', '~> 3.6'
20
+ s.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
21
+ s.add_development_dependency 'rack-test', '~> 1.1'
22
+
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26
+ s.require_paths = ['lib']
27
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stealth-smooch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Mauricio Gomes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: stealth
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "<"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "<"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: smooch-api
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec_junit_formatter
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rack-test
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.1'
97
+ description: Smooch driver for Stealth.
98
+ email: mauricio@edge14.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - ".gitignore"
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - LICENSE
107
+ - README.md
108
+ - VERSION
109
+ - lib/stealth-smooch.rb
110
+ - lib/stealth/services/smooch/client.rb
111
+ - lib/stealth/services/smooch/events/message_event.rb
112
+ - lib/stealth/services/smooch/events/postback_event.rb
113
+ - lib/stealth/services/smooch/message_handler.rb
114
+ - lib/stealth/services/smooch/reply_handler.rb
115
+ - lib/stealth/services/smooch/setup.rb
116
+ - lib/stealth/services/smooch/version.rb
117
+ - lib/stealth/smooch.rb
118
+ - spec/spec_helper.rb
119
+ - spec/version_spec.rb
120
+ - stealth-smooch.gemspec
121
+ homepage: https://github.com/hellostealth/stealth-smooch
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.7.7
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Stealth Smooch driver
145
+ test_files:
146
+ - spec/spec_helper.rb
147
+ - spec/version_spec.rb