sinbotra 0.1.6 → 0.2.0

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