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