stealth 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|