xip-facebook 0.22.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +108 -0
- data/LICENSE +7 -0
- data/README.md +498 -0
- data/VERSION +1 -0
- data/fb-config.png +0 -0
- data/lib/xip-facebook.rb +1 -0
- data/lib/xip/facebook.rb +2 -0
- data/lib/xip/services/facebook/client.rb +133 -0
- data/lib/xip/services/facebook/events/message_event.rb +74 -0
- data/lib/xip/services/facebook/events/message_reads_event.rb +46 -0
- data/lib/xip/services/facebook/events/messaging_referral_event.rb +30 -0
- data/lib/xip/services/facebook/events/postback_event.rb +35 -0
- data/lib/xip/services/facebook/message_handler.rb +104 -0
- data/lib/xip/services/facebook/reply_handler.rb +502 -0
- data/lib/xip/services/facebook/setup.rb +24 -0
- data/lib/xip/services/facebook/version.rb +15 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/version_spec.rb +16 -0
- data/xip-facebook.gemspec +25 -0
- metadata +120 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.22.0
|
data/fb-config.png
ADDED
Binary file
|
data/lib/xip-facebook.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'xip/facebook'
|
data/lib/xip/facebook.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
|
5
|
+
require 'xip/services/facebook/message_handler'
|
6
|
+
require 'xip/services/facebook/reply_handler'
|
7
|
+
require 'xip/services/facebook/setup'
|
8
|
+
|
9
|
+
module Xip
|
10
|
+
module Services
|
11
|
+
module Facebook
|
12
|
+
class Client < Xip::Services::BaseClient
|
13
|
+
|
14
|
+
FB_ENDPOINT = if ENV['FACEBOOK_API_VERSION'].present?
|
15
|
+
"https://graph.facebook.com/v#{ENV['FACEBOOK_API_VERSION']}/me"
|
16
|
+
else
|
17
|
+
"https://graph.facebook.com/v3.2/me"
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :api_endpoint, :reply
|
21
|
+
|
22
|
+
def initialize(reply:, endpoint: 'messages')
|
23
|
+
@reply = reply
|
24
|
+
access_token = "access_token=#{Xip.config.facebook.page_access_token}"
|
25
|
+
@api_endpoint = [[FB_ENDPOINT, endpoint].join('/'), access_token].join('?')
|
26
|
+
end
|
27
|
+
|
28
|
+
def transmit
|
29
|
+
res = self
|
30
|
+
.class
|
31
|
+
.http_client
|
32
|
+
.post(api_endpoint, body: MultiJson.dump(reply))
|
33
|
+
|
34
|
+
if res.status.client_error? # HTTP 4xx error
|
35
|
+
# Messenger error sub-codes (https://developers.facebook.com/docs/messenger-platform/reference/send-api/error-codes)
|
36
|
+
case res.body
|
37
|
+
when /1545041/
|
38
|
+
raise Xip::Errors::UserOptOut
|
39
|
+
when /2018108/
|
40
|
+
raise Xip::Errors::UserOptOut
|
41
|
+
when /2018028/
|
42
|
+
raise Xip::Errors::InvalidSessionID.new('Cannot message users who are not admins, developers or testers of the app until pages_messaging permission is reviewed and the app is live.')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
Xip::Logger.l(
|
47
|
+
topic: "facebook",
|
48
|
+
message: "Transmitted. Response: #{res.status.code}: #{res.body}"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.http_client
|
53
|
+
headers = {
|
54
|
+
'Content-Type' => 'application/json'
|
55
|
+
}
|
56
|
+
HTTP.timeout(connect: 15, read: 60).headers(headers)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.fetch_profile(recipient_id:, fields: nil)
|
60
|
+
if fields.blank?
|
61
|
+
fields = [:id, :name, :first_name, :last_name, :profile_pic]
|
62
|
+
end
|
63
|
+
|
64
|
+
query_hash = {
|
65
|
+
fields: fields.join(','),
|
66
|
+
access_token: Xip.config.facebook.page_access_token
|
67
|
+
}
|
68
|
+
|
69
|
+
uri = URI::HTTPS.build(
|
70
|
+
host: "graph.facebook.com",
|
71
|
+
path: "/#{recipient_id}",
|
72
|
+
query: query_hash.to_query
|
73
|
+
)
|
74
|
+
|
75
|
+
res = http_client.get(uri.to_s)
|
76
|
+
Xip::Logger.l(topic:
|
77
|
+
'facebook',
|
78
|
+
message: "Requested user profile for #{recipient_id}. Response: #{res.status.code}: #{res.body}"
|
79
|
+
)
|
80
|
+
|
81
|
+
if res.status.success?
|
82
|
+
MultiJson.load(res.body.to_s)
|
83
|
+
else
|
84
|
+
raise(
|
85
|
+
Xip::Errors::ServiceError,
|
86
|
+
"Facebook error #{res.status}: #{res.body}"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.track(recipient_id:, metric:, value:, options: {})
|
92
|
+
metric_values = [{
|
93
|
+
'_eventName' => metric,
|
94
|
+
'_valueToSum' => value
|
95
|
+
}]
|
96
|
+
|
97
|
+
metric_values.first.merge!(options)
|
98
|
+
|
99
|
+
params = {
|
100
|
+
event: 'CUSTOM_APP_EVENTS',
|
101
|
+
custom_events: MultiJson.dump(metric_values),
|
102
|
+
advertiser_tracking_enabled: 1,
|
103
|
+
application_tracking_enabled: 1,
|
104
|
+
extinfo: MultiJson.dump(['mb1']),
|
105
|
+
page_scoped_user_id: recipient_id,
|
106
|
+
page_id: Xip.config.facebook.page_id
|
107
|
+
}
|
108
|
+
|
109
|
+
uri = URI::HTTPS.build(
|
110
|
+
host: "graph.facebook.com",
|
111
|
+
path: "/#{Xip.config.facebook.app_id}/activities"
|
112
|
+
)
|
113
|
+
|
114
|
+
res = http_client.post(uri.to_s, body: MultiJson.dump(params))
|
115
|
+
Xip::Logger.l(
|
116
|
+
topic: "facebook",
|
117
|
+
message: "Sent custom event for metric: #{metric} and value: #{value}. Response: #{res.status}: #{res.body}"
|
118
|
+
)
|
119
|
+
|
120
|
+
if res.status.success?
|
121
|
+
MultiJson.load(res.body.to_s)
|
122
|
+
else
|
123
|
+
raise(
|
124
|
+
Xip::Errors::ServiceError,
|
125
|
+
"Facebook error #{res.status}: #{res.body}"
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module Services
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
class MessageEvent
|
8
|
+
|
9
|
+
attr_reader :service_message, :params
|
10
|
+
|
11
|
+
def initialize(service_message:, params:)
|
12
|
+
@service_message = service_message
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def process
|
17
|
+
fetch_message
|
18
|
+
fetch_location
|
19
|
+
fetch_attachments
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fetch_message
|
25
|
+
if params.dig('message', 'quick_reply').present?
|
26
|
+
service_message.message = params.dig('message', 'text')
|
27
|
+
service_message.payload = params.dig('message', 'quick_reply', 'payload')
|
28
|
+
elsif params.dig('message', 'text').present?
|
29
|
+
service_message.message = params.dig('message', 'text')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_location
|
34
|
+
if params.dig('message', 'attachments').present? && params.dig('message', 'attachments').is_a?(Array)
|
35
|
+
params.dig('message', 'attachments').each do |attachment|
|
36
|
+
next unless attachment['type'] == 'location'
|
37
|
+
|
38
|
+
lat = attachment.dig('payload', 'coordinates', 'lat')
|
39
|
+
lng = attachment.dig('payload', 'coordinates', 'long')
|
40
|
+
|
41
|
+
service_message.location = {
|
42
|
+
lat: lat,
|
43
|
+
lng: lng
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def fetch_attachments
|
50
|
+
if params.dig('message', 'attachments').present? && params.dig('message', 'attachments').is_a?(Array)
|
51
|
+
params.dig('message', 'attachments').each do |attachment|
|
52
|
+
# Seems to be a bug in Messenger, but in attachments of type `fallback`
|
53
|
+
# we are seeing the URL come in at the attachment-level rather than
|
54
|
+
# nested within the payload as the API specifies:
|
55
|
+
# https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messages
|
56
|
+
payload_url = if attachment.dig('payload', 'url').present?
|
57
|
+
attachment['payload']['url']
|
58
|
+
else
|
59
|
+
attachment['url']
|
60
|
+
end
|
61
|
+
|
62
|
+
service_message.attachments << {
|
63
|
+
type: attachment['type'],
|
64
|
+
url: payload_url
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module Services
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
class MessageReadsEvent
|
8
|
+
|
9
|
+
attr_reader :service_message, :params
|
10
|
+
|
11
|
+
def initialize(service_message:, params:)
|
12
|
+
@service_message = service_message
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def process
|
17
|
+
fetch_read
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def fetch_read
|
23
|
+
service_message.read = { watermark: get_timestamp, seq: params['read']['seq'] }
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_timestamp
|
27
|
+
Time.at(params['read']['watermark']/1000).to_datetime
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ReadsEvent
|
32
|
+
attr_accessor :read
|
33
|
+
def initialize(service:)
|
34
|
+
@read = {}
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class ServiceMessage
|
44
|
+
prepend Services::Facebook::ReadsEvent
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module Services
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
class MessagingReferralEvent
|
8
|
+
|
9
|
+
attr_reader :service_message, :params
|
10
|
+
|
11
|
+
def initialize(service_message:, params:)
|
12
|
+
@service_message = service_message
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def process
|
17
|
+
fetch_referral
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def fetch_referral
|
23
|
+
service_message.referral = params['referral']
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module Services
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
class PostbackEvent
|
8
|
+
|
9
|
+
attr_reader :service_message, :params
|
10
|
+
|
11
|
+
def initialize(service_message:, params:)
|
12
|
+
@service_message = service_message
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def process
|
17
|
+
fetch_payload
|
18
|
+
fetch_referral
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fetch_payload
|
24
|
+
service_message.payload = params.dig('postback', 'payload')
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_referral
|
28
|
+
service_message.referral = params.dig('postback', 'referral')
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xip/services/facebook/events/message_event'
|
4
|
+
require 'xip/services/facebook/events/postback_event'
|
5
|
+
require 'xip/services/facebook/events/message_reads_event'
|
6
|
+
require 'xip/services/facebook/events/messaging_referral_event'
|
7
|
+
|
8
|
+
module Xip
|
9
|
+
module Services
|
10
|
+
module Facebook
|
11
|
+
|
12
|
+
class MessageHandler < Xip::Services::BaseMessageHandler
|
13
|
+
|
14
|
+
attr_reader :service_message, :params, :headers, :facebook_message
|
15
|
+
|
16
|
+
def initialize(params:, headers:)
|
17
|
+
@params = params
|
18
|
+
@headers = headers
|
19
|
+
end
|
20
|
+
|
21
|
+
def coordinate
|
22
|
+
if facebook_is_validating_webhook?
|
23
|
+
respond_with_validation
|
24
|
+
else
|
25
|
+
# Queue the request processing so we can respond quickly to FB
|
26
|
+
# and also keep track of this message
|
27
|
+
Xip::Services::HandleMessageJob.perform_async(
|
28
|
+
'facebook',
|
29
|
+
params,
|
30
|
+
headers
|
31
|
+
)
|
32
|
+
|
33
|
+
# Relay our acceptance
|
34
|
+
[200, 'OK']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process
|
39
|
+
@service_message = ServiceMessage.new(service: 'facebook')
|
40
|
+
@facebook_message = params['entry'].first['messaging'].first
|
41
|
+
service_message.sender_id = get_sender_id
|
42
|
+
service_message.target_id = get_target_id
|
43
|
+
service_message.timestamp = get_timestamp
|
44
|
+
process_facebook_event
|
45
|
+
|
46
|
+
service_message
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def facebook_is_validating_webhook?
|
52
|
+
params['hub.verify_token'].present?
|
53
|
+
end
|
54
|
+
|
55
|
+
def respond_with_validation
|
56
|
+
if params['hub.verify_token'] == Xip.config.facebook.verify_token
|
57
|
+
[200, params['hub.challenge']]
|
58
|
+
else
|
59
|
+
[401, "Verify token did not match environment variable."]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_sender_id
|
64
|
+
facebook_message['sender']['id']
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_target_id
|
68
|
+
facebook_message['recipient']['id']
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_timestamp
|
72
|
+
Time.at(facebook_message['timestamp']/1000).to_datetime
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_facebook_event
|
76
|
+
if facebook_message['message'].present?
|
77
|
+
message_event = Xip::Services::Facebook::MessageEvent.new(
|
78
|
+
service_message: service_message,
|
79
|
+
params: facebook_message
|
80
|
+
)
|
81
|
+
elsif facebook_message['postback'].present?
|
82
|
+
message_event = Xip::Services::Facebook::PostbackEvent.new(
|
83
|
+
service_message: service_message,
|
84
|
+
params: facebook_message
|
85
|
+
)
|
86
|
+
elsif facebook_message['read'].present?
|
87
|
+
message_event = Xip::Services::Facebook::MessageReadsEvent.new(
|
88
|
+
service_message: service_message,
|
89
|
+
params: facebook_message
|
90
|
+
)
|
91
|
+
elsif facebook_message['referral'].present?
|
92
|
+
message_event = Xip::Services::Facebook::MessagingReferralEvent.new(
|
93
|
+
service_message: service_message,
|
94
|
+
params: facebook_message
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
message_event.process
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,502 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xip
|
4
|
+
module Services
|
5
|
+
module Facebook
|
6
|
+
|
7
|
+
class ReplyHandler < Xip::Services::BaseReplyHandler
|
8
|
+
|
9
|
+
attr_reader :recipient_id, :reply
|
10
|
+
|
11
|
+
def initialize(recipient_id: nil, reply: nil)
|
12
|
+
@recipient_id = recipient_id
|
13
|
+
@reply = reply
|
14
|
+
end
|
15
|
+
|
16
|
+
def text
|
17
|
+
check_if_arguments_are_valid!(
|
18
|
+
suggestions: reply['suggestions'],
|
19
|
+
buttons: reply['buttons']
|
20
|
+
)
|
21
|
+
|
22
|
+
template = unstructured_template
|
23
|
+
template['message']['text'] = reply['text']
|
24
|
+
|
25
|
+
if reply['suggestions'].present?
|
26
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
27
|
+
template["message"]["quick_replies"] = fb_suggestions
|
28
|
+
end
|
29
|
+
|
30
|
+
# If buttons are present, we need to convert this to a button template
|
31
|
+
if reply['buttons'].present?
|
32
|
+
template['message'].delete('text')
|
33
|
+
|
34
|
+
fb_buttons = generate_buttons(buttons: reply['buttons'])
|
35
|
+
attachment = button_attachment_template(text: reply['text'], buttons: fb_buttons)
|
36
|
+
template['message']['attachment'] = attachment
|
37
|
+
end
|
38
|
+
|
39
|
+
template
|
40
|
+
end
|
41
|
+
|
42
|
+
def image
|
43
|
+
check_if_arguments_are_valid!(
|
44
|
+
suggestions: reply['suggestions'],
|
45
|
+
buttons: reply['buttons']
|
46
|
+
)
|
47
|
+
|
48
|
+
template = unstructured_template
|
49
|
+
attachment = attachment_template(
|
50
|
+
attachment_type: 'image',
|
51
|
+
attachment_url: reply['image_url']
|
52
|
+
)
|
53
|
+
template['message']['attachment'] = attachment
|
54
|
+
|
55
|
+
if reply['suggestions'].present?
|
56
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
57
|
+
template["message"]["quick_replies"] = fb_suggestions
|
58
|
+
end
|
59
|
+
|
60
|
+
template
|
61
|
+
end
|
62
|
+
|
63
|
+
def audio
|
64
|
+
check_if_arguments_are_valid!(
|
65
|
+
suggestions: reply['suggestions'],
|
66
|
+
buttons: reply['buttons']
|
67
|
+
)
|
68
|
+
|
69
|
+
template = unstructured_template
|
70
|
+
attachment = attachment_template(
|
71
|
+
attachment_type: 'audio',
|
72
|
+
attachment_url: reply['audio_url']
|
73
|
+
)
|
74
|
+
template['message']['attachment'] = attachment
|
75
|
+
|
76
|
+
if reply['suggestions'].present?
|
77
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
78
|
+
template["message"]["quick_replies"] = fb_suggestions
|
79
|
+
end
|
80
|
+
|
81
|
+
template
|
82
|
+
end
|
83
|
+
|
84
|
+
def video
|
85
|
+
check_if_arguments_are_valid!(
|
86
|
+
suggestions: reply['suggestions'],
|
87
|
+
buttons: reply['buttons']
|
88
|
+
)
|
89
|
+
|
90
|
+
template = unstructured_template
|
91
|
+
attachment = attachment_template(
|
92
|
+
attachment_type: 'video',
|
93
|
+
attachment_url: reply['video_url']
|
94
|
+
)
|
95
|
+
template['message']['attachment'] = attachment
|
96
|
+
|
97
|
+
if reply['suggestions'].present?
|
98
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
99
|
+
template["message"]["quick_replies"] = fb_suggestions
|
100
|
+
end
|
101
|
+
|
102
|
+
template
|
103
|
+
end
|
104
|
+
|
105
|
+
def file
|
106
|
+
check_if_arguments_are_valid!(
|
107
|
+
suggestions: reply['suggestions'],
|
108
|
+
buttons: reply['buttons']
|
109
|
+
)
|
110
|
+
|
111
|
+
template = unstructured_template
|
112
|
+
attachment = attachment_template(
|
113
|
+
attachment_type: 'file',
|
114
|
+
attachment_url: reply['file_url']
|
115
|
+
)
|
116
|
+
template['message']['attachment'] = attachment
|
117
|
+
|
118
|
+
if reply['suggestions'].present?
|
119
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
120
|
+
template["message"]["quick_replies"] = fb_suggestions
|
121
|
+
end
|
122
|
+
|
123
|
+
template
|
124
|
+
end
|
125
|
+
|
126
|
+
def cards
|
127
|
+
template = card_template(
|
128
|
+
sharable: reply["sharable"],
|
129
|
+
aspect_ratio: reply["aspect_ratio"]
|
130
|
+
)
|
131
|
+
|
132
|
+
fb_elements = generate_card_elements(elements: reply["elements"])
|
133
|
+
template["message"]["attachment"]["payload"]["elements"] = fb_elements
|
134
|
+
|
135
|
+
if reply['suggestions'].present?
|
136
|
+
fb_suggestions = generate_suggestions(suggestions: reply['suggestions'])
|
137
|
+
template["message"]["quick_replies"] = fb_suggestions
|
138
|
+
end
|
139
|
+
|
140
|
+
template
|
141
|
+
end
|
142
|
+
|
143
|
+
def list
|
144
|
+
template = list_template(
|
145
|
+
top_element_style: reply["top_element_style"]
|
146
|
+
)
|
147
|
+
|
148
|
+
fb_elements = generate_list_elements(elements: reply["elements"])
|
149
|
+
template["message"]["attachment"]["payload"]["elements"] = fb_elements
|
150
|
+
|
151
|
+
if reply["buttons"].present?
|
152
|
+
if reply["buttons"].size > 1
|
153
|
+
raise(ArgumentError, "Facebook lists support a single button attached to the list itsef.")
|
154
|
+
end
|
155
|
+
|
156
|
+
template["message"]["attachment"]["payload"]["buttons"] = generate_buttons(buttons: reply["buttons"])
|
157
|
+
end
|
158
|
+
|
159
|
+
template
|
160
|
+
end
|
161
|
+
|
162
|
+
def mark_seen
|
163
|
+
sender_action_template(action: 'mark_seen')
|
164
|
+
end
|
165
|
+
|
166
|
+
def enable_typing_indicator
|
167
|
+
sender_action_template(action: 'typing_on')
|
168
|
+
end
|
169
|
+
|
170
|
+
def disable_typing_indicator
|
171
|
+
sender_action_template(action: 'typing_off')
|
172
|
+
end
|
173
|
+
|
174
|
+
def delay
|
175
|
+
enable_typing_indicator
|
176
|
+
end
|
177
|
+
|
178
|
+
# generates property/value pairs required to set the profile
|
179
|
+
def messenger_profile
|
180
|
+
unless Xip.config.facebook.setup.present?
|
181
|
+
raise Xip::Errors::ConfigurationError, "Setup for Facebook is not specified in services.yml."
|
182
|
+
end
|
183
|
+
|
184
|
+
profile = {}
|
185
|
+
Xip.config.facebook.setup.each do |profile_option, _|
|
186
|
+
profile[profile_option] = self.send(profile_option)
|
187
|
+
end
|
188
|
+
|
189
|
+
profile
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def unstructured_template
|
195
|
+
{
|
196
|
+
"recipient" => {
|
197
|
+
"id" => recipient_id
|
198
|
+
},
|
199
|
+
"message" => { }
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def card_template(sharable: nil, aspect_ratio: nil)
|
204
|
+
template = {
|
205
|
+
"recipient" => {
|
206
|
+
"id" => recipient_id
|
207
|
+
},
|
208
|
+
"message" => {
|
209
|
+
"attachment" => {
|
210
|
+
"type" => "template",
|
211
|
+
"payload" => {
|
212
|
+
"template_type" => "generic",
|
213
|
+
"elements" => []
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
if sharable.present?
|
220
|
+
template["message"]["payload"]["sharable"] = sharable
|
221
|
+
end
|
222
|
+
|
223
|
+
if aspect_ratio.present?
|
224
|
+
template["message"]["payload"]["image_aspect_ratio"] = aspect_ratio
|
225
|
+
end
|
226
|
+
|
227
|
+
template
|
228
|
+
end
|
229
|
+
|
230
|
+
def list_template(top_element_style: nil, buttons: [])
|
231
|
+
template = {
|
232
|
+
"recipient" => {
|
233
|
+
"id" => recipient_id
|
234
|
+
},
|
235
|
+
"message" => {
|
236
|
+
"attachment" => {
|
237
|
+
"type" => "template",
|
238
|
+
"payload" => {
|
239
|
+
"template_type" => "list",
|
240
|
+
"elements" => []
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}
|
244
|
+
}
|
245
|
+
|
246
|
+
if top_element_style.present?
|
247
|
+
unless ['large', 'compact'].include?(top_element_style)
|
248
|
+
raise(ArgumentError, "Facebook list replies only support 'large' or 'compact' as the top_element_style.")
|
249
|
+
end
|
250
|
+
|
251
|
+
template["message"]['attachment']["payload"]["top_element_style"] = top_element_style
|
252
|
+
end
|
253
|
+
|
254
|
+
if buttons.present?
|
255
|
+
unless buttons.size > 1
|
256
|
+
raise(ArgumentError, "Facebook lists only support a single button in the top element.")
|
257
|
+
end
|
258
|
+
|
259
|
+
template["message"]["payload"]["buttons"] = aspect_ratio
|
260
|
+
end
|
261
|
+
|
262
|
+
template
|
263
|
+
end
|
264
|
+
|
265
|
+
def element_template(element_type:, element:)
|
266
|
+
unless element["title"].present?
|
267
|
+
raise(ArgumentError, "Facebook card and list elements must have a 'title' attribute.")
|
268
|
+
end
|
269
|
+
|
270
|
+
template = {
|
271
|
+
"title" => element["title"]
|
272
|
+
}
|
273
|
+
|
274
|
+
if element["subtitle"].present?
|
275
|
+
template["subtitle"] = element["subtitle"]
|
276
|
+
end
|
277
|
+
|
278
|
+
if element["image_url"].present?
|
279
|
+
template["image_url"] = element["image_url"]
|
280
|
+
end
|
281
|
+
|
282
|
+
if element["default_action"].present?
|
283
|
+
default_action = generate_default_action(action_params: element["default_action"].first)
|
284
|
+
template["default_action"] = default_action
|
285
|
+
end
|
286
|
+
|
287
|
+
if element["buttons"].present?
|
288
|
+
if element_type == 'card' && element["buttons"].size > 3
|
289
|
+
raise(ArgumentError, "Facebook card elements only support 3 buttons.")
|
290
|
+
end
|
291
|
+
|
292
|
+
if element_type == 'list' && element["buttons"].size > 1
|
293
|
+
raise(ArgumentError, "Facebook list elements only support 1 button.")
|
294
|
+
end
|
295
|
+
|
296
|
+
fb_buttons = generate_buttons(buttons: element["buttons"])
|
297
|
+
template["buttons"] = fb_buttons
|
298
|
+
end
|
299
|
+
|
300
|
+
template
|
301
|
+
end
|
302
|
+
|
303
|
+
def attachment_template(attachment_type:, attachment_url:)
|
304
|
+
{
|
305
|
+
"type" => attachment_type,
|
306
|
+
"payload" => {
|
307
|
+
"url" => attachment_url
|
308
|
+
}
|
309
|
+
}
|
310
|
+
end
|
311
|
+
|
312
|
+
def button_attachment_template(text:, buttons:)
|
313
|
+
{
|
314
|
+
"type" => "template",
|
315
|
+
"payload" => {
|
316
|
+
"template_type" => "button",
|
317
|
+
"text" => text,
|
318
|
+
"buttons" => buttons
|
319
|
+
}
|
320
|
+
}
|
321
|
+
end
|
322
|
+
|
323
|
+
def sender_action_template(action:)
|
324
|
+
{
|
325
|
+
"recipient" => {
|
326
|
+
"id" => recipient_id
|
327
|
+
},
|
328
|
+
"sender_action" => action
|
329
|
+
}
|
330
|
+
end
|
331
|
+
|
332
|
+
def generate_card_elements(elements:)
|
333
|
+
if elements.size > 10
|
334
|
+
raise(ArgumentError, "Facebook cards can have at most 10 cards.")
|
335
|
+
end
|
336
|
+
|
337
|
+
fb_elements = elements.collect do |element|
|
338
|
+
element_template(element_type: 'card', element: element)
|
339
|
+
end
|
340
|
+
|
341
|
+
fb_elements
|
342
|
+
end
|
343
|
+
|
344
|
+
def generate_list_elements(elements:)
|
345
|
+
if elements.size < 2 || elements.size > 4
|
346
|
+
raise(ArgumentError, "Facebook lists must have 2-4 elements.")
|
347
|
+
end
|
348
|
+
|
349
|
+
fb_elements = elements.collect do |element|
|
350
|
+
element_template(element_type: 'list', element: element)
|
351
|
+
end
|
352
|
+
|
353
|
+
fb_elements
|
354
|
+
end
|
355
|
+
|
356
|
+
def generate_suggestions(suggestions:)
|
357
|
+
quick_replies = suggestions.collect do |suggestion|
|
358
|
+
case suggestion["type"]
|
359
|
+
when 'location'
|
360
|
+
quick_reply = { "content_type" => "location" }
|
361
|
+
when 'phone'
|
362
|
+
quick_reply = { "content_type" => "user_phone_number" }
|
363
|
+
when 'email'
|
364
|
+
quick_reply = { "content_type" => "user_email" }
|
365
|
+
when 'birthday'
|
366
|
+
quick_reply = { "content_type" => "user_birthday" }
|
367
|
+
when 'state'
|
368
|
+
quick_reply = { "content_type" => "user_state" }
|
369
|
+
when 'zip_code'
|
370
|
+
quick_reply = { "content_type" => "user_zip_code" }
|
371
|
+
else
|
372
|
+
quick_reply = {
|
373
|
+
"content_type" => "text",
|
374
|
+
"title" => suggestion["text"]
|
375
|
+
}
|
376
|
+
|
377
|
+
if suggestion["payload"].present?
|
378
|
+
quick_reply["payload"] = suggestion["payload"]
|
379
|
+
else
|
380
|
+
quick_reply["payload"] = suggestion["text"]
|
381
|
+
end
|
382
|
+
|
383
|
+
if suggestion["image_url"].present?
|
384
|
+
quick_reply["image_url"] = suggestion["image_url"]
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
quick_reply
|
389
|
+
end
|
390
|
+
|
391
|
+
quick_replies
|
392
|
+
end
|
393
|
+
|
394
|
+
# Requires adding support for Buy, Log In, Log Out, and Share button types
|
395
|
+
def generate_buttons(buttons:)
|
396
|
+
fb_buttons = buttons.collect do |button|
|
397
|
+
case button['type']
|
398
|
+
when 'url'
|
399
|
+
_button = {
|
400
|
+
"type" => "web_url",
|
401
|
+
"url" => button["url"],
|
402
|
+
"title" => button["text"]
|
403
|
+
}
|
404
|
+
|
405
|
+
if button["webview_height"].present?
|
406
|
+
_button["webview_height_ratio"] = button["webview_height"]
|
407
|
+
end
|
408
|
+
|
409
|
+
if button['messenger_extensions'].present?
|
410
|
+
_button['messenger_extensions'] = true
|
411
|
+
end
|
412
|
+
|
413
|
+
_button
|
414
|
+
|
415
|
+
when 'payload'
|
416
|
+
_button = {
|
417
|
+
"type" => "postback",
|
418
|
+
"payload" => button["payload"],
|
419
|
+
"title" => button["text"]
|
420
|
+
}
|
421
|
+
|
422
|
+
when 'call'
|
423
|
+
_button = {
|
424
|
+
"type" => "phone_number",
|
425
|
+
"payload" => button["phone_number"],
|
426
|
+
"title" => button["text"]
|
427
|
+
}
|
428
|
+
|
429
|
+
when 'login'
|
430
|
+
_button = {
|
431
|
+
"type" => "account_link",
|
432
|
+
"url" => button["url"]
|
433
|
+
}
|
434
|
+
|
435
|
+
when 'logout'
|
436
|
+
_button = {
|
437
|
+
"type" => "account_unlink"
|
438
|
+
}
|
439
|
+
|
440
|
+
when 'nested'
|
441
|
+
_button = {
|
442
|
+
"type" => "nested",
|
443
|
+
"title" => button["text"],
|
444
|
+
"call_to_actions" => generate_buttons(buttons: button["buttons"])
|
445
|
+
}
|
446
|
+
|
447
|
+
else
|
448
|
+
raise(Xip::Errors::ServiceImpaired, "Sorry, we don't yet support #{button["type"]} buttons yet!")
|
449
|
+
end
|
450
|
+
|
451
|
+
_button
|
452
|
+
end
|
453
|
+
|
454
|
+
fb_buttons
|
455
|
+
end
|
456
|
+
|
457
|
+
def generate_default_action(action_params:)
|
458
|
+
default_action = {
|
459
|
+
"type" => "web_url",
|
460
|
+
"url" => action_params["url"]
|
461
|
+
}
|
462
|
+
|
463
|
+
if action_params["webview_height"].present?
|
464
|
+
action_params["webview_height_ratio"] = action_params["webview_height"]
|
465
|
+
end
|
466
|
+
|
467
|
+
default_action
|
468
|
+
end
|
469
|
+
|
470
|
+
def check_if_arguments_are_valid!(suggestions:, buttons:)
|
471
|
+
if suggestions.present? && buttons.present?
|
472
|
+
raise(ArgumentError, "A reply cannot have buttons and suggestions!")
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def greeting
|
477
|
+
Xip.config.facebook.setup.greeting.map do |greeting|
|
478
|
+
{
|
479
|
+
"locale" => greeting["locale"],
|
480
|
+
"text" => greeting["text"]
|
481
|
+
}
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def persistent_menu
|
486
|
+
Xip.config.facebook.setup.persistent_menu.map do |persistent_menu|
|
487
|
+
{
|
488
|
+
"locale" => persistent_menu['locale'],
|
489
|
+
"composer_input_disabled" => (persistent_menu['composer_input_disabled'] || false),
|
490
|
+
"call_to_actions" => generate_buttons(buttons: persistent_menu['call_to_actions'])
|
491
|
+
}
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
def get_started
|
496
|
+
Xip.config.facebook.setup.get_started
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|