wittyflow 0.1.0 → 1.0.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/CHANGELOG.md +160 -0
- data/Guardfile +30 -0
- data/README.md +463 -11
- data/examples/rails_initializer.rb +56 -0
- data/lib/wittyflow/client.rb +193 -0
- data/lib/wittyflow/configuration.rb +20 -0
- data/lib/wittyflow/sms.rb +32 -0
- data/lib/wittyflow/version.rb +1 -1
- data/lib/wittyflow.rb +27 -67
- data/wittyflow-0.1.0.gem +0 -0
- data/wittyflow.gemspec +34 -11
- metadata +221 -16
- data/.gitignore +0 -8
- data/.rubocop.yml +0 -13
- data/Gemfile +0 -10
- data/Rakefile +0 -8
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# config/initializers/wittyflow.rb
|
4
|
+
#
|
5
|
+
# Wittyflow SMS Gem Configuration
|
6
|
+
# This initializer sets up the Wittyflow SMS service for your Rails application
|
7
|
+
|
8
|
+
Wittyflow.configure do |config|
|
9
|
+
# Required: Your Wittyflow API credentials
|
10
|
+
# Get these from your Wittyflow dashboard
|
11
|
+
config.app_id = Rails.application.credentials.wittyflow_app_id || ENV.fetch("WITTYFLOW_APP_ID", nil)
|
12
|
+
config.app_secret = Rails.application.credentials.wittyflow_app_secret || ENV.fetch("WITTYFLOW_APP_SECRET", nil)
|
13
|
+
|
14
|
+
# Optional: API endpoint (default is production endpoint)
|
15
|
+
# config.api_endpoint = "https://api.wittyflow.com/v1"
|
16
|
+
|
17
|
+
# Optional: Request timeout in seconds (default: 30)
|
18
|
+
config.timeout = 30
|
19
|
+
|
20
|
+
# Optional: Number of retries for failed requests (default: 3)
|
21
|
+
config.retries = 3
|
22
|
+
|
23
|
+
# Optional: Delay between retries in seconds (default: 1)
|
24
|
+
config.retry_delay = 1
|
25
|
+
|
26
|
+
# Optional: Default country code for phone number formatting (default: "233" for Ghana)
|
27
|
+
config.default_country_code = "233"
|
28
|
+
|
29
|
+
# Optional: Custom logger (default: Logger to STDOUT)
|
30
|
+
# Use Rails logger in Rails applications
|
31
|
+
config.logger = Rails.logger
|
32
|
+
|
33
|
+
# Optional: Set log level for Wittyflow-specific logs
|
34
|
+
# config.logger.level = Logger::INFO
|
35
|
+
end
|
36
|
+
|
37
|
+
# Validate configuration in development and production
|
38
|
+
if (Rails.env.development? || Rails.env.production?) && !Wittyflow.config.valid?
|
39
|
+
Rails.logger.warn "Wittyflow configuration is invalid. SMS functionality will not work."
|
40
|
+
Rails.logger.warn "Please ensure WITTYFLOW_APP_ID and WITTYFLOW_APP_SECRET are set."
|
41
|
+
end
|
42
|
+
|
43
|
+
# Example: Test the configuration in development
|
44
|
+
if Rails.env.development? && Wittyflow.config.valid?
|
45
|
+
Rails.application.configure do
|
46
|
+
config.after_initialize do
|
47
|
+
Wittyflow::Client.new
|
48
|
+
# You can uncomment the line below to test account balance on startup
|
49
|
+
# balance = client.account_balance
|
50
|
+
# Rails.logger.info "Wittyflow account balance: #{balance}"
|
51
|
+
Rails.logger.info "Wittyflow SMS gem initialized successfully"
|
52
|
+
rescue Wittyflow::Error => e
|
53
|
+
Rails.logger.warn "Wittyflow initialization test failed: #{e.message}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "timeout"
|
5
|
+
|
6
|
+
module Wittyflow
|
7
|
+
class Client
|
8
|
+
include HTTParty
|
9
|
+
|
10
|
+
attr_reader :app_id, :app_secret, :config
|
11
|
+
|
12
|
+
def initialize(app_id: nil, app_secret: nil, **options)
|
13
|
+
@app_id = app_id || Wittyflow.config.app_id
|
14
|
+
@app_secret = app_secret || Wittyflow.config.app_secret
|
15
|
+
@config = build_config(options)
|
16
|
+
|
17
|
+
validate_credentials!
|
18
|
+
setup_httparty
|
19
|
+
end
|
20
|
+
|
21
|
+
# Send regular SMS
|
22
|
+
def send_sms(from:, to:, message:)
|
23
|
+
send_message(from: from, to: to, message: message, type: :regular)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send flash SMS (appears directly on screen)
|
27
|
+
def send_flash_sms(from:, to:, message:)
|
28
|
+
send_message(from: from, to: to, message: message, type: :flash)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check SMS delivery status
|
32
|
+
def message_status(message_id)
|
33
|
+
validate_presence!(:message_id, message_id)
|
34
|
+
|
35
|
+
url = "#{config.api_endpoint}/messages/#{message_id}/retrieve"
|
36
|
+
params = auth_params
|
37
|
+
|
38
|
+
with_error_handling do
|
39
|
+
response = self.class.get(url, query: params, timeout: config.timeout)
|
40
|
+
parse_response(response)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get account balance
|
45
|
+
def account_balance
|
46
|
+
url = "#{config.api_endpoint}/account/balance"
|
47
|
+
params = auth_params
|
48
|
+
|
49
|
+
with_error_handling do
|
50
|
+
response = self.class.get(url, query: params, timeout: config.timeout)
|
51
|
+
parse_response(response)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Send bulk SMS to multiple recipients
|
56
|
+
def send_bulk_sms(from:, to:, message:, type: :regular)
|
57
|
+
validate_presence!(:from, from)
|
58
|
+
validate_presence!(:to, to)
|
59
|
+
validate_presence!(:message, message)
|
60
|
+
|
61
|
+
recipients = Array(to).map { |phone| format_phone_number(phone) }
|
62
|
+
|
63
|
+
recipients.map do |recipient|
|
64
|
+
send_message(from: from, to: recipient, message: message, type: type)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def build_config(options)
|
71
|
+
config = Configuration.new
|
72
|
+
config.api_endpoint = options[:api_endpoint] || Wittyflow.config.api_endpoint
|
73
|
+
config.timeout = options[:timeout] || Wittyflow.config.timeout
|
74
|
+
config.retries = options[:retries] || Wittyflow.config.retries
|
75
|
+
config.retry_delay = options[:retry_delay] || Wittyflow.config.retry_delay
|
76
|
+
config.logger = options[:logger] || Wittyflow.config.logger
|
77
|
+
config.default_country_code = options[:default_country_code] || Wittyflow.config.default_country_code
|
78
|
+
config
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup_httparty
|
82
|
+
self.class.base_uri config.api_endpoint
|
83
|
+
self.class.headers({
|
84
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
85
|
+
"Accept" => "application/json",
|
86
|
+
"User-Agent" => "Wittyflow Ruby Gem #{Wittyflow::VERSION}"
|
87
|
+
})
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_credentials!
|
91
|
+
raise ConfigurationError, "app_id is required" if app_id.nil? || app_id.empty?
|
92
|
+
raise ConfigurationError, "app_secret is required" if app_secret.nil? || app_secret.empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate_presence!(field, value)
|
96
|
+
raise ValidationError, "#{field} is required" if value.nil? || value.to_s.empty?
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_message(from:, to:, message:, type:)
|
100
|
+
validate_presence!(:from, from)
|
101
|
+
validate_presence!(:to, to)
|
102
|
+
validate_presence!(:message, message)
|
103
|
+
|
104
|
+
formatted_to = format_phone_number(to)
|
105
|
+
|
106
|
+
body = {
|
107
|
+
from: from.to_s,
|
108
|
+
to: formatted_to,
|
109
|
+
message: message.to_s,
|
110
|
+
type: type == :flash ? 0 : 1,
|
111
|
+
app_id: app_id,
|
112
|
+
app_secret: app_secret
|
113
|
+
}
|
114
|
+
|
115
|
+
url = "#{config.api_endpoint}/messages/send"
|
116
|
+
|
117
|
+
with_error_handling do
|
118
|
+
response = self.class.post(url, body: body, timeout: config.timeout)
|
119
|
+
parse_response(response)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def format_phone_number(phone)
|
124
|
+
phone_str = phone.to_s.strip
|
125
|
+
|
126
|
+
# Remove any non-digit characters except +
|
127
|
+
phone_str = phone_str.gsub(/[^\d+]/, "")
|
128
|
+
|
129
|
+
# Handle different phone number formats
|
130
|
+
case phone_str
|
131
|
+
when /^\+233/ # International format for Ghana
|
132
|
+
phone_str.sub(/^\+/, "")
|
133
|
+
when /^233/ # Ghana country code without +
|
134
|
+
phone_str
|
135
|
+
when /^0/ # Local format starting with 0
|
136
|
+
"#{config.default_country_code}#{phone_str[1..]}"
|
137
|
+
else # Assume it's a local number without leading 0
|
138
|
+
"#{config.default_country_code}#{phone_str}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def auth_params
|
143
|
+
{
|
144
|
+
app_id: app_id,
|
145
|
+
app_secret: app_secret
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def with_error_handling
|
150
|
+
attempts = 0
|
151
|
+
begin
|
152
|
+
attempts += 1
|
153
|
+
yield
|
154
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Timeout::Error => e
|
155
|
+
raise NetworkError, "Network timeout: #{e.message}"
|
156
|
+
rescue HTTParty::Error, SocketError => e
|
157
|
+
unless attempts < config.retries
|
158
|
+
raise NetworkError, "Network error after #{config.retries} attempts: #{e.message}"
|
159
|
+
end
|
160
|
+
|
161
|
+
config.logger&.warn(
|
162
|
+
"Request failed (attempt #{attempts}/#{config.retries}), " \
|
163
|
+
"retrying in #{config.retry_delay}s: #{e.message}"
|
164
|
+
)
|
165
|
+
sleep(config.retry_delay)
|
166
|
+
retry
|
167
|
+
rescue Wittyflow::Error => e
|
168
|
+
# Re-raise our custom errors without modification
|
169
|
+
raise e
|
170
|
+
rescue StandardError => e
|
171
|
+
raise APIError, "Unexpected error: #{e.message}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def parse_response(response)
|
176
|
+
case response.code
|
177
|
+
when 200..299
|
178
|
+
response.parsed_response
|
179
|
+
when 401
|
180
|
+
raise AuthenticationError, "Invalid app_id or app_secret"
|
181
|
+
when 400
|
182
|
+
error_message = response.parsed_response&.dig("message") || "Bad request"
|
183
|
+
raise ValidationError, error_message
|
184
|
+
when 429
|
185
|
+
raise APIError, "Rate limit exceeded"
|
186
|
+
when 500..599
|
187
|
+
raise APIError, "Server error (#{response.code})"
|
188
|
+
else
|
189
|
+
raise APIError, "Unexpected response (#{response.code}): #{response.body}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wittyflow
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :app_id, :app_secret, :api_endpoint, :timeout, :retries, :retry_delay, :logger, :default_country_code
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@api_endpoint = "https://api.wittyflow.com/v1"
|
9
|
+
@timeout = 30
|
10
|
+
@retries = 3
|
11
|
+
@retry_delay = 1
|
12
|
+
@logger = Logger.new($stdout, level: Logger::INFO)
|
13
|
+
@default_country_code = "233" # Ghana
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
!app_id.nil? && !app_secret.nil? && !app_id.empty? && !app_secret.empty?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wittyflow
|
4
|
+
# Backward compatibility wrapper for the old Sms class
|
5
|
+
# @deprecated Use Wittyflow::Client instead
|
6
|
+
class Sms
|
7
|
+
def initialize(app_id, app_secret)
|
8
|
+
warn "[DEPRECATION] Wittyflow::Sms is deprecated. Use Wittyflow::Client instead."
|
9
|
+
@client = Client.new(app_id: app_id, app_secret: app_secret)
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_sms(sender, receiver_phone_number, message_to_send)
|
13
|
+
@client.send_sms(from: sender, to: receiver_phone_number, message: message_to_send)
|
14
|
+
end
|
15
|
+
|
16
|
+
def send_flash_sms(sender, receiver_phone_number, message_to_send)
|
17
|
+
@client.send_flash_sms(from: sender, to: receiver_phone_number, message: message_to_send)
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_sms_status(sms_id)
|
21
|
+
@client.message_status(sms_id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def account_balance
|
25
|
+
@client.account_balance
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :client
|
31
|
+
end
|
32
|
+
end
|
data/lib/wittyflow/version.rb
CHANGED
data/lib/wittyflow.rb
CHANGED
@@ -1,76 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require
|
3
|
+
require "zeitwerk"
|
4
|
+
require "httparty"
|
5
|
+
require "logger"
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
class Sms
|
9
|
-
attr_accessor :app_id, :app_secret, :api_endpoint
|
10
|
-
|
11
|
-
def initialize(app_id, app_secret)
|
12
|
-
@app_id = app_id
|
13
|
-
@app_secret = app_secret
|
14
|
-
@api_endpoint = "https://api.wittyflow.com/v1"
|
15
|
-
end
|
16
|
-
|
17
|
-
def send_sms(sender, receiver_phone_number, message_to_send)
|
18
|
-
send_general_sms(sender, receiver_phone_number, message_to_send)
|
19
|
-
end
|
20
|
-
|
21
|
-
def send_flash_sms(sender, receiver_phone_number, message_to_send)
|
22
|
-
send_general_sms(sender, receiver_phone_number, message_to_send, is_flash=true)
|
23
|
-
end
|
24
|
-
|
25
|
-
def check_sms_status(sms_id)
|
26
|
-
make_request(form_sms_status_check_url(sms_id), {}, "get")
|
27
|
-
end
|
28
|
-
|
29
|
-
def account_balance
|
30
|
-
make_request(account_balance_url, {}, "get")
|
31
|
-
end
|
7
|
+
loader = Zeitwerk::Loader.for_gem
|
8
|
+
loader.setup
|
32
9
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
rescue => e
|
45
|
-
puts "ERROR: #{e}"
|
46
|
-
end
|
47
|
-
|
48
|
-
def form_send_sms_body(sender, receiver_phone_number, message_to_send, is_flash=false)
|
49
|
-
res = {
|
50
|
-
from: "#{sender}",
|
51
|
-
to: "233#{receiver_phone_number[1..-1]}",
|
52
|
-
type: 1,
|
53
|
-
message: "#{message_to_send}",
|
54
|
-
app_id: "#{@app_id}",
|
55
|
-
app_secret: "#{@app_secret}"
|
56
|
-
}
|
57
|
-
res[:type] = 0 if is_flash
|
58
|
-
res
|
59
|
-
end
|
60
|
-
|
61
|
-
def send_general_sms(sender, receiver_phone_number, message_to_send, is_flash=false)
|
62
|
-
body_to_send = form_send_sms_body(sender, receiver_phone_number, message_to_send, is_flash)
|
63
|
-
send_sms_endpoint = "#{@api_endpoint}/messages/send"
|
64
|
-
make_request(send_sms_endpoint, body_to_send)
|
65
|
-
end
|
10
|
+
module Wittyflow
|
11
|
+
class Error < StandardError; end
|
12
|
+
class ConfigurationError < Error; end
|
13
|
+
class APIError < Error; end
|
14
|
+
class NetworkError < Error; end
|
15
|
+
class AuthenticationError < Error; end
|
16
|
+
class ValidationError < Error; end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :configuration
|
20
|
+
end
|
66
21
|
|
67
|
-
|
68
|
-
|
69
|
-
|
22
|
+
def self.configure
|
23
|
+
self.configuration ||= Configuration.new
|
24
|
+
yield(configuration) if block_given?
|
25
|
+
configuration
|
26
|
+
end
|
70
27
|
|
71
|
-
|
72
|
-
|
73
|
-
end
|
28
|
+
def self.config
|
29
|
+
configuration || configure
|
74
30
|
end
|
75
31
|
|
32
|
+
# For backward compatibility
|
33
|
+
def self.new(app_id, app_secret, options = {})
|
34
|
+
Client.new(app_id: app_id, app_secret: app_secret, **options)
|
35
|
+
end
|
76
36
|
end
|
data/wittyflow-0.1.0.gem
ADDED
Binary file
|
data/wittyflow.gemspec
CHANGED
@@ -8,28 +8,51 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ["charlesagyemang"]
|
9
9
|
spec.email = ["micnkru@gmail.com"]
|
10
10
|
|
11
|
-
spec.summary = "
|
12
|
-
spec.description = "
|
11
|
+
spec.summary = "A production-ready Ruby gem for sending SMS via Wittyflow API"
|
12
|
+
spec.description = "Wittyflow is a robust, well-tested Ruby gem for integrating with the Wittyflow SMS service. " \
|
13
|
+
"Features include SMS sending, delivery tracking, account balance checking, retry logic, " \
|
14
|
+
"and comprehensive error handling."
|
13
15
|
spec.homepage = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
|
14
16
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">=
|
16
|
-
|
17
|
-
# spec.metadata["allowed_push_host"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
|
17
|
+
spec.required_ruby_version = ">= 3.0.0"
|
18
18
|
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
19
20
|
spec.metadata["homepage_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
|
20
21
|
spec.metadata["source_code_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
|
21
|
-
spec.metadata["changelog_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem"
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/blob/master/CHANGELOG.md"
|
23
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/issues"
|
24
|
+
spec.metadata["documentation_uri"] = "https://github.com/charlesagyemang/wittyflow_sms_ruby_gem/blob/master/README.md"
|
25
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
22
26
|
|
23
27
|
# Specify which files should be added to the gem when it is released.
|
24
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
28
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
-
`git ls-files -z`.split("\x0").reject
|
29
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
30
|
+
f.match(%r{\A(?:test|spec|features)/}) ||
|
31
|
+
f.start_with?(".") ||
|
32
|
+
f.match(/\A(Gemfile|Rakefile)/)
|
33
|
+
end
|
27
34
|
end
|
35
|
+
|
28
36
|
spec.bindir = "exe"
|
29
37
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
38
|
spec.require_paths = ["lib"]
|
31
39
|
|
32
|
-
#
|
33
|
-
spec.add_dependency "
|
34
|
-
|
40
|
+
# Runtime dependencies (alphabetically ordered)
|
41
|
+
spec.add_dependency "csv", "~> 3.2"
|
42
|
+
spec.add_dependency "httparty", "~> 0.21.0"
|
43
|
+
spec.add_dependency "zeitwerk", "~> 2.6"
|
44
|
+
|
45
|
+
# Development dependencies
|
46
|
+
spec.add_development_dependency "guard", "~> 2.18"
|
47
|
+
spec.add_development_dependency "guard-rspec", "~> 4.7"
|
48
|
+
spec.add_development_dependency "pry", "~> 0.14"
|
49
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
50
|
+
spec.add_development_dependency "rspec", "~> 3.12"
|
51
|
+
spec.add_development_dependency "rubocop", "~> 1.50"
|
52
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.16"
|
53
|
+
spec.add_development_dependency "rubocop-rspec", "~> 2.20"
|
54
|
+
spec.add_development_dependency "simplecov", "~> 0.22"
|
55
|
+
spec.add_development_dependency "vcr", "~> 6.1"
|
56
|
+
spec.add_development_dependency "webmock", "~> 3.18"
|
57
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
35
58
|
end
|