sinbotra 0.1.6 → 0.2.0

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.
@@ -0,0 +1,7 @@
1
+ class Sinbotra::Bot
2
+ class UserStore < RedisStore
3
+ def make_key(id)
4
+ ["sinbotra", "users", id].join(":")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ class Sinbotra::MessageHandler
2
+ class << self
3
+ attr_reader :handlers
4
+
5
+ def add_handler(provider, handler)
6
+ @handlers ||= {}
7
+ @handlers[provider] = handler
8
+ end
9
+ end
10
+
11
+ attr_reader :provider
12
+
13
+ def initialize(provider, async=true)
14
+ @provider = provider
15
+ @is_async = async
16
+ end
17
+
18
+ def receive_messages(messages)
19
+ messages.each do |msg|
20
+ bot = handler_class.new(msg)
21
+ bot.respond
22
+ end
23
+ end
24
+
25
+ def handler_class; self.class.handlers[@provider]; end
26
+ end
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ module Sinbotra
4
+ class MessageStore
5
+ def self.log_in_message!(provider, message, sender)
6
+ timestamp = Time.now
7
+ m = { sender: :user, timestamp: timestamp.to_i, message: message.to_json}.to_json
8
+ $redis.lpush(key(provider, sender.id), m)
9
+ end
10
+
11
+ def self.log_out_message!(provider, message, receiver)
12
+ timestamp = Time.now
13
+ m = { sender: :bot, timestamp: timestamp.to_i, message: message.to_json}.to_json
14
+ $redis.lpush(key(provider, receiver.id), m)
15
+ end
16
+
17
+ def self.key(provider, user_id)
18
+ ["sinbotra", "messages", user_id.to_s, provider].join(":")
19
+ end
20
+ end
21
+ end
@@ -1,4 +1,5 @@
1
+ require "sinbotra/messenger/platform"
1
2
  require "sinbotra/messenger/bot"
2
- require "sinbotra/messenger/handler"
3
3
  require "sinbotra/messenger/message"
4
- require "sinbotra/messenger/user"
4
+ require "sinbotra/messenger/message_presenter"
5
+ require "sinbotra/messenger/user_presenter"
@@ -1,91 +1,142 @@
1
1
  module Sinbotra::Messenger
2
2
  class Bot
3
- attr_reader :current_user
3
+ GET_STARTED_ID = "GET_STARTED"
4
4
 
5
- def initialize(user)
6
- @current_user = user
7
- end
5
+ class << self
6
+ attr_reader :_listeners, :_default_listener, :_conversations, :_dont_understand_phrases, :get_started_method
8
7
 
9
- def say(text)
10
- client.text(
11
- recipient_id: recipient_id,
12
- text: text
13
- )
14
- end
15
- alias_method :text, :say
8
+ Listener = Struct.new(:matcher, :method_name)
9
+ Conversation = Struct.new(:id, :klass)
10
+
11
+ def get_started(method_name)
12
+ @get_started_method = method_name
13
+ Sinbotra::Messenger::Platform.get_started(GET_STARTED_ID)
14
+ end
16
15
 
17
- def quick_replies(text, replies)
18
- client.qr(
19
- recipient_id: recipient_id,
20
- text: text,
21
- replies: replies
22
- )
16
+ def listeners(methods)
17
+ @_listeners = methods.map { |name, matcher| Listener.new(matcher, name) }
18
+ end
19
+
20
+ def default_listener(listener)
21
+ @_default_listener = listener
22
+ end
23
+
24
+ def conversations(conversation_map)
25
+ c = conversation_map.each_with_object({}) { |(id, klass), h|
26
+ h[id] = Conversation.new(id, klass)
27
+ }
28
+ @_conversations = c
29
+ end
30
+
31
+ def dont_understand_phrases(phrases)
32
+ @_dont_understand_phrases = phrases
33
+ end
23
34
  end
24
35
 
