stealth 0.9.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/.circleci/config.yml +58 -0
- data/.gitignore +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +81 -0
- data/LICENSE +20 -0
- data/README.md +1 -0
- data/VERSION +1 -0
- data/bin/stealth +5 -0
- data/lib/stealth/base.rb +87 -0
- data/lib/stealth/cli.rb +82 -0
- data/lib/stealth/cli_base.rb +25 -0
- data/lib/stealth/commands/command.rb +14 -0
- data/lib/stealth/commands/console.rb +75 -0
- data/lib/stealth/commands/server.rb +20 -0
- data/lib/stealth/configuration.rb +54 -0
- data/lib/stealth/controller.rb +190 -0
- data/lib/stealth/dispatcher.rb +48 -0
- data/lib/stealth/errors.rb +32 -0
- data/lib/stealth/flow/base.rb +256 -0
- data/lib/stealth/flow/errors.rb +25 -0
- data/lib/stealth/flow/event.rb +43 -0
- data/lib/stealth/flow/event_collection.rb +41 -0
- data/lib/stealth/flow/specification.rb +67 -0
- data/lib/stealth/flow/state.rb +48 -0
- data/lib/stealth/jobs.rb +10 -0
- data/lib/stealth/logger.rb +16 -0
- data/lib/stealth/reply.rb +19 -0
- data/lib/stealth/server.rb +38 -0
- data/lib/stealth/service_message.rb +17 -0
- data/lib/stealth/service_reply.rb +30 -0
- data/lib/stealth/services/base_client.rb +28 -0
- data/lib/stealth/services/base_message_handler.rb +28 -0
- data/lib/stealth/services/base_reply_handler.rb +65 -0
- data/lib/stealth/services/facebook/client.rb +35 -0
- data/lib/stealth/services/facebook/events/message_event.rb +59 -0
- data/lib/stealth/services/facebook/events/postback_event.rb +36 -0
- data/lib/stealth/services/facebook/message_handler.rb +84 -0
- data/lib/stealth/services/facebook/reply_handler.rb +471 -0
- data/lib/stealth/services/facebook/setup.rb +25 -0
- data/lib/stealth/services/jobs/handle_message_job.rb +22 -0
- data/lib/stealth/session.rb +74 -0
- data/lib/stealth/version.rb +12 -0
- data/lib/stealth.rb +1 -0
- data/spec/configuration_spec.rb +52 -0
- data/spec/flow/custom_transitions_spec.rb +99 -0
- data/spec/flow/flow_spec.rb +91 -0
- data/spec/flow/transition_callbacks_spec.rb +228 -0
- data/spec/replies/nested_reply_with_erb.yml +16 -0
- data/spec/sample_services_yml/services.yml +31 -0
- data/spec/sample_services_yml/services_with_erb.yml +31 -0
- data/spec/service_reply_spec.rb +34 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/version_spec.rb +16 -0
- data/stealth.gemspec +30 -0
- metadata +247 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Flow
|
6
|
+
class State
|
7
|
+
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
attr_accessor :name, :events, :meta, :on_entry, :on_exit
|
11
|
+
attr_reader :spec
|
12
|
+
|
13
|
+
def initialize(name, spec, meta = {})
|
14
|
+
@name, @spec, @events, @meta = name, spec, EventCollection.new, meta
|
15
|
+
end
|
16
|
+
|
17
|
+
def draw(graph)
|
18
|
+
defaults = {
|
19
|
+
:label => to_s,
|
20
|
+
:width => '1',
|
21
|
+
:height => '1',
|
22
|
+
:shape => 'ellipse'
|
23
|
+
}
|
24
|
+
|
25
|
+
node = graph.add_nodes(to_s, defaults.merge(meta))
|
26
|
+
|
27
|
+
# Add open arrow for initial state
|
28
|
+
# graph.add_edge(graph.add_node('starting_state', :shape => 'point'), node) if initial?
|
29
|
+
|
30
|
+
node
|
31
|
+
end
|
32
|
+
|
33
|
+
def <=>(other_state)
|
34
|
+
states = spec.states.keys
|
35
|
+
raise ArgumentError, "state `#{other_state}' does not exist" unless states.include?(other_state.to_sym)
|
36
|
+
states.index(self.to_sym) <=> states.index(other_state.to_sym)
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
"#{name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_sym
|
44
|
+
name.to_sym
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/stealth/jobs.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
class Reply
|
6
|
+
|
7
|
+
attr_accessor :reply_type, :reply
|
8
|
+
|
9
|
+
def initialize(unstructured_reply:)
|
10
|
+
@reply_type = unstructured_reply["reply_type"]
|
11
|
+
@reply = unstructured_reply
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@reply[key]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'multi_json'
|
6
|
+
|
7
|
+
module Stealth
|
8
|
+
class Server < Sinatra::Base
|
9
|
+
|
10
|
+
def self.get_or_post(url, &block)
|
11
|
+
get(url, &block)
|
12
|
+
post(url, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
"Welcome to stealth."
|
17
|
+
end
|
18
|
+
|
19
|
+
get_or_post '/incoming/:service' do
|
20
|
+
Stealth::Logger.l(topic: "incoming", message: "Received webhook from #{params[:service]}.")
|
21
|
+
|
22
|
+
# JSON params need to be parsed and added to the params
|
23
|
+
if request.env['CONTENT_TYPE'] == 'application/json'
|
24
|
+
json_params = MultiJson.load(request.body.read)
|
25
|
+
params.merge!(json_params)
|
26
|
+
end
|
27
|
+
|
28
|
+
dispatcher = Stealth::Dispatcher.new(
|
29
|
+
service: params[:service],
|
30
|
+
params: params,
|
31
|
+
headers: request.env
|
32
|
+
)
|
33
|
+
|
34
|
+
dispatcher.coordinate
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
class ServiceMessage
|
6
|
+
|
7
|
+
attr_accessor :sender_id, :timestamp, :service, :message, :location,
|
8
|
+
:attachments, :payload, :referral
|
9
|
+
|
10
|
+
def initialize(service:)
|
11
|
+
@service = service
|
12
|
+
@attachments = []
|
13
|
+
@location = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
class ServiceReply
|
6
|
+
|
7
|
+
attr_accessor :recipient_id, :replies
|
8
|
+
|
9
|
+
def initialize(recipient_id:, yaml_reply:, context:)
|
10
|
+
@recipient_id = recipient_id
|
11
|
+
|
12
|
+
begin
|
13
|
+
erb_reply = ERB.new(yaml_reply).result(context)
|
14
|
+
rescue NameError => e
|
15
|
+
raise(Stealth::Errors::UndefinedVariable, e.message)
|
16
|
+
end
|
17
|
+
|
18
|
+
@replies = load_replies(YAML.load(erb_reply))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def load_replies(unstructured_replies)
|
24
|
+
unstructured_replies.collect do |reply|
|
25
|
+
Stealth::Reply.new(unstructured_reply: reply)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'stealth/services/base_reply_handler'
|
5
|
+
require 'stealth/services/base_message_handler'
|
6
|
+
|
7
|
+
require 'stealth/services/jobs/handle_message_job'
|
8
|
+
|
9
|
+
module Stealth
|
10
|
+
module Services
|
11
|
+
class BaseClient
|
12
|
+
|
13
|
+
attr_reader :reply
|
14
|
+
|
15
|
+
def initialize(reply:)
|
16
|
+
@reply = reply
|
17
|
+
end
|
18
|
+
|
19
|
+
def transmit
|
20
|
+
raise(Stealth::Errors::ServiceImpaired, "Service implementation does not implement 'transmit'.")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
require 'stealth/services/facebook/client'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
class BaseMessageHandler
|
7
|
+
|
8
|
+
attr_reader :params, :headers
|
9
|
+
|
10
|
+
def initialize(params:, headers:)
|
11
|
+
@params = params
|
12
|
+
@headers = headers
|
13
|
+
end
|
14
|
+
|
15
|
+
# Should respond with a Rack response (https://github.com/sinatra/sinatra#return-values)
|
16
|
+
def coordinate
|
17
|
+
raise(Stealth::Errors::ServiceImpaired, "Service request handler does not implement 'process'.")
|
18
|
+
end
|
19
|
+
|
20
|
+
# After coordinate responds to the service, an optional async job
|
21
|
+
# may be fired that will continue the work via this method
|
22
|
+
def process
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
class BaseReplyHandler
|
7
|
+
|
8
|
+
attr_reader :recipient_id, :reply
|
9
|
+
|
10
|
+
def initialize(recipient_id:, reply:)
|
11
|
+
@client = client
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def text
|
16
|
+
reply_format_not_supported(format: 'text')
|
17
|
+
end
|
18
|
+
|
19
|
+
def image
|
20
|
+
reply_format_not_supported(format: 'image')
|
21
|
+
end
|
22
|
+
|
23
|
+
def audio
|
24
|
+
reply_format_not_supported(format: 'audio')
|
25
|
+
end
|
26
|
+
|
27
|
+
def video
|
28
|
+
reply_format_not_supported(format: 'video')
|
29
|
+
end
|
30
|
+
|
31
|
+
def file
|
32
|
+
reply_format_not_supported(format: 'file')
|
33
|
+
end
|
34
|
+
|
35
|
+
def cards
|
36
|
+
reply_format_not_supported(format: 'cards')
|
37
|
+
end
|
38
|
+
|
39
|
+
def list
|
40
|
+
reply_format_not_supported(format: 'list')
|
41
|
+
end
|
42
|
+
|
43
|
+
def receipt
|
44
|
+
reply_format_not_supported(format: 'receipt')
|
45
|
+
end
|
46
|
+
|
47
|
+
def mark_seen
|
48
|
+
reply_format_not_supported(format: 'mark_seen')
|
49
|
+
end
|
50
|
+
|
51
|
+
def enable_typing_indicator
|
52
|
+
reply_format_not_supported(format: 'enable_typing_indicator')
|
53
|
+
end
|
54
|
+
|
55
|
+
def disable_typing_indicator
|
56
|
+
reply_format_not_supported(format: 'disable_typing_indicator')
|
57
|
+
end
|
58
|
+
|
59
|
+
def delay
|
60
|
+
reply_format_not_supported(format: 'delay')
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'faraday'
|
5
|
+
|
6
|
+
require 'stealth/services/facebook/message_handler'
|
7
|
+
require 'stealth/services/facebook/reply_handler'
|
8
|
+
require 'stealth/services/facebook/setup'
|
9
|
+
|
10
|
+
module Stealth
|
11
|
+
module Services
|
12
|
+
module Facebook
|
13
|
+
|
14
|
+
class Client < Stealth::Services::BaseClient
|
15
|
+
FB_ENDPOINT = "https://graph.facebook.com/v2.10/me"
|
16
|
+
|
17
|
+
attr_reader :api_endpoint, :reply
|
18
|
+
|
19
|
+
def initialize(reply:, endpoint: 'messages')
|
20
|
+
@reply = reply
|
21
|
+
access_token = "access_token=#{Stealth.config.facebook.page_access_token}"
|
22
|
+
@api_endpoint = [[FB_ENDPOINT, endpoint].join('/'), access_token].join('?')
|
23
|
+
end
|
24
|
+
|
25
|
+
def transmit
|
26
|
+
headers = { "Content-Type" => "application/json" }
|
27
|
+
response = Faraday.post(api_endpoint, reply.to_json, headers)
|
28
|
+
Stealth::Logger.l(topic: "facebook", message: "Transmitting. Response: #{response.status}: #{response.body}")
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
module Facebook
|
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
|
+
if params['message']['quick_reply'].present?
|
27
|
+
service_message.message = params['message']['quick_reply']['payload']
|
28
|
+
elsif params['message']['text'].present?
|
29
|
+
service_message.message = params['message']['text']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_location
|
34
|
+
if params['location'].present?
|
35
|
+
lat = params['location']['coordinates']['lat']
|
36
|
+
lng = params['location']['coordinates']['long']
|
37
|
+
service_message.location = {
|
38
|
+
lat: lat,
|
39
|
+
lng: lng
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_attachments
|
45
|
+
if params['attachments'].present? && params['attachments'].is_a?(Array)
|
46
|
+
params['attachments'].each do |attachment|
|
47
|
+
service_message.attachments << {
|
48
|
+
type: attachment['type'],
|
49
|
+
url: attachment['payload']['url']
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Stealth
|
5
|
+
module Services
|
6
|
+
module Facebook
|
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
|
+
fetch_referral
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fetch_payload
|
25
|
+
service_message.payload = params['postback']['payload']
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch_referral
|
29
|
+
service_message.referral = params['postback']['referral']
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'stealth/services/facebook/events/message_event'
|
5
|
+
require 'stealth/services/facebook/events/postback_event'
|
6
|
+
|
7
|
+
module Stealth
|
8
|
+
module Services
|
9
|
+
module Facebook
|
10
|
+
|
11
|
+
class MessageHandler < Stealth::Services::BaseMessageHandler
|
12
|
+
|
13
|
+
attr_reader :service_message, :params, :headers, :facebook_message
|
14
|
+
|
15
|
+
def initialize(params:, headers:)
|
16
|
+
@params = params
|
17
|
+
@headers = headers
|
18
|
+
end
|
19
|
+
|
20
|
+
def coordinate
|
21
|
+
if facebook_is_validating_webhook?
|
22
|
+
respond_with_validation
|
23
|
+
else
|
24
|
+
# Queue the request processing so we can respond quickly to FB
|
25
|
+
# and also keep track of this message
|
26
|
+
Stealth::Services::HandleMessageJob.perform_async('facebook', params, {})
|
27
|
+
|
28
|
+
# Relay our acceptance
|
29
|
+
[200, 'OK']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def process
|
34
|
+
@service_message = ServiceMessage.new(service: 'facebook')
|
35
|
+
@facebook_message = params['entry'].first['messaging'].first
|
36
|
+
service_message.sender_id = get_sender_id
|
37
|
+
service_message.timestamp = get_timestamp
|
38
|
+
process_facebook_event
|
39
|
+
|
40
|
+
service_message
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def facebook_is_validating_webhook?
|
46
|
+
params['hub.verify_token'].present?
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_with_validation
|
50
|
+
if params['hub.verify_token'] == Stealth.config.facebook.verify_token
|
51
|
+
[200, params['hub.challenge']]
|
52
|
+
else
|
53
|
+
[401, "Verify token did not match environment variable."]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_sender_id
|
58
|
+
facebook_message['sender']['id']
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_timestamp
|
62
|
+
Time.at(facebook_message['timestamp']).to_datetime
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_facebook_event
|
66
|
+
if facebook_message['message'].present?
|
67
|
+
message_event = Stealth::Services::Facebook::MessageEvent.new(
|
68
|
+
service_message: service_message,
|
69
|
+
params: facebook_message
|
70
|
+
)
|
71
|
+
elsif facebook_message['postback'].present?
|
72
|
+
message_event = Stealth::Services::Facebook::PostbackEvent.new(
|
73
|
+
service_message: service_message,
|
74
|
+
params: facebook_message
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
message_event.process
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|