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.
- 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
|