rubotnik 0.1.1
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +414 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/button_template.png +0 -0
- data/docs/carousel.png +0 -0
- data/docs/quick_replies.PNG +0 -0
- data/exe/rubotnik +4 -0
- data/lib/rubotnik/autoloader.rb +17 -0
- data/lib/rubotnik/cli.rb +15 -0
- data/lib/rubotnik/commands.rb +3 -0
- data/lib/rubotnik/generator.rb +34 -0
- data/lib/rubotnik/helpers.rb +71 -0
- data/lib/rubotnik/message_dispatch.rb +77 -0
- data/lib/rubotnik/postback_dispatch.rb +64 -0
- data/lib/rubotnik/user.rb +22 -0
- data/lib/rubotnik/user_store.rb +34 -0
- data/lib/rubotnik/version.rb +3 -0
- data/lib/rubotnik.rb +39 -0
- data/lib/ui/fb_button_template.rb +47 -0
- data/lib/ui/fb_carousel.rb +65 -0
- data/lib/ui/image_attachment.rb +31 -0
- data/lib/ui/quick_replies.rb +39 -0
- data/rubotnik.gemspec +33 -0
- data/templates/.env +3 -0
- data/templates/.gitignore +1 -0
- data/templates/Gemfile +2 -0
- data/templates/Procfile +1 -0
- data/templates/bot.rb +91 -0
- data/templates/commands.rb +38 -0
- data/templates/config.ru +12 -0
- data/templates/location.rb +36 -0
- data/templates/profile.rb +54 -0
- data/templates/puma.rb +9 -0
- data/templates/ui_examples.rb +69 -0
- metadata +225 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'rubotnik/helpers'
|
2
|
+
require 'rubotnik/commands'
|
3
|
+
|
4
|
+
module Rubotnik
|
5
|
+
# Routing for messages
|
6
|
+
class MessageDispatch
|
7
|
+
include Rubotnik::Helpers
|
8
|
+
include Commands
|
9
|
+
|
10
|
+
attr_reader :message, :user
|
11
|
+
|
12
|
+
def initialize(message)
|
13
|
+
@message = message
|
14
|
+
p @message.class
|
15
|
+
p @message
|
16
|
+
@user = UserStore.instance.find_or_create_user(@message.sender['id'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def route(&block)
|
20
|
+
if @user.current_command
|
21
|
+
command = @user.current_command
|
22
|
+
execute(command)
|
23
|
+
puts "Command #{command} is executed for user #{@user.id}" # log
|
24
|
+
else
|
25
|
+
bind_commands(&block)
|
26
|
+
end
|
27
|
+
rescue StandardError => error
|
28
|
+
raise unless ENV["DEBUG"] == "true"
|
29
|
+
stop_thread
|
30
|
+
say "There was an error: #{error}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def bind_commands(&block)
|
36
|
+
@matched = false
|
37
|
+
instance_eval(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def bind(*regex_strings, all: false, to: nil, reply_with: {})
|
41
|
+
regexps = regex_strings.map { |rs| /\b#{rs}/i }
|
42
|
+
proceed = regexps.any? { |regex| @message.text =~ regex }
|
43
|
+
proceed = regexps.all? { |regex| @message.text =~ regex } if all
|
44
|
+
return unless proceed
|
45
|
+
@matched = true
|
46
|
+
if block_given?
|
47
|
+
yield
|
48
|
+
return
|
49
|
+
end
|
50
|
+
handle_command(to, reply_with)
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_command(to, reply_with)
|
54
|
+
if reply_with.empty?
|
55
|
+
puts "Command #{to} is executed for user #{@user.id}"
|
56
|
+
execute(to)
|
57
|
+
@user.reset_command
|
58
|
+
puts "Command is reset for user #{@user.id}"
|
59
|
+
else
|
60
|
+
say(reply_with[:text], quick_replies: reply_with[:quick_replies])
|
61
|
+
@user.assign_command(to)
|
62
|
+
puts "Command #{to} is set for user #{@user.id}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def default
|
67
|
+
return if @matched
|
68
|
+
puts 'None of the commands were recognized' # log
|
69
|
+
yield
|
70
|
+
@user.reset_command
|
71
|
+
end
|
72
|
+
|
73
|
+
def execute(command)
|
74
|
+
method(command).call
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rubotnik/helpers'
|
2
|
+
require 'rubotnik/commands'
|
3
|
+
|
4
|
+
module Rubotnik
|
5
|
+
# Routing for postbacks
|
6
|
+
class PostbackDispatch
|
7
|
+
include Rubotnik::Helpers
|
8
|
+
include Commands
|
9
|
+
|
10
|
+
attr_reader :postback, :user
|
11
|
+
|
12
|
+
def initialize(postback)
|
13
|
+
@postback = postback
|
14
|
+
p @postback.class
|
15
|
+
p @postback
|
16
|
+
@user = UserStore.instance.find_or_create_user(@postback.sender['id'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def route(&block)
|
20
|
+
@matched = false
|
21
|
+
instance_eval(&block)
|
22
|
+
rescue StandardError => error
|
23
|
+
raise unless ENV["DEBUG"] == "true"
|
24
|
+
stop_thread
|
25
|
+
say "There was an error: #{error}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def bind(regex_string, to: nil, reply_with: {})
|
31
|
+
return unless @postback.payload == regex_string.upcase
|
32
|
+
clear_user_state
|
33
|
+
@matched = true
|
34
|
+
puts "Matched #{regex_string} to #{to.nil? ? 'block' : to}"
|
35
|
+
if block_given?
|
36
|
+
yield
|
37
|
+
return
|
38
|
+
end
|
39
|
+
handle_commands(to, reply_with)
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_commands(to, reply_with)
|
43
|
+
if reply_with.empty?
|
44
|
+
execute(to)
|
45
|
+
puts "Command #{to} is executed for user #{@user.id}"
|
46
|
+
@user.reset_command
|
47
|
+
puts "Command is reset for user #{@user.id}"
|
48
|
+
else
|
49
|
+
say(reply_with[:message], quick_replies: reply_with[:quick_replies])
|
50
|
+
@user.assign_command(to)
|
51
|
+
puts "Command #{to} is set for user #{@user.id}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_user_state
|
56
|
+
@user.reset_command # Stop any current interaction
|
57
|
+
@user.session = {} # Reset whatever you stored in the user
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(command)
|
61
|
+
method(command).call
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class User
|
2
|
+
attr_reader :id
|
3
|
+
attr_accessor :session
|
4
|
+
|
5
|
+
def initialize(id)
|
6
|
+
@id = id
|
7
|
+
@commands = []
|
8
|
+
@session = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_command
|
12
|
+
@commands.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def assign_command(command)
|
16
|
+
@commands << command
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset_command
|
20
|
+
@commands = []
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require_relative 'user'
|
3
|
+
# In-memory storage for users
|
4
|
+
class UserStore
|
5
|
+
include Singleton
|
6
|
+
attr_reader :users
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@users = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_or_create_user(id)
|
13
|
+
find(id) || add(User.new(id))
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(user)
|
17
|
+
@users << user
|
18
|
+
user = @users.last
|
19
|
+
if user
|
20
|
+
p "user #{user.inspect} added to store"
|
21
|
+
p "we got #{@users.count} users: #{@users}"
|
22
|
+
else
|
23
|
+
p 'user not found in store yet'
|
24
|
+
end
|
25
|
+
user
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(id)
|
29
|
+
user = @users.find { |u| u.id == id }
|
30
|
+
p "user #{user} found in store" if user
|
31
|
+
p "we got #{@users.count} users: #{@users}" if user
|
32
|
+
user
|
33
|
+
end
|
34
|
+
end
|
data/lib/rubotnik.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubotnik/version'
|
2
|
+
require 'rubotnik/message_dispatch'
|
3
|
+
require 'rubotnik/postback_dispatch'
|
4
|
+
require 'rubotnik/user_store'
|
5
|
+
require 'rubotnik/user'
|
6
|
+
require 'rubotnik/cli'
|
7
|
+
require 'rubotnik/generator'
|
8
|
+
require 'rubotnik/autoloader'
|
9
|
+
require 'ui/fb_button_template'
|
10
|
+
require 'ui/fb_carousel'
|
11
|
+
require 'ui/image_attachment'
|
12
|
+
require 'ui/quick_replies'
|
13
|
+
require 'sinatra'
|
14
|
+
require 'facebook/messenger'
|
15
|
+
include Facebook::Messenger
|
16
|
+
|
17
|
+
module Rubotnik
|
18
|
+
def self.subscribe(token)
|
19
|
+
Facebook::Messenger::Subscriptions.subscribe(access_token: token)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.route(event, &block)
|
23
|
+
Bot.on(event, &block) unless [:message, :postback].include?(event)
|
24
|
+
Bot.on event do |e|
|
25
|
+
case e
|
26
|
+
when Facebook::Messenger::Incoming::Message
|
27
|
+
Rubotnik::MessageDispatch.new(e).route(&block)
|
28
|
+
when Facebook::Messenger::Incoming::Postback
|
29
|
+
Rubotnik::PostbackDispatch.new(e).route(&block)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.set_profile(*payloads)
|
35
|
+
payloads.each do |payload|
|
36
|
+
Facebook::Messenger::Profile.set(payload, access_token: ENV['ACCESS_TOKEN'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# rubocop:disable Metrics/MethodLength
|
2
|
+
|
3
|
+
module UI
|
4
|
+
########################### BUTTON TEMPLATE #############################
|
5
|
+
# https://developers.facebook.com/docs/messenger-platform/send-api-reference/button-template
|
6
|
+
class FBButtonTemplate
|
7
|
+
def initialize(text, buttons)
|
8
|
+
@template = {
|
9
|
+
recipient: {
|
10
|
+
id: nil
|
11
|
+
},
|
12
|
+
message: {
|
13
|
+
attachment: {
|
14
|
+
type: 'template',
|
15
|
+
payload: {
|
16
|
+
template_type: 'button',
|
17
|
+
text: text,
|
18
|
+
buttons: parse_buttons(buttons)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sends the valid JSON to Messenger API
|
26
|
+
def send(user)
|
27
|
+
formed = build(user)
|
28
|
+
Bot.deliver(formed, access_token: ENV['ACCESS_TOKEN'])
|
29
|
+
end
|
30
|
+
|
31
|
+
# Use this method to return a valid hash and save it for later
|
32
|
+
def build(user)
|
33
|
+
@template[:recipient][:id] = user.id
|
34
|
+
@template
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def parse_buttons(buttons)
|
40
|
+
return [] if buttons.nil? || buttons.empty?
|
41
|
+
buttons.map do |button|
|
42
|
+
button[:type] = button[:type].to_s
|
43
|
+
button
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# rubocop:disable Metrics/MethodLength
|
2
|
+
module UI
|
3
|
+
################## GENERIC TEMPLATE (aka CAROUSEL) #######################
|
4
|
+
# https://developers.facebook.com/docs/messenger-platform/send-api-reference/generic-template
|
5
|
+
class FBCarousel
|
6
|
+
def initialize(elements)
|
7
|
+
@template = {
|
8
|
+
recipient: { id: nil },
|
9
|
+
message: {
|
10
|
+
attachment: {
|
11
|
+
type: 'template',
|
12
|
+
payload: {
|
13
|
+
template_type: 'generic',
|
14
|
+
image_aspect_ratio: 'horizontal',
|
15
|
+
elements: parse_elements(elements)
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sends the valid JSON to Messenger API
|
23
|
+
def send(user)
|
24
|
+
template = build(user)
|
25
|
+
Bot.deliver(template, access_token: ENV['ACCESS_TOKEN'])
|
26
|
+
end
|
27
|
+
|
28
|
+
# Use this method to return a valid hash and save it for later
|
29
|
+
def build(user)
|
30
|
+
@template[:recipient][:id] = user.id
|
31
|
+
@template
|
32
|
+
end
|
33
|
+
|
34
|
+
# set image aspect ratio to 'square'
|
35
|
+
def square_images
|
36
|
+
@template[:message][:attachment][:payload][:image_aspect_ratio] = 'square'
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# set image aspect ratio to 'square'
|
41
|
+
def horizontal_images
|
42
|
+
hrz = 'horizontal'
|
43
|
+
@template[:message][:attachment][:payload][:image_aspect_ratio] = hrz
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def parse_elements(elements)
|
50
|
+
elements = [elements] if elements.class == Hash
|
51
|
+
elements.map do |elt|
|
52
|
+
elt[:buttons] = parse_buttons(elt[:buttons])
|
53
|
+
elt
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_buttons(buttons)
|
58
|
+
return [] if buttons.nil? || buttons.empty?
|
59
|
+
buttons.map do |button|
|
60
|
+
button[:type] = button[:type].to_s
|
61
|
+
button
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# rubocop:disable Metrics/MethodLength
|
2
|
+
module UI
|
3
|
+
# https://developers.facebook.com/docs/messenger-platform/send-api-reference/image-attachment
|
4
|
+
class ImageAttachment
|
5
|
+
def initialize(url)
|
6
|
+
@template = {
|
7
|
+
recipient: {
|
8
|
+
id: nil
|
9
|
+
},
|
10
|
+
message: {
|
11
|
+
attachment: {
|
12
|
+
type: 'image',
|
13
|
+
payload: {
|
14
|
+
url: url
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def send(user)
|
22
|
+
formed = build(user)
|
23
|
+
Bot.deliver(formed, access_token: ENV['ACCESS_TOKEN'])
|
24
|
+
end
|
25
|
+
|
26
|
+
def build(user)
|
27
|
+
@template[:recipient][:id] = user.id
|
28
|
+
@template
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module UI
|
2
|
+
# https://developers.facebook.com/docs/messenger-platform/send-api-reference/quick-replies
|
3
|
+
class QuickReplies
|
4
|
+
def self.build(*replies)
|
5
|
+
replies.map do |reply|
|
6
|
+
case reply
|
7
|
+
when Hash then build_from_hash(reply)
|
8
|
+
when Array then build_from_array(reply)
|
9
|
+
when String then build_from_string(reply)
|
10
|
+
else
|
11
|
+
raise ArgumentError, 'Arguments should be hashes or arrays of two'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.location
|
17
|
+
[{ content_type: 'location' }]
|
18
|
+
end
|
19
|
+
|
20
|
+
private_class_method def self.build_from_hash(reply)
|
21
|
+
unless reply.key?(:content_type)
|
22
|
+
reply[:content_type] = 'text'
|
23
|
+
error_msg = "type 'text' should have a payload"
|
24
|
+
raise ArgumentError, error_msg unless reply.key?(:payload)
|
25
|
+
end
|
26
|
+
reply
|
27
|
+
end
|
28
|
+
|
29
|
+
private_class_method def self.build_from_string(reply)
|
30
|
+
build_from_array([reply, reply.upcase])
|
31
|
+
end
|
32
|
+
|
33
|
+
private_class_method def self.build_from_array(reply)
|
34
|
+
error_msg = 'Only accepts arrays of two elements'
|
35
|
+
raise ArgumentError, error_msg if reply.length != 2
|
36
|
+
{ content_type: 'text', title: reply[0].to_s, payload: reply[1].to_s }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/rubotnik.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rubotnik/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rubotnik"
|
8
|
+
spec.version = Rubotnik::VERSION
|
9
|
+
spec.authors = ["Andy Barnov"]
|
10
|
+
spec.email = ["andy.barnov@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Ruby "bot-end" micro-framework for Facebook Messenger}
|
13
|
+
spec.homepage = "https://github.com/progapandist/rubotnik"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'facebook-messenger'
|
23
|
+
spec.add_dependency 'addressable'
|
24
|
+
spec.add_dependency 'dotenv'
|
25
|
+
spec.add_dependency 'httparty'
|
26
|
+
spec.add_dependency 'puma'
|
27
|
+
spec.add_dependency 'sinatra'
|
28
|
+
spec.add_dependency 'thor'
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
end
|
data/templates/.env
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.env
|
data/templates/Gemfile
ADDED
data/templates/Procfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
web: bundle exec puma -C config/puma.rb
|
data/templates/bot.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rubotnik'
|
2
|
+
# require_relative all files in "bot" folder or do it by hand
|
3
|
+
Rubotnik::Autoloader.load('bot')
|
4
|
+
|
5
|
+
# Subscribe your bot to a Facebook Page (put access and verify tokens in .env)
|
6
|
+
Rubotnik.subscribe(ENV['ACCESS_TOKEN'])
|
7
|
+
|
8
|
+
# Set welcome screen, "get started" button and a menu (all optional)
|
9
|
+
# Edit profile.rb before uncommenting the following lines:
|
10
|
+
|
11
|
+
# Rubotnik.set_profile(
|
12
|
+
# Profile::START_BUTTON, Profile::START_GREETING, Profile::SIMPLE_MENU
|
13
|
+
# )
|
14
|
+
|
15
|
+
# Generates a location prompt for quick_replies
|
16
|
+
LOCATION_PROMPT = UI::QuickReplies.location
|
17
|
+
|
18
|
+
####################### HANDLE INCOMING MESSAGES ##############################
|
19
|
+
|
20
|
+
Rubotnik.route :message do
|
21
|
+
# Will work for all variations of these three greetings
|
22
|
+
bind 'hi', 'hello', 'bonjour' do
|
23
|
+
say 'Hello from your new bot!'
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start a thread (and provide an opening message with optional quick replies).
|
27
|
+
# You have to define method named as a symbol inside a Command module
|
28
|
+
# and treat user's response to your "reply_with" message there.
|
29
|
+
# commands/commands.rb already has this start_conversation method
|
30
|
+
# defined as an example.
|
31
|
+
|
32
|
+
bind 'how', 'do', 'you', all: true, to: :start_conversation, reply_with: {
|
33
|
+
text: "I'm doing fine! You?",
|
34
|
+
quick_replies: [['Good!', 'OK'], ['Not so well', 'NOT_OK']]
|
35
|
+
# second item in nested array will be the contents of message.quick_reply,
|
36
|
+
# once the user makes a selection. Quick reply text in ALL CAPS will be
|
37
|
+
# used as default values of payloads if you pass strings instead of arrays
|
38
|
+
# (e.g. quick_replies: ['Yes', 'No'], payloads "YES" and "NO" are inferred)
|
39
|
+
}
|
40
|
+
|
41
|
+
# Use 'all' flag if you want to trigger a command only if all words
|
42
|
+
# are present in a message (will trigger with each of them by default)
|
43
|
+
bind 'what', 'my', 'name', all: true do
|
44
|
+
info = get_user_info(:first_name) # helper to get fields from Graph API
|
45
|
+
say info[:first_name]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Look for example of an API call with HTTParty in commands/location.rb
|
49
|
+
bind 'where', 'am', 'I', all: true, to: :lookup_location, reply_with: {
|
50
|
+
text: 'Let me know your location',
|
51
|
+
quick_replies: LOCATION_PROMPT
|
52
|
+
}
|
53
|
+
|
54
|
+
# Look for more UI examples in commands/ui_examples.rb
|
55
|
+
# Rubotnik currently supports Image, Button Template and Carousel
|
56
|
+
bind 'image', to: :show_image
|
57
|
+
|
58
|
+
# Invoked if none of the commands recognized. Has to come last, after all binds
|
59
|
+
default do
|
60
|
+
say "Sorry I did not get it"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
####################### HANDLE INCOMING POSTBACKS ##############################
|
65
|
+
|
66
|
+
Rubotnik.route :postback do
|
67
|
+
# postback from "Get Started" button
|
68
|
+
bind 'START' do
|
69
|
+
say "Welcome!"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
####################### HANDLE OTHER REQUESTS (NON-FB) #########################
|
74
|
+
|
75
|
+
get '/' do
|
76
|
+
'I can have a landing page too!'
|
77
|
+
end
|
78
|
+
|
79
|
+
############################ TEST ON LOCALHOST #################################
|
80
|
+
|
81
|
+
# 0. Have both Heroku CLI and ngrok
|
82
|
+
# 1. Set up "Messenger" app on Facebook for Developers, fill in .env
|
83
|
+
# 2. Run 'heroku local' from console, it will load Puma on port 5000
|
84
|
+
# 3. Expose port 5000 to the Internet with 'ngrok http 5000'
|
85
|
+
# 4. Provide your ngrok http_s_(!) address in Facebook Developer Dashboard
|
86
|
+
# for webhook validation.
|
87
|
+
# 5. Open Messenger and talk to your bot!
|
88
|
+
|
89
|
+
# P.S. While you have DEBUG environment variable set to "true" (default in .env)
|
90
|
+
# All StandardError exceptions will go to the message dialog instead of
|
91
|
+
# breaking the server.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Commands
|
2
|
+
# You can write all your commands as methods here
|
3
|
+
|
4
|
+
# If the command is bound with reply_with specified,
|
5
|
+
# you have to deal with user response to the last message and react on it.
|
6
|
+
def start_conversation
|
7
|
+
# Quick replies are accessible through message object's quick_reply property,
|
8
|
+
# by default it's the quick reply text in ALL CAPS
|
9
|
+
# you can also react on the text itself
|
10
|
+
message.typing_on
|
11
|
+
case message.quick_reply
|
12
|
+
when 'OK'
|
13
|
+
say "Glad you're doing well!"
|
14
|
+
stop_thread
|
15
|
+
when 'NOT_OK'
|
16
|
+
say "Too bad. What happened?"
|
17
|
+
next_command :appear_nice
|
18
|
+
else
|
19
|
+
say "🤖"
|
20
|
+
# it's always a good idea to have an else, quick replies don't
|
21
|
+
# prevent user from typing any message in the dialogue
|
22
|
+
stop_thread
|
23
|
+
end
|
24
|
+
message.typing_off
|
25
|
+
end
|
26
|
+
|
27
|
+
def appear_nice
|
28
|
+
message.typing_on
|
29
|
+
case message.text
|
30
|
+
when /job/i then say "We've all been there"
|
31
|
+
when /family/i then say "That's just life"
|
32
|
+
else
|
33
|
+
say "It shall pass"
|
34
|
+
end
|
35
|
+
message.typing_off
|
36
|
+
stop_thread # future messages from user will be handled from top-level bindings
|
37
|
+
end
|
38
|
+
end
|
data/templates/config.ru
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# require 'dotenv/load' # Use this if you want plain 'rackup' over 'heroku local'
|
2
|
+
require 'facebook/messenger'
|
3
|
+
require 'sinatra'
|
4
|
+
|
5
|
+
require_relative './bot/bot.rb'
|
6
|
+
|
7
|
+
map('/webhook') do
|
8
|
+
run Facebook::Messenger::Server
|
9
|
+
end
|
10
|
+
|
11
|
+
# run regular Sinatra too
|
12
|
+
run Sinatra::Application
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Commands
|
2
|
+
API_URL = 'https://maps.googleapis.com/maps/api/geocode/json?latlng='.freeze
|
3
|
+
|
4
|
+
# Lookup based on location data from user's device
|
5
|
+
def lookup_location
|
6
|
+
if message_contains_location?
|
7
|
+
handle_user_location
|
8
|
+
else
|
9
|
+
say("Please try your request again and use 'Send location' button")
|
10
|
+
end
|
11
|
+
stop_thread
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_user_location
|
15
|
+
coords = message.attachments.first['payload']['coordinates']
|
16
|
+
lat = coords['lat']
|
17
|
+
long = coords['long']
|
18
|
+
message.typing_on
|
19
|
+
parsed = get_parsed_response(API_URL, "#{lat},#{long}")
|
20
|
+
address = extract_full_address(parsed)
|
21
|
+
say "Coordinates of your location: Latitude #{lat}, Longitude #{long}. " \
|
22
|
+
"Looks like you're at #{address}"
|
23
|
+
message.typing_off
|
24
|
+
end
|
25
|
+
|
26
|
+
# Talk to API
|
27
|
+
def get_parsed_response(url, query)
|
28
|
+
response = HTTParty.get(url + query)
|
29
|
+
parsed = JSON.parse(response.body)
|
30
|
+
parsed['status'] != 'ZERO_RESULTS' ? parsed : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract_full_address(parsed)
|
34
|
+
parsed['results'].first['formatted_address']
|
35
|
+
end
|
36
|
+
end
|