flow_chat 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77864972a34ad8761c6ce77a4f3574b9f45b4f8ef3210c34ca376812d0b54a41
4
- data.tar.gz: 47b860cf8170d1cda13fa280ba92ba731c3060ca7d955b727b22ce8367ee8e97
3
+ metadata.gz: 6c1e5cd739c50f112b6a5637e712756e0b96bdb18c2276c7d30db2b2a071cc23
4
+ data.tar.gz: f701ad5050599c06b87eba80841823b04eef156e7ea3e77527a0be91519b0d48
5
5
  SHA512:
6
- metadata.gz: fd997d03b94ce1051ab2ad6483b5864fb2b223b510d96d45833c7908a6928b419fdc4c5512730c80bf86e5f1b9741f2fc3d7c377a1f4ba8ea2b7baa0ed2244bf
7
- data.tar.gz: c39477b7915c3abe9f2b1ff5c31b99eb3a618ecd2ab53e5405fb34e8564822510ad16dd94a2ceb664e33ada5549910c096b38e1a925e8609a3f73c855b392d3d
6
+ metadata.gz: ef7e1721b7c5453130999b2ef899af4f20ba6db81ac4b3956ce7ade5956bf2b61e248b2be66ace885c633d4d612025208b31d1ff081ef1a0d671f579e0d3db38
7
+ data.tar.gz: 2581189b102229a0e8843691f38eaf91a9c7bf1d2db7c7a71e685124a849621cf55bdd33c6cdd1c928e2bfac051bca597601875dc9cff921063a98ff954a7b82
data/.DS_Store ADDED
Binary file
data/README.md CHANGED
@@ -1,28 +1,147 @@
1
1
  # FlowChat
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/flow_chat`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ FlowChat is a Rails framework designed for crafting Menu-based conversation workflows, such as those used in USSD systems. It introduces an intuitive approach to defining conversation flows in Ruby, facilitating clear and logical flow development. Currently supporting USSD with plans to extend functionality to WhatsApp and Telegram, FlowChat makes multi-channel user interaction seamless and efficient.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The framework's architecture leverages a middleware processing pipeline, offering flexibility in customizing the conversation handling process.
6
6
 
7
- ## Installation
7
+ ## Getting Started
8
8
 
9
- Add this line to your application's Gemfile:
9
+ ### Installation
10
+
11
+ Incorporate FlowChat into your Rails project by adding the following line to your Gemfile:
10
12
 
11
13
  ```ruby
12
- gem 'flow_chat'
14
+ gem 'flow_chat', '~> 0.2.0'
15
+ ```
16
+
17
+ Then, execute:
18
+
19
+ ```bash
20
+ bundle install
13
21
  ```
14
22
 
15
- And then execute:
23
+ Alternatively, you can install it directly using:
24
+
25
+ ```bash
26
+ gem install flow_chat
27
+ ```
28
+
29
+ ### Basic Usage
30
+
31
+ #### Building Your First Flow
32
+
33
+ Create a new class derived from `FlowChat::Flow` to define your conversation flow. It's recommended to place your flow definitions under `app/flow_chat`.
34
+
35
+ For a simple "Hello World" flow:
36
+
37
+ ```ruby
38
+ class HelloWorldFlow < FlowChat::Flow
39
+ def main_page
40
+ app.say "Hello World!"
41
+ end
42
+ end
43
+ ```
44
+
45
+ The `app` instance within `FlowChat::Flow` provides methods to interact with and respond to the user, such as `app.say`, which sends a message to the user.
46
+
47
+ #### Integration with USSD
48
+
49
+ Given that most USSD gateways interact via HTTP, set up a controller to handle the conversation flow:
50
+
51
+ ```ruby
52
+ class UssdDemoController < ApplicationController
53
+ skip_forgery_protection
54
+
55
+ def hello_world
56
+ ussd_processor.run HelloWorldFlow, :main_page
57
+ end
58
+
59
+ private
60
+
61
+ def ussd_processor
62
+ @ussd_processor ||= FlowChat::Ussd::Processor.new(self) do |processor|
63
+ processor.use_gateway FlowChat::Ussd::Gateway::Nalo
64
+ processor.use_session_store FlowChat::Session::RailsSessionStore
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ This controller initializes a `FlowChat::Ussd::Processor` specifying the use of Nalo Solutions' gateway and a session storage mechanism. Here, `RailsSessionStore` is chosen for simplicity and demonstration purposes.
71
+
72
+ Bind the controller action to a route:
16
73
 
