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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +642 -86
  4. data/examples/initializer.rb +31 -0
  5. data/examples/media_prompts_examples.rb +28 -0
  6. data/examples/multi_tenant_whatsapp_controller.rb +244 -0
  7. data/examples/ussd_controller.rb +264 -0
  8. data/examples/whatsapp_controller.rb +140 -0
  9. data/examples/whatsapp_media_examples.rb +406 -0
  10. data/examples/whatsapp_message_job.rb +111 -0
  11. data/lib/flow_chat/base_processor.rb +67 -0
  12. data/lib/flow_chat/config.rb +36 -0
  13. data/lib/flow_chat/session/cache_session_store.rb +84 -0
  14. data/lib/flow_chat/session/middleware.rb +14 -6
  15. data/lib/flow_chat/simulator/controller.rb +78 -0
  16. data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
  17. data/lib/flow_chat/ussd/app.rb +25 -0
  18. data/lib/flow_chat/ussd/gateway/nalo.rb +2 -0
  19. data/lib/flow_chat/ussd/gateway/nsano.rb +6 -0
  20. data/lib/flow_chat/ussd/middleware/resumable_session.rb +1 -1
  21. data/lib/flow_chat/ussd/processor.rb +14 -42
  22. data/lib/flow_chat/ussd/prompt.rb +39 -5
  23. data/lib/flow_chat/version.rb +1 -1
  24. data/lib/flow_chat/whatsapp/app.rb +64 -0
  25. data/lib/flow_chat/whatsapp/client.rb +439 -0
  26. data/lib/flow_chat/whatsapp/configuration.rb +113 -0
  27. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
  28. data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
  29. data/lib/flow_chat/whatsapp/processor.rb +26 -0
  30. data/lib/flow_chat/whatsapp/prompt.rb +251 -0
  31. data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
  32. data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
  33. data/lib/flow_chat.rb +1 -0
  34. metadata +21 -3
  35. data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
  36. 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
@@ -1,5 +1,6 @@
1
1
  require "zeitwerk"
2
2
  require "active_support"
3
+ require "active_support/core_ext/time"
3
4
 
4
5
  loader = Zeitwerk::Loader.for_gem
5
6
  loader.enable_reloading if defined?(Rails.env) && Rails.env.development?
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.3.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>