sinbotra 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +126 -0
- data/examples/.keep +0 -0
- data/lib/sinbotra.rb +7 -1
- data/lib/sinbotra/bot.rb +9 -7
- data/lib/sinbotra/bot/conversation.rb +49 -50
- data/lib/sinbotra/bot/redis_store.rb +20 -0
- data/lib/sinbotra/bot/user.rb +9 -46
- data/lib/sinbotra/bot/user_repo.rb +33 -7
- data/lib/sinbotra/bot/user_store.rb +7 -0
- data/lib/sinbotra/message_handler.rb +26 -0
- data/lib/sinbotra/message_store.rb +21 -0
- data/lib/sinbotra/messenger.rb +3 -2
- data/lib/sinbotra/messenger/bot.rb +103 -52
- data/lib/sinbotra/messenger/message.rb +12 -51
- data/lib/sinbotra/messenger/message_presenter.rb +6 -0
- data/lib/sinbotra/messenger/middleware/facebook_signature.rb +32 -0
- data/lib/sinbotra/messenger/middleware/parse_message.rb +81 -0
- data/lib/sinbotra/messenger/platform.rb +72 -0
- data/lib/sinbotra/messenger/user_presenter.rb +46 -0
- data/lib/sinbotra/server.rb +13 -17
- data/lib/sinbotra/version.rb +1 -1
- data/sinbotra.gemspec +1 -0
- metadata +27 -9
- data/examples/pushup_bot.rb +0 -47
- data/examples/simple_conversations.rb +0 -38
- data/lib/sinbotra/bot/conversation_repo.rb +0 -17
- data/lib/sinbotra/bot/default_message_history.rb +0 -15
- data/lib/sinbotra/messenger/handler.rb +0 -126
- data/lib/sinbotra/messenger/user.rb +0 -10
@@ -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
|
data/lib/sinbotra/messenger.rb
CHANGED
@@ -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/
|
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
|
-
|
3
|
+
GET_STARTED_ID = "GET_STARTED"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
end
|
5
|
+
class << self
|
6
|
+
attr_reader :_listeners, :_default_listener, :_conversations, :_dont_understand_phrases, :get_started_method
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
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
|
30
|
-
|
56
|
+
def respond
|
57
|
+
$logger.debug("Responding...")
|
58
|
+
in_conversation? ? send_to_conversation! : send_to_listeners!
|
31
59
|
end
|
32
60
|
|
33
|
-
def
|
34
|
-
|
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
|
-
|
37
|
-
|
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
|
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
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
52
|
-
|
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
|
56
|
-
|
97
|
+
def handle_get_started!
|
98
|
+
send(get_started_method, message)
|
57
99
|
end
|
58
100
|
|
59
|
-
def
|
60
|
-
|
101
|
+
def handle_default!
|
102
|
+
send(default_listener, message)
|
61
103
|
end
|
62
104
|
|
63
|
-
def
|
64
|
-
|
105
|
+
def has_conversation?(intent)
|
106
|
+
# TODO: Figure this out ;)
|
107
|
+
false
|
65
108
|
end
|
66
109
|
|
67
|
-
def
|
68
|
-
|
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
|
74
|
-
|
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
|
80
|
-
|
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
|
84
|
-
|
133
|
+
def setup_phrases
|
134
|
+
phrases = self.class._dont_understand_phrases || []
|
135
|
+
phrases.shuffle
|
85
136
|
end
|
86
137
|
|
87
|
-
def
|
88
|
-
Sinbotra::
|
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
|
-
|
4
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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,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
|