stealth 0.9.1

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