arafa 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/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +174 -0
- data/Rakefile +12 -0
- data/implementation_plan.md +610 -0
- data/lib/arafa/airtime/africas_talking.rb +107 -0
- data/lib/arafa/airtime/base.rb +107 -0
- data/lib/arafa/airtime/wasiliana.rb +95 -0
- data/lib/arafa/configuration.rb +33 -0
- data/lib/arafa/contracts/africas_talking_airtime_contract.rb +29 -0
- data/lib/arafa/contracts/africas_talking_contract.rb +27 -0
- data/lib/arafa/contracts/airtime_contract.rb +52 -0
- data/lib/arafa/contracts/base_contract.rb +21 -0
- data/lib/arafa/contracts/mobile_data_contract.rb +60 -0
- data/lib/arafa/contracts/wasiliana_contract.rb +30 -0
- data/lib/arafa/data/africas_talking.rb +157 -0
- data/lib/arafa/errors.rb +23 -0
- data/lib/arafa/message.rb +17 -0
- data/lib/arafa/phone_number.rb +25 -0
- data/lib/arafa/providers/africas_talking.rb +116 -0
- data/lib/arafa/providers/base.rb +118 -0
- data/lib/arafa/providers/wasiliana.rb +98 -0
- data/lib/arafa/schemas/airtime_request_schema.rb +28 -0
- data/lib/arafa/schemas/delivery_report_schema.rb +15 -0
- data/lib/arafa/schemas/ussd_callback_schema.rb +17 -0
- data/lib/arafa/send_result.rb +37 -0
- data/lib/arafa/types.rb +21 -0
- data/lib/arafa/ussd/africas_talking.rb +26 -0
- data/lib/arafa/ussd/request.rb +41 -0
- data/lib/arafa/ussd/response.rb +25 -0
- data/lib/arafa/ussd/wasiliana.rb +26 -0
- data/lib/arafa/version.rb +5 -0
- data/lib/arafa/whatsapp/africas_talking.rb +174 -0
- data/lib/arafa/whatsapp/message_body.rb +146 -0
- data/lib/arafa.rb +68 -0
- data/sig/arafa.rbs +4 -0
- metadata +209 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../schemas/ussd_callback_schema"
|
|
4
|
+
|
|
5
|
+
module Arafa
|
|
6
|
+
module Ussd
|
|
7
|
+
# Parses Africa's Talking' form-encoded USSD callback params (`sessionId`,
|
|
8
|
+
# `phoneNumber`, `serviceCode`, `text`, `networkCode`) into the shared
|
|
9
|
+
# `Schemas::UssdCallbackSchema` shape. `networkCode` has no equivalent in
|
|
10
|
+
# the canonical shape and is dropped.
|
|
11
|
+
module AfricasTalking
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def parse(params)
|
|
15
|
+
params = params.transform_keys(&:to_s)
|
|
16
|
+
|
|
17
|
+
Schemas::UssdCallbackSchema.call(
|
|
18
|
+
session_id: params["sessionId"],
|
|
19
|
+
phone_number: params["phoneNumber"],
|
|
20
|
+
service_code: params["serviceCode"],
|
|
21
|
+
text: params["text"]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/struct"
|
|
4
|
+
|
|
5
|
+
require_relative "../types"
|
|
6
|
+
require_relative "../errors"
|
|
7
|
+
require_relative "africas_talking"
|
|
8
|
+
require_relative "wasiliana"
|
|
9
|
+
|
|
10
|
+
module Arafa
|
|
11
|
+
module Ussd
|
|
12
|
+
# Normalized inbound USSD request, built after a provider's raw webhook
|
|
13
|
+
# callback params have been remapped onto the shared
|
|
14
|
+
# `Schemas::UssdCallbackSchema` keys and validated. Parse/build only —
|
|
15
|
+
# no outbound HTTP call.
|
|
16
|
+
class Request < Dry::Struct
|
|
17
|
+
attribute :session_id, Types::String
|
|
18
|
+
attribute :phone_number, Types::String
|
|
19
|
+
attribute :service_code, Types::String
|
|
20
|
+
attribute :text, Types::String.optional
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def from_africas_talking(params)
|
|
24
|
+
build(Ussd::AfricasTalking.parse(params))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def from_wasiliana(params)
|
|
28
|
+
build(Ussd::Wasiliana.parse(params))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def build(result)
|
|
34
|
+
raise Arafa::ValidationError, result.errors.to_h if result.failure?
|
|
35
|
+
|
|
36
|
+
new(**result.to_h)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arafa
|
|
4
|
+
module Ussd
|
|
5
|
+
# Builds the `CON `/`END `-prefixed plain-text reply body both providers'
|
|
6
|
+
# USSD callback contract expects within the 10-second response window
|
|
7
|
+
# (§1.2, §1.5). Not a network client — the adapter's own web framework is
|
|
8
|
+
# responsible for actually returning this body with `Content-Type:
|
|
9
|
+
# text/plain`.
|
|
10
|
+
module Response
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Session continues; the gateway will prompt the subscriber again and
|
|
14
|
+
# POST their next input as a new callback.
|
|
15
|
+
def continue(text)
|
|
16
|
+
"CON #{text}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Session terminates; this is the final screen shown to the subscriber.
|
|
20
|
+
def end(text)
|
|
21
|
+
"END #{text}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../schemas/ussd_callback_schema"
|
|
4
|
+
|
|
5
|
+
module Arafa
|
|
6
|
+
module Ussd
|
|
7
|
+
# Parses Wasiliana's JSON USSD callback params (`sessionId`,
|
|
8
|
+
# `phoneNumber`, `serviceCode`, `text`, `ussdId`) into the shared
|
|
9
|
+
# `Schemas::UssdCallbackSchema` shape. `ussdId` has no equivalent in the
|
|
10
|
+
# canonical shape and is dropped.
|
|
11
|
+
module Wasiliana
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def parse(params)
|
|
15
|
+
params = params.transform_keys(&:to_s)
|
|
16
|
+
|
|
17
|
+
Schemas::UssdCallbackSchema.call(
|
|
18
|
+
session_id: params["sessionId"],
|
|
19
|
+
phone_number: params["phoneNumber"],
|
|
20
|
+
service_code: params["serviceCode"],
|
|
21
|
+
text: params["text"]
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/initializer"
|
|
4
|
+
require "dry/monads"
|
|
5
|
+
require "dry/validation"
|
|
6
|
+
|
|
7
|
+
require_relative "../types"
|
|
8
|
+
require_relative "../errors"
|
|
9
|
+
require_relative "../send_result"
|
|
10
|
+
require_relative "../providers/base"
|
|
11
|
+
require_relative "message_body"
|
|
12
|
+
|
|
13
|
+
module Arafa
|
|
14
|
+
module Contracts
|
|
15
|
+
# Structural + business rules for an outgoing WhatsApp send (§1.13).
|
|
16
|
+
# `body`'s own shape is already validated by `Whatsapp::MessageBody`
|
|
17
|
+
# (a `Dry::Struct` raises on construction), so this only covers the
|
|
18
|
+
# envelope fields shared by every message kind.
|
|
19
|
+
class WhatsappContract < Dry::Validation::Contract
|
|
20
|
+
option :username, default: -> { Arafa.config.africas_talking.username }
|
|
21
|
+
option :wa_number, default: -> { Arafa.config.africas_talking.wa_number }
|
|
22
|
+
|
|
23
|
+
params do
|
|
24
|
+
required(:phone_number).filled(:string)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule do
|
|
28
|
+
base.failure("Africa's Talking username is not configured") if username.to_s.empty?
|
|
29
|
+
base.failure("Africa's Talking WhatsApp number is not configured") if wa_number.to_s.empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module Whatsapp
|
|
35
|
+
# Africa's Talking WhatsApp adapter (§1.13). Unlike bulk SMS/airtime/
|
|
36
|
+
# mobile data, AT's WhatsApp API sends to one recipient per request
|
|
37
|
+
# (`phoneNumber` is a single string, not an array), so `SendResult` here
|
|
38
|
+
# always carries exactly one recipient. All five documented message
|
|
39
|
+
# kinds are supported — see `Whatsapp::MessageBody`.
|
|
40
|
+
class AfricasTalking
|
|
41
|
+
extend Dry::Initializer
|
|
42
|
+
include Dry::Monads[:result]
|
|
43
|
+
|
|
44
|
+
CONTRACT = Contracts::WhatsappContract
|
|
45
|
+
BODY_TYPE = Types.Instance(MessageBody::Text) | Types.Instance(MessageBody::Media) |
|
|
46
|
+
Types.Instance(MessageBody::Template) | Types.Instance(MessageBody::Interactive::Buttons) |
|
|
47
|
+
Types.Instance(MessageBody::Interactive::List)
|
|
48
|
+
|
|
49
|
+
option :phone_number, type: Types::PhoneNumber
|
|
50
|
+
option :body, type: BODY_TYPE
|
|
51
|
+
option :username, optional: true, type: Types::String.optional, default: -> { Arafa.config.africas_talking.username }
|
|
52
|
+
option :wa_number, optional: true, type: Types::String.optional, default: -> { Arafa.config.africas_talking.wa_number }
|
|
53
|
+
option :idempotency_key, optional: true, type: Types::String.optional, default: -> { nil }
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def send(*args, **kwargs)
|
|
57
|
+
return super if args.any? || kwargs.empty?
|
|
58
|
+
|
|
59
|
+
new(**kwargs).send
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Reuses `Providers::Base`'s connection (retry, JSON, apiKey-redacting
|
|
63
|
+
# logger) rather than building a second one.
|
|
64
|
+
def connection
|
|
65
|
+
Providers::Base.connection
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Contract → HTTP call → response parsing. Returns a
|
|
70
|
+
# `Dry::Monads::Result`: `Success(SendResult)` or
|
|
71
|
+
# `Failure(Arafa::Error subclass)`.
|
|
72
|
+
def send(*args, **kwargs)
|
|
73
|
+
return super if args.any? || kwargs.any?
|
|
74
|
+
|
|
75
|
+
validation = validate_contract
|
|
76
|
+
return validation if validation.failure?
|
|
77
|
+
|
|
78
|
+
response = connection.post(endpoint, build_payload, headers)
|
|
79
|
+
parse_response(response)
|
|
80
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
81
|
+
Failure(Arafa::NetworkError.new(e.message))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def connection
|
|
85
|
+
self.class.connection
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def endpoint
|
|
89
|
+
"https://chat.africastalking.com/whatsapp/message/send"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# AT's WhatsApp API is the one product that spells this header
|
|
93
|
+
# lowercase (`apikey`) instead of `apiKey`.
|
|
94
|
+
def headers
|
|
95
|
+
base = {
|
|
96
|
+
"apikey" => Arafa.config.africas_talking.api_key,
|
|
97
|
+
"Content-Type" => "application/json",
|
|
98
|
+
"Accept" => "application/json"
|
|
99
|
+
}
|
|
100
|
+
idempotency_key.nil? ? base : base.merge("Idempotency-Key" => idempotency_key)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_payload
|
|
104
|
+
{ username: username, waNumber: wa_number, phoneNumber: phone_number, body: body.to_h }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_response(response)
|
|
108
|
+
return build_failure(response) unless response.status.between?(200, 299)
|
|
109
|
+
|
|
110
|
+
body = response.body
|
|
111
|
+
status = body["status"]
|
|
112
|
+
return Failure(Arafa::InvalidRequestError.new(body["errorMessage"] || body.to_s)) if status.nil?
|
|
113
|
+
|
|
114
|
+
build_success(body, status)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def build_success(body, status)
|
|
120
|
+
recipients = [build_recipient(body, status)]
|
|
121
|
+
Success(SendResult.new(provider: :whatsapp_africas_talking, recipients: recipients, raw: body))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_recipient(body, status)
|
|
125
|
+
success = status != "FAILED"
|
|
126
|
+
|
|
127
|
+
SendResult::Recipient.new(
|
|
128
|
+
number: body["phoneNumber"] || phone_number,
|
|
129
|
+
success: success,
|
|
130
|
+
message_id: body["messageId"],
|
|
131
|
+
status: status,
|
|
132
|
+
error: success ? nil : (body["errorMessage"] || status)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
ERROR_CLASSES_BY_STATUS = {
|
|
137
|
+
401 => Arafa::AuthenticationError,
|
|
138
|
+
400 => Arafa::InvalidRequestError,
|
|
139
|
+
403 => Arafa::InvalidRequestError,
|
|
140
|
+
406 => Arafa::InvalidRequestError,
|
|
141
|
+
429 => Arafa::RateLimitError,
|
|
142
|
+
500 => Arafa::ProviderServerError,
|
|
143
|
+
501 => Arafa::ProviderServerError,
|
|
144
|
+
502 => Arafa::ProviderServerError,
|
|
145
|
+
503 => Arafa::ProviderServerError
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
148
|
+
def build_failure(response)
|
|
149
|
+
error_class = ERROR_CLASSES_BY_STATUS.fetch(response.status, Arafa::InvalidRequestError)
|
|
150
|
+
Failure(error_class.new(error_message(response)))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def error_message(response)
|
|
154
|
+
body = response.body
|
|
155
|
+
return body.to_s unless body.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
body["errorMessage"] || body.to_s
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_contract
|
|
161
|
+
result = self.class::CONTRACT.new(username: username, wa_number: wa_number).call(contract_params)
|
|
162
|
+
return Failure(Arafa::ValidationError.new(result.errors.to_h)) if result.failure?
|
|
163
|
+
|
|
164
|
+
Success(result)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def contract_params
|
|
168
|
+
{ phone_number: phone_number }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
Arafa.register(:whatsapp_africas_talking, Arafa::Whatsapp::AfricasTalking)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/struct"
|
|
4
|
+
|
|
5
|
+
require_relative "../types"
|
|
6
|
+
|
|
7
|
+
module Arafa
|
|
8
|
+
module Whatsapp
|
|
9
|
+
# Sum type over WhatsApp's polymorphic `body` field (§1.13). Each message
|
|
10
|
+
# kind gets its own `Dry::Struct` (coerced/validated the moment it's
|
|
11
|
+
# built) instead of one contract with every field across every kind
|
|
12
|
+
# marked optional — `Whatsapp::AfricasTalking#body` accepts any of these
|
|
13
|
+
# and calls `#to_h` to get the wire shape.
|
|
14
|
+
module MessageBody
|
|
15
|
+
# Plain-text WhatsApp message body: `{message}`.
|
|
16
|
+
class Text < Dry::Struct
|
|
17
|
+
attribute :message, Types::String.constrained(min_size: 1)
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{ message: message }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Media WhatsApp message body: `{mediaType, url, caption}`.
|
|
25
|
+
class Media < Dry::Struct
|
|
26
|
+
MEDIA_TYPES = %w[Image Video Audio Sticker Document Voice].freeze
|
|
27
|
+
|
|
28
|
+
attribute :media_type, Types::String.enum(*MEDIA_TYPES)
|
|
29
|
+
attribute :url, Types::String.constrained(min_size: 1)
|
|
30
|
+
attribute? :caption, Types::String.optional.default(nil)
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
body = { mediaType: media_type, url: url }
|
|
34
|
+
body[:caption] = caption unless caption.nil?
|
|
35
|
+
body
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Template WhatsApp message body: `{templateId, headerValue, bodyValues}`.
|
|
40
|
+
class Template < Dry::Struct
|
|
41
|
+
attribute :template_id, Types::String.constrained(min_size: 1)
|
|
42
|
+
attribute :body_values, Types::Array.of(Types::String)
|
|
43
|
+
attribute? :header_value, Types::String.optional.default(nil)
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
body = { templateId: template_id, bodyValues: body_values }
|
|
47
|
+
body[:headerValue] = header_value unless header_value.nil?
|
|
48
|
+
body
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Nested types shared by the two interactive message kinds.
|
|
53
|
+
module Interactive
|
|
54
|
+
# One tappable button: `{id, title}`.
|
|
55
|
+
class Button < Dry::Struct
|
|
56
|
+
attribute :id, Types::String.constrained(min_size: 1)
|
|
57
|
+
attribute :title, Types::String.constrained(min_size: 1)
|
|
58
|
+
|
|
59
|
+
def to_h
|
|
60
|
+
{ id: id, title: title }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# One row of an interactive list section: `{id, title, description}`.
|
|
65
|
+
class Row < Dry::Struct
|
|
66
|
+
attribute :id, Types::String.constrained(min_size: 1)
|
|
67
|
+
attribute :title, Types::String.constrained(min_size: 1)
|
|
68
|
+
attribute? :description, Types::String.optional.default(nil)
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
body = { id: id, title: title }
|
|
72
|
+
body[:description] = description unless description.nil?
|
|
73
|
+
body
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# One section of an interactive list: `{title, rows, product_items}`.
|
|
78
|
+
class Section < Dry::Struct
|
|
79
|
+
attribute :title, Types::String.constrained(min_size: 1)
|
|
80
|
+
attribute :rows, Types::Array.of(Row)
|
|
81
|
+
attribute? :product_items, Types::Array.optional.default(nil)
|
|
82
|
+
|
|
83
|
+
def to_h
|
|
84
|
+
body = { title: title, rows: rows.map(&:to_h) }
|
|
85
|
+
body[:product_items] = product_items unless product_items.nil?
|
|
86
|
+
body
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Interactive buttons WhatsApp message body:
|
|
91
|
+
# `{action: {buttons}, body: {text}, header: {text}}`.
|
|
92
|
+
class Buttons < Dry::Struct
|
|
93
|
+
attribute :buttons, Types::Array.of(Button)
|
|
94
|
+
attribute :body_text, Types::String.constrained(min_size: 1)
|
|
95
|
+
attribute? :header_text, Types::String.optional.default(nil)
|
|
96
|
+
|
|
97
|
+
def to_h
|
|
98
|
+
body = { action: { buttons: buttons.map(&:to_h) }, body: { text: body_text } }
|
|
99
|
+
body[:header] = { text: header_text } unless header_text.nil?
|
|
100
|
+
body
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Interactive list WhatsApp message body:
|
|
105
|
+
# `{action: {button, sections}, body: {text}, header: {text}, footer: {text}}`.
|
|
106
|
+
class List < Dry::Struct
|
|
107
|
+
attribute :button, Types::String.constrained(min_size: 1)
|
|
108
|
+
attribute :sections, Types::Array.of(Section)
|
|
109
|
+
attribute :body_text, Types::String.constrained(min_size: 1)
|
|
110
|
+
attribute? :header_text, Types::String.optional.default(nil)
|
|
111
|
+
attribute? :footer_text, Types::String.optional.default(nil)
|
|
112
|
+
|
|
113
|
+
def to_h
|
|
114
|
+
body = { action: { button: button, sections: sections.map(&:to_h) }, body: { text: body_text } }
|
|
115
|
+
body[:header] = { text: header_text } unless header_text.nil?
|
|
116
|
+
body[:footer] = { text: footer_text } unless footer_text.nil?
|
|
117
|
+
body
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class << self
|
|
123
|
+
def text(message:)
|
|
124
|
+
Text.new(message: message)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def media(media_type:, url:, caption: nil)
|
|
128
|
+
Media.new(media_type: media_type, url: url, caption: caption)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def template(template_id:, body_values:, header_value: nil)
|
|
132
|
+
Template.new(template_id: template_id, body_values: body_values, header_value: header_value)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def interactive_buttons(buttons:, body_text:, header_text: nil)
|
|
136
|
+
Interactive::Buttons.new(buttons: buttons, body_text: body_text, header_text: header_text)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def interactive_list(button:, sections:, body_text:, header_text: nil, footer_text: nil)
|
|
140
|
+
Interactive::List.new(button: button, sections: sections, body_text: body_text,
|
|
141
|
+
header_text: header_text, footer_text: footer_text)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/arafa.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "arafa/version"
|
|
4
|
+
require_relative "arafa/errors"
|
|
5
|
+
require_relative "arafa/phone_number"
|
|
6
|
+
require_relative "arafa/types"
|
|
7
|
+
require_relative "arafa/message"
|
|
8
|
+
require_relative "arafa/send_result"
|
|
9
|
+
require_relative "arafa/configuration"
|
|
10
|
+
require_relative "arafa/contracts/base_contract"
|
|
11
|
+
require_relative "arafa/contracts/africas_talking_contract"
|
|
12
|
+
require_relative "arafa/contracts/wasiliana_contract"
|
|
13
|
+
require_relative "arafa/contracts/airtime_contract"
|
|
14
|
+
require_relative "arafa/contracts/africas_talking_airtime_contract"
|
|
15
|
+
require_relative "arafa/contracts/mobile_data_contract"
|
|
16
|
+
require_relative "arafa/schemas/ussd_callback_schema"
|
|
17
|
+
require_relative "arafa/schemas/delivery_report_schema"
|
|
18
|
+
require_relative "arafa/schemas/airtime_request_schema"
|
|
19
|
+
require_relative "arafa/providers/base"
|
|
20
|
+
require_relative "arafa/airtime/base"
|
|
21
|
+
require_relative "arafa/ussd/africas_talking"
|
|
22
|
+
require_relative "arafa/ussd/wasiliana"
|
|
23
|
+
require_relative "arafa/ussd/response"
|
|
24
|
+
require_relative "arafa/ussd/request"
|
|
25
|
+
|
|
26
|
+
# A single, uniform interface to Kenyan SMS/USSD/Airtime gateways.
|
|
27
|
+
module Arafa
|
|
28
|
+
class << self
|
|
29
|
+
def configure(&block)
|
|
30
|
+
Configuration.configure(&block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def config
|
|
34
|
+
Configuration.config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def logger
|
|
38
|
+
config.logger
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Registers a provider class under a name so it can be dispatched to at
|
|
42
|
+
# runtime, e.g. `Arafa.register(:wasiliana, Arafa::Wasiliana)`.
|
|
43
|
+
def register(name, klass)
|
|
44
|
+
registry[name.to_sym] = klass
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Dispatches to a registered provider by name, e.g.
|
|
48
|
+
# `Arafa.send(:wasiliana, text: "Hello", to: "0712345678")`.
|
|
49
|
+
def send(name, **kwargs)
|
|
50
|
+
provider = registry.fetch(name.to_sym) { raise ArgumentError, "unknown provider: #{name.inspect}" }
|
|
51
|
+
provider.send(**kwargs)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def registry
|
|
57
|
+
@registry ||= {}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
require_relative "arafa/providers/africas_talking"
|
|
63
|
+
require_relative "arafa/providers/wasiliana"
|
|
64
|
+
require_relative "arafa/airtime/wasiliana"
|
|
65
|
+
require_relative "arafa/airtime/africas_talking"
|
|
66
|
+
require_relative "arafa/data/africas_talking"
|
|
67
|
+
require_relative "arafa/whatsapp/message_body"
|
|
68
|
+
require_relative "arafa/whatsapp/africas_talking"
|
data/sig/arafa.rbs
ADDED