flow_chat 0.3.0 → 0.4.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 +4 -4
- data/Gemfile +1 -0
- data/README.md +642 -86
- data/examples/initializer.rb +31 -0
- data/examples/media_prompts_examples.rb +28 -0
- data/examples/multi_tenant_whatsapp_controller.rb +244 -0
- data/examples/ussd_controller.rb +264 -0
- data/examples/whatsapp_controller.rb +140 -0
- data/examples/whatsapp_media_examples.rb +406 -0
- data/examples/whatsapp_message_job.rb +111 -0
- data/lib/flow_chat/base_processor.rb +67 -0
- data/lib/flow_chat/config.rb +36 -0
- data/lib/flow_chat/session/cache_session_store.rb +84 -0
- data/lib/flow_chat/session/middleware.rb +14 -6
- data/lib/flow_chat/simulator/controller.rb +78 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
- data/lib/flow_chat/ussd/app.rb +25 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +2 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +6 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +14 -42
- data/lib/flow_chat/ussd/prompt.rb +39 -5
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +64 -0
- data/lib/flow_chat/whatsapp/client.rb +439 -0
- data/lib/flow_chat/whatsapp/configuration.rb +113 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
- data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
- data/lib/flow_chat/whatsapp/processor.rb +26 -0
- data/lib/flow_chat/whatsapp/prompt.rb +251 -0
- data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
- data/lib/flow_chat.rb +1 -0
- metadata +21 -3
- data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
- data/lib/flow_chat/ussd/simulator/views/simulator.html.erb +0 -239
@@ -0,0 +1,162 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module FlowChat
|
5
|
+
module Whatsapp
|
6
|
+
class TemplateManager
|
7
|
+
def initialize(config = nil)
|
8
|
+
@config = config || Configuration.from_credentials
|
9
|
+
end
|
10
|
+
|
11
|
+
# Send a template message (used to initiate conversations)
|
12
|
+
def send_template(to:, template_name:, language: "en_US", components: [])
|
13
|
+
message_data = {
|
14
|
+
messaging_product: "whatsapp",
|
15
|
+
to: to,
|
16
|
+
type: "template",
|
17
|
+
template: {
|
18
|
+
name: template_name,
|
19
|
+
language: { code: language },
|
20
|
+
components: components
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
send_message(message_data)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Common template structures
|
28
|
+
def send_welcome_template(to:, name: nil)
|
29
|
+
components = []
|
30
|
+
|
31
|
+
if name
|
32
|
+
components << {
|
33
|
+
type: "header",
|
34
|
+
parameters: [
|
35
|
+
{
|
36
|
+
type: "text",
|
37
|
+
text: name
|
38
|
+
}
|
39
|
+
]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
send_template(
|
44
|
+
to: to,
|
45
|
+
template_name: "hello_world", # Default Meta template
|
46
|
+
language: "en_US",
|
47
|
+
components: components
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_notification_template(to:, message:, button_text: nil)
|
52
|
+
components = [
|
53
|
+
{
|
54
|
+
type: "body",
|
55
|
+
parameters: [
|
56
|
+
{
|
57
|
+
type: "text",
|
58
|
+
text: message
|
59
|
+
}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
]
|
63
|
+
|
64
|
+
if button_text
|
65
|
+
components << {
|
66
|
+
type: "button",
|
67
|
+
sub_type: "quick_reply",
|
68
|
+
index: "0",
|
69
|
+
parameters: [
|
70
|
+
{
|
71
|
+
type: "payload",
|
72
|
+
payload: "continue"
|
73
|
+
}
|
74
|
+
]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
send_template(
|
79
|
+
to: to,
|
80
|
+
template_name: "notification_template", # Custom template
|
81
|
+
language: "en_US",
|
82
|
+
components: components
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create a new template (requires approval from Meta)
|
87
|
+
def create_template(name:, category:, language: "en_US", components: [])
|
88
|
+
business_account_id = @config.business_account_id
|
89
|
+
uri = URI("https://graph.facebook.com/v18.0/#{business_account_id}/message_templates")
|
90
|
+
|
91
|
+
template_data = {
|
92
|
+
name: name,
|
93
|
+
category: category, # AUTHENTICATION, MARKETING, UTILITY
|
94
|
+
language: language,
|
95
|
+
components: components
|
96
|
+
}
|
97
|
+
|
98
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
99
|
+
http.use_ssl = true
|
100
|
+
|
101
|
+
request = Net::HTTP::Post.new(uri)
|
102
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
103
|
+
request["Content-Type"] = "application/json"
|
104
|
+
request.body = template_data.to_json
|
105
|
+
|
106
|
+
response = http.request(request)
|
107
|
+
JSON.parse(response.body)
|
108
|
+
end
|
109
|
+
|
110
|
+
# List all templates
|
111
|
+
def list_templates
|
112
|
+
business_account_id = @config.business_account_id
|
113
|
+
uri = URI("https://graph.facebook.com/v18.0/#{business_account_id}/message_templates")
|
114
|
+
|
115
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
116
|
+
http.use_ssl = true
|
117
|
+
|
118
|
+
request = Net::HTTP::Get.new(uri)
|
119
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
120
|
+
|
121
|
+
response = http.request(request)
|
122
|
+
JSON.parse(response.body)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get template status
|
126
|
+
def template_status(template_id)
|
127
|
+
uri = URI("https://graph.facebook.com/v18.0/#{template_id}")
|
128
|
+
|
129
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
130
|
+
http.use_ssl = true
|
131
|
+
|
132
|
+
request = Net::HTTP::Get.new(uri)
|
133
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
134
|
+
|
135
|
+
response = http.request(request)
|
136
|
+
JSON.parse(response.body)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def send_message(message_data)
|
142
|
+
uri = URI(@config.messages_url)
|
143
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
144
|
+
http.use_ssl = true
|
145
|
+
|
146
|
+
request = Net::HTTP::Post.new(uri)
|
147
|
+
request["Authorization"] = "Bearer #{@config.access_token}"
|
148
|
+
request["Content-Type"] = "application/json"
|
149
|
+
request.body = message_data.to_json
|
150
|
+
|
151
|
+
response = http.request(request)
|
152
|
+
|
153
|
+
unless response.is_a?(Net::HTTPSuccess)
|
154
|
+
Rails.logger.error "WhatsApp Template API error: #{response.body}"
|
155
|
+
return nil
|
156
|
+
end
|
157
|
+
|
158
|
+
JSON.parse(response.body)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
data/lib/flow_chat.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flow_chat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Froelich
|
@@ -97,15 +97,26 @@ files:
|
|
97
97
|
- Rakefile
|
98
98
|
- bin/console
|
99
99
|
- bin/setup
|
100
|
+
- examples/initializer.rb
|
101
|
+
- examples/media_prompts_examples.rb
|
102
|
+
- examples/multi_tenant_whatsapp_controller.rb
|
103
|
+
- examples/ussd_controller.rb
|
104
|
+
- examples/whatsapp_controller.rb
|
105
|
+
- examples/whatsapp_media_examples.rb
|
106
|
+
- examples/whatsapp_message_job.rb
|
100
107
|
- flow_chat.gemspec
|
101
108
|
- images/ussd_simulator.png
|
102
109
|
- lib/flow_chat.rb
|
110
|
+
- lib/flow_chat/base_processor.rb
|
103
111
|
- lib/flow_chat/config.rb
|
104
112
|
- lib/flow_chat/context.rb
|
105
113
|
- lib/flow_chat/flow.rb
|
106
114
|
- lib/flow_chat/interrupt.rb
|
115
|
+
- lib/flow_chat/session/cache_session_store.rb
|
107
116
|
- lib/flow_chat/session/middleware.rb
|
108
117
|
- lib/flow_chat/session/rails_session_store.rb
|
118
|
+
- lib/flow_chat/simulator/controller.rb
|
119
|
+
- lib/flow_chat/simulator/views/simulator.html.erb
|
109
120
|
- lib/flow_chat/ussd/app.rb
|
110
121
|
- lib/flow_chat/ussd/gateway/nalo.rb
|
111
122
|
- lib/flow_chat/ussd/gateway/nsano.rb
|
@@ -115,9 +126,16 @@ files:
|
|
115
126
|
- lib/flow_chat/ussd/processor.rb
|
116
127
|
- lib/flow_chat/ussd/prompt.rb
|
117
128
|
- lib/flow_chat/ussd/renderer.rb
|
118
|
-
- lib/flow_chat/ussd/simulator/controller.rb
|
119
|
-
- lib/flow_chat/ussd/simulator/views/simulator.html.erb
|
120
129
|
- lib/flow_chat/version.rb
|
130
|
+
- lib/flow_chat/whatsapp/app.rb
|
131
|
+
- lib/flow_chat/whatsapp/client.rb
|
132
|
+
- lib/flow_chat/whatsapp/configuration.rb
|
133
|
+
- lib/flow_chat/whatsapp/gateway/cloud_api.rb
|
134
|
+
- lib/flow_chat/whatsapp/middleware/executor.rb
|
135
|
+
- lib/flow_chat/whatsapp/processor.rb
|
136
|
+
- lib/flow_chat/whatsapp/prompt.rb
|
137
|
+
- lib/flow_chat/whatsapp/send_job_support.rb
|
138
|
+
- lib/flow_chat/whatsapp/template_manager.rb
|
121
139
|
homepage: https://github.com/radioactive-labs/flow_chat
|
122
140
|
licenses:
|
123
141
|
- MIT
|
@@ -1,51 +0,0 @@
|
|
1
|
-
module FlowChat
|
2
|
-
module Ussd
|
3
|
-
module Simulator
|
4
|
-
module Controller
|
5
|
-
def ussd_simulator
|
6
|
-
respond_to do |format|
|
7
|
-
format.html do
|
8
|
-
render inline: simulator_view_template, layout: false, locals: simulator_locals
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
protected
|
14
|
-
|
15
|
-
def show_options
|
16
|
-
true
|
17
|
-
end
|
18
|
-
|
19
|
-
def default_msisdn
|
20
|
-
"233200123456"
|
21
|
-
end
|
22
|
-
|
23
|
-
def default_endpoint
|
24
|
-
"/ussd"
|
25
|
-
end
|
26
|
-
|
27
|
-
def default_provider
|
28
|
-
:nalo
|
29
|
-
end
|
30
|
-
|
31
|
-
def simulator_view_template
|
32
|
-
File.read simulator_view_path
|
33
|
-
end
|
34
|
-
|
35
|
-
def simulator_view_path
|
36
|
-
File.join FlowChat.root.join("flow_chat", "ussd", "simulator", "views", "simulator.html.erb")
|
37
|
-
end
|
38
|
-
|
39
|
-
def simulator_locals
|
40
|
-
{
|
41
|
-
pagesize: FlowChat::Config.ussd.pagination_page_size,
|
42
|
-
show_options: show_options,
|
43
|
-
default_msisdn: default_msisdn,
|
44
|
-
default_endpoint: default_endpoint,
|
45
|
-
default_provider: default_provider
|
46
|
-
}
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,239 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>FlowChat Ussd Simulator</title>
|
5
|
-
<style>
|
6
|
-
.content {
|
7
|
-
width: 320px;
|
8
|
-
margin: 100px auto;
|
9
|
-
}
|
10
|
-
.label {
|
11
|
-
display: inline-block;
|
12
|
-
width: 80px;
|
13
|
-
font-weight: bold;
|
14
|
-
}
|
15
|
-
.value {
|
16
|
-
display: inline;
|
17
|
-
}
|
18
|
-
.value select, input {
|
19
|
-
width: 200px;
|
20
|
-
}
|
21
|
-
.field {
|
22
|
-
margin: 5px;
|
23
|
-
}
|
24
|
-
#screen {
|
25
|
-
border: 1px black solid;
|
26
|
-
height:400px;
|
27
|
-
width:300px;
|
28
|
-
margin-top: 10px;
|
29
|
-
margin-bottom: 10px;
|
30
|
-
padding: 10px 5px;
|
31
|
-
}
|
32
|
-
#char-count {
|
33
|
-
text-align: center;
|
34
|
-
font-size: 10px;
|
35
|
-
}
|
36
|
-
.hidden {
|
37
|
-
display: none;
|
38
|
-
}
|
39
|
-
</style>
|
40
|
-
</head>
|
41
|
-
<body>
|
42
|
-
<div class="content">
|
43
|
-
<div class="field <%= show_options ? '' : 'hidden' %>">
|
44
|
-
<div class="label">Provider </div>
|
45
|
-
<div class="value">
|
46
|
-
<select id="provider">
|
47
|
-
<option <%= default_provider == :nalo ? 'selected' : '' %> value="nalo">Nalo</option>
|
48
|
-
<option <%= default_provider == :nsano ? 'selected' : '' %> value="nsano">Nsano</option>
|
49
|
-
</select>
|
50
|
-
</div>
|
51
|
-
</div>
|
52
|
-
<div class="field <%= show_options ? '' : 'hidden' %>">
|
53
|
-
<div class="label">Endpoint </div>
|
54
|
-
<div class="value">
|
55
|
-
<input id="endpoint" value="<%= default_endpoint %>" />
|
56
|
-
</div>
|
57
|
-
</div>
|
58
|
-
<div class="field <%= show_options ? '' : 'hidden' %>">
|
59
|
-
<div class="label">MSISDN </div>
|
60
|
-
<div class="value">
|
61
|
-
<input id="msisdn" value="<%= default_msisdn %>" />
|
62
|
-
</div>
|
63
|
-
</div>
|
64
|
-
<div id="screen"></div>
|
65
|
-
<div id="char-count"></div>
|
66
|
-
<div class="field">
|
67
|
-
<input id="data" disabled> <button id="respond" disabled>Respond</button>
|
68
|
-
</div>
|
69
|
-
<div class="field">
|
70
|
-
<button id="initiate" disabled>Initiate</button>
|
71
|
-
<button id="reset" disabled>Reset</button>
|
72
|
-
</div>
|
73
|
-
</div>
|
74
|
-
<script>
|
75
|
-
// Config
|
76
|
-
const pagesize = <%= pagesize %>
|
77
|
-
|
78
|
-
// View
|
79
|
-
const $screen = document.getElementById('screen')
|
80
|
-
const $charCount = document.getElementById('char-count')
|
81
|
-
|
82
|
-
const $provider = document.getElementById('provider')
|
83
|
-
const $endpoint = document.getElementById('endpoint')
|
84
|
-
const $msisdn = document.getElementById('msisdn')
|
85
|
-
|
86
|
-
const $data = document.getElementById('data')
|
87
|
-
const $respondBtn = document.getElementById('respond')
|
88
|
-
const $initiateBtn = document.getElementById('initiate')
|
89
|
-
const $resetBtn = document.getElementById('reset')
|
90
|
-
|
91
|
-
$provider.addEventListener('change', function (e) {
|
92
|
-
state.provider = $provider.value
|
93
|
-
render()
|
94
|
-
}, false)
|
95
|
-
|
96
|
-
$endpoint.addEventListener('keyup', function (e) {
|
97
|
-
state.endpoint = $endpoint.value
|
98
|
-
render()
|
99
|
-
}, false)
|
100
|
-
|
101
|
-
$msisdn.addEventListener('keyup', function (e) {
|
102
|
-
state.msisdn = $msisdn.value
|
103
|
-
render()
|
104
|
-
}, false)
|
105
|
-
|
106
|
-
$initiateBtn.addEventListener('click', function (e) {
|
107
|
-
makeRequest()
|
108
|
-
}, false)
|
109
|
-
|
110
|
-
$resetBtn.addEventListener('click',function(e){
|
111
|
-
reset()
|
112
|
-
}, false)
|
113
|
-
|
114
|
-
$respondBtn.addEventListener('click', function (e) {
|
115
|
-
makeRequest()
|
116
|
-
}, false)
|
117
|
-
|
118
|
-
function disableInputs() {
|
119
|
-
$data.disabled = 'disabled'
|
120
|
-
$respondBtn.disabled = 'disabled'
|
121
|
-
$initiateBtn.disabled = 'disabled'
|
122
|
-
$resetBtn.disabled = 'disabled'
|
123
|
-
$data.disabled = 'disabled'
|
124
|
-
}
|
125
|
-
|
126
|
-
function enableResponse() {
|
127
|
-
$data.disabled = false
|
128
|
-
$respondBtn.disabled = false
|
129
|
-
$resetBtn.disabled = false
|
130
|
-
}
|
131
|
-
|
132
|
-
function display(text) {
|
133
|
-
$screen.innerText = text.substr(0, pagesize)
|
134
|
-
if(text.length > 0)
|
135
|
-
$charCount.innerText = `${text.length} chars`
|
136
|
-
else
|
137
|
-
$charCount.innerText = ''
|
138
|
-
}
|
139
|
-
|
140
|
-
function render() {
|
141
|
-
disableInputs()
|
142
|
-
|
143
|
-
if(!state.isRunning){
|
144
|
-
if(state.provider && state.endpoint && state.msisdn)
|
145
|
-
$initiateBtn.disabled = false
|
146
|
-
else
|
147
|
-
$initiateBtn.disabled = 'disabled'
|
148
|
-
}
|
149
|
-
else {
|
150
|
-
enableResponse()
|
151
|
-
}
|
152
|
-
}
|
153
|
-
|
154
|
-
// State
|
155
|
-
const state = {}
|
156
|
-
|
157
|
-
function reset(shouldRender) {
|
158
|
-
state.isRunning = false
|
159
|
-
state.request_id = btoa(Math.random().toString()).substr(10, 10)
|
160
|
-
state.provider = $provider.value
|
161
|
-
state.endpoint = $endpoint.value
|
162
|
-
state.msisdn = $msisdn.value
|
163
|
-
|
164
|
-
$data.value = null
|
165
|
-
|
166
|
-
display("")
|
167
|
-
if(shouldRender !== false) render()
|
168
|
-
}
|
169
|
-
|
170
|
-
|
171
|
-
// API
|
172
|
-
|
173
|
-
function makeRequest() {
|
174
|
-
var data = {}
|
175
|
-
|
176
|
-
switch (state.provider) {
|
177
|
-
case "nalo":
|
178
|
-
data = {
|
179
|
-
USERID: state.request_id,
|
180
|
-
MSISDN: state.msisdn,
|
181
|
-
USERDATA: $data.value,
|
182
|
-
MSGTYPE: !state.isRunning,
|
183
|
-
}
|
184
|
-
break;
|
185
|
-
case "nsano":
|
186
|
-
data = {
|
187
|
-
network: 'MTN',
|
188
|
-
msisdn: state.msisdn,
|
189
|
-
msg: $data.value,
|
190
|
-
UserSessionID: state.request_id,
|
191
|
-
}
|
192
|
-
break;
|
193
|
-
|
194
|
-
default:
|
195
|
-
alert(`Unhandled provider request: ${state.provider}`)
|
196
|
-
return
|
197
|
-
}
|
198
|
-
|
199
|
-
disableInputs()
|
200
|
-
fetch(state.endpoint, {
|
201
|
-
method: 'POST',
|
202
|
-
headers: {
|
203
|
-
'Content-Type': 'application/json'
|
204
|
-
},
|
205
|
-
redirect: 'error',
|
206
|
-
body: JSON.stringify(data)
|
207
|
-
})
|
208
|
-
.then(response => {
|
209
|
-
if (!response.ok) {
|
210
|
-
throw Error(`${response.status}: ${response.statusText}`);
|
211
|
-
}
|
212
|
-
return response.json()
|
213
|
-
})
|
214
|
-
.then(data => {
|
215
|
-
switch (state.provider) {
|
216
|
-
case "nalo":
|
217
|
-
display(data.MSG)
|
218
|
-
state.isRunning = data.MSGTYPE
|
219
|
-
break;
|
220
|
-
case "nsano":
|
221
|
-
display(data.USSDResp.title)
|
222
|
-
state.isRunning = data.USSDResp.action == "input"
|
223
|
-
break;
|
224
|
-
|
225
|
-
default:
|
226
|
-
alert(`Unhandled provider response: ${state.provider}`)
|
227
|
-
return
|
228
|
-
}
|
229
|
-
$data.value = null
|
230
|
-
})
|
231
|
-
.catch(error => alert(error.message))
|
232
|
-
.finally(render);
|
233
|
-
}
|
234
|
-
|
235
|
-
// run the app
|
236
|
-
reset()
|
237
|
-
</script>
|
238
|
-
</body>
|
239
|
-
</html>
|