25
- def typing(seconds=0, &blk)
26
- client.typing(recipient_id: recipient_id, seconds: seconds, &blk)
36
+ attr_reader :current_user
37
+ attr_reader :message
38
+ attr_reader :platform
39
+ attr_reader :get_started_method
40
+
41
+ def listeners; self.class._listeners; end
42
+ def default_listener; self.class._default_listener; end
43
+ def conversations; self.class._conversations; end
44
+ def get_started_method; self.class.get_started_method; end
45
+
46
+ include Sinbotra::Messenger
47
+
48
+ def initialize(message)
49
+ @current_user = UserPresenter.new(message.sender)
50
+ @message = MessagePresenter.new(message)
51
+ @platform = Platform.new(@current_user.id)
52
+ @phrases = setup_phrases
53
+ log_message!
27
54
  end
28
55
 
29
- def qr(text, postback=nil)
30
- MessengerClient::QuickReply.new(text, postback)
56
+ def respond
57
+ $logger.debug("Responding...")
58
+ in_conversation? ? send_to_conversation! : send_to_listeners!
31
59
  end
32
60
 
33
- def payload_button(text, payload)
34
- MessengerClient::PayloadButton.new(text, payload)
61
+ def in_conversation?; current_user.in_conversation?; end
62
+
63
+ def start_conversation(convo_id)
64
+ current_user.start_conversation(convo_id)
65
+ convo = make_conversation(convo_id)
66
+ $logger.debug("Start Conversation: #{convo}")
67
+ convo.start
35
68
  end
36
- def url_button(text, url)
37
- MessengerClient::URLButton.new(text, url)
69
+
70
+ def make_conversation(convo_id)
71
+ convo = conversations[convo_id]
72
+ convo.klass.new(self, platform)
38
73
  end
39
74
 
40
- def generic_template(title, subtitle, image, link, buttons)
75
+ def send_to_conversation!
76
+ $logger.debug("Send message to conversation")
77
+ id = current_user.conversation.id
78
+ convo = make_conversation(id)
79
+ convo.continue_dialogue
41
80
  end
42
81
 
43
- def text_with_buttons(text, buttons)
44
- client.button_template(
45
- recipient_id: recipient_id,
46
- text: text,
47
- buttons: buttons
48
- )
82
+ def send_to_listeners!
83
+ $logger.debug("Send message through handler stack")
84
+ listener = listeners.find { |l| matches_message?(l) }
85
+ if listener.nil?
86
+ get_started_message? ? handle_get_started! : handle_default!
87
+ else
88
+ send(listener.method_name, message)
89
+ end
49
90
  end
50
91
 
51
- def video(url)
52
- client.video(recipient_id: recipient_id, url: url)
92
+ def get_started_message?
93
+ pb = message.postback
94
+ pb.to_s == self.class::GET_STARTED_ID
53
95
  end
54
96
 
55
- def image(url)
56
- client.image(recipient_id: recipient_id, url: url)
97
+ def handle_get_started!
98
+ send(get_started_method, message)
57
99
  end
58
100
 
59
- def location
60
- client.location(recipient_id: recipient_id)
101
+ def handle_default!
102
+ send(default_listener, message)
61
103
  end
62
104
 
63
- def current_message
64
- current_user.current_message
105
+ def has_conversation?(intent)
106
+ # TODO: Figure this out ;)
107
+ false
65
108
  end
66
109
 
67
- def start_conversation(convo_id)
68
- convo = find_convo(convo_id)
69
- current_user.start_conversation(convo)
70
- convo.perform_current_step(self)
110
+ def get_intent(msg)
111
+ # TODO: Send msg to NLP API
71
112
  end
72
113
 
73
- def has_conversation?(convo_id)
74
- !!find_convo(convo_id)
114
+ def next_dont_understand_phrase
115
+ return "I don't understand" unless @phrases.any?
116
+ @phrase_index ||= 0
117
+ phrase = @phrases[@phrase_index % @phrases.size]
118
+ @phrase_index += 1
119
+ phrase
75
120
  end
76
121
 
77
122
  private
78
123
 