17
- $ bundle install
74
+ ```ruby
75
+ Rails.application.routes.draw do
76
+ post 'ussd_hello_world' => 'ussd_demo#hello_world'
77
+ end
78
+ ```
79
+
80
+ #### Testing with the USSD Simulator
81
+
82
+ FlowChat comes with a USSD simulator for local testing:
83
+
84
+ ```ruby
85
+ class UssdSimulatorController < ApplicationController
86
+ include FlowChat::Ussd::Simulator::Controller
87
+
88
+ protected
89
+
90
+ def default_endpoint
91
+ '/ussd_hello_world'
92
+ end
93
+
94
+ def default_provider
95
+ :nalo
96
+ end
97
+ end
98
+ ```
99
+
100
+ And set up the corresponding route:
101
+
102
+ ```ruby
103
+ Rails.application.routes.draw do
104
+ get 'ussd_simulator' => 'ussd_simulator#ussd_simulator'
105
+ end
106
+ ```
107
+
108
+ Visit [http://localhost:3000/ussd_simulator](http://localhost:3000/ussd_simulator) to initiate and test your flow.
109
+
110
+ ### Advanced Usage: Implementing Multiple Screens
111
+
112
+ To engage users with a multi-step interaction, define a flow with multiple screens:
113
+
114
+ ```ruby
115
+ class MultipleScreensFlow < FlowChat::Flow
116
+ def main_page
117
+ name = app.screen(:name) { |prompt|
118
+ prompt.ask "What is your name?", transform: ->(input) { input.squish }
119
+ }
120
+
121
+ age = app.screen(:age) do |prompt|
122
+ prompt.ask "How old are you?",
123
+ convert: ->(input) { input.to_i },
124
+ validate: ->(input) { "You must be at least 13 years old" unless input >= 13 }
125
+ end
126
+
127
+ gender = app.screen(:gender) { |prompt| prompt.select "What is your gender", ["Male", "Female"] }
128
+
129
+ confirm = app.screen(:confirm) do |prompt|
130
+ prompt.yes?("Is this correct?\n\nName: #{name}\nAge: #{age}\nGender: #{gender}")
131
+ end
132
+
133
+ app.say confirm ? "Thank you for confirming" : "Please try again"
134
+ end
135
+ end
136
+ ```
18
137
 
19
- Or install it yourself as:
138
+ This example illustrates a flow that collects and confirms user information across multiple interaction steps, showcasing FlowChat's capability to handle complex conversation logic effortlessly.
20
139
 
21
- $ gem install flow_chat
140
+ TODO:
22
141
 
23
- ## Usage
142
+ ### Sub Flows
24
143
 
25
- TODO: Write usage instructions here
144
+ TODO:
26
145
 
27
146
  ## Development
28
147
 
data/flow_chat.gemspec CHANGED
@@ -6,9 +6,9 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Stefan Froelich"]
7
7
  spec.email = ["sfroelich01@gmail.com"]
8
8
 
9
- spec.summary = "Enable USSD processing support in Rails."
10
- spec.description = "Enable USSD processing support in Rails."
11
- spec.homepage = "https://github.com/ussd-engine/api-base"
9
+ spec.summary = "Framework for building Menu based conversations (e.g. USSD) in Rails."
10
+ spec.description = "Framework for building Menu based conversations (e.g. USSD) in Rails."
11
+ spec.homepage = "https://github.com/radioactive-labs/flow_chat"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
14
 
Binary file
@@ -12,6 +12,8 @@ module FlowChat
12
12
  @data[key] = value
13
13
  end
14
14
 
15
+ def input = @data["request.input"]
16
+
15
17
  def session = @data["session"]
16
18
 
17
19
  def controller = @data["controller"]
@@ -0,0 +1,9 @@
1
+ module FlowChat
2
+ class Flow
3
+ attr_reader :app
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+ end
9
+ end
@@ -7,8 +7,8 @@ module FlowChat
7
7
  @session_data = (session_store[session_id] || {}).with_indifferent_access
8
8
  end
9
9
 
10
- def get(key, default = nil)
11
- session_data[key] || default
10
+ def get(key)
11
+ session_data[key]
12
12
  end
13
13
 
14
14
  def set(key, value)
@@ -5,8 +5,8 @@ module FlowChat
5
5
 
6
6
  def initialize(context)
7
7
  @context = context
8
- @session = context["session"]
9
- @input = context["request.input"]
8
+ @session = context.session
9
+ @input = context.input
10
10
  @navigation_stack = []
11
11
  end
12
12
 
@@ -25,7 +25,7 @@ module FlowChat
25
25
  value
26
26
  end
27
27
 
28
- def terminate!(msg)
28
+ def say(msg)
29
29
  raise FlowChat::Interrupt::Terminate.new(msg)
30
30
  end
31
31
  end
@@ -18,26 +18,20 @@ module FlowChat
18
18
  # context["request.type"] = params["MSGTYPE"] ? :initial : :response
19
19
  context["request.input"] = params["USERDATA"].presence
20
20
 
21
- type, msg, choices = @app.call(context)
21
+ type, prompt, choices = @app.call(context)
22
22
 
23
23
  context.controller.render json: {
24
24
  USERID: params["USERID"],
25
25
  MSISDN: params["MSISDN"],
26
- MSG: build_message(msg, choices),
26
+ MSG: render_prompt(prompt, choices),
27
27
  MSGTYPE: type == :prompt
28
28
  }
29
29
  end
30
30
 
31
31
  private
32
32
 
33
- def build_message(msg, choices)
34
- [msg, build_choices(choices)].compact.join "\n\n"
35
- end
36
-
37
- def build_choices(choices)
38
- return unless choices.present?
39
-
40
- choices.map { |i, c| "#{i}. #{c}" }.join "\n"
33
+ def render_prompt(prompt, choices)
34
+ FlowChat::Ussd::Renderer.new(prompt, choices).render
41
35
  end
42
36
  end
43
37
  end
@@ -11,17 +11,16 @@ module FlowChat
11
11
  @session = context.session
12
12
 
13
13
  if intercept?
14
- @context["ussd.response"] = handle_intercepted_request
15
- [200, {}, [""]]
14
+ type, prompt = handle_intercepted_request
15
+ [type, prompt, []]
16
16
  else
17
17
  @session.delete "ussd.pagination"
18
- res = @app.call(context)
18
+ type, prompt, choices = @app.call(context)
19
19
 
20
- if @context["ussd.response"].present?
21
- @context["ussd.response"] = maybe_paginate @context["ussd.response"]
22
- end
20
+ prompt = FlowChat::Ussd::Renderer.new(prompt, choices).render
21
+ type, prompt = maybe_paginate(type, prompt) if prompt.present?
23
22
 
24
- res
23
+ [type, prompt, []]
25
24
  end
26
25
  end
27
26
 
@@ -29,57 +28,59 @@ module FlowChat
29
28
 
30
29
  def intercept?
31
30
  pagination_state.present? &&
32
- (pagination_state[:type].to_sym == :terminal ||
33
- ([Config.pagination_next_option, Config.pagination_back_option].include? @context["ussd.request"][:input]))
31
+ (pagination_state["type"].to_sym == :terminal ||
32
+ ([Config.pagination_next_option, Config.pagination_back_option].include? @context.input))
34
33
  end
35
34
 
36
35
  def handle_intercepted_request
37
36
  Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
38
37
  start, finish, has_more = calculate_offsets
39
- type = (pagination_state[:type].to_sym == :terminal && !has_more) ? :terminal : :prompt
40
- body = pagination_state[:body][start..finish].strip + build_pagination_options(type, has_more)
38
+ type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
39
+ prompt = pagination_state["prompt"][start..finish].strip + build_pagination_options(type, has_more)
41
40
  set_pagination_state(current_page, start, finish)
42
41
 
43
- {body: body, type: type}
42
+ [type, prompt]
44
43
  end
45
44
 
46
- def maybe_paginate(response)
47
- if response[:body].length > Config.pagination_page_size
48
- Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{response[:body].length}) exceeds page size (#{Config.pagination_page_size}). Paginating."
49
- body = response[:body][0..single_option_slice_size]
45
+ def maybe_paginate(type, prompt)
46
+ if prompt.length > Config.pagination_page_size
47
+ original_prompt = prompt
48
+ Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{Config.pagination_page_size}). Paginating."
49
+ prompt = prompt[0..single_option_slice_size]
50
50
  # Ensure we do not cut words and options off in the middle.
