messengerr 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/LICENSE +21 -0
- data/README.md +77 -0
- data/exe/messengerr +5 -0
- data/lib/messengerr/cli/credentials_command.rb +73 -0
- data/lib/messengerr/cli/integrations_command.rb +27 -0
- data/lib/messengerr/cli/send_command.rb +373 -0
- data/lib/messengerr/cli.rb +50 -0
- data/lib/messengerr/contact_validator.rb +48 -0
- data/lib/messengerr/credentials.rb +117 -0
- data/lib/messengerr/crypto.rb +39 -0
- data/lib/messengerr/csv_source.rb +31 -0
- data/lib/messengerr/delivery.rb +15 -0
- data/lib/messengerr/errors.rb +10 -0
- data/lib/messengerr/integrations/base.rb +59 -0
- data/lib/messengerr/integrations/clicksend.rb +92 -0
- data/lib/messengerr/integrations/postmark.rb +77 -0
- data/lib/messengerr/integrations.rb +5 -0
- data/lib/messengerr/ledger.rb +78 -0
- data/lib/messengerr/paths.rb +19 -0
- data/lib/messengerr/placeholder.rb +10 -0
- data/lib/messengerr/prompter.rb +59 -0
- data/lib/messengerr/recipient.rb +4 -0
- data/lib/messengerr/registry.rb +49 -0
- data/lib/messengerr/renderer.rb +30 -0
- data/lib/messengerr/report.rb +24 -0
- data/lib/messengerr/result.rb +12 -0
- data/lib/messengerr/sender.rb +104 -0
- data/lib/messengerr/sending_options/base.rb +45 -0
- data/lib/messengerr/sending_options/clicksend.rb +18 -0
- data/lib/messengerr/sending_options/postmark.rb +12 -0
- data/lib/messengerr/sending_options.rb +5 -0
- data/lib/messengerr/setups/base.rb +80 -0
- data/lib/messengerr/setups/clicksend.rb +13 -0
- data/lib/messengerr/setups/postmark.rb +9 -0
- data/lib/messengerr/setups.rb +5 -0
- data/lib/messengerr/version.rb +3 -0
- data/lib/messengerr.rb +22 -0
- metadata +201 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
|
|
3
|
+
module Messengerr
|
|
4
|
+
# Substitutes {{placeholder}} tokens with values from a CSV row, using a
|
|
5
|
+
# user-supplied mapping of placeholder name → column name. Any leftover
|
|
6
|
+
# {{ or }} after substitution is a render failure — never sent literally.
|
|
7
|
+
#
|
|
8
|
+
# With `escape: true` (HTML messages) every substituted value is HTML-escaped,
|
|
9
|
+
# so CSV data can never inject markup. The template itself is trusted.
|
|
10
|
+
module Renderer
|
|
11
|
+
def self.render(template, row, mapping, escape: false)
|
|
12
|
+
rendered = template.gsub(Placeholder::PATTERN) do
|
|
13
|
+
key = Regexp.last_match(1)
|
|
14
|
+
column = mapping[key]
|
|
15
|
+
raise RenderError, "no column mapped for placeholder {{#{key}}}" if column.nil?
|
|
16
|
+
|
|
17
|
+
value = row[column]
|
|
18
|
+
raise RenderError, "column \"#{column}\" is empty for {{#{key}}}" if value.nil? || value.strip.empty?
|
|
19
|
+
|
|
20
|
+
escape ? CGI.escapeHTML(value.strip) : value.strip
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if rendered.match?(/\{\{|\}\}/)
|
|
24
|
+
raise RenderError, "unrenderable placeholder left in message: #{rendered[/\{\{[^}]*\}?\}?/]}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rendered
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
# Aggregate outcome of a send run: Results plus dropped validation defects.
|
|
3
|
+
class Report
|
|
4
|
+
attr_reader :results, :dropped
|
|
5
|
+
|
|
6
|
+
def initialize(results: [], dropped: [])
|
|
7
|
+
@results = results
|
|
8
|
+
@dropped = dropped # validation defects the user chose to drop
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def successes = @results.select(&:sent?)
|
|
12
|
+
def failures = @results.reject(&:sent?)
|
|
13
|
+
|
|
14
|
+
def sent_count = successes.length
|
|
15
|
+
def failed_count = failures.length
|
|
16
|
+
def dropped_count = @dropped.length
|
|
17
|
+
|
|
18
|
+
def ok? = failures.empty?
|
|
19
|
+
|
|
20
|
+
def summary
|
|
21
|
+
"sent: #{sent_count}, failed: #{failed_count}, dropped: #{dropped_count}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
# Per-recipient send outcome. `uncertain` marks failures where the provider
|
|
3
|
+
# may still have delivered (timeouts, unreadable responses) — retrying these
|
|
4
|
+
# can duplicate the message.
|
|
5
|
+
Result = Data.define(:recipient, :status, :reason, :provider_id, :uncertain) do
|
|
6
|
+
def initialize(recipient:, status:, reason: nil, provider_id: nil, uncertain: false)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sent? = status == :sent
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
# Dispatches Deliveries through an integration in batch_size slices,
|
|
3
|
+
# throttled by rate_limit. Every delivery produces exactly one Result:
|
|
4
|
+
# exceptions become failed Results, and integrations that return too few
|
|
5
|
+
# results get the gaps filled rather than silently losing contacts.
|
|
6
|
+
#
|
|
7
|
+
# Integrations that send one message at a time may yield each Result from
|
|
8
|
+
# send_batch as it lands; the Sender records those to the ledger immediately,
|
|
9
|
+
# so a crash or Ctrl-C mid-batch never loses what was already delivered.
|
|
10
|
+
class Sender
|
|
11
|
+
# Why the run stopped early, if it did (currently: the ledger became
|
|
12
|
+
# unwritable, so no further batch is sent without a record).
|
|
13
|
+
attr_reader :halted_reason
|
|
14
|
+
|
|
15
|
+
def initialize(integration, options: integration.class.options_class.new, ledger: nil)
|
|
16
|
+
@integration = integration
|
|
17
|
+
@options = options
|
|
18
|
+
@options.validate!
|
|
19
|
+
@ledger = ledger
|
|
20
|
+
@recorded = {}
|
|
21
|
+
@halted_reason = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# deliveries → [Result], one per delivery, in slice order.
|
|
25
|
+
def deliver(deliveries)
|
|
26
|
+
deliveries.each_slice(@options.batch_size).with_index.flat_map do |slice, index|
|
|
27
|
+
throttle if index.positive? && !@halted_reason
|
|
28
|
+
@halted_reason ? halted_results(slice) : dispatch(slice)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def dispatch(slice)
|
|
35
|
+
@recorded = {} # dedupe is per batch: a retry must be recorded again
|
|
36
|
+
seen = {}
|
|
37
|
+
results =
|
|
38
|
+
begin
|
|
39
|
+
returned = @integration.send_batch(slice, @options) do |result|
|
|
40
|
+
seen[key(result.recipient)] = result
|
|
41
|
+
record(result)
|
|
42
|
+
end
|
|
43
|
+
reconcile(slice, returned, seen)
|
|
44
|
+
rescue Interrupt
|
|
45
|
+
# Whatever hadn't reported back may or may not have gone out.
|
|
46
|
+
slice.each { |d| record(seen[key(d.recipient)] || failure(d, "interrupted", uncertain: true)) }
|
|
47
|
+
raise
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
failure_results(slice, seen, e)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
results.each { |r| record(r) }
|
|
53
|
+
results
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# One Result per delivery. Results the integration yielded survive an
|
|
57
|
+
# exception raised later in the same batch.
|
|
58
|
+
def failure_results(slice, seen, error)
|
|
59
|
+
# A plain IntegrationError means the provider definitely rejected it;
|
|
60
|
+
# anything else (timeouts, UncertainError, bugs) may have gone out.
|
|
61
|
+
uncertain = !(error.is_a?(IntegrationError) && !error.is_a?(UncertainError))
|
|
62
|
+
slice.map { |d| seen[key(d.recipient)] || failure(d, error.message, uncertain:) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The contract: one Result per delivery. Gaps are filled with uncertain
|
|
66
|
+
# failures rather than silently dropping contacts.
|
|
67
|
+
def reconcile(slice, returned, seen)
|
|
68
|
+
by_recipient = Array(returned).group_by { |r| key(r.recipient) }
|
|
69
|
+
slice.map do |d|
|
|
70
|
+
k = key(d.recipient)
|
|
71
|
+
by_recipient[k]&.first || seen[k] || failure(d, "integration returned no result", uncertain: true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def failure(delivery, reason, uncertain: false)
|
|
76
|
+
Result.new(recipient: delivery.recipient, status: :failed, reason:, uncertain:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def halted_results(slice)
|
|
80
|
+
slice.map { |d| failure(d, "not sent — #{@halted_reason}") }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def record(result)
|
|
84
|
+
return unless @ledger
|
|
85
|
+
|
|
86
|
+
k = key(result.recipient)
|
|
87
|
+
return if @recorded[k]
|
|
88
|
+
|
|
89
|
+
@ledger.record(result)
|
|
90
|
+
@recorded[k] = true
|
|
91
|
+
rescue SystemCallError, IOError => e
|
|
92
|
+
# Never crash after a send: keep the in-memory result, stop sending more.
|
|
93
|
+
@halted_reason ||= "ledger write failed (#{e.message}); stopped so no message goes unrecorded"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def key(recipient)
|
|
97
|
+
[ recipient.row_number, recipient.contact ]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def throttle
|
|
101
|
+
sleep(1.0 / @options.rate_limit)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
module SendingOptions
|
|
3
|
+
# Shared sending options every service supports. Subclasses call `option`
|
|
4
|
+
# to add their own; each class validates its values in `validate!`.
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
def option(name)
|
|
8
|
+
options << name
|
|
9
|
+
attr_accessor name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def options
|
|
13
|
+
@options ||= superclass.respond_to?(:options) ? superclass.options.dup : []
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
option :from
|
|
18
|
+
option :batch_size
|
|
19
|
+
option :rate_limit
|
|
20
|
+
option :metadata
|
|
21
|
+
|
|
22
|
+
def initialize(attributes = {})
|
|
23
|
+
attributes.each do |key, value|
|
|
24
|
+
public_send("#{key}=", value)
|
|
25
|
+
end
|
|
26
|
+
@batch_size ||= 50
|
|
27
|
+
@rate_limit ||= 10
|
|
28
|
+
@metadata ||= {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate!
|
|
32
|
+
errors = []
|
|
33
|
+
errors << "batch_size must be a positive integer" unless batch_size.is_a?(Integer) && batch_size.positive?
|
|
34
|
+
errors << "rate_limit must be positive" unless rate_limit.is_a?(Numeric) && rate_limit.positive?
|
|
35
|
+
raise ValidationError, errors.join(", ") unless errors.empty?
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
self.class.options.to_h { |name| [ name, public_send(name) ] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
module SendingOptions
|
|
3
|
+
class ClickSend < Base
|
|
4
|
+
MAX_BATCH = 1_000 # ClickSend caps /sms/send at 1000 messages per request
|
|
5
|
+
|
|
6
|
+
option :schedule
|
|
7
|
+
|
|
8
|
+
def validate!
|
|
9
|
+
super
|
|
10
|
+
if batch_size > MAX_BATCH
|
|
11
|
+
raise ValidationError, "batch_size can't exceed #{MAX_BATCH} for ClickSend"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
module SendingOptions
|
|
3
|
+
class Postmark < Base
|
|
4
|
+
# `subject` is a fallback only; the wizard supplies a (rendered) subject
|
|
5
|
+
# per Delivery, and the integration fails any delivery that has neither.
|
|
6
|
+
option :subject
|
|
7
|
+
option :message_stream
|
|
8
|
+
option :tag
|
|
9
|
+
option :track_opens
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Messengerr
|
|
2
|
+
module Setups
|
|
3
|
+
# Interactive credential setup for an integration. The subclass declares
|
|
4
|
+
# which keys it needs; this framework prompts for them, runs the
|
|
5
|
+
# integration's live verify!, and writes the values into the encrypted
|
|
6
|
+
# credentials store.
|
|
7
|
+
class Base
|
|
8
|
+
# Keys the user must supply, e.g. [:server_token]. Optional keys go in
|
|
9
|
+
# .optional_keys and may be left blank.
|
|
10
|
+
def self.required_keys
|
|
11
|
+
[]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.optional_keys
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The integration class this setup belongs to — inferred by convention.
|
|
19
|
+
def self.integration_class
|
|
20
|
+
Messengerr::Integrations.const_get(name.split("::").last, false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(integration_class: self.class.integration_class,
|
|
24
|
+
prompter: Prompter.new,
|
|
25
|
+
credentials: Credentials.new)
|
|
26
|
+
@integration_class = integration_class
|
|
27
|
+
@prompter = prompter
|
|
28
|
+
@credentials = credentials
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns true when credentials were verified and stored.
|
|
32
|
+
def run
|
|
33
|
+
@prompter.say("Setting up #{@integration_class.integration_key} (#{@integration_class.delivery_method}):")
|
|
34
|
+
values = collect_values
|
|
35
|
+
return false if values.nil?
|
|
36
|
+
|
|
37
|
+
if verified?(values)
|
|
38
|
+
@credentials.write(@integration_class.integration_key, values)
|
|
39
|
+
@prompter.say("#{integration_label} is configured and verified.")
|
|
40
|
+
true
|
|
41
|
+
else
|
|
42
|
+
@prompter.confirm("Try again?") ? run : false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def collect_values
|
|
49
|
+
values = {}
|
|
50
|
+
|
|
51
|
+
self.class.required_keys.each do |key|
|
|
52
|
+
values[key] = @prompter.secret("#{key.to_s.tr('_', ' ')}:").to_s.strip
|
|
53
|
+
end
|
|
54
|
+
if values.values.any?(&:empty?)
|
|
55
|
+
@prompter.say("All required fields must have a value — aborting.")
|
|
56
|
+
return nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
self.class.optional_keys.each do |key|
|
|
60
|
+
value = @prompter.ask("#{key.to_s.tr('_', ' ')} (optional, enter to skip):").to_s.strip
|
|
61
|
+
values[key] = value unless value.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
values.transform_keys(&:to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def verified?(values)
|
|
68
|
+
@integration_class.new(values).verify!
|
|
69
|
+
true
|
|
70
|
+
rescue IntegrationError => e
|
|
71
|
+
@prompter.say("Verification failed: #{e.message}")
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def integration_label
|
|
76
|
+
@integration_class.integration_key.to_s.capitalize
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/messengerr.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "zeitwerk"
|
|
2
|
+
require_relative "messengerr/version"
|
|
3
|
+
require_relative "messengerr/errors"
|
|
4
|
+
|
|
5
|
+
module Messengerr
|
|
6
|
+
class << self
|
|
7
|
+
def loader
|
|
8
|
+
@loader ||= Zeitwerk::Loader.for_gem.tap do |loader|
|
|
9
|
+
loader.inflector.inflect("cli" => "CLI", "clicksend" => "ClickSend")
|
|
10
|
+
loader.ignore("#{__dir__}/messengerr/version.rb", "#{__dir__}/messengerr/errors.rb")
|
|
11
|
+
loader.setup
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Third-party gems call this on load to register an Integrations::Base subclass.
|
|
16
|
+
def register_integration(integration_class)
|
|
17
|
+
Registry.register(integration_class)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Messengerr.loader
|
metadata
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: messengerr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Daiwick
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: base64
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: csv
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.3'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: phonelib
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.8'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.8'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: postmark
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.25'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.25'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: zeitwerk
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.6'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.6'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rake
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '13.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '13.0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rspec
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.13'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.13'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: webmock
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.23'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.23'
|
|
125
|
+
description: Interactive CLI that renders {{placeholder}} messages against a CSV contact
|
|
126
|
+
list and dispatches through pluggable services scoped by delivery method (email,
|
|
127
|
+
sms).
|
|
128
|
+
email:
|
|
129
|
+
- developers@daiwick.com
|
|
130
|
+
executables:
|
|
131
|
+
- messengerr
|
|
132
|
+
extensions: []
|
|
133
|
+
extra_rdoc_files: []
|
|
134
|
+
files:
|
|
135
|
+
- LICENSE
|
|
136
|
+
- README.md
|
|
137
|
+
- exe/messengerr
|
|
138
|
+
- lib/messengerr.rb
|
|
139
|
+
- lib/messengerr/cli.rb
|
|
140
|
+
- lib/messengerr/cli/credentials_command.rb
|
|
141
|
+
- lib/messengerr/cli/integrations_command.rb
|
|
142
|
+
- lib/messengerr/cli/send_command.rb
|
|
143
|
+
- lib/messengerr/contact_validator.rb
|
|
144
|
+
- lib/messengerr/credentials.rb
|
|
145
|
+
- lib/messengerr/crypto.rb
|
|
146
|
+
- lib/messengerr/csv_source.rb
|
|
147
|
+
- lib/messengerr/delivery.rb
|
|
148
|
+
- lib/messengerr/errors.rb
|
|
149
|
+
- lib/messengerr/integrations.rb
|
|
150
|
+
- lib/messengerr/integrations/base.rb
|
|
151
|
+
- lib/messengerr/integrations/clicksend.rb
|
|
152
|
+
- lib/messengerr/integrations/postmark.rb
|
|
153
|
+
- lib/messengerr/ledger.rb
|
|
154
|
+
- lib/messengerr/paths.rb
|
|
155
|
+
- lib/messengerr/placeholder.rb
|
|
156
|
+
- lib/messengerr/prompter.rb
|
|
157
|
+
- lib/messengerr/recipient.rb
|
|
158
|
+
- lib/messengerr/registry.rb
|
|
159
|
+
- lib/messengerr/renderer.rb
|
|
160
|
+
- lib/messengerr/report.rb
|
|
161
|
+
- lib/messengerr/result.rb
|
|
162
|
+
- lib/messengerr/sender.rb
|
|
163
|
+
- lib/messengerr/sending_options.rb
|
|
164
|
+
- lib/messengerr/sending_options/base.rb
|
|
165
|
+
- lib/messengerr/sending_options/clicksend.rb
|
|
166
|
+
- lib/messengerr/sending_options/postmark.rb
|
|
167
|
+
- lib/messengerr/setups.rb
|
|
168
|
+
- lib/messengerr/setups/base.rb
|
|
169
|
+
- lib/messengerr/setups/clicksend.rb
|
|
170
|
+
- lib/messengerr/setups/postmark.rb
|
|
171
|
+
- lib/messengerr/version.rb
|
|
172
|
+
homepage: https://github.com/jalappeno-apps/messengerr
|
|
173
|
+
licenses:
|
|
174
|
+
- MIT
|
|
175
|
+
metadata:
|
|
176
|
+
documentation_uri: https://jalappeno-apps.github.io/messengerr/
|
|
177
|
+
source_code_uri: https://github.com/jalappeno-apps/messengerr
|
|
178
|
+
bug_tracker_uri: https://github.com/jalappeno-apps/messengerr/issues
|
|
179
|
+
changelog_uri: https://github.com/jalappeno-apps/messengerr/blob/main/CHANGELOG.md
|
|
180
|
+
rubygems_mfa_required: 'true'
|
|
181
|
+
allowed_push_host: https://rubygems.org
|
|
182
|
+
post_install_message:
|
|
183
|
+
rdoc_options: []
|
|
184
|
+
require_paths:
|
|
185
|
+
- lib
|
|
186
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
187
|
+
requirements:
|
|
188
|
+
- - ">="
|
|
189
|
+
- !ruby/object:Gem::Version
|
|
190
|
+
version: '3.2'
|
|
191
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
|
+
requirements:
|
|
193
|
+
- - ">="
|
|
194
|
+
- !ruby/object:Gem::Version
|
|
195
|
+
version: '0'
|
|
196
|
+
requirements: []
|
|
197
|
+
rubygems_version: 3.5.22
|
|
198
|
+
signing_key:
|
|
199
|
+
specification_version: 4
|
|
200
|
+
summary: Send templated messages to CSV contact lists over email and SMS.
|
|
201
|
+
test_files: []
|