notey 0.1.0 → 0.3.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 +4 -4
- data/README.md +77 -40
- data/app/delivery_methods/notey/email.rb +4 -0
- data/app/models/notey/attempt.rb +10 -0
- data/app/models/notey/delete_old_attempts.rb +19 -0
- data/app/models/notey/notification.rb +2 -1
- data/lib/notey/channels.rb +1 -1
- data/lib/notey/version.rb +1 -1
- data/lib/notey.rb +24 -55
- data/the_local/agents/notey-develop.md +104 -74
- data/the_local/agents/notey-info.md +18 -14
- data/the_local/agents/notey-install.md +67 -45
- data/the_local/interface.yml +16 -10
- metadata +2 -4
- data/app/models/concerns/notey/notifier.rb +0 -19
- data/lib/notey/catalog.rb +0 -51
- data/lib/notey/event_delivery.rb +0 -14
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52c58921ef29966128ff879a465b658aecc03be72d9854b20e35fcf374bdf227
|
|
4
|
+
data.tar.gz: f33f53d08767a17f9409255954c1561a9d334e58c8d4e141410a3f1e3af8d429
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 679238d4b8f289a4e653a11a86002544d1eabbba16d381e691bbbfc2d8a813c4c86d4a988d2d008f7d88d637bbfd8ab24b9f8b844bae12270d04f63150dbb6b0
|
|
7
|
+
data.tar.gz: 4a0fe1f0a1f63ce23242147a896c8f45a6acad9bee3118b668a553aa9af68bf565e358d0ecc821511c5fa55b66c60dec7005bc99dd11bc43a9af2d9220608508
|
data/README.md
CHANGED
|
@@ -15,8 +15,8 @@ leaves to the host.
|
|
|
15
15
|
|
|
16
16
|
## What Notey adds
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
|
|
18
|
+
- **Channels.** One registration per channel the application can send on, made
|
|
19
|
+
once for the whole application rather than on every notification.
|
|
20
20
|
- **Preferences.** Which channels a person wants for each type, per account,
|
|
21
21
|
with a page they set them on.
|
|
22
22
|
- **Digest windows.** A type set to daily or weekly is held back and sent as one
|
|
@@ -24,8 +24,8 @@ leaves to the host.
|
|
|
24
24
|
- **An inbox.** In-app notification pages scoped to the account the person is in.
|
|
25
25
|
- **Destinations.** An address and an encrypted credential per account per
|
|
26
26
|
channel, so an account points a channel at its own workspace or endpoint.
|
|
27
|
-
- **
|
|
28
|
-
|
|
27
|
+
- **Notification types.** A class describing what a notification carries and
|
|
28
|
+
nothing about how it is sent.
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
@@ -54,20 +54,21 @@ That gives you `/notey/preferences`, `/notey/notifications` and
|
|
|
54
54
|
Notey owns its own tables and never owns your person or account records. Six
|
|
55
55
|
things connect it to yours.
|
|
56
56
|
|
|
57
|
-
### 1.
|
|
57
|
+
### 1. Register the channels your application has
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
notifier naming an undeclared type raises when the app boots.
|
|
59
|
+
Every application has email and in-app without registering anything. Register a
|
|
60
|
+
channel for anything else you send on, once, for the whole application.
|
|
62
61
|
|
|
63
62
|
```ruby
|
|
64
63
|
# config/initializers/notey.rb
|
|
65
|
-
Notey.
|
|
66
|
-
notification :comment, channels: %w[email sms], default: %w[email]
|
|
67
|
-
notification :mention, channels: %w[email], default: []
|
|
68
|
-
end
|
|
64
|
+
Notey.channel :sms, delivery_method: "Noticed::DeliveryMethods::TwilioMessaging", addressed: true
|
|
69
65
|
```
|
|
70
66
|
|
|
67
|
+
A registration names the channel, the delivery method that sends it, and whether
|
|
68
|
+
it needs an address before anything can go out on it. Nothing else names a
|
|
69
|
+
channel: a notification type does not, and a person's preferences offer exactly
|
|
70
|
+
what is registered.
|
|
71
|
+
|
|
71
72
|
### 2. Make your person model a recipient
|
|
72
73
|
|
|
73
74
|
```ruby
|
|
@@ -113,23 +114,41 @@ ActiveSupport.on_load :noticed_event do
|
|
|
113
114
|
end
|
|
114
115
|
```
|
|
115
116
|
|
|
116
|
-
### 5.
|
|
117
|
+
### 5. Define a notification type
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
A notification type describes what the notification carries and says nothing
|
|
120
|
+
about channels. Notey decides which channels each recipient gets.
|
|
120
121
|
|
|
121
122
|
```ruby
|
|
122
|
-
class
|
|
123
|
-
include Notey::Notifier
|
|
123
|
+
class CommentNotification < Notey::Notification
|
|
124
124
|
notey_type :comment
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
required_params :comment_id
|
|
127
|
+
|
|
128
|
+
def title
|
|
129
|
+
"New comment"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def body
|
|
133
|
+
"Someone replied to you"
|
|
129
134
|
end
|
|
130
135
|
end
|
|
131
136
|
```
|
|
132
137
|
|
|
138
|
+
`notey_type` is the name a person's stored preferences are keyed by, so renaming
|
|
139
|
+
the class does not orphan what they chose. Send it by naming the recipients your
|
|
140
|
+
own code resolved:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
CommentNotification.notify(recipients, comment_id: comment.id)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Notey builds the delivery list from the registered channels each time, so a
|
|
147
|
+
channel registered later needs no change here. For each recipient and each
|
|
148
|
+
channel it answers three things before sending: the person wants that channel
|
|
149
|
+
for that type, their window is immediate, and they have an address if the
|
|
150
|
+
channel needs one.
|
|
151
|
+
|
|
133
152
|
### 6. Tell Notey where a notification lives
|
|
134
153
|
|
|
135
154
|
Digest emails link to each notification through a lambda you supply, so the link
|
|
@@ -179,17 +198,34 @@ Each run enqueues one job per person, so one failing send does not stop the
|
|
|
179
198
|
rest. A window is sent once even if the run overlaps itself; a send that fails
|
|
180
199
|
releases the window so it can be sent again.
|
|
181
200
|
|
|
201
|
+
## Deleting old delivery records
|
|
202
|
+
|
|
203
|
+
Notey writes one record per notification per channel that leaves the
|
|
204
|
+
application, and nothing deletes them on its own. Say how long to keep them:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# config/initializers/notey.rb
|
|
208
|
+
Notey.attempt_retention = 90.days
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Then run the deletion on a schedule, the way you run the digest windows:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
Notey::DeleteOldAttempts.new.call
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Everything older than the period goes and everything inside it stays, so
|
|
218
|
+
running it twice leaves the same records as running it once. With no period set
|
|
219
|
+
it refuses to run rather than deleting nothing quietly.
|
|
220
|
+
|
|
182
221
|
## Destinations
|
|
183
222
|
|
|
184
223
|
A channel like email reaches a person at an address the app already holds. A
|
|
185
224
|
channel like SMS, a webhook, Slack or Discord does not, so somebody has to say
|
|
186
|
-
where it goes.
|
|
225
|
+
where it goes. Say so when you register the channel:
|
|
187
226
|
|
|
188
227
|
```ruby
|
|
189
|
-
Notey.
|
|
190
|
-
notification :comment, channels: %w[email sms], default: %w[email]
|
|
191
|
-
addressed :sms
|
|
192
|
-
end
|
|
228
|
+
Notey.channel :sms, delivery_method: "Noticed::DeliveryMethods::TwilioMessaging", addressed: true
|
|
193
229
|
```
|
|
194
230
|
|
|
195
231
|
**A person sets their own.** Their phone number, their endpoint. A notification
|
|
@@ -209,12 +245,14 @@ Bureau.section :notification_destinations, area: :account, title: "Notification
|
|
|
209
245
|
renders: "notey/destinations", runs: "Notey::SaveDestination", capability: :configure_site
|
|
210
246
|
```
|
|
211
247
|
|
|
212
|
-
|
|
248
|
+
Notey sends nothing on an addressed channel until the person has set an address.
|
|
249
|
+
The delivery method reads that address when it sends:
|
|
213
250
|
|
|
214
251
|
```ruby
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
252
|
+
class WebhookDeliveryMethod < Noticed::DeliveryMethod
|
|
253
|
+
def deliver
|
|
254
|
+
post_to Notey::Destinations.for(event.account_id, :webhook, member: recipient).address
|
|
255
|
+
end
|
|
218
256
|
end
|
|
219
257
|
```
|
|
220
258
|
|
|
@@ -226,13 +264,9 @@ it as a settings section so the shell's capability check guards it.
|
|
|
226
264
|
|
|
227
265
|
## Domain events
|
|
228
266
|
|
|
229
|
-
Notey does not depend on any event pipeline
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```ruby
|
|
233
|
-
# config/initializers/notey.rb
|
|
234
|
-
Notey.deliver_on :comment_posted, CommentNotifier
|
|
235
|
-
```
|
|
267
|
+
Notey does not depend on any event pipeline and holds no mapping from an event
|
|
268
|
+
to a notification. Your subscriber resolves who should hear about something and
|
|
269
|
+
calls the notification type.
|
|
236
270
|
|
|
237
271
|
```ruby
|
|
238
272
|
# app/subscribers/notey_notifications.rb
|
|
@@ -240,14 +274,17 @@ class NoteyNotifications < EventEngine::Subscribers::Base
|
|
|
240
274
|
subscribes_to :comment_posted
|
|
241
275
|
|
|
242
276
|
def handle(event)
|
|
243
|
-
|
|
277
|
+
payload = event.payload.to_h.symbolize_keys
|
|
278
|
+
|
|
279
|
+
Notey::Current.set(account_id: payload[:account_id]) do
|
|
280
|
+
CommentNotification.notify(User.where(id: payload[:user_ids]), comment_id: payload[:comment_id])
|
|
281
|
+
end
|
|
244
282
|
end
|
|
245
283
|
end
|
|
246
284
|
```
|
|
247
285
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
found. An event with no mapping does nothing.
|
|
286
|
+
Who receives a notification is decided outside notey; how each of them receives
|
|
287
|
+
it is decided inside.
|
|
251
288
|
|
|
252
289
|
## Development
|
|
253
290
|
|
data/app/models/notey/attempt.rb
CHANGED
|
@@ -3,5 +3,15 @@
|
|
|
3
3
|
module Notey
|
|
4
4
|
class Attempt < ApplicationRecord
|
|
5
5
|
belongs_to :notification, class_name: "Noticed::Notification"
|
|
6
|
+
|
|
7
|
+
def send_again
|
|
8
|
+
raise UnsendableAttempt, "#{channel} was already sent for this notification" if state == "sent"
|
|
9
|
+
|
|
10
|
+
delivery = notification.event.class.delivery_methods[channel.to_sym]
|
|
11
|
+
raise UnsendableAttempt, "#{channel} is not a channel this application has" if delivery.nil?
|
|
12
|
+
|
|
13
|
+
destroy
|
|
14
|
+
delivery.perform_later(notification)
|
|
15
|
+
end
|
|
6
16
|
end
|
|
7
17
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
class DeleteOldAttempts
|
|
5
|
+
UNSET = "notey deletes delivery records older than attempt_retention, which is not set"
|
|
6
|
+
|
|
7
|
+
def call
|
|
8
|
+
raise MissingRetention, UNSET if retention.nil?
|
|
9
|
+
|
|
10
|
+
Attempt.where(created_at: ...retention.ago).delete_all
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def retention
|
|
16
|
+
Notey.attempt_retention
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -27,7 +27,8 @@ module Notey
|
|
|
27
27
|
config = ActiveSupport::OrderedOptions.new
|
|
28
28
|
config[:class] = channel.delivery_method if channel.delivery_method
|
|
29
29
|
config[:notey_channel] = channel.name
|
|
30
|
-
config[:if] = Notey.sends(notey_notification_type, on: channel.name, addressed: channel.addressed
|
|
30
|
+
config[:if] = Notey.sends(notey_notification_type, on: channel.name, addressed: channel.addressed?,
|
|
31
|
+
via: Notey.known_delivery_method(channel))
|
|
31
32
|
config
|
|
32
33
|
end
|
|
33
34
|
private_class_method :delivery_config
|
data/lib/notey/channels.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Notey
|
|
|
10
10
|
stored = stored_for(member, notification_type, account_id)
|
|
11
11
|
|
|
12
12
|
Decision.new(
|
|
13
|
-
channels: stored
|
|
13
|
+
channels: channels_of(stored, notification_type) & Notey.channels,
|
|
14
14
|
window: stored&.digest_window || "immediate"
|
|
15
15
|
)
|
|
16
16
|
end
|
data/lib/notey/version.rb
CHANGED
data/lib/notey.rb
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
require "notey/version"
|
|
2
2
|
require "notey/engine"
|
|
3
|
-
require "notey/catalog"
|
|
4
3
|
require "notey/channels"
|
|
5
4
|
require "notey/destinations"
|
|
6
|
-
require "notey/event_delivery"
|
|
7
5
|
require "notey/inbox"
|
|
8
6
|
require "notey/digest_run"
|
|
9
7
|
require "notey/records_attempt"
|
|
@@ -18,12 +16,14 @@ module Notey
|
|
|
18
16
|
end
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
class UndeclaredType < StandardError; end
|
|
22
|
-
class UndeliverableChannel < StandardError; end
|
|
23
19
|
class MissingSender < StandardError; end
|
|
20
|
+
class UnsendableChannel < StandardError; end
|
|
21
|
+
class UnsendableAttempt < StandardError; end
|
|
22
|
+
class MissingRetention < StandardError; end
|
|
24
23
|
|
|
25
24
|
class << self
|
|
26
25
|
attr_writer :notification_url, :mailer_sender
|
|
26
|
+
attr_accessor :attempt_retention
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def self.mailer_sender
|
|
@@ -85,72 +85,43 @@ module Notey
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def self.check!
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
check_declared_types!
|
|
91
|
-
check_deliverable_channels!
|
|
88
|
+
registered_channels.each { |channel| check_channel!(channel) }
|
|
92
89
|
check_sender!
|
|
93
90
|
end
|
|
94
91
|
|
|
95
|
-
def self.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
raise MissingSender, "notey sends digests by email and no mailer_sender is set"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def self.check_deliverable_channels!
|
|
102
|
-
delivered = notifiers.flat_map { |notifier| notifier.delivery_methods.keys.map(&:to_s) }.uniq
|
|
103
|
-
|
|
104
|
-
(catalog.channels - delivered).each do |channel|
|
|
105
|
-
raise UndeliverableChannel, "the catalog offers the channel #{channel}, which no notifier delivers on"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def self.check_declared_types!
|
|
110
|
-
notifiers.each do |notifier|
|
|
111
|
-
notification_type = notifier.notey_notification_type
|
|
112
|
-
next if notification_type.nil? || catalog.declared?(notification_type)
|
|
92
|
+
def self.check_channel!(channel)
|
|
93
|
+
required = delivery_method_for(channel).required_option_names
|
|
94
|
+
return if required.empty?
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def self.destination_address(channel)
|
|
120
|
-
-> { Destinations.for(event.account_id, channel, member: recipient)&.address }
|
|
96
|
+
raise UnsendableChannel,
|
|
97
|
+
"the channel #{channel.name} needs the option #{required.first}, which notey does not supply"
|
|
121
98
|
end
|
|
122
99
|
|
|
123
|
-
def self.
|
|
124
|
-
|
|
100
|
+
def self.known_delivery_method(channel)
|
|
101
|
+
delivery_method_for(channel)
|
|
102
|
+
rescue UnsendableChannel
|
|
103
|
+
nil
|
|
125
104
|
end
|
|
126
105
|
|
|
127
|
-
def self.
|
|
128
|
-
|
|
106
|
+
def self.delivery_method_for(channel)
|
|
107
|
+
name = channel.delivery_method || "Noticed::DeliveryMethods::#{channel.name.camelize}"
|
|
108
|
+
name.to_s.constantize
|
|
109
|
+
rescue NameError
|
|
110
|
+
raise UnsendableChannel, "the channel #{channel.name} names the delivery method #{name}, which does not exist"
|
|
129
111
|
end
|
|
130
112
|
|
|
131
|
-
def self.
|
|
132
|
-
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def self.event_notifiers
|
|
136
|
-
@event_notifiers ||= {}
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def self.catalog(&block)
|
|
140
|
-
@catalog ||= Catalog.new
|
|
141
|
-
@catalog.instance_eval(&block) if block
|
|
142
|
-
@catalog
|
|
143
|
-
end
|
|
113
|
+
def self.check_sender!
|
|
114
|
+
return if mailer_sender.present?
|
|
144
115
|
|
|
145
|
-
|
|
146
|
-
sends(notification_type, on: on)
|
|
116
|
+
raise MissingSender, "notey sends digests by email and no mailer_sender is set"
|
|
147
117
|
end
|
|
148
118
|
|
|
149
|
-
def self.sends(notification_type, on:, addressed: false)
|
|
119
|
+
def self.sends(notification_type, on:, addressed: false, via: nil)
|
|
150
120
|
lambda do
|
|
151
121
|
decision = Channels.decision_for(recipient, notification_type, account_id: event.account_id)
|
|
152
122
|
|
|
153
123
|
next false unless decision.window == "immediate" && decision.channels.include?(on.to_s)
|
|
124
|
+
next false if via.respond_to?(:reachable?) && !via.reachable?(recipient)
|
|
154
125
|
next true unless addressed
|
|
155
126
|
|
|
156
127
|
Destinations.for(event.account_id, on, member: recipient).present?
|
|
@@ -158,8 +129,6 @@ module Notey
|
|
|
158
129
|
end
|
|
159
130
|
|
|
160
131
|
def self.reset!
|
|
161
|
-
@catalog = nil
|
|
162
|
-
@event_notifiers = nil
|
|
163
132
|
@host_channels = nil
|
|
164
133
|
forget_notifiers
|
|
165
134
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: notey-develop
|
|
3
|
-
description: Use PROACTIVELY for
|
|
3
|
+
description: Use PROACTIVELY for defining a notification type, sending one to the recipients your own code resolved, sending one when a domain event fires, addressing a channel at a person, scheduling daily and weekly digest runs, reading a person's inbox, and reading their channels and window — MUST BE USED instead of hand-rolling preference checks, per-person delivery addresses, or a digest loop.
|
|
4
4
|
tools: Read, Write, Edit, Grep
|
|
5
|
-
scope: notifications —
|
|
5
|
+
scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
This local follows the steps below exactly and invents no others. Where a step
|
|
@@ -12,31 +12,37 @@ picking one.
|
|
|
12
12
|
## What notey is
|
|
13
13
|
|
|
14
14
|
notey is the notification layer for a multi-tenant Rails app on top of Noticed.
|
|
15
|
-
It holds which
|
|
16
|
-
each account they belong to, and whether they want them as they happen
|
|
17
|
-
daily or weekly digest. Fire this local when a
|
|
18
|
-
changed, when a domain event should send one, when digests need
|
|
19
|
-
the host's own code needs to read a person's notifications or
|
|
15
|
+
It holds which channels the application can send on, which channels each person
|
|
16
|
+
wants in each account they belong to, and whether they want them as they happen
|
|
17
|
+
or in a daily or weekly digest. Fire this local when a notification type is
|
|
18
|
+
being written or changed, when a domain event should send one, when digests need
|
|
19
|
+
running, or when the host's own code needs to read a person's notifications or
|
|
20
|
+
their preferences.
|
|
20
21
|
|
|
21
22
|
This local assumes the gem is already hooked into the app; if it is not, that is
|
|
22
23
|
`notey-install`'s work and it comes first.
|
|
23
24
|
|
|
24
25
|
## Interface
|
|
25
26
|
|
|
26
|
-
- `
|
|
27
|
-
|
|
28
|
-
- `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
- `Notey::Notification` — the class a notification type inherits, which builds
|
|
28
|
+
its delivery list from the registered channels and decides each one itself.
|
|
29
|
+
- `notey_type` — declared inside a notification type, names the key a person's
|
|
30
|
+
stored preferences are held under, so renaming the class keeps them.
|
|
31
|
+
- `required_params` — declared inside a notification type, names the information
|
|
32
|
+
a caller must pass, and a call that omits any of it is refused.
|
|
33
|
+
- `notify(recipients, **information)` — sends one notification to the recipients
|
|
34
|
+
the caller resolved, recording one row for each.
|
|
35
|
+
- `Notey.channel(name, delivery_method:, addressed:)` — registers one channel
|
|
36
|
+
the application can send on, for the whole application.
|
|
37
|
+
- `Notey::Destinations.for(account_id, channel, member:)` — returns the stored
|
|
38
|
+
destination for that person on that channel, which a delivery method reads to
|
|
39
|
+
learn where to send.
|
|
37
40
|
- `Notey::DigestRun` — sends one window's digests, with `call` for everyone on
|
|
38
41
|
that window and `deliver_to_member(member, account_id)` for one person in one
|
|
39
42
|
account.
|
|
43
|
+
- `Notey::DeleteOldAttempts` — deletes every record of what was sent that is
|
|
44
|
+
older than `Notey.attempt_retention`, and refuses to run when no retention is
|
|
45
|
+
set.
|
|
40
46
|
- `Notey::Inbox.for(member, account_id:)` — returns that person's notifications
|
|
41
47
|
for that account, as a relation.
|
|
42
48
|
- `wants?(type, on:, account_id:)` — on the recipient model, true when that
|
|
@@ -45,65 +51,76 @@ This local assumes the gem is already hooked into the app; if it is not, that is
|
|
|
45
51
|
person gets that type on in that account.
|
|
46
52
|
- `digest_window_for(type, account_id:)` — on the recipient model, `immediate`,
|
|
47
53
|
`daily` or `weekly` for that type in that account.
|
|
48
|
-
- `Notey.reset!` — drops the
|
|
49
|
-
tests that
|
|
54
|
+
- `Notey.reset!` — drops the registered channels and notification types, for
|
|
55
|
+
tests that register their own.
|
|
50
56
|
|
|
51
57
|
## How to use it
|
|
52
58
|
|
|
53
|
-
1.
|
|
59
|
+
1. Write a notification type that says nothing about channels:
|
|
54
60
|
|
|
55
61
|
```ruby
|
|
56
|
-
class
|
|
62
|
+
class CommentNotification < Notey::Notification
|
|
57
63
|
notey_type :comment
|
|
64
|
+
|
|
65
|
+
required_params :comment_id
|
|
66
|
+
|
|
67
|
+
def title
|
|
68
|
+
"New comment"
|
|
69
|
+
end
|
|
58
70
|
end
|
|
59
71
|
```
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
Ask the developer what information the notification carries and what a person
|
|
74
|
+
should read on it. Two types may name the same `notey_type`, and renaming the
|
|
75
|
+
class keeps the preferences people have already stored.
|
|
63
76
|
|
|
64
|
-
2.
|
|
77
|
+
2. Send it by naming the recipients your own code resolved:
|
|
65
78
|
|
|
66
79
|
```ruby
|
|
67
|
-
|
|
68
|
-
config.if = Notey.wanted(:comment, on: :email)
|
|
69
|
-
end
|
|
80
|
+
CommentNotification.notify(recipients, comment_id: comment.id)
|
|
70
81
|
```
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
Working out who should receive it, and removing duplicates from that list, is
|
|
84
|
+
the caller's and never notey's. The call is refused before anything is
|
|
85
|
+
recorded when it omits information the type requires.
|
|
75
86
|
|
|
76
|
-
3.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
3. Write no condition on any channel. notey builds the delivery list from the
|
|
88
|
+
registered channels and answers three things per recipient per channel before
|
|
89
|
+
sending: the person wants that channel for that type, their window is
|
|
90
|
+
immediate, and they have an address if the channel needs one. A type that
|
|
91
|
+
tries to gate its own channels is doing notey's job twice.
|
|
80
92
|
|
|
81
|
-
4. For a channel
|
|
82
|
-
stored address and skip the delivery when there is none:
|
|
93
|
+
4. For a channel that needs an address, read it inside the delivery method:
|
|
83
94
|
|
|
84
95
|
```ruby
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
class WebhookDeliveryMethod < Noticed::DeliveryMethod
|
|
97
|
+
def deliver
|
|
98
|
+
post_to Notey::Destinations.for(event.account_id, :webhook, member: recipient).address
|
|
99
|
+
end
|
|
88
100
|
end
|
|
89
101
|
```
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
notey has already refused the delivery when no address is set, so the lookup
|
|
104
|
+
never comes back empty here. Ask the developer which channels need an address,
|
|
105
|
+
since that is named once on the registration and only they know which.
|
|
94
106
|
|
|
95
|
-
5.
|
|
107
|
+
5. Send from a domain event in the host's own subscriber:
|
|
96
108
|
|
|
97
109
|
```ruby
|
|
98
|
-
|
|
110
|
+
def handle(event)
|
|
111
|
+
payload = event.payload.to_h.symbolize_keys
|
|
112
|
+
|
|
113
|
+
Notey::Current.set(account_id: payload[:account_id]) do
|
|
114
|
+
CommentNotification.notify(User.where(id: payload[:user_ids]), comment_id: payload[:comment_id])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
99
117
|
```
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
against no account and never reaches an inbox.
|
|
119
|
+
notey holds no mapping from an event to a notification and depends on no event
|
|
120
|
+
pipeline. The subscriber resolves the recipients and sets the account, because
|
|
121
|
+
a delivery runs in a job where the current account is gone. Without
|
|
122
|
+
`account_id` the notification is stored against no account and never reaches
|
|
123
|
+
an inbox.
|
|
107
124
|
|
|
108
125
|
6. Schedule the digest windows. notey registers no schedule of its own, so a
|
|
109
126
|
window nobody runs never sends:
|
|
@@ -119,7 +136,21 @@ This local assumes the gem is already hooked into the app; if it is not, that is
|
|
|
119
136
|
in this app — cron, a scheduler gem, or a platform scheduler — and at what
|
|
120
137
|
hour each window should go out.
|
|
121
138
|
|
|
122
|
-
7.
|
|
139
|
+
7. Schedule the deletion of old delivery records alongside the windows:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
Notey::DeleteOldAttempts.new.call
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
notey writes one record per notification per channel that leaves the
|
|
146
|
+
application and deletes none of them on its own, so a host that never runs
|
|
147
|
+
this keeps every row forever. It deletes everything older than
|
|
148
|
+
`Notey.attempt_retention` and refuses to run when that is not set, which
|
|
149
|
+
`notey-install` is the step that sets. Running it twice leaves the same
|
|
150
|
+
records as running it once, so a schedule that overlaps itself is safe. Ask
|
|
151
|
+
the developer how often it should run.
|
|
152
|
+
|
|
153
|
+
8. Read a person's notifications through the inbox rather than querying Noticed
|
|
123
154
|
directly:
|
|
124
155
|
|
|
125
156
|
```ruby
|
|
@@ -129,7 +160,7 @@ This local assumes the gem is already hooked into the app; if it is not, that is
|
|
|
129
160
|
It returns a relation, so chain `.unread`, `.order` and `.limit` onto it. With
|
|
130
161
|
no account it returns an empty relation, never another account's rows.
|
|
131
162
|
|
|
132
|
-
|
|
163
|
+
9. Read a person's preferences from the host's own code when the app needs to
|
|
133
164
|
branch on them:
|
|
134
165
|
|
|
135
166
|
```ruby
|
|
@@ -139,27 +170,26 @@ This local assumes the gem is already hooked into the app; if it is not, that is
|
|
|
139
170
|
```
|
|
140
171
|
|
|
141
172
|
All three take the account from the current request when it is left out. All
|
|
142
|
-
three read a person's stored row for that type, and fall back to
|
|
143
|
-
|
|
173
|
+
three read a person's stored row for that type, and fall back to email and
|
|
174
|
+
in-app when they have never stored one.
|
|
144
175
|
|
|
145
|
-
|
|
176
|
+
10. In tests that register their own channels or notification types, call
|
|
146
177
|
`Notey.reset!` in teardown. Both are held for the life of the process, so a
|
|
147
|
-
test that skips this leaves its
|
|
178
|
+
test that skips this leaves its channels and types in place for every test
|
|
148
179
|
after it.
|
|
149
180
|
|
|
150
181
|
## Conventions
|
|
151
182
|
|
|
152
|
-
- **Every
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- **Every channel
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- **
|
|
161
|
-
|
|
162
|
-
the delivery silently never runs.
|
|
183
|
+
- **Every notification type names a `notey_type`.** Without one no digest ever
|
|
184
|
+
includes it, because a digest picks its rows by that name.
|
|
185
|
+
- **No notification type names a channel.** Channels are registered once for the
|
|
186
|
+
whole application, and a type that declares one is describing something it
|
|
187
|
+
does not decide.
|
|
188
|
+
- **Every registered channel is offered for every type.** A type cannot be kept
|
|
189
|
+
off a channel, so a channel nobody should get for a given notification is a
|
|
190
|
+
channel that should not be registered.
|
|
191
|
+
- **Email and in-app exist without being registered.** An application that
|
|
192
|
+
registers nothing can still send on both.
|
|
163
193
|
- **A digest is mailed to the person's `email`.** A recipient model without one
|
|
164
194
|
raises when its digest is sent, not when the run starts.
|
|
165
195
|
- **A run sends one window once.** Overlapping runs of the same window send one
|
|
@@ -170,9 +200,9 @@ This local assumes the gem is already hooked into the app; if it is not, that is
|
|
|
170
200
|
nothing.
|
|
171
201
|
- **A digest sends nothing when there is nothing to send** — no empty mail, and
|
|
172
202
|
no row saying it went out.
|
|
173
|
-
- **
|
|
174
|
-
code**, never in the gem.
|
|
175
|
-
- **Out of scope.**
|
|
176
|
-
a model a recipient, setting the
|
|
177
|
-
account's addresses, and the pages a
|
|
178
|
-
belong to `notey-install`.
|
|
203
|
+
- **Notification types, subscribers and digest schedules all belong in the
|
|
204
|
+
host's code**, never in the gem.
|
|
205
|
+
- **Out of scope.** Registering the channels the application has, setting how
|
|
206
|
+
long delivery records are kept, making a model a recipient, setting the
|
|
207
|
+
current person and account, storing an account's addresses, and the pages a
|
|
208
|
+
person picks their own channels on all belong to `notey-install`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: notey-info
|
|
3
3
|
description: Use to learn what notey offers — notification types and the channels they allow, per-person per-account delivery preferences, digest windows, an in-app inbox, and per-account destinations.
|
|
4
4
|
tools: Read
|
|
5
|
-
scope: notifications —
|
|
5
|
+
scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
This local explains what notey is and which of its other two locals you need. It
|
|
@@ -52,15 +52,12 @@ has already been sent, you are building, and `notey-develop` owns it.
|
|
|
52
52
|
## Conventions
|
|
53
53
|
|
|
54
54
|
- **Notification type** — the name of one kind of notification, as a string. It
|
|
55
|
-
is the unit everything else is keyed by: a person's preference
|
|
56
|
-
|
|
57
|
-
- **
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **Channel** — a named way of delivering, such as email or a chat webhook. The
|
|
62
|
-
catalog offers them per notification type; a notifier is what actually
|
|
63
|
-
delivers on one.
|
|
55
|
+
is the unit everything else is keyed by: a person's preference and the name a
|
|
56
|
+
notification type declares.
|
|
57
|
+
- **Channel** — a named way of delivering, such as email or a chat webhook,
|
|
58
|
+
registered once for the whole application. Email and in-app exist without
|
|
59
|
+
being registered; anything else exists only if the application registered it.
|
|
60
|
+
Every registered channel is offered for every notification type.
|
|
64
61
|
- **Member** — the person a preference belongs to. It is polymorphic, so a host
|
|
65
62
|
app names whichever model of its own receives notifications.
|
|
66
63
|
- **Account** — the tenant. Preferences, notifications, digests and destinations
|
|
@@ -72,9 +69,16 @@ has already been sent, you are building, and `notey-develop` owns it.
|
|
|
72
69
|
notification happens; the other two hold it for a grouped email covering the
|
|
73
70
|
current day or week.
|
|
74
71
|
- **Destination** — one address per account per channel, with an optional stored
|
|
75
|
-
credential,
|
|
72
|
+
credential, optionally owned by one person. A notification for a person uses
|
|
73
|
+
the address that person set, never the account's.
|
|
76
74
|
- **Inbox** — the notifications a member has received within one account, which
|
|
77
75
|
is what the in-app list reads and what a digest gathers from.
|
|
78
|
-
- **
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
- **Notification type** — the class describing what one notification carries and
|
|
77
|
+
what a person reads on it. It names no channel, because notey builds the
|
|
78
|
+
delivery list from the registered channels and decides each one against the
|
|
79
|
+
recipient's stored preference.
|
|
80
|
+
- **Attempt** — one row per notification per outbound channel, claimed before the
|
|
81
|
+
send and marked after, holding whether it was sent and what a failure said. It
|
|
82
|
+
is what answers why one person was not reached, and it is what stops the same
|
|
83
|
+
channel sending twice for the same notification. The host says how long these
|
|
84
|
+
are kept and runs the deletion that enforces it.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: notey-install
|
|
3
|
-
description: Use to hook notey into a project — installing its tables, mounting its engine,
|
|
3
|
+
description: Use to hook notey into a project — installing its tables, mounting its engine, registering the channels the application can send on, making a model a recipient, setting the current person and account, putting the account on Noticed's rows, supplying a notification url, and reaching the preferences, inbox and destinations pages.
|
|
4
4
|
tools: Bash, Read, Edit
|
|
5
|
-
scope: notifications —
|
|
5
|
+
scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
This local follows the steps below exactly and invents no others. Where a step
|
|
@@ -17,31 +17,35 @@ different notifications in each.
|
|
|
17
17
|
|
|
18
18
|
## Interface
|
|
19
19
|
|
|
20
|
-
- `bin/rails notey:install:migrations` — copies notey's
|
|
20
|
+
- `bin/rails notey:install:migrations` — copies notey's migrations into the
|
|
21
21
|
host's `db/migrate`.
|
|
22
22
|
- `mount Notey::Engine` — in the host's `config/routes.rb`, putting notey's
|
|
23
23
|
three pages under a path the host chooses.
|
|
24
|
-
- `Notey.
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
- `Notey.channel` — registers one channel the application can send on, naming
|
|
25
|
+
the delivery method that sends it and whether it needs an address. Email and
|
|
26
|
+
in-app exist without being registered.
|
|
27
27
|
- `Notey::Recipient` — included in the host model that receives notifications,
|
|
28
28
|
which gives that model its stored preferences.
|
|
29
|
-
- `Notey::
|
|
30
|
-
|
|
29
|
+
- `Notey::Notification` — the class each notification type inherits, which
|
|
30
|
+
registers the type and builds its delivery list from the registered channels.
|
|
31
31
|
- `Notey::Current` — holds the person and the account id for the current
|
|
32
32
|
request, and every preference read, preference save and inbox read goes
|
|
33
33
|
through it.
|
|
34
34
|
- `Notey.notification_url=` — takes a lambda returning the host's own url for
|
|
35
35
|
one notification, used to link the rows in a digest email.
|
|
36
|
-
- `Notey.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
- `Notey.attempt_retention=` — takes how long notey keeps the record of what it
|
|
37
|
+
sent on each channel, such as `90.days`. Nothing is deleted until the host
|
|
38
|
+
runs the deletion.
|
|
39
|
+
- `Notey.check!` — raises when a registered channel names a delivery method that
|
|
40
|
+
does not exist or needs an option notey does not supply, and when notey has no
|
|
41
|
+
sender address for its digests. notey runs it itself after initialization when
|
|
42
|
+
the app eager loads.
|
|
39
43
|
- `/notey/preferences` — the page a person picks their channels and their
|
|
40
44
|
immediate, daily or weekly window on, per notification type.
|
|
41
45
|
- `/notey/notifications` — the person's inbox for the account they are in, 25
|
|
42
46
|
rows newest first with an unread count, and a button that marks one read.
|
|
43
47
|
- `/notey/destinations` — the page an account's address and credential are
|
|
44
|
-
stored on, one pair per channel
|
|
48
|
+
stored on, one pair per registered channel that needs an address.
|
|
45
49
|
|
|
46
50
|
The three paths assume the engine is mounted at `/notey`; a different mount path
|
|
47
51
|
moves all three.
|
|
@@ -51,8 +55,8 @@ moves all three.
|
|
|
51
55
|
1. Add `gem "notey"` to the host's `Gemfile` and run `bundle install`.
|
|
52
56
|
|
|
53
57
|
2. Run `bin/rails notey:install:migrations` and then `bin/rails db:migrate`.
|
|
54
|
-
This creates `notey_preferences`, `notey_digests
|
|
55
|
-
the host's database.
|
|
58
|
+
This creates `notey_preferences`, `notey_digests`, `notey_destinations` and
|
|
59
|
+
`notey_attempts` in the host's database.
|
|
56
60
|
|
|
57
61
|
3. Mount the engine in the host's `config/routes.rb`:
|
|
58
62
|
|
|
@@ -63,18 +67,18 @@ moves all three.
|
|
|
63
67
|
Ask the developer which path to mount at if the app already has a convention
|
|
64
68
|
for engine paths, since the path prefixes all three pages.
|
|
65
69
|
|
|
66
|
-
4.
|
|
70
|
+
4. Register the channels the application sends on, in
|
|
71
|
+
`config/initializers/notey.rb`:
|
|
67
72
|
|
|
68
73
|
```ruby
|
|
69
|
-
Notey.
|
|
70
|
-
notification :comment, channels: %w[email sms], default: %w[email]
|
|
71
|
-
end
|
|
74
|
+
Notey.channel :sms, delivery_method: "Noticed::DeliveryMethods::TwilioMessaging", addressed: true
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
Every application has email and in-app already, so register nothing to get
|
|
78
|
+
those two. Ask the developer which other channels this application sends on,
|
|
79
|
+
which delivery method sends each one, and which of them need an address
|
|
80
|
+
before anything can go out. A channel registered here is offered for every
|
|
81
|
+
notification type; there is no way to keep a type off one.
|
|
78
82
|
|
|
79
83
|
5. Include the recipient concern in the model that receives notifications:
|
|
80
84
|
|
|
@@ -99,7 +103,7 @@ moves all three.
|
|
|
99
103
|
|
|
100
104
|
Ask the developer what supplies the signed-in person and the current account
|
|
101
105
|
in this app, since both names above are guesses about the host. A read with
|
|
102
|
-
no account set returns
|
|
106
|
+
no account set returns email and in-app and an empty inbox,
|
|
103
107
|
never another account's rows, and a save on the preferences page fails
|
|
104
108
|
because a stored preference needs an account.
|
|
105
109
|
|
|
@@ -125,17 +129,18 @@ moves all three.
|
|
|
125
129
|
Without these columns the inbox is empty and every digest is, because a
|
|
126
130
|
delivery runs in a job where the current account is gone.
|
|
127
131
|
|
|
128
|
-
8.
|
|
129
|
-
the catalog's types:
|
|
132
|
+
8. Define one notification type so there is something to send:
|
|
130
133
|
|
|
131
134
|
```ruby
|
|
132
|
-
class
|
|
133
|
-
|
|
135
|
+
class CommentNotification < Notey::Notification
|
|
136
|
+
notey_type :comment
|
|
137
|
+
|
|
138
|
+
required_params :comment_id
|
|
134
139
|
end
|
|
135
140
|
```
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
What each type carries, what a person reads on it, and sending one are
|
|
143
|
+
`notey-develop`'s steps rather than this local's.
|
|
139
144
|
|
|
140
145
|
9. Point digest links at the host's own pages in
|
|
141
146
|
`config/initializers/notey.rb`:
|
|
@@ -150,8 +155,9 @@ moves all three.
|
|
|
150
155
|
set, a digest still sends and its rows are plain text instead of links.
|
|
151
156
|
|
|
152
157
|
10. Boot the app with eager loading on and watch it raise or come up. notey runs
|
|
153
|
-
`Notey.check!` itself at that point
|
|
154
|
-
to
|
|
158
|
+
`Notey.check!` itself at that point, which refuses a channel that cannot
|
|
159
|
+
send and refuses to run with no sender address for its digests; call it
|
|
160
|
+
directly in a test or a console to check the same thing without a boot.
|
|
155
161
|
|
|
156
162
|
11. Decide the pages' layout. They render in notey's own layout, which loads
|
|
157
163
|
notey's stylesheet and nothing of the host's, unless the host defines
|
|
@@ -159,8 +165,18 @@ moves all three.
|
|
|
159
165
|
the engine's copy. Ask the developer whether these pages should carry the
|
|
160
166
|
app's navigation.
|
|
161
167
|
|
|
162
|
-
12. Set
|
|
163
|
-
|
|
168
|
+
12. Set the retention for delivery records in
|
|
169
|
+
`config/initializers/notey.rb`:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Notey.attempt_retention = 90.days
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Ask the developer how long this application should keep the record of what
|
|
176
|
+
was sent on each channel; there is no default. Scheduling the deletion is
|
|
177
|
+
`notey-develop`'s step, and nothing is deleted until it runs.
|
|
178
|
+
|
|
179
|
+
13. Set up destinations only if a registered channel needs an address. The stored credential is encrypted, so
|
|
164
180
|
Active Record encryption keys must be configured in the host's credentials
|
|
165
181
|
(`bin/rails db:encryption:init` generates a set) or saving a destination
|
|
166
182
|
raises. Ask the developer whether any channel needs this before doing it.
|
|
@@ -168,16 +184,18 @@ moves all three.
|
|
|
168
184
|
## Conventions
|
|
169
185
|
|
|
170
186
|
- **Check each page while signed in with an account set.** `/notey/preferences`
|
|
171
|
-
lists one panel per
|
|
172
|
-
|
|
173
|
-
address and credential field per
|
|
187
|
+
lists one panel per notification type with every registered channel against
|
|
188
|
+
it, `/notey/notifications` lists that person's notifications for the current
|
|
189
|
+
account, and `/notey/destinations` lists one address and credential field per
|
|
190
|
+
registered channel that needs an address.
|
|
174
191
|
- **A preferences page that will not save is the symptom of a missing account.**
|
|
175
|
-
Reads fall back to
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
Reads fall back to email and in-app when `Notey::Current.account_id` is nil
|
|
193
|
+
and a save is refused, so check step 6 before anything else.
|
|
194
|
+
- **A registration for a channel notey already has replaces it.** Registering
|
|
195
|
+
`:email` with no delivery method leaves notey's own email delivery behind, and
|
|
196
|
+
the app refuses to boot rather than sending nothing, so register a channel
|
|
197
|
+
notey supplies only to replace it deliberately and with a delivery method that
|
|
198
|
+
can send.
|
|
181
199
|
- **Authentication on the three pages is the host's.** Its controllers inherit
|
|
182
200
|
from the host's `ApplicationController`, so the host's own filters run and
|
|
183
201
|
notey adds no authorization of its own — restricting who may set an account's
|
|
@@ -186,6 +204,10 @@ moves all three.
|
|
|
186
204
|
that changes it.
|
|
187
205
|
- **Re-run `bin/rails notey:install:migrations` after upgrading the gem.** It
|
|
188
206
|
copies only the migrations the host does not already have.
|
|
189
|
-
- **
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
- **Nothing deletes a delivery record until the host runs the deletion.** The
|
|
208
|
+
table grows for every notification on every outbound channel, so a host that
|
|
209
|
+
sets no retention and schedules no deletion keeps every row forever.
|
|
210
|
+
- **Out of scope.** Scheduling the digest runs and the deletion of old delivery
|
|
211
|
+
records, writing a notification type,
|
|
212
|
+
sending one when a domain event fires, and reading the inbox from the host's
|
|
213
|
+
own code all belong to `notey-develop`.
|
data/the_local/interface.yml
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
scope: notifications —
|
|
1
|
+
scope: notifications — application-wide channel registrations, notification types that name no channel, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a record of what was sent on each channel that the host keeps for a period it sets
|
|
2
2
|
|
|
3
3
|
install:
|
|
4
4
|
- bin/rails notey:install:migrations
|
|
5
5
|
- mount Notey::Engine
|
|
6
|
-
- Notey.
|
|
6
|
+
- Notey.channel
|
|
7
7
|
- Notey::Recipient
|
|
8
|
-
- Notey::
|
|
8
|
+
- Notey::Notification
|
|
9
9
|
- Notey::Current
|
|
10
10
|
- Notey.notification_url=
|
|
11
|
+
- Notey.attempt_retention=
|
|
11
12
|
- Notey.check!
|
|
12
13
|
- /notey/preferences
|
|
13
14
|
- /notey/notifications
|
|
@@ -15,11 +16,11 @@ install:
|
|
|
15
16
|
|
|
16
17
|
develop:
|
|
17
18
|
- notey_type
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Notey.
|
|
21
|
-
- Notey.addressed
|
|
19
|
+
- required_params
|
|
20
|
+
- notify
|
|
21
|
+
- Notey::Destinations.for
|
|
22
22
|
- Notey::DigestRun
|
|
23
|
+
- Notey::DeleteOldAttempts
|
|
23
24
|
- Notey::Inbox.for
|
|
24
25
|
- wants?
|
|
25
26
|
- channels_for
|
|
@@ -29,20 +30,24 @@ develop:
|
|
|
29
30
|
sources:
|
|
30
31
|
- lib/notey.rb
|
|
31
32
|
- lib/notey/engine.rb
|
|
32
|
-
- lib/notey/catalog.rb
|
|
33
33
|
- lib/notey/channels.rb
|
|
34
34
|
- lib/notey/destinations.rb
|
|
35
35
|
- lib/notey/digest_run.rb
|
|
36
|
-
- lib/notey/event_subscriber.rb
|
|
37
36
|
- lib/notey/inbox.rb
|
|
37
|
+
- lib/notey/records_attempt.rb
|
|
38
38
|
- app/models/concerns/notey/recipient.rb
|
|
39
|
-
- app/models/
|
|
39
|
+
- app/models/notey/notification.rb
|
|
40
40
|
- app/models/notey/current.rb
|
|
41
41
|
- app/models/notey/preference.rb
|
|
42
42
|
- app/models/notey/destination.rb
|
|
43
43
|
- app/models/notey/digest.rb
|
|
44
|
+
- app/models/notey/attempt.rb
|
|
45
|
+
- app/models/notey/delete_old_attempts.rb
|
|
46
|
+
- app/delivery_methods/notey/email.rb
|
|
47
|
+
- app/delivery_methods/notey/in_app.rb
|
|
44
48
|
- app/jobs/notey/digest_job.rb
|
|
45
49
|
- app/mailers/notey/digest_mailer.rb
|
|
50
|
+
- app/mailers/notey/notification_mailer.rb
|
|
46
51
|
- app/controllers/notey/preferences_controller.rb
|
|
47
52
|
- app/controllers/notey/notifications_controller.rb
|
|
48
53
|
- app/controllers/notey/destinations_controller.rb
|
|
@@ -50,3 +55,4 @@ sources:
|
|
|
50
55
|
- db/migrate/20260917000002_create_notey_preferences.rb
|
|
51
56
|
- db/migrate/20260918030002_create_notey_digests.rb
|
|
52
57
|
- db/migrate/20260918050001_create_notey_destinations.rb
|
|
58
|
+
- db/migrate/20260921000001_create_notey_attempts.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: notey
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -84,11 +84,11 @@ files:
|
|
|
84
84
|
- app/mailers/notey/application_mailer.rb
|
|
85
85
|
- app/mailers/notey/digest_mailer.rb
|
|
86
86
|
- app/mailers/notey/notification_mailer.rb
|
|
87
|
-
- app/models/concerns/notey/notifier.rb
|
|
88
87
|
- app/models/concerns/notey/recipient.rb
|
|
89
88
|
- app/models/notey/application_record.rb
|
|
90
89
|
- app/models/notey/attempt.rb
|
|
91
90
|
- app/models/notey/current.rb
|
|
91
|
+
- app/models/notey/delete_old_attempts.rb
|
|
92
92
|
- app/models/notey/destination.rb
|
|
93
93
|
- app/models/notey/digest.rb
|
|
94
94
|
- app/models/notey/kept.rb
|
|
@@ -115,12 +115,10 @@ files:
|
|
|
115
115
|
- db/migrate/20260919010001_add_member_to_notey_destinations.rb
|
|
116
116
|
- db/migrate/20260921000001_create_notey_attempts.rb
|
|
117
117
|
- lib/notey.rb
|
|
118
|
-
- lib/notey/catalog.rb
|
|
119
118
|
- lib/notey/channels.rb
|
|
120
119
|
- lib/notey/destinations.rb
|
|
121
120
|
- lib/notey/digest_run.rb
|
|
122
121
|
- lib/notey/engine.rb
|
|
123
|
-
- lib/notey/event_delivery.rb
|
|
124
122
|
- lib/notey/inbox.rb
|
|
125
123
|
- lib/notey/records_attempt.rb
|
|
126
124
|
- lib/notey/version.rb
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Notey
|
|
4
|
-
module Notifier
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
included do
|
|
8
|
-
class_attribute :notey_notification_type, instance_writer: false
|
|
9
|
-
|
|
10
|
-
Notey.register_notifier(self)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
class_methods do
|
|
14
|
-
def notey_type(name)
|
|
15
|
-
self.notey_notification_type = name.to_s
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/lib/notey/catalog.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Notey
|
|
4
|
-
class Catalog
|
|
5
|
-
Notification = Struct.new(:channels, :default, keyword_init: true)
|
|
6
|
-
|
|
7
|
-
def initialize
|
|
8
|
-
@notifications = {}
|
|
9
|
-
@addressed = []
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def addressed(*channels)
|
|
13
|
-
return @addressed if channels.empty?
|
|
14
|
-
|
|
15
|
-
@addressed |= channels.map(&:to_s)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def notification(name, channels: [], default: [])
|
|
19
|
-
@notifications[name.to_s] = Notification.new(
|
|
20
|
-
channels: Array(channels).map(&:to_s),
|
|
21
|
-
default: Array(default).map(&:to_s)
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def notifications
|
|
26
|
-
@notifications
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def channels
|
|
30
|
-
@notifications.values.flat_map(&:channels).uniq
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def declared?(notification_type)
|
|
34
|
-
@notifications.key?(notification_type.to_s)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def channels_for(notification_type)
|
|
38
|
-
declared(notification_type)&.channels || []
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def default_channels_for(notification_type)
|
|
42
|
-
declared(notification_type)&.default || []
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def declared(notification_type)
|
|
48
|
-
@notifications[notification_type.to_s]
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/lib/notey/event_delivery.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Notey
|
|
4
|
-
class EventDelivery
|
|
5
|
-
def self.call(event)
|
|
6
|
-
notifier = Notey.notifier_for(event.event_name)
|
|
7
|
-
return if notifier.nil?
|
|
8
|
-
|
|
9
|
-
payload = event.payload.to_h.symbolize_keys
|
|
10
|
-
|
|
11
|
-
Current.set(account_id: payload[:account_id]) { notifier.with(**payload).deliver }
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|