51
- current_pagebreak = response[:body][single_option_slice_size + 1].blank? ? single_option_slice_size : body.rindex("\n") || body.rindex(" ") || single_option_slice_size
52
- set_pagination_state(1, 0, current_pagebreak, response[:body], response[:type])
53
- response[:body] = body[0..current_pagebreak].strip + "\n\n" + next_option
54
- response[:type] = :prompt
51
+ current_pagebreak = prompt[single_option_slice_size + 1].blank? ? single_option_slice_size : prompt.rindex("\n") || prompt.rindex(" ") || single_option_slice_size
52
+ set_pagination_state(1, 0, current_pagebreak, original_prompt, type)
53
+ prompt = prompt[0..current_pagebreak].strip + "\n\n" + next_option
54
+ type = :prompt
55
55
  end
56
- response
56
+ [type, prompt]
57
57
  end
58
58
 
59
59
  def calculate_offsets
60
60
  page = current_page
61
- offset = pagination_state[:offsets][page]
61
+ offset = pagination_state["offsets"][page.to_s]
62
62
  if offset.present?
63
63
  Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
64
- start = offset[:start]
65
- finish = offset[:finish]
66
- has_more = pagination_state[:body].length > finish
64
+ start = offset["start"]
65
+ finish = offset["finish"]
66
+ has_more = pagination_state["prompt"].length > finish
67
67
  else
