xip-facebook 0.22.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 +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
|