ovh-http2sms 0.1.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/.dockerignore +9 -0
- data/.env.example +12 -0
- data/CHANGELOG.md +50 -0
- data/Dockerfile +25 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +503 -0
- data/Rakefile +12 -0
- data/docker-compose.yml +29 -0
- data/lib/generators/ovh/http2sms/install_generator.rb +42 -0
- data/lib/generators/ovh/http2sms/templates/initializer.rb +52 -0
- data/lib/ovh/http2sms/client.rb +253 -0
- data/lib/ovh/http2sms/configuration.rb +141 -0
- data/lib/ovh/http2sms/errors.rb +148 -0
- data/lib/ovh/http2sms/gsm_encoding.rb +169 -0
- data/lib/ovh/http2sms/phone_number.rb +134 -0
- data/lib/ovh/http2sms/railtie.rb +23 -0
- data/lib/ovh/http2sms/response.rb +259 -0
- data/lib/ovh/http2sms/validators.rb +190 -0
- data/lib/ovh/http2sms/version.rb +7 -0
- data/lib/ovh/http2sms.rb +151 -0
- data/ovh-http2sms.gemspec +47 -0
- data/sig/ovh/http2sms.rbs +6 -0
- metadata +205 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Ovh
|
|
6
|
+
module Http2sms
|
|
7
|
+
module Generators
|
|
8
|
+
# Rails generator for OVH HTTP2SMS initializer
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# rails g ovh:http2sms:install
|
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
desc "Creates an OVH HTTP2SMS initializer file"
|
|
16
|
+
|
|
17
|
+
# Create the initializer file
|
|
18
|
+
def create_initializer_file
|
|
19
|
+
template "initializer.rb", "config/initializers/ovh_http2sms.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Show instructions after installation
|
|
23
|
+
def show_instructions
|
|
24
|
+
say ""
|
|
25
|
+
say "OVH HTTP2SMS initializer created!", :green
|
|
26
|
+
say ""
|
|
27
|
+
say "Next steps:"
|
|
28
|
+
say " 1. Edit config/initializers/ovh_http2sms.rb with your credentials"
|
|
29
|
+
say " 2. Or set environment variables: OVH_SMS_ACCOUNT, OVH_SMS_LOGIN, OVH_SMS_PASSWORD"
|
|
30
|
+
say " 3. Or use Rails credentials: rails credentials:edit"
|
|
31
|
+
say ""
|
|
32
|
+
say "Example credentials structure:"
|
|
33
|
+
say " ovh_sms:"
|
|
34
|
+
say " account: sms-xx11111-1"
|
|
35
|
+
say " login: your_login"
|
|
36
|
+
say " password: your_password"
|
|
37
|
+
say ""
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OVH HTTP2SMS configuration
|
|
4
|
+
#
|
|
5
|
+
# You can configure the gem using:
|
|
6
|
+
# 1. This initializer (direct values or Rails credentials)
|
|
7
|
+
# 2. Environment variables: OVH_SMS_ACCOUNT, OVH_SMS_LOGIN, OVH_SMS_PASSWORD, etc.
|
|
8
|
+
#
|
|
9
|
+
# Environment variables take precedence if this block doesn't set values.
|
|
10
|
+
|
|
11
|
+
Ovh::Http2sms.configure do |config|
|
|
12
|
+
# Required credentials
|
|
13
|
+
# Option 1: Direct configuration (not recommended for production)
|
|
14
|
+
# config.account = "sms-xx11111-1"
|
|
15
|
+
# config.login = "your_login"
|
|
16
|
+
# config.password = "your_password"
|
|
17
|
+
|
|
18
|
+
# Option 2: Using Rails credentials (recommended)
|
|
19
|
+
# Run: rails credentials:edit
|
|
20
|
+
# Add:
|
|
21
|
+
# ovh_sms:
|
|
22
|
+
# account: sms-xx11111-1
|
|
23
|
+
# login: your_login
|
|
24
|
+
# password: your_password
|
|
25
|
+
#
|
|
26
|
+
if Rails.application.credentials.ovh_sms.present?
|
|
27
|
+
config.account = Rails.application.credentials.dig(:ovh_sms, :account)
|
|
28
|
+
config.login = Rails.application.credentials.dig(:ovh_sms, :login)
|
|
29
|
+
config.password = Rails.application.credentials.dig(:ovh_sms, :password)
|
|
30
|
+
config.default_sender = Rails.application.credentials.dig(:ovh_sms, :default_sender)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Optional settings
|
|
34
|
+
|
|
35
|
+
# Default sender name (must be registered in OVH account)
|
|
36
|
+
# config.default_sender = "MyApp"
|
|
37
|
+
|
|
38
|
+
# Default country code for phone number formatting (default: "33" for France)
|
|
39
|
+
# config.default_country_code = "33"
|
|
40
|
+
|
|
41
|
+
# Response content type: application/json, text/xml, text/plain, text/html
|
|
42
|
+
# config.default_content_type = "application/json"
|
|
43
|
+
|
|
44
|
+
# HTTP timeout in seconds (default: 15)
|
|
45
|
+
# config.timeout = 15
|
|
46
|
+
|
|
47
|
+
# Raise error if message exceeds SMS length limits (default: true)
|
|
48
|
+
# config.raise_on_length_error = true
|
|
49
|
+
|
|
50
|
+
# Logger for debugging (uses Rails.logger by default in Rails apps)
|
|
51
|
+
config.logger = Rails.logger if defined?(Rails.logger)
|
|
52
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Ovh
|
|
7
|
+
module Http2sms
|
|
8
|
+
# HTTP client for OVH HTTP2SMS API
|
|
9
|
+
#
|
|
10
|
+
# Handles building requests, making HTTP calls, and processing responses.
|
|
11
|
+
# Thread-safe for use in multi-threaded environments.
|
|
12
|
+
#
|
|
13
|
+
# @example Direct usage
|
|
14
|
+
# client = Client.new(account: "sms-xx111-1", login: "user", password: "pass")
|
|
15
|
+
# response = client.deliver(to: "33601020304", message: "Hello!")
|
|
16
|
+
class Client
|
|
17
|
+
# @return [Configuration] Client configuration
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
# Initialize a new client
|
|
21
|
+
#
|
|
22
|
+
# @param options [Hash] Configuration options (overrides global config)
|
|
23
|
+
# @option options [String] :account SMS account identifier
|
|
24
|
+
# @option options [String] :login SMS user login
|
|
25
|
+
# @option options [String] :password SMS user password
|
|
26
|
+
# @option options [String] :default_sender Default sender name
|
|
27
|
+
# @option options [String] :default_content_type Response format
|
|
28
|
+
# @option options [Integer] :timeout HTTP timeout in seconds
|
|
29
|
+
# @option options [Logger] :logger Logger for debugging
|
|
30
|
+
# @option options [String] :default_country_code Default country code
|
|
31
|
+
def initialize(**options)
|
|
32
|
+
@config = build_config(options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Send an SMS message
|
|
36
|
+
#
|
|
37
|
+
# @param to [String, Array<String>] Recipient phone number(s)
|
|
38
|
+
# @param message [String] SMS content
|
|
39
|
+
# @param sender [String, nil] Sender name (uses default if nil)
|
|
40
|
+
# @param deferred [Time, String, nil] Scheduled send time
|
|
41
|
+
# @param tag [String, nil] Custom tag for tracking (max 20 chars)
|
|
42
|
+
# @param sms_class [Integer, nil] SMS class (0-3)
|
|
43
|
+
# @param sms_coding [Integer, nil] Encoding (1=7bit, 2=Unicode)
|
|
44
|
+
# @param no_stop [Boolean] Set to true for non-commercial SMS
|
|
45
|
+
# @param sender_for_response [Boolean] Enable reply capability
|
|
46
|
+
# @param content_type [String, nil] Response format (uses default if nil)
|
|
47
|
+
# @return [Response] Parsed response object
|
|
48
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
49
|
+
# @raise [ValidationError] if parameters are invalid
|
|
50
|
+
# @raise [NetworkError] if HTTP request fails
|
|
51
|
+
# @raise [AuthenticationError] if IP is not authorized
|
|
52
|
+
# @raise [MissingParameterError] if required parameter is missing
|
|
53
|
+
# @raise [InvalidParameterError] if parameter value is invalid
|
|
54
|
+
#
|
|
55
|
+
# @example Simple send
|
|
56
|
+
# client.deliver(to: "33601020304", message: "Hello!")
|
|
57
|
+
#
|
|
58
|
+
# @example With options
|
|
59
|
+
# client.deliver(
|
|
60
|
+
# to: ["33601020304", "33602030405"],
|
|
61
|
+
# message: "Meeting at 3pm",
|
|
62
|
+
# sender: "MyCompany",
|
|
63
|
+
# deferred: 1.hour.from_now,
|
|
64
|
+
# tag: "reminders"
|
|
65
|
+
# )
|
|
66
|
+
# rubocop:disable Metrics/ParameterLists
|
|
67
|
+
def deliver(to:, message:, sender: nil, deferred: nil, tag: nil,
|
|
68
|
+
sms_class: nil, sms_coding: nil, no_stop: false,
|
|
69
|
+
sender_for_response: false, content_type: nil)
|
|
70
|
+
# rubocop:enable Metrics/ParameterLists
|
|
71
|
+
@config.validate!
|
|
72
|
+
|
|
73
|
+
params = build_delivery_params(to, message, sender, deferred, tag, sms_class,
|
|
74
|
+
sms_coding, no_stop, sender_for_response)
|
|
75
|
+
Validators.validate!(params)
|
|
76
|
+
|
|
77
|
+
query_params = build_query_params(params, content_type)
|
|
78
|
+
execute_request(query_params, content_type)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ERROR_HANDLERS = {
|
|
82
|
+
401 => AuthenticationError,
|
|
83
|
+
201 => MissingParameterError,
|
|
84
|
+
202 => InvalidParameterError,
|
|
85
|
+
241 => SenderNotFoundError
|
|
86
|
+
}.freeze
|
|
87
|
+
private_constant :ERROR_HANDLERS
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# rubocop:disable Metrics/ParameterLists
|
|
92
|
+
def build_delivery_params(to, message, sender, deferred, tag, sms_class,
|
|
93
|
+
sms_coding, no_stop, sender_for_response)
|
|
94
|
+
# rubocop:enable Metrics/ParameterLists
|
|
95
|
+
{
|
|
96
|
+
to: to, message: message, sender: sender, deferred: deferred, tag: tag,
|
|
97
|
+
sms_class: sms_class, sms_coding: sms_coding, no_stop: no_stop,
|
|
98
|
+
sender_for_response: sender_for_response
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_config(options)
|
|
103
|
+
return Ovh::Http2sms.configuration.dup if options.empty?
|
|
104
|
+
|
|
105
|
+
Configuration.new.tap { |config| merge_config_options(config, options) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def merge_config_options(config, options)
|
|
109
|
+
global = Ovh::Http2sms.configuration
|
|
110
|
+
merge_credentials(config, options, global)
|
|
111
|
+
merge_defaults(config, options, global)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def merge_credentials(config, options, global)
|
|
115
|
+
config.account = options[:account] || global.account
|
|
116
|
+
config.login = options[:login] || global.login
|
|
117
|
+
config.password = options[:password] || global.password
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def merge_defaults(config, options, global)
|
|
121
|
+
config.default_sender = options[:default_sender] || global.default_sender
|
|
122
|
+
config.default_content_type = options[:default_content_type] || global.default_content_type
|
|
123
|
+
config.timeout = options[:timeout] || global.timeout
|
|
124
|
+
config.logger = options[:logger] || global.logger
|
|
125
|
+
config.default_country_code = options[:default_country_code] || global.default_country_code
|
|
126
|
+
config.raise_on_length_error = options.fetch(:raise_on_length_error, global.raise_on_length_error)
|
|
127
|
+
config.api_endpoint = options[:api_endpoint] || global.api_endpoint
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_query_params(params, content_type)
|
|
131
|
+
query = build_base_params(params)
|
|
132
|
+
add_sender_params(query, params)
|
|
133
|
+
add_optional_params(query, params)
|
|
134
|
+
query[:contentType] = content_type || @config.default_content_type
|
|
135
|
+
query
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_base_params(params)
|
|
139
|
+
{
|
|
140
|
+
account: @config.account,
|
|
141
|
+
login: @config.login,
|
|
142
|
+
password: @config.password,
|
|
143
|
+
to: format_recipients(params[:to]),
|
|
144
|
+
message: encode_message(params[:message])
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def add_sender_params(query, params)
|
|
149
|
+
if params[:sender_for_response]
|
|
150
|
+
query[:from] = ""
|
|
151
|
+
query[:senderForResponse] = "1"
|
|
152
|
+
else
|
|
153
|
+
sender = params[:sender] || @config.default_sender
|
|
154
|
+
query[:from] = sender if sender
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def add_optional_params(query, params)
|
|
159
|
+
query[:deferred] = format_deferred(params[:deferred]) if params[:deferred]
|
|
160
|
+
query[:tag] = params[:tag] if params[:tag]
|
|
161
|
+
query[:class] = params[:sms_class].to_s if params[:sms_class]
|
|
162
|
+
query[:smsCoding] = params[:sms_coding].to_s if params[:sms_coding]
|
|
163
|
+
query[:noStop] = "1" if params[:no_stop]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format_recipients(to)
|
|
167
|
+
phones = PhoneNumber.format_multiple(to, country_code: @config.default_country_code)
|
|
168
|
+
phones.join(",")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def encode_message(message)
|
|
172
|
+
# URL encoding is handled by Faraday, but we need to handle line breaks
|
|
173
|
+
# OVH uses %0d for line breaks in the URL
|
|
174
|
+
message.to_s.gsub("\n", "%0d").gsub("\r", "")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def format_deferred(deferred)
|
|
178
|
+
if deferred.respond_to?(:strftime)
|
|
179
|
+
# Format Time/DateTime as hhmmddMMYYYY
|
|
180
|
+
deferred.strftime("%H%M%d%m%Y")
|
|
181
|
+
else
|
|
182
|
+
deferred.to_s
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def execute_request(query_params, content_type)
|
|
187
|
+
log_request(query_params)
|
|
188
|
+
|
|
189
|
+
response = make_http_request(query_params)
|
|
190
|
+
parsed_response = parse_response(response, content_type)
|
|
191
|
+
|
|
192
|
+
log_response(parsed_response)
|
|
193
|
+
handle_error_response(parsed_response) if parsed_response.failure?
|
|
194
|
+
|
|
195
|
+
parsed_response
|
|
196
|
+
rescue Faraday::Error => e
|
|
197
|
+
raise NetworkError.new(
|
|
198
|
+
"HTTP request failed: #{e.message}",
|
|
199
|
+
original_error: e
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def make_http_request(query_params)
|
|
204
|
+
connection.get do |req|
|
|
205
|
+
req.params = query_params
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def connection
|
|
210
|
+
@connection ||= Faraday.new(url: @config.api_endpoint) do |faraday|
|
|
211
|
+
faraday.options.timeout = @config.timeout
|
|
212
|
+
faraday.options.open_timeout = @config.timeout
|
|
213
|
+
faraday.adapter Faraday.default_adapter
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_response(http_response, content_type)
|
|
218
|
+
Response.parse(
|
|
219
|
+
http_response.body,
|
|
220
|
+
content_type: content_type || @config.default_content_type
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def handle_error_response(response)
|
|
225
|
+
error_class = ERROR_HANDLERS[response.status]
|
|
226
|
+
return unless error_class
|
|
227
|
+
|
|
228
|
+
message = response.error_message || default_error_message(response.status)
|
|
229
|
+
raise error_class.new(message, raw_response: response.raw_response)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def default_error_message(status)
|
|
233
|
+
status == 401 ? "IP not authorized" : nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def log_request(params)
|
|
237
|
+
return unless @config.logger
|
|
238
|
+
|
|
239
|
+
safe_params = params.dup
|
|
240
|
+
safe_params[:password] = "[FILTERED]"
|
|
241
|
+
@config.logger.debug("[OVH HTTP2SMS] Request: #{safe_params}")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def log_response(response)
|
|
245
|
+
return unless @config.logger
|
|
246
|
+
|
|
247
|
+
@config.logger.debug(
|
|
248
|
+
"[OVH HTTP2SMS] Response: status=#{response.status} success=#{response.success?}"
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ovh
|
|
4
|
+
module Http2sms
|
|
5
|
+
# Configuration class for OVH HTTP2SMS gem
|
|
6
|
+
#
|
|
7
|
+
# @example Block configuration
|
|
8
|
+
# Ovh::Http2sms.configure do |config|
|
|
9
|
+
# config.account = "sms-xx11111-1"
|
|
10
|
+
# config.login = "user"
|
|
11
|
+
# config.password = "secret"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Environment variables
|
|
15
|
+
# # Set OVH_SMS_ACCOUNT, OVH_SMS_LOGIN, OVH_SMS_PASSWORD
|
|
16
|
+
# Ovh::Http2sms.deliver(to: "33601020304", message: "Hello!")
|
|
17
|
+
#
|
|
18
|
+
class Configuration
|
|
19
|
+
# @return [String, nil] SMS account identifier (ex: sms-xx11111-1)
|
|
20
|
+
attr_accessor :account
|
|
21
|
+
|
|
22
|
+
# @return [String, nil] SMS user login
|
|
23
|
+
attr_accessor :login
|
|
24
|
+
|
|
25
|
+
# @return [String, nil] SMS user password
|
|
26
|
+
attr_accessor :password
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] Default sender name
|
|
29
|
+
attr_accessor :default_sender
|
|
30
|
+
|
|
31
|
+
# @return [String] Default response content type
|
|
32
|
+
attr_accessor :default_content_type
|
|
33
|
+
|
|
34
|
+
# @return [Integer] HTTP request timeout in seconds
|
|
35
|
+
attr_accessor :timeout
|
|
36
|
+
|
|
37
|
+
# @return [Logger, nil] Optional logger for debugging
|
|
38
|
+
attr_accessor :logger
|
|
39
|
+
|
|
40
|
+
# @return [String] Default country code for phone number formatting
|
|
41
|
+
attr_accessor :default_country_code
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] Whether to raise errors on message length violations
|
|
44
|
+
attr_accessor :raise_on_length_error
|
|
45
|
+
|
|
46
|
+
# @return [String] API endpoint URL
|
|
47
|
+
attr_accessor :api_endpoint
|
|
48
|
+
|
|
49
|
+
# Environment variable prefix
|
|
50
|
+
ENV_PREFIX = "OVH_SMS_"
|
|
51
|
+
|
|
52
|
+
# Default values
|
|
53
|
+
DEFAULTS = {
|
|
54
|
+
default_content_type: "application/json",
|
|
55
|
+
timeout: 15,
|
|
56
|
+
default_country_code: "33",
|
|
57
|
+
raise_on_length_error: true,
|
|
58
|
+
api_endpoint: "https://www.ovh.com/cgi-bin/sms/http2sms.cgi"
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
def initialize
|
|
62
|
+
reset!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Reset configuration to defaults and environment variables
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
def reset!
|
|
69
|
+
# Set defaults
|
|
70
|
+
DEFAULTS.each do |key, value|
|
|
71
|
+
send("#{key}=", value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Clear credentials
|
|
75
|
+
self.account = nil
|
|
76
|
+
self.login = nil
|
|
77
|
+
self.password = nil
|
|
78
|
+
self.default_sender = nil
|
|
79
|
+
self.logger = nil
|
|
80
|
+
|
|
81
|
+
# Load from environment variables
|
|
82
|
+
load_from_env
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if the configuration is valid for making API requests
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] true if required fields are present
|
|
88
|
+
def valid?
|
|
89
|
+
!account.nil? && !account.empty? &&
|
|
90
|
+
!login.nil? && !login.empty? &&
|
|
91
|
+
!password.nil? && !password.empty?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Validate configuration and raise error if invalid
|
|
95
|
+
#
|
|
96
|
+
# @raise [ConfigurationError] if configuration is invalid
|
|
97
|
+
# @return [void]
|
|
98
|
+
def validate!
|
|
99
|
+
missing = find_missing_credentials
|
|
100
|
+
return if missing.empty?
|
|
101
|
+
|
|
102
|
+
raise ConfigurationError, build_validation_error_message(missing)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def find_missing_credentials
|
|
106
|
+
missing = []
|
|
107
|
+
missing << "account" if blank?(account)
|
|
108
|
+
missing << "login" if blank?(login)
|
|
109
|
+
missing << "password" if blank?(password)
|
|
110
|
+
missing
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def blank?(value)
|
|
114
|
+
value.nil? || value.empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_validation_error_message(missing)
|
|
118
|
+
env_vars = missing.map { |m| "#{ENV_PREFIX}#{m.upcase}" }.join(", ")
|
|
119
|
+
"Missing required configuration: #{missing.join(", ")}. " \
|
|
120
|
+
"Set via Ovh::Http2sms.configure block or environment variables (#{env_vars})"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def load_from_env
|
|
126
|
+
self.account ||= ENV.fetch("#{ENV_PREFIX}ACCOUNT", nil)
|
|
127
|
+
self.login ||= ENV.fetch("#{ENV_PREFIX}LOGIN", nil)
|
|
128
|
+
self.password ||= ENV.fetch("#{ENV_PREFIX}PASSWORD", nil)
|
|
129
|
+
self.default_sender ||= ENV.fetch("#{ENV_PREFIX}DEFAULT_SENDER", nil)
|
|
130
|
+
self.default_content_type = ENV.fetch("#{ENV_PREFIX}DEFAULT_CONTENT_TYPE", default_content_type)
|
|
131
|
+
self.default_country_code = ENV.fetch("#{ENV_PREFIX}DEFAULT_COUNTRY_CODE", default_country_code)
|
|
132
|
+
|
|
133
|
+
env_timeout = ENV.fetch("#{ENV_PREFIX}TIMEOUT", nil)
|
|
134
|
+
self.timeout = env_timeout.to_i if env_timeout
|
|
135
|
+
|
|
136
|
+
env_raise_on_length = ENV.fetch("#{ENV_PREFIX}RAISE_ON_LENGTH_ERROR", nil)
|
|
137
|
+
self.raise_on_length_error = env_raise_on_length != "false" if env_raise_on_length
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ovh
|
|
4
|
+
module Http2sms
|
|
5
|
+
# Base error class for all OVH HTTP2SMS errors
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
# @return [Integer, nil] API status code if available
|
|
8
|
+
attr_reader :status_code
|
|
9
|
+
|
|
10
|
+
# @return [String, nil] Raw API response if available
|
|
11
|
+
attr_reader :raw_response
|
|
12
|
+
|
|
13
|
+
def initialize(message = nil, status_code: nil, raw_response: nil)
|
|
14
|
+
@status_code = status_code
|
|
15
|
+
@raw_response = raw_response
|
|
16
|
+
super(message)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised when configuration is missing or invalid
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# raise ConfigurationError, "Missing required configuration: account"
|
|
24
|
+
class ConfigurationError < Error; end
|
|
25
|
+
|
|
26
|
+
# Raised when authentication fails (API status 401)
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# raise AuthenticationError.new("IP not authorized", status_code: 401)
|
|
30
|
+
class AuthenticationError < Error
|
|
31
|
+
def initialize(message = "Authentication failed: IP not authorized", **options)
|
|
32
|
+
super(message, **options.merge(status_code: 401))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Raised when a required parameter is missing (API status 201)
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# raise MissingParameterError.new("Missing message")
|
|
40
|
+
class MissingParameterError < Error
|
|
41
|
+
# @return [String, nil] Name of the missing parameter
|
|
42
|
+
attr_reader :parameter
|
|
43
|
+
|
|
44
|
+
def initialize(message = "Missing required parameter", parameter: nil, **options)
|
|
45
|
+
@parameter = parameter
|
|
46
|
+
super(message, **options.merge(status_code: 201))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Raised when a parameter value is invalid (API status 202)
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# raise InvalidParameterError.new("Invalid tag: is too long", parameter: "tag")
|
|
54
|
+
class InvalidParameterError < Error
|
|
55
|
+
# @return [String, nil] Name of the invalid parameter
|
|
56
|
+
attr_reader :parameter
|
|
57
|
+
|
|
58
|
+
def initialize(message = "Invalid parameter", parameter: nil, **options)
|
|
59
|
+
@parameter = parameter
|
|
60
|
+
super(message, **options.merge(status_code: 202))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Raised when a network error occurs (timeout, connection failure, etc.)
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# raise NetworkError.new("Connection timed out")
|
|
68
|
+
class NetworkError < Error
|
|
69
|
+
# @return [Exception, nil] Original exception that caused the network error
|
|
70
|
+
attr_reader :original_error
|
|
71
|
+
|
|
72
|
+
def initialize(message = "Network error occurred", original_error: nil, **options)
|
|
73
|
+
@original_error = original_error
|
|
74
|
+
super(message, **options)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Raised when message length exceeds SMS limits
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# raise MessageLengthError.new("Message exceeds GSM encoding limit", encoding: :gsm, length: 161)
|
|
82
|
+
class MessageLengthError < Error
|
|
83
|
+
# @return [Symbol] Encoding type (:gsm or :unicode)
|
|
84
|
+
attr_reader :encoding
|
|
85
|
+
|
|
86
|
+
# @return [Integer] Actual message length
|
|
87
|
+
attr_reader :length
|
|
88
|
+
|
|
89
|
+
# @return [Integer] Maximum allowed length
|
|
90
|
+
attr_reader :max_length
|
|
91
|
+
|
|
92
|
+
def initialize(message = "Message length error", encoding: nil, length: nil, max_length: nil, **options)
|
|
93
|
+
@encoding = encoding
|
|
94
|
+
@length = length
|
|
95
|
+
@max_length = max_length
|
|
96
|
+
super(message, **options)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Raised when phone number format is invalid
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# raise PhoneNumberError.new("Invalid phone number format", phone_number: "abc123")
|
|
104
|
+
class PhoneNumberError < Error
|
|
105
|
+
# @return [String, nil] The invalid phone number
|
|
106
|
+
attr_reader :phone_number
|
|
107
|
+
|
|
108
|
+
def initialize(message = "Invalid phone number", phone_number: nil, **options)
|
|
109
|
+
@phone_number = phone_number
|
|
110
|
+
super(message, **options)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Raised when validation fails before sending
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# raise ValidationError.new("Tag exceeds maximum length of 20 characters")
|
|
118
|
+
class ValidationError < Error; end
|
|
119
|
+
|
|
120
|
+
# Raised when the sender does not exist (API status 241)
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# raise SenderNotFoundError.new("Sender 'MyApp' not found")
|
|
124
|
+
class SenderNotFoundError < Error
|
|
125
|
+
# @return [String, nil] The sender that was not found
|
|
126
|
+
attr_reader :sender
|
|
127
|
+
|
|
128
|
+
def initialize(message = "Sender not found", sender: nil, **options)
|
|
129
|
+
@sender = sender
|
|
130
|
+
super(message, **options.merge(status_code: 241))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Raised when the API response cannot be parsed
|
|
135
|
+
#
|
|
136
|
+
# @example
|
|
137
|
+
# raise ResponseParseError.new("Invalid JSON", raw_response: "not json")
|
|
138
|
+
class ResponseParseError < Error
|
|
139
|
+
# @return [String, nil] The content type that failed to parse
|
|
140
|
+
attr_reader :content_type
|
|
141
|
+
|
|
142
|
+
def initialize(message = "Failed to parse API response", content_type: nil, **options)
|
|
143
|
+
@content_type = content_type
|
|
144
|
+
super(message, **options)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|