68
68
  Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
69
69
  # We are guaranteed a previous offset because it was set in maybe_paginate
70
- previous_offset = pagination_state[:offsets][page - 1]
71
- start = previous_offset[:finish] + 1
72
- has_more, len = (pagination_state[:body].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
70
+ previous_page = page - 1
71
+ previous_offset = pagination_state["offsets"][previous_page.to_s]
72
+ start = previous_offset["finish"] + 1
73
+ has_more, len = (pagination_state["prompt"].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
73
74
  finish = start + len
74
- if start > pagination_state[:body].length
75
+ if start > pagination_state["prompt"].length
75
76
  Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
76
77
  page -= 1
77
78
  has_more = false
78
- start = previous_offset[:start]
79
- finish = previous_offset[:finish]
79
+ start = previous_offset["start"]
80
+ finish = previous_offset["finish"]
80
81
  else
81
- body = pagination_state[:body][start..finish]
82
- current_pagebreak = pagination_state[:body][finish + 1].blank? ? len : body.rindex("\n") || body.rindex(" ") || len
82
+ prompt = pagination_state["prompt"][start..finish]
83
+ current_pagebreak = pagination_state["prompt"][finish + 1].blank? ? len : prompt.rindex("\n") || prompt.rindex(" ") || len
83
84
  finish = start + current_pagebreak
84
85
  end
85
86
  end
@@ -126,28 +127,31 @@ module FlowChat
126
127
  end
127
128
 
128
129
  def current_page
129
- current_page = pagination_state[:page]
130
- if @context["ussd.request"][:input] == Config.pagination_back_option
131
- current_page -= 1
132
- elsif @context["ussd.request"][:input] == Config.pagination_next_option
133
- current_page += 1
130
+ page = pagination_state["page"]
131
+ if @context.input == Config.pagination_back_option
132
+ page -= 1
133
+ elsif @context.input == Config.pagination_next_option
134
+ page += 1
134
135
  end
135
- [current_page, 1].max
136
+ [page, 1].max
136
137
  end
137
138
 
138
139
  def pagination_state
139
- @context.session.get("pagination", {})
140
+ @pagination_state ||= @context.session.get("ussd.pagination") || {}
140
141
  end
141
142
 
142
- def set_pagination_state(page, offset_start, offset_finish, body = nil, type = nil)
143
- offsets = pagination_state[:offsets] || {}
143
+ def set_pagination_state(page, offset_start, offset_finish, prompt = nil, type = nil)
144
+ offsets = pagination_state["offsets"] || {}
144
145
  offsets[page] = {start: offset_start, finish: offset_finish}
145
- @session["ussd.pagination"] = {
146
- page: page,
147
- offsets: offsets,
148
- body: body || pagination_state[:body],
149
- type: type || pagination_state[:type]
146
+ prompt ||= pagination_state["prompt"]
147
+ type ||= pagination_state["type"]
148
+ @pagination_state = {
149
+ "page" => page,
150
+ "offsets" => offsets,
151
+ "prompt" => prompt,
152
+ "type" => type
150
153
  }
154
+ @session.set "ussd.pagination", @pagination_state
151
155
  end
152
156
  end
153
157
  end
@@ -33,21 +33,22 @@ module FlowChat
33
33
  self
34
34
  end
35
35
 
36
- def use_pagination
37
- middleware.use FlowChat::Ussd::Middleware::Pagination
38
- end
39
-
40
- def run(flow, action)
41
- @context["flow.class"] = flow
36
+ def run(flow_class, action)
37
+ @context["flow.name"] = flow_class.name.underscore
38
+ @context["flow.class"] = flow_class
42
39
  @context["flow.action"] = action
43
40
 
44
- ::Middleware::Builder.new name: "ussd" do |b|
41
+ stack = ::Middleware::Builder.new name: "ussd" do |b|
45
42
  b.use gateway
46
43
  b.use FlowChat::Session::Middleware
47
- # b.use FlowChat::Middleware::Pagination
44
+ b.use FlowChat::Ussd::Middleware::Pagination
48
45
  b.use middleware
49
46
  b.use FlowChat::Ussd::Middleware::Executor
50
- end.inject_logger(Rails.logger).call(@context)
47
+ end.inject_logger(Rails.logger)
48
+
49
+ yield stack if block_given?
50
+
51
+ stack.call(@context)
51
52
  end
52
53
  end
53
54
  end
@@ -0,0 +1,26 @@
1
+ module FlowChat
2
+ module Ussd
3
+ class Renderer
4
+ attr_reader :prompt, :choices
5
+
6
+ def initialize(prompt, choices)
7
+ @prompt = prompt
8
+ @choices = choices
9
+ end
10
+
11
+ def render = build_prompt
12
+
13
+ private
14
+
15
+ def build_prompt
16
+ [prompt, build_choices].compact.join "\n\n"
17
+ end
18
+
19
+ def build_choices
20
+ return unless choices.present?
21
+
22
+ choices.map { |i, c| "#{i}. #{c}" }.join "\n"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -40,22 +40,22 @@
40
40
  </head>
41
41
  <body>
42
42
  <div class="content">
43
- <div class="field hidden<%= show_options ? '' : 'hidden' %>">
43
+ <div class="field <%= show_options ? '' : 'hidden' %>">
44
44
  <div class="label">Provider </div>
45
45
  <div class="value">
46
46
  <select id="provider">
47
- <option <%= default_provider == :nalo ? 'selected' : '' %> value="nalo">Nalo</option>
48
- <option <%= default_provider == :nsano ? 'selected' : '' %> value="nsano">Nsano</option>
47
+ <option <%= default_provider == :nalo ? 'selected' : '' %> value="nalo">Nalo</option>
48
+ <option <%= default_provider == :nsano ? 'selected' : '' %> value="nsano">Nsano</option>
49
49
  </select>
50
50
  </div>
51
51
  </div>
52
- <div class="field <%= show_options ? '' : 'hidden' %>">
52
+ <div class="field <%= show_options ? '' : 'hidden' %>">
53
53
  <div class="label">Endpoint </div>
54
54
  <div class="value">
55
55
  <input id="endpoint" value="<%= default_endpoint %>" />
56
56
  </div>
57
57
  </div>
58
- <div class="field <%= show_options ? '' : 'hidden' %>">
58
+ <div class="field <%= show_options ? '' : 'hidden' %>">
59
59
  <div class="label">MSISDN </div>
60
60
  <div class="value">
61
61
  <input id="msisdn" value="<%= default_msisdn %>" />
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-04 00:00:00.000000000 Z
11
+ date: 2024-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -80,13 +80,14 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.4.2
83
- description: Enable USSD processing support in Rails.
83
+ description: Framework for building Menu based conversations (e.g. USSD) in Rails.
84
84
  email:
85
85
  - sfroelich01@gmail.com
86
86
  executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
+ - ".DS_Store"
90
91
  - ".gitignore"
91
92
  - ".rspec"
92
93
  - ".travis.yml"
@@ -97,9 +98,11 @@ files:
97
98
  - bin/console
98
99
  - bin/setup
99
100
  - flow_chat.gemspec
101
+ - images/ussd_simulator.png
100
102
  - lib/flow_chat.rb
101
103
  - lib/flow_chat/config.rb
102
104
  - lib/flow_chat/context.rb
105
+ - lib/flow_chat/flow.rb
103
106
  - lib/flow_chat/interrupt.rb
104
107
  - lib/flow_chat/session/middleware.rb
105
108
  - lib/flow_chat/session/rails_session_store.rb
@@ -111,17 +114,18 @@ files:
111
114
  - lib/flow_chat/ussd/middleware/resumable_session.rb
112
115
  - lib/flow_chat/ussd/processor.rb
113
116
  - lib/flow_chat/ussd/prompt.rb
117
+ - lib/flow_chat/ussd/renderer.rb
114
118
  - lib/flow_chat/ussd/simulator/controller.rb
115
119
  - lib/flow_chat/ussd/simulator/views/simulator.html.erb
116
120
  - lib/flow_chat/version.rb
117
- homepage: https://github.com/ussd-engine/api-base
121
+ homepage: https://github.com/radioactive-labs/flow_chat
118
122
  licenses:
119
123
  - MIT
120
124
  metadata:
121
125
  allowed_push_host: https://rubygems.org
122
- homepage_uri: https://github.com/ussd-engine/api-base
123
- source_code_uri: https://github.com/ussd-engine/api-base
124
- changelog_uri: https://github.com/ussd-engine/api-base
126
+ homepage_uri: https://github.com/radioactive-labs/flow_chat
127
+ source_code_uri: https://github.com/radioactive-labs/flow_chat
128
+ changelog_uri: https://github.com/radioactive-labs/flow_chat
125
129
  post_install_message:
126
130
  rdoc_options: []
127
131
  require_paths:
@@ -140,5 +144,5 @@ requirements: []
140
144
  rubygems_version: 3.5.6
141
145
  signing_key:
142
146
  specification_version: 4
143
- summary: Enable USSD processing support in Rails.
147
+ summary: Framework for building Menu based conversations (e.g. USSD) in Rails.
144
148
  test_files: []