79
- def client
80
- @client ||= MessengerClient::Client.new(ENV.fetch("FACEBOOK_PAGE_TOKEN"))
124
+ def matches_message?(listener)
125
+ if txt = message.text
126
+ return true if txt.match(listener.matcher)
127
+ elsif pb = message.postback
128
+ return true if pb.to_s.match(listener.matcher)
129
+ end
130
+ return false
81
131
  end
82
132
 
83
- def recipient_id
84
- current_user.id
133
+ def setup_phrases
134
+ phrases = self.class._dont_understand_phrases || []
135
+ phrases.shuffle
85
136
  end
86
137
 
87
- def find_convo(convo_id)
88
- Sinbotra::Bot::ConversationRepo.find(convo_id)
138
+ def log_message!
139
+ Sinbotra::MessageStore.log_in_message!(:facebook, message, message.sender)
89
140
  end
90
141
  end
91
142
  end
@@ -1,57 +1,18 @@
1
1
  module Sinbotra::Messenger
2
2
  class Message
3
- Sender = Struct.new(:id)
4
- Location = Struct.new(:title, :url, :lat, :lng)
5
-
6
- def self.load(msg)
7
- new(msg).parse
8
- end
9
-
10
- attr_reader :id
11
- attr_reader :timestamp
12
- attr_reader :sender
13
- attr_reader :postback
14
- attr_reader :text
15
- attr_reader :location
16
-
17
- def initialize(msg)
18
- @raw_message = msg
19
- end
20
-
21
- def parse
22
- parse_message
23
- self
24
- end
25
-
26
- def postback?
27
- !!postback
3
+ class Base < Struct
4
+ def postback; respond_to?(:_postback) ? _postback : nil; end
5
+ def text; respond_to?(:_text) ? _text : nil; end
28
6
  end
29
7
 
30
- private
31
-
32
- def parse_message
33
- @timestamp = @raw_message["timestamp"]
34
- @sender = Sender.new(@raw_message["sender"]["id"])
35
- if pb = @raw_message["postback"]
36
- # From button postback
37
- @postback = pb["payload"].to_sym if pb["payload"]
38
- elsif msg = @raw_message["message"]
39
- # Message or quick reply (or location)
40
- @text = msg["text"]
41
- @id = msg["id"]
42
- if qr = msg["quick_reply"]
43
- # Is quick reply
44
- @postback = qr["payload"].to_sym if qr["payload"]
45
- end
46
- attch = msg["attachments"]
47
- if !attch.nil? && attch["type"] == "location"
48
- # Is location
49
- coords = attch["payload"]["coordinates"]
50
- @location = Location.new(attch["title"], attch["url"], coords.first, coords.last)
51
- end
52
- else
53
- raise ArgumentError, "Dunno what this message is about: #{message.to_s}"
54
- end
55
- end
8
+ Sender = Base.new(:id)
9
+ Text = Base.new(:id, :timestamp, :sender, :_text)
10
+ Image = Base.new(:id, :timestamp, :sender, :url, :sticker_id)
11
+ Video = Base.new(:id, :timestamp, :sender, :url)
12
+ Audio = Base.new(:id, :timestamp, :sender, :url)
13
+ File = Base.new(:id, :timestamp, :sender, :url)
14
+ Location = Base.new(:id, :timestamp, :sender, :url, :lat, :lng)
15
+ QuickReply = Base.new(:id, :timestamp, :sender, :_postback, :_text)
16
+ Postback = Base.new(:timestamp, :sender, :_postback)
56
17
  end
57
18
  end
