belt-messaging 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 +24 -0
- data/LICENSE +21 -0
- data/README.md +224 -0
- data/lib/belt/generators/messaging_generator.rb +316 -0
- data/lib/belt/messaging/configuration.rb +41 -0
- data/lib/belt/messaging/controllers/messaging_events_controller.rb +57 -0
- data/lib/belt/messaging/controllers/sms_verification_controller.rb +54 -0
- data/lib/belt/messaging/email.rb +107 -0
- data/lib/belt/messaging/email_gate.rb +35 -0
- data/lib/belt/messaging/logging.rb +20 -0
- data/lib/belt/messaging/phone_formatter.rb +43 -0
- data/lib/belt/messaging/sms.rb +64 -0
- data/lib/belt/messaging/sms_gate.rb +34 -0
- data/lib/belt/messaging/templates/config/messaging_events.yml.erb +41 -0
- data/lib/belt/messaging/templates/controllers/messaging_events_controller.rb.erb +43 -0
- data/lib/belt/messaging/templates/controllers/sms_verification_controller.rb.erb +46 -0
- data/lib/belt/messaging/templates/lambda/messaging_events.rb.erb +76 -0
- data/lib/belt/messaging/templates/terraform/main.tf.erb +266 -0
- data/lib/belt/messaging/templates/terraform/outputs.tf.erb +52 -0
- data/lib/belt/messaging/templates/terraform/variables.tf.erb +66 -0
- data/lib/belt/messaging/version.rb +7 -0
- data/lib/belt/messaging.rb +58 -0
- data/lib/belt-messaging.rb +3 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 57f12b22980eee716c98dcd00612c63b7b72a964cb3fef007fc0c1e594893cef
|
|
4
|
+
data.tar.gz: 81c05386abb62cfd11aceb9c68fb089dc81e5aa870904cfad5f27920d7bf6590
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: add692f4eed8cf815613228ed61aa4e9905c9be7bf07289e7f31e736ae943fae216749938282f766bb39a848f4ea7aab5f6e0bfa8e7e2bf85a02c600b68f8910
|
|
7
|
+
data.tar.gz: b016a16412ad2bfad897cddd6dd3a765e1a2a0a4760bd1229d960843021331e6964d5512236c09679a0a2ed396a3e6ab8f29e07bc035793b380ba6d1abd9ba96
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Add `Belt::Messaging.send_email(to:, subject:, body:, html:)` — send email via AWS SESv2
|
|
6
|
+
- Add `Belt::Messaging::Email` — core email sending module
|
|
7
|
+
- Add `Belt::Messaging::EmailGate` — mode-based send gating (none/whitelist/all)
|
|
8
|
+
- Add `Belt::Messaging::Logging` — shared logging concern extracted from all modules
|
|
9
|
+
- Add email configuration: `ENABLE_EMAIL`, `EMAIL_MODE`, `EMAIL_WHITELIST`, `EMAIL_FROM_ADDRESS`
|
|
10
|
+
- Add `aws-sdk-sesv2` dependency
|
|
11
|
+
- Terraform module: SES email identity verification (domain or address), email send IAM policy
|
|
12
|
+
- Terraform outputs: `email_identity`, `email_send_policy_arn`, `email_domain_dkim_tokens`
|
|
13
|
+
|
|
14
|
+
## 0.0.1
|
|
15
|
+
|
|
16
|
+
- Initial release
|
|
17
|
+
- `Belt::Messaging.send_sms(to:, message:)` — send SMS via AWS PinpointSMSVoiceV2
|
|
18
|
+
- `Belt::Messaging::PhoneFormatter` — E.164 phone number formatting
|
|
19
|
+
- `Belt::Messaging::SmsGate` — mode-based send gating (none/whitelist/all)
|
|
20
|
+
- `Belt::Messaging::Configuration` — environment-based configuration
|
|
21
|
+
- Generator: `belt g messaging` — installs Terraform module, Lambda entry point, controllers
|
|
22
|
+
- Destroy: `belt d messaging` — cleanly removes all generated files
|
|
23
|
+
- Default controllers (Devise-style — work from gem, override by generating)
|
|
24
|
+
- Terraform module: Pinpoint toll-free number, two-way messaging, SNS, IAM
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stowzilla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# belt-messaging
|
|
2
|
+
|
|
3
|
+
SMS and email messaging for [Belt](https://github.com/stowzilla/belt) applications via AWS.
|
|
4
|
+
|
|
5
|
+
- **SMS** via AWS End User Messaging (PinpointSMSVoiceV2)
|
|
6
|
+
- **Email** via AWS Simple Email Service (SESv2)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add to your Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem 'belt-messaging'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
belt generate messaging
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What You Get
|
|
24
|
+
|
|
25
|
+
### From the gem (no generation needed)
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Send an SMS from anywhere in your app
|
|
29
|
+
Belt::Messaging.send_sms(to: '+15551234567', message: 'Your order is ready!')
|
|
30
|
+
Belt::Messaging.send_sms(to: '555-123-4567', message: 'Hello!') # auto-formats to E.164
|
|
31
|
+
|
|
32
|
+
# Send an email from anywhere in your app
|
|
33
|
+
Belt::Messaging.send_email(
|
|
34
|
+
to: 'user@example.com',
|
|
35
|
+
subject: 'Welcome!',
|
|
36
|
+
body: 'Thanks for signing up.'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# With HTML body
|
|
40
|
+
Belt::Messaging.send_email(
|
|
41
|
+
to: ['user1@example.com', 'user2@example.com'],
|
|
42
|
+
subject: 'Weekly Update',
|
|
43
|
+
body: 'Plain text version...',
|
|
44
|
+
html: '<h1>Weekly Update</h1><p>HTML version...</p>'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Phone number utilities
|
|
48
|
+
Belt::Messaging::PhoneFormatter.format_e164('(555) 123-4567') #=> '+15551234567'
|
|
49
|
+
Belt::Messaging::PhoneFormatter.valid?('555-123-4567') #=> true
|
|
50
|
+
|
|
51
|
+
# Email validation
|
|
52
|
+
Belt::Messaging::Email.valid_email?('user@example.com') #=> true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### From the generator (`belt g messaging`)
|
|
56
|
+
|
|
57
|
+
- **Terraform module** — Pinpoint phone number, SES identity, SNS topics, IAM policies
|
|
58
|
+
- **Lambda entry point** — Dedicated Lambda for inbound SMS (STOP/START handling)
|
|
59
|
+
- **Controllers** — SMS verification + messaging events (override to customize)
|
|
60
|
+
- **Route injection** — Adds `sms-verification` to your routes
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Environment variables:
|
|
65
|
+
|
|
66
|
+
### SMS Configuration
|
|
67
|
+
|
|
68
|
+
| Variable | Purpose | Default |
|
|
69
|
+
|----------|---------|---------|
|
|
70
|
+
| `ENABLE_SMS` | Master switch for sending SMS | `false` |
|
|
71
|
+
| `SMS_MODE` | Gating mode: `none`, `whitelist`, `all` | `none` |
|
|
72
|
+
| `SMS_WHITELIST` | Comma-separated E.164 numbers (for whitelist mode) | — |
|
|
73
|
+
| `SMS_ORIGINATION_NUMBER` | Your toll-free number (E.164) | — |
|
|
74
|
+
|
|
75
|
+
### Email Configuration
|
|
76
|
+
|
|
77
|
+
| Variable | Purpose | Default |
|
|
78
|
+
|----------|---------|---------|
|
|
79
|
+
| `ENABLE_EMAIL` | Master switch for sending email | `false` |
|
|
80
|
+
| `EMAIL_MODE` | Gating mode: `none`, `whitelist`, `all` | `none` |
|
|
81
|
+
| `EMAIL_WHITELIST` | Comma-separated addresses (for whitelist mode) | — |
|
|
82
|
+
| `EMAIL_FROM_ADDRESS` | Your verified sender address | — |
|
|
83
|
+
| `AWS_REGION` | AWS region for SES/Pinpoint | `us-east-1` |
|
|
84
|
+
|
|
85
|
+
### Programmatic configuration
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Belt::Messaging.configure do |config|
|
|
89
|
+
# SMS
|
|
90
|
+
config.sms_mode = 'whitelist'
|
|
91
|
+
config.sms_whitelist = ['+15551234567']
|
|
92
|
+
config.origination_number = '+18005550000'
|
|
93
|
+
|
|
94
|
+
# Email
|
|
95
|
+
config.email_mode = 'whitelist'
|
|
96
|
+
config.email_whitelist = ['dev@example.com']
|
|
97
|
+
config.email_from_address = 'noreply@example.com'
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Messaging Modes
|
|
102
|
+
|
|
103
|
+
Both SMS and email use the same gating pattern:
|
|
104
|
+
|
|
105
|
+
| Mode | Behavior |
|
|
106
|
+
|------|----------|
|
|
107
|
+
| `none` | Never send (safe default for development) |
|
|
108
|
+
| `whitelist` | Only send to addresses in the whitelist |
|
|
109
|
+
| `all` | Send to everyone (production) |
|
|
110
|
+
|
|
111
|
+
## Customizing Controllers
|
|
112
|
+
|
|
113
|
+
By default, the gem provides controllers that handle SMS verification and opt-in/opt-out.
|
|
114
|
+
To customize behavior, generate controller overrides into your app:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
belt g messaging --controllers # Generate overrides you can edit
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The generated controllers are scaffolds with TODO comments showing where to add your persistence logic.
|
|
121
|
+
If you don't generate them, the gem's built-in controllers handle everything with sensible defaults.
|
|
122
|
+
|
|
123
|
+
## Terraform Module
|
|
124
|
+
|
|
125
|
+
After running `belt g messaging`, add the module to your environment's `main.tf`:
|
|
126
|
+
|
|
127
|
+
```hcl
|
|
128
|
+
module "messaging" {
|
|
129
|
+
source = "../modules/messaging"
|
|
130
|
+
app_name = var.app_name
|
|
131
|
+
environment = var.environment
|
|
132
|
+
aws_region = var.aws_region
|
|
133
|
+
|
|
134
|
+
# SMS Configuration
|
|
135
|
+
enable_sms = true
|
|
136
|
+
|
|
137
|
+
# Optional: request a new toll-free number
|
|
138
|
+
# request_toll_free_number = true
|
|
139
|
+
|
|
140
|
+
# Optional: use a shared number for dev
|
|
141
|
+
# sms_origination_number = "+18005550000"
|
|
142
|
+
|
|
143
|
+
# Email Configuration
|
|
144
|
+
enable_email = true
|
|
145
|
+
|
|
146
|
+
# Option 1: Verify entire domain (recommended for production)
|
|
147
|
+
# email_domain = "example.com"
|
|
148
|
+
|
|
149
|
+
# Option 2: Verify single address (quick setup)
|
|
150
|
+
email_from_address = "noreply@example.com"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Email Identity Verification
|
|
155
|
+
|
|
156
|
+
For **domain verification** (production):
|
|
157
|
+
1. Set `email_domain = "your-domain.com"` and deploy
|
|
158
|
+
2. Add the DKIM DNS records output by Terraform
|
|
159
|
+
3. Wait for verification (usually minutes)
|
|
160
|
+
4. Set `EMAIL_FROM_ADDRESS` to any address on that domain
|
|
161
|
+
|
|
162
|
+
For **address verification** (quick dev setup):
|
|
163
|
+
1. Set `email_from_address = "noreply@example.com"` and deploy
|
|
164
|
+
2. Check the inbox and click the verification link
|
|
165
|
+
3. Set `EMAIL_FROM_ADDRESS` to the verified address
|
|
166
|
+
|
|
167
|
+
### SES Sandbox
|
|
168
|
+
|
|
169
|
+
New AWS accounts start in the SES sandbox, which limits sending to verified addresses only.
|
|
170
|
+
For production, request production access in the AWS Console → SES → Account Dashboard.
|
|
171
|
+
|
|
172
|
+
## AWS Approval Process
|
|
173
|
+
|
|
174
|
+
### SMS: Toll-Free Verification (Recommended)
|
|
175
|
+
|
|
176
|
+
1. Set `request_toll_free_number = true` and deploy
|
|
177
|
+
2. Go to AWS Console → End User Messaging → Registrations
|
|
178
|
+
3. Submit toll-free verification:
|
|
179
|
+
- Company name and address
|
|
180
|
+
- Use case description
|
|
181
|
+
- Sample messages
|
|
182
|
+
4. Wait 2-15 business days for approval
|
|
183
|
+
5. Once ACTIVE, update `SMS_ORIGINATION_NUMBER` with the new number
|
|
184
|
+
|
|
185
|
+
**Cost:** ~$2/month + $0.0075/message segment
|
|
186
|
+
|
|
187
|
+
### SMS: 10DLC (Higher Volume)
|
|
188
|
+
|
|
189
|
+
For higher throughput at lower per-message cost:
|
|
190
|
+
|
|
191
|
+
1. Register your brand in the 10DLC registry
|
|
192
|
+
2. Create a campaign describing your use case
|
|
193
|
+
3. Provision a 10DLC number
|
|
194
|
+
4. Update configuration
|
|
195
|
+
|
|
196
|
+
**Cost:** $0.0025-$0.005/segment, requires brand ($4/month) + campaign registration
|
|
197
|
+
|
|
198
|
+
### SMS: Short Code (Highest Throughput)
|
|
199
|
+
|
|
200
|
+
For very high volume (100+ messages/second):
|
|
201
|
+
|
|
202
|
+
1. Apply for a short code through AWS
|
|
203
|
+
2. Complete carrier vetting process
|
|
204
|
+
3. Wait 8-12 weeks for approval
|
|
205
|
+
|
|
206
|
+
**Cost:** $1000+/month
|
|
207
|
+
|
|
208
|
+
### Email: SES Pricing
|
|
209
|
+
|
|
210
|
+
- **First 62,000 emails/month:** Free (from EC2/Lambda)
|
|
211
|
+
- **After that:** $0.10 per 1,000 emails
|
|
212
|
+
- Attachments: $0.12 per GB
|
|
213
|
+
|
|
214
|
+
## Removing
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
belt destroy messaging
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then remove the module reference from your environment `main.tf` files.
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module Generators
|
|
8
|
+
class MessagingGenerator
|
|
9
|
+
TEMPLATE_DIR = File.expand_path('../messaging/templates', __dir__)
|
|
10
|
+
|
|
11
|
+
def self.description
|
|
12
|
+
'Install two-way SMS messaging (AWS PinpointSMSVoiceV2)'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.run(args)
|
|
16
|
+
if args.include?('--help') || args.include?('-h')
|
|
17
|
+
print_help
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
new(args).generate
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.destroy(args)
|
|
25
|
+
new(args).destroy
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.print_help
|
|
29
|
+
puts <<~HELP
|
|
30
|
+
Install two-way SMS messaging infrastructure for your Belt app.
|
|
31
|
+
|
|
32
|
+
Usage: belt generate messaging [options]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--controllers Generate controller overrides (to customize behavior)
|
|
36
|
+
--force Overwrite existing files
|
|
37
|
+
|
|
38
|
+
What gets created:
|
|
39
|
+
infrastructure/modules/messaging/ Terraform module (Pinpoint, SNS, IAM)
|
|
40
|
+
config/lambda/messaging_events.yml Lambda configuration (timeout, memory, env, triggers)
|
|
41
|
+
config/routes.tf.rb Routes for sms-verification (updated)
|
|
42
|
+
lambda/messaging_events.rb Lambda entry point for inbound SMS
|
|
43
|
+
|
|
44
|
+
What stays in the gem (no generation needed):
|
|
45
|
+
Belt::Messaging.send_sms(to:, message:) Send SMS from anywhere
|
|
46
|
+
Belt::Messaging::PhoneFormatter E.164 formatting
|
|
47
|
+
Belt::Messaging::SmsGate Whitelist/mode gating
|
|
48
|
+
SmsVerificationController Phone verification (gem default)
|
|
49
|
+
SmsEventsController Opt-in/opt-out handling (gem default)
|
|
50
|
+
|
|
51
|
+
To override controllers (like `rails g devise:views`):
|
|
52
|
+
belt g messaging --controllers
|
|
53
|
+
|
|
54
|
+
After generation:
|
|
55
|
+
1. Add module reference to your environment's main.tf
|
|
56
|
+
2. Configure environment variables (ENABLE_SMS, SMS_MODE, etc.)
|
|
57
|
+
3. Deploy: belt apply <env>
|
|
58
|
+
4. Request toll-free number via AWS console (if not already acquired)
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
belt g messaging # Infrastructure only (use gem defaults)
|
|
62
|
+
belt g messaging --controllers # Also generate controller overrides
|
|
63
|
+
belt d messaging
|
|
64
|
+
HELP
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize(args)
|
|
68
|
+
@force = args.include?('--force')
|
|
69
|
+
@with_controllers = args.include?('--controllers')
|
|
70
|
+
@app_name = detect_namespace
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def generate
|
|
74
|
+
generate_terraform_module
|
|
75
|
+
generate_lambda_config
|
|
76
|
+
generate_lambda_entry_point
|
|
77
|
+
generate_controllers if @with_controllers
|
|
78
|
+
inject_routes
|
|
79
|
+
print_success
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def destroy
|
|
83
|
+
remove_terraform_module
|
|
84
|
+
remove_lambda_config
|
|
85
|
+
remove_lambda_entry_point
|
|
86
|
+
remove_controllers
|
|
87
|
+
remove_routes
|
|
88
|
+
puts "\n✓ Messaging removed!"
|
|
89
|
+
puts " Don't forget to remove the module reference from your environment main.tf files."
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def detect_namespace
|
|
95
|
+
routes_file = find_routes_file_path
|
|
96
|
+
if routes_file && File.exist?(routes_file)
|
|
97
|
+
match = File.read(routes_file).match(/namespace :(\w+)/)
|
|
98
|
+
return match[1] if match
|
|
99
|
+
end
|
|
100
|
+
File.basename(Dir.pwd)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Finds routes.tf.rb checking config/ first, then infrastructure/ (legacy).
|
|
104
|
+
def find_routes_file_path
|
|
105
|
+
candidates = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
|
|
106
|
+
candidates.find { |f| File.exist?(f) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# ---- Generate ----
|
|
110
|
+
|
|
111
|
+
def generate_terraform_module
|
|
112
|
+
module_dir = 'infrastructure/modules/messaging'
|
|
113
|
+
|
|
114
|
+
if Dir.exist?(module_dir) && !@force
|
|
115
|
+
puts " skip #{module_dir}/ (already exists, use --force to overwrite)"
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
FileUtils.mkdir_p(module_dir)
|
|
120
|
+
|
|
121
|
+
write_template('terraform/main.tf.erb', "#{module_dir}/main.tf")
|
|
122
|
+
write_template('terraform/variables.tf.erb', "#{module_dir}/variables.tf")
|
|
123
|
+
write_template('terraform/outputs.tf.erb', "#{module_dir}/outputs.tf")
|
|
124
|
+
|
|
125
|
+
puts " create #{module_dir}/main.tf"
|
|
126
|
+
puts " create #{module_dir}/variables.tf"
|
|
127
|
+
puts " create #{module_dir}/outputs.tf"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def generate_lambda_config
|
|
131
|
+
config_dir = 'config/lambda'
|
|
132
|
+
dest = "#{config_dir}/messaging_events.yml"
|
|
133
|
+
|
|
134
|
+
if File.exist?(dest) && !@force
|
|
135
|
+
puts " skip #{dest} (already exists)"
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
FileUtils.mkdir_p(config_dir)
|
|
140
|
+
write_template('config/messaging_events.yml.erb', dest)
|
|
141
|
+
puts " create #{dest}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def generate_lambda_entry_point
|
|
145
|
+
dest = 'lambda/messaging_events.rb'
|
|
146
|
+
|
|
147
|
+
if File.exist?(dest) && !@force
|
|
148
|
+
puts " skip #{dest} (already exists)"
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
write_template('lambda/messaging_events.rb.erb', dest)
|
|
153
|
+
puts " create #{dest}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def generate_controllers
|
|
157
|
+
controller_dir = "lambda/controllers/#{@app_name}"
|
|
158
|
+
FileUtils.mkdir_p(controller_dir)
|
|
159
|
+
|
|
160
|
+
# SMS Verification controller
|
|
161
|
+
verification_dest = "#{controller_dir}/sms_verification_controller.rb"
|
|
162
|
+
if File.exist?(verification_dest) && !@force
|
|
163
|
+
puts " skip #{verification_dest} (already exists)"
|
|
164
|
+
else
|
|
165
|
+
write_template('controllers/sms_verification_controller.rb.erb', verification_dest)
|
|
166
|
+
puts " create #{verification_dest}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# SMS Events controller (for override)
|
|
170
|
+
events_dir = 'lambda/controllers/messaging_events'
|
|
171
|
+
FileUtils.mkdir_p(events_dir)
|
|
172
|
+
|
|
173
|
+
events_dest = "#{events_dir}/messaging_events_controller.rb"
|
|
174
|
+
if File.exist?(events_dest) && !@force
|
|
175
|
+
puts " skip #{events_dest} (already exists)"
|
|
176
|
+
else
|
|
177
|
+
write_template('controllers/messaging_events_controller.rb.erb', events_dest)
|
|
178
|
+
puts " create #{events_dest}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def inject_routes
|
|
183
|
+
routes_file = find_routes_file_path
|
|
184
|
+
return unless routes_file && File.exist?(routes_file)
|
|
185
|
+
|
|
186
|
+
content = File.read(routes_file)
|
|
187
|
+
|
|
188
|
+
# Add sms-verification route to app namespace
|
|
189
|
+
unless content.include?('sms_verification') || content.include?('sms-verification')
|
|
190
|
+
namespace_pattern = /^(\s*)namespace :#{Regexp.escape(@app_name)}\b[^\n]*do\s*\n(.*?)^\1end/m
|
|
191
|
+
if content.match?(namespace_pattern)
|
|
192
|
+
sms_route = " resource :sms_verification, only: [:create, :update]"
|
|
193
|
+
content.sub!(namespace_pattern) do |match|
|
|
194
|
+
indent = ::Regexp.last_match(1)
|
|
195
|
+
match.sub(/^(#{indent})end\z/m, "#{sms_route}\n#{indent}end")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
File.write(routes_file, content)
|
|
201
|
+
puts " update #{routes_file}"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# ---- Destroy ----
|
|
205
|
+
|
|
206
|
+
def remove_terraform_module
|
|
207
|
+
module_dir = 'infrastructure/modules/messaging'
|
|
208
|
+
if Dir.exist?(module_dir)
|
|
209
|
+
FileUtils.rm_rf(module_dir)
|
|
210
|
+
puts " remove #{module_dir}/"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def remove_lambda_config
|
|
215
|
+
path = 'config/lambda/messaging_events.yml'
|
|
216
|
+
if File.exist?(path)
|
|
217
|
+
File.delete(path)
|
|
218
|
+
puts " remove #{path}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def remove_lambda_entry_point
|
|
223
|
+
path = 'lambda/messaging_events.rb'
|
|
224
|
+
if File.exist?(path)
|
|
225
|
+
File.delete(path)
|
|
226
|
+
puts " remove #{path}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def remove_controllers
|
|
231
|
+
events_dir = 'lambda/controllers/messaging_events'
|
|
232
|
+
if Dir.exist?(events_dir)
|
|
233
|
+
FileUtils.rm_rf(events_dir)
|
|
234
|
+
puts " remove #{events_dir}/"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
verification = "lambda/controllers/#{@app_name}/sms_verification_controller.rb"
|
|
238
|
+
if File.exist?(verification)
|
|
239
|
+
File.delete(verification)
|
|
240
|
+
puts " remove #{verification}"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def remove_routes
|
|
245
|
+
routes_file = find_routes_file_path
|
|
246
|
+
return unless routes_file && File.exist?(routes_file)
|
|
247
|
+
|
|
248
|
+
content = File.read(routes_file)
|
|
249
|
+
original = content.dup
|
|
250
|
+
|
|
251
|
+
content.gsub!(/^\s*resource :sms_verification.*\n/, '')
|
|
252
|
+
|
|
253
|
+
if content != original
|
|
254
|
+
File.write(routes_file, content)
|
|
255
|
+
puts " update #{routes_file}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def write_template(template_name, dest_path)
|
|
260
|
+
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
261
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
262
|
+
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
263
|
+
File.write(dest_path, content)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def print_success
|
|
267
|
+
puts <<~SUCCESS
|
|
268
|
+
|
|
269
|
+
✓ Messaging installed!
|
|
270
|
+
|
|
271
|
+
Next steps:
|
|
272
|
+
1. Add the module to your environment's main.tf:
|
|
273
|
+
|
|
274
|
+
module "messaging" {
|
|
275
|
+
source = "../modules/messaging"
|
|
276
|
+
app_name = var.app_name
|
|
277
|
+
environment = var.environment
|
|
278
|
+
aws_region = var.aws_region
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
2. Configure environment variables:
|
|
282
|
+
ENABLE_SMS=true
|
|
283
|
+
SMS_MODE=whitelist
|
|
284
|
+
SMS_WHITELIST=+15551234567
|
|
285
|
+
SMS_ORIGINATION_NUMBER=+1XXXXXXXXXX
|
|
286
|
+
|
|
287
|
+
3. Deploy:
|
|
288
|
+
belt apply <env>
|
|
289
|
+
|
|
290
|
+
4. Request a toll-free number (if needed):
|
|
291
|
+
Set request_toll_free_number = true in your module call,
|
|
292
|
+
or provide sms_origination_number for a shared number.
|
|
293
|
+
|
|
294
|
+
5. Complete AWS Toll-Free Verification:
|
|
295
|
+
- Go to AWS Console → End User Messaging → Registrations
|
|
296
|
+
- Submit toll-free verification (company info, use case, sample messages)
|
|
297
|
+
- Typical approval: 2-15 business days
|
|
298
|
+
|
|
299
|
+
6. Test:
|
|
300
|
+
belt c <env> --run "Belt::Messaging.send_sms(to: '+15551234567', message: 'Hello!')"
|
|
301
|
+
|
|
302
|
+
SMS Modes:
|
|
303
|
+
none — Never send (default, safe for dev)
|
|
304
|
+
whitelist — Only send to numbers in SMS_WHITELIST
|
|
305
|
+
all — Send to everyone (production)
|
|
306
|
+
|
|
307
|
+
Documentation:
|
|
308
|
+
Toll-Free: Simplest approval, ~$2/mo + $0.0075/segment
|
|
309
|
+
10DLC: Cheaper at volume, requires brand + campaign registration
|
|
310
|
+
Short Code: Highest throughput, $1000+/mo, lengthy approval
|
|
311
|
+
|
|
312
|
+
SUCCESS
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module Messaging
|
|
5
|
+
class Configuration
|
|
6
|
+
# SMS configuration
|
|
7
|
+
attr_accessor :aws_region, :origination_number, :sms_mode, :sms_whitelist,
|
|
8
|
+
:opt_in_message, :logger
|
|
9
|
+
|
|
10
|
+
# Email configuration
|
|
11
|
+
attr_accessor :email_from_address, :email_mode, :email_whitelist
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@aws_region = ENV['AWS_REGION'] || 'us-east-1'
|
|
15
|
+
|
|
16
|
+
# SMS
|
|
17
|
+
@origination_number = ENV['SMS_ORIGINATION_NUMBER']
|
|
18
|
+
@sms_mode = ENV['SMS_MODE'] || 'none'
|
|
19
|
+
@sms_whitelist = (ENV['SMS_WHITELIST'] || '').split(',').map(&:strip)
|
|
20
|
+
@opt_in_message = 'You have been re-subscribed to SMS notifications.'
|
|
21
|
+
|
|
22
|
+
# Email
|
|
23
|
+
@email_from_address = ENV['EMAIL_FROM_ADDRESS']
|
|
24
|
+
@email_mode = ENV['EMAIL_MODE'] || 'none'
|
|
25
|
+
@email_whitelist = (ENV['EMAIL_WHITELIST'] || '').split(',').map(&:strip)
|
|
26
|
+
|
|
27
|
+
@logger = nil # Falls back to Belt::Observability::Logger if available
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# SMS enabled check
|
|
31
|
+
def enabled?
|
|
32
|
+
ENV['ENABLE_SMS'] == 'true'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Email enabled check
|
|
36
|
+
def email_enabled?
|
|
37
|
+
ENV['ENABLE_EMAIL'] == 'true'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module Messaging
|
|
5
|
+
module Controllers
|
|
6
|
+
# Default SMS events controller for handling inbound opt-out/opt-in messages.
|
|
7
|
+
# This controller processes STOP/START keywords from two-way messaging.
|
|
8
|
+
#
|
|
9
|
+
# To customize behavior, generate the controller override:
|
|
10
|
+
# belt g messaging
|
|
11
|
+
#
|
|
12
|
+
# Then modify lambda/controllers/messaging_events/messaging_events_controller.rb
|
|
13
|
+
class MessagingEventsController < BeltController::Base
|
|
14
|
+
OPT_OUT_KEYWORDS = %w[ARRET CANCEL END OPT-OUT OPTOUT QUIT REMOVE STOP TD UNSUBSCRIBE].freeze
|
|
15
|
+
OPT_IN_KEYWORDS = %w[START UNSTOP].freeze
|
|
16
|
+
|
|
17
|
+
def opt_out
|
|
18
|
+
phone = sms_origin_phone
|
|
19
|
+
|
|
20
|
+
Belt::Messaging.log(:info, 'Processing SMS opt-out', phone: phone)
|
|
21
|
+
|
|
22
|
+
# Default: log the opt-out. Users should override this controller
|
|
23
|
+
# to update their own user/customer model.
|
|
24
|
+
Belt::Messaging.log(:warn,
|
|
25
|
+
'SMS opt-out received but no handler configured. Override MessagingEventsController to handle.',
|
|
26
|
+
phone: phone
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
success_response(success: true, processed: false, reason: 'no_handler_configured')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def opt_in
|
|
33
|
+
phone = sms_origin_phone
|
|
34
|
+
|
|
35
|
+
Belt::Messaging.log(:info, 'Processing SMS opt-in', phone: phone)
|
|
36
|
+
|
|
37
|
+
Belt::Messaging.log(:warn,
|
|
38
|
+
'SMS opt-in received but no handler configured. Override MessagingEventsController to handle.',
|
|
39
|
+
phone: phone
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
success_response(success: true, processed: false, reason: 'no_handler_configured')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def sms_origin_phone
|
|
48
|
+
records = @event['Records']
|
|
49
|
+
return nil unless records&.first
|
|
50
|
+
|
|
51
|
+
message = JSON.parse(records.first.dig('Sns', 'Message') || '{}')
|
|
52
|
+
message['originationNumber']
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|