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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +58 -0
  3. data/.gitignore +12 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE +20 -0
  7. data/README.md +1 -0
  8. data/VERSION +1 -0
  9. data/bin/stealth +5 -0
  10. data/lib/stealth/base.rb +87 -0
  11. data/lib/stealth/cli.rb +82 -0
  12. data/lib/stealth/cli_base.rb +25 -0
  13. data/lib/stealth/commands/command.rb +14 -0
  14. data/lib/stealth/commands/console.rb +75 -0
  15. data/lib/stealth/commands/server.rb +20 -0
  16. data/lib/stealth/configuration.rb +54 -0
  17. data/lib/stealth/controller.rb +190 -0
  18. data/lib/stealth/dispatcher.rb +48 -0
  19. data/lib/stealth/errors.rb +32 -0
  20. data/lib/stealth/flow/base.rb +256 -0
  21. data/lib/stealth/flow/errors.rb +25 -0
  22. data/lib/stealth/flow/event.rb +43 -0
  23. data/lib/stealth/flow/event_collection.rb +41 -0
  24. data/lib/stealth/flow/specification.rb +67 -0
  25. data/lib/stealth/flow/state.rb +48 -0
  26. data/lib/stealth/jobs.rb +10 -0
  27. data/lib/stealth/logger.rb +16 -0
  28. data/lib/stealth/reply.rb +19 -0
  29. data/lib/stealth/server.rb +38 -0
  30. data/lib/stealth/service_message.rb +17 -0
  31. data/lib/stealth/service_reply.rb +30 -0
  32. data/lib/stealth/services/base_client.rb +28 -0
  33. data/lib/stealth/services/base_message_handler.rb +28 -0
  34. data/lib/stealth/services/base_reply_handler.rb +65 -0
  35. data/lib/stealth/services/facebook/client.rb +35 -0
  36. data/lib/stealth/services/facebook/events/message_event.rb +59 -0
  37. data/lib/stealth/services/facebook/events/postback_event.rb +36 -0
  38. data/lib/stealth/services/facebook/message_handler.rb +84 -0
  39. data/lib/stealth/services/facebook/reply_handler.rb +471 -0
  40. data/lib/stealth/services/facebook/setup.rb +25 -0
  41. data/lib/stealth/services/jobs/handle_message_job.rb +22 -0
  42. data/lib/stealth/session.rb +74 -0
  43. data/lib/stealth/version.rb +12 -0
  44. data/lib/stealth.rb +1 -0
  45. data/spec/configuration_spec.rb +52 -0
  46. data/spec/flow/custom_transitions_spec.rb +99 -0
  47. data/spec/flow/flow_spec.rb +91 -0
  48. data/spec/flow/transition_callbacks_spec.rb +228 -0
  49. data/spec/replies/nested_reply_with_erb.yml +16 -0
  50. data/spec/sample_services_yml/services.yml +31 -0
  51. data/spec/sample_services_yml/services_with_erb.yml +31 -0
  52. data/spec/service_reply_spec.rb +34 -0
  53. data/spec/spec_helper.rb +13 -0
  54. data/spec/version_spec.rb +16 -0
  55. data/stealth.gemspec +30 -0
  56. 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
@@ -0,0 +1,10 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Jobs
6
+
7
+ include Sidekiq::Worker
8
+
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module Stealth
5
+ class Logger
6
+
7
+ def self.log(topic:, message:)
8
+ puts "[#{topic.upcase}] #{message}"
9
+ end
10
+
11
+ class << self
12
+ alias_method :l, :log
13
+ end
14
+
15
+ end
16
+ end
@@ -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