@@ -0,0 +1,6 @@
1
+ require "delegate"
2
+
3
+ module Sinbotra::Messenger
4
+ class MessagePresenter < SimpleDelegator
5
+ end
6
+ end
@@ -0,0 +1,32 @@
1
+ require "openssl"
2
+
3
+ module Sinbotra::Messenger::Middleware
4
+ class FacebookSignature
5
+ def initialize(app)
6
+ raise ArgumentError, "You need to set a FACEBOOK_PAGE_TOKEN environmental variable to run the server!" unless ENV["FACEBOOK_PAGE_TOKEN"]
7
+ raise ArgumentError, "You need to set a FACEBOOK_APP_SECRET environmental variable to run the server!" unless ENV["FACEBOOK_APP_SECRET"]
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ req = Rack::Request.new(env)
13
+ return @app.call(env) unless req.post?
14
+
15
+ payload = req.body.read
16
+ unless signature_valid?(payload, env)
17
+ return Rack::Response.new([], 401, {}).finish
18
+ end
19
+ @app.call(env)
20
+ end
21
+
22
+ def signature_valid?(payload_body, env)
23
+ digest = OpenSSL::HMAC.hexdigest(
24
+ OpenSSL::Digest.new("sha1"),
25
+ ENV["FACEBOOK_APP_SECRET"],
26
+ payload_body
27
+ )
28
+ signature = "sha1=" + digest
29
+ Rack::Utils.secure_compare(signature, env["HTTP_X_HUB_SIGNATURE"])
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,81 @@
1
+ module Sinbotra::Messenger::Middleware
2
+ class ParseMessage
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ req = Rack::Request.new(env)
9
+ return @app.call(env) unless req.post?
10
+ data = json(req)
11
+ env["facebook.messages"] = parse_messages(data)
12
+ @app.call(env)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_messages(data)
18
+ $logger.debug("Data: #{data}")
19
+ e = data["entry"].first
20
+ e["messaging"].map { |message|
21
+ parse_message(message)
22
+ }.compact
23
+ end
24
+
25
+ def parse_message(message)
26
+ sender = Sinbotra::Messenger::Message::Sender.new(message["sender"]["id"])
27
+ timestamp = message["timestamp"]
28
+ m = message["message"]
29
+ if m.nil?
30
+ return parse_postback(message, timestamp, sender)
31
+ end
32
+ if attchs = m["attachments"]
33
+ attch = attchs.first
34
+ pl = attch["payload"]
35
+ url = pl["url"]
36
+ case attch["type"]
37
+ when "image"
38
+ Sinbotra::Messenger::Message::Image.new(m["mid"], timestamp, sender, url, pl["sticker_id"])
39
+ when "video"
40
+ Sinbotra::Messenger::Message::Video.new(m["mid"], timestamp, sender, url)
41
+ when "audio"
42
+ Sinbotra::Messenger::Message::Audio.new(m["mid"], timestamp, sender, url)
43
+ when "file"
44
+ Sinbotra::Messenger::Message::File.new(m["mid"], timestamp, sender, url)
45
+ when "location"
46
+ url = attch["url"]
47
+ lat, lng = pl["coordinates"]["lat"], pl["coordinates"]["long"]
48
+ Sinbotra::Messenger::Message::Location.new(m["mid"], timestamp, sender, url, lat, lng)
49
+ else
50
+ end
51
+ else
52
+ parse_non_media(m, timestamp, sender)
53
+ end
54
+ end
55
+
56
+ def parse_non_media(message, timestamp, sender)
57
+ if qr = message["quick_reply"]
58
+ payload = qr["payload"].to_sym
59
+ return Sinbotra::Messenger::Message::QuickReply.new(message["mid"], timestamp, sender, payload, message["text"])
60
+ end
61
+ if txt = message["text"]
62
+ return Sinbotra::Messenger::Message::Text.new(message["mid"], timestamp, sender, txt)
63
+ end
64
+ return nil
65
+ end
66
+
67
+ def parse_postback(message, timestamp, sender)
68
+ if pb = message["postback"]
69
+ payload = pb["payload"].to_sym
70
+ return Sinbotra::Messenger::Message::Postback.new(timestamp, sender, payload)
71
+ end
72
+ return nil
73
+ end
74
+
75
+ def json(req)
76
+ req.body.rewind
77
+ body = req.body.read
78
+ JSON.load(body)
79
+ end
80
+ end
81
+ end