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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf9f791db654a77800eacc7033b1566c2083b2dd30750d71888f33725b83db33
4
- data.tar.gz: d942224462d599deb0525be5443b3b81a8f9a901a23edbaa730378cbed4ce9f8
3
+ metadata.gz: 52c58921ef29966128ff879a465b658aecc03be72d9854b20e35fcf374bdf227
4
+ data.tar.gz: f33f53d08767a17f9409255954c1561a9d334e58c8d4e141410a3f1e3af8d429
5
5
  SHA512:
6
- metadata.gz: 0e764a54cababf319b8d8259ed775fd6dfdca0346c98281d6f08971d8b6ee3169d9804dc82d6a178712b914afb736d368ccc3e26060acb811520c7a544b2ed49
7
- data.tar.gz: cc0023e05f0cb136f7a3cfb5c395afed39210135348470d4fec84105b9e108f47830bf29bc15d6eedab841ebf51d77a48c92a7589ab18b51774a936e1a18cb94
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
- - **A catalog.** One declaration naming every notification type, the channels it
19
- may be delivered on, and the channels a person gets by default.
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
- - **Domain events.** A mapping from an event your app already publishes to the
28
- notifier that should deliver it.
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. Declare the catalog
57
+ ### 1. Register the channels your application has
58
58
 
59
- Nothing works until a type is declared. A preference for an undeclared type is
60
- refused, a channel the catalog does not offer for a type is refused, and a
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.catalog do
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. Point your notifiers at the catalog
117
+ ### 5. Define a notification type
117
118
 
118
- Each notifier names its type, and each delivery method asks whether the
119
- recipient wants that channel.
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 CommentNotifier < Noticed::Event
123
- include Notey::Notifier
123
+ class CommentNotification < Notey::Notification
124
124
  notey_type :comment
125
125
 
126
- deliver_by :email do |config|
127
- config.mailer = "CommentMailer"
128
- config.if = Notey.wanted(:comment, on: :email)
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. Declare which channels take an address:
225
+ where it goes. Say so when you register the channel:
187
226
 
188
227
  ```ruby
189
- Notey.catalog do
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
- A notifier reads the address through the options Noticed already evaluates:
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
- deliver_by :webhook do |config|
216
- config.url = Notey.destination_address(:webhook)
217
- config.if = Notey.addressed(:webhook)
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. It holds the mapping from an event
230
- name to a notifier, and your app owns the one class that knows both.
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
- Notey::EventDelivery.call(event)
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
- `Notey::EventDelivery.call` takes anything answering `event_name` and `payload`,
249
- sets the account from the payload for the delivery, and restores the account it
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
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Notey
4
4
  class Email < Noticed::DeliveryMethod
5
+ def self.reachable?(recipient)
6
+ recipient.email.present?
7
+ end
8
+
5
9
  def deliver
6
10
  NotificationMailer.with(notification: notification, recipient: recipient).notification.deliver_now
7
11
  end
@@ -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
@@ -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 ? Array(stored.channels).map(&:to_s) : Notey.default_channels,
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
@@ -1,3 +1,3 @@
1
1
  module Notey
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
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
- return if catalog.notifications.empty?
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.check_sender!
96
- return if mailer_sender.present?
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
- raise UndeclaredType,
115
- "#{notifier} declares the notification type #{notification_type}, which the catalog does not hold"
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.addressed(channel)
124
- -> { Destinations.for(event.account_id, channel, member: recipient).present? }
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.deliver_on(event_name, notifier)
128
- event_notifiers[event_name.to_s] = notifier
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.notifier_for(event_name)
132
- event_notifiers[event_name.to_s]
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
- def self.wanted(notification_type, on:)
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 naming a notifier's notification type, gating a delivery method on what a person wants, addressing a channel at the account, firing a notifier from a domain event, 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-account delivery addresses, or a digest loop.
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 — a catalog of notification types, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a mapping from domain events to notifiers
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 notification types exist, which channels each person wants in
16
- each account they belong to, and whether they want them as they happen or in a
17
- daily or weekly digest. Fire this local when a notifier is being written or
18
- changed, when a domain event should send one, when digests need running, or when
19
- the host's own code needs to read a person's notifications or their preferences.
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
- - `notey_type` — declared inside a notifier class, names which of the catalog's
27
- notification types that class delivers.
28
- - `Notey.wanted(type, on:)` — returns a condition for one delivery method that
29
- is true only when the person wants that type on that channel and wants it
30
- immediately.
31
- - `Notey.deliver_on(event_name, notifier)` maps a domain event to a notifier,
32
- so the notifier is delivered whenever that event fires.
33
- - `Notey.destination_address(channel)` returns the stored address for the
34
- channel on the account the notification belongs to.
35
- - `Notey.addressed(channel)` returns a condition that is true only when that
36
- account has a stored address for the channel.
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 catalog and the event-to-notifier mappings, for
49
- tests that declare their own.
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. Name the type at the top of each notifier class:
59
+ 1. Write a notification type that says nothing about channels:
54
60
 
55
61
  ```ruby
56
- class CommentNotifier < Noticed::Event
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
- The name must be one the catalog declares, or the app raises when it boots
62
- with eager loading on. Two notifier classes may name the same type.
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. Gate every delivery method on what the person wants:
77
+ 2. Send it by naming the recipients your own code resolved:
65
78
 
66
79
  ```ruby
67
- deliver_by :email do |config|
68
- config.if = Notey.wanted(:comment, on: :email)
69
- end
80
+ CommentNotification.notify(recipients, comment_id: comment.id)
70
81
  ```
71
82
 
72
- Pass the same type the class names and the channel this delivery method
73
- sends on. Without this the delivery runs for everyone regardless of their
74
- preferences.
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. Leave the digest to notey. `Notey.wanted` is false when the person's window
77
- for that type is daily or weekly, so nothing sends at the time and the
78
- notification waits in their inbox for the next run. Never write a second
79
- condition for the window.
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 addressed at the account rather than at the person, read the
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
- deliver_by :webhook, class: "WebhookDeliveryMethod" do |config|
86
- config.url = Notey.destination_address(:webhook)
87
- config.if = Notey.addressed(:webhook)
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
- Ask the developer which channels are addressed at the account, since the two
92
- kinds of channel are gated differently and only they know which is which. A
93
- channel can carry both conditions, one from step 2 and one from here.
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. Map a domain event to a notifier in an initializer, one line per mapping:
107
+ 5. Send from a domain event in the host's own subscriber:
96
108
 
97
109
  ```ruby
98
- Notey.deliver_on :comment_posted, CommentNotifier
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
- The subscriber sets the account from the event's payload, delivers the
102
- notifier with the whole payload as its params, and puts back the account that
103
- was set before. An event with no mapping delivers nothing and raises nothing.
104
- Ask the developer which events map to which notifiers; nothing in the gem
105
- infers it. The payload must carry `account_id`, or the notification is stored
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. Read a person's notifications through the inbox rather than querying Noticed
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
- 8. Read a person's preferences from the host's own code when the app needs to
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 the
143
- catalog's declared defaults when they have never stored one.
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
- 9. In tests that declare their own notification types or event mappings, call
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 types and mappings in place for every test
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 notifier names a type.** A notifier without one delivers, but no
153
- digest ever includes it, because a digest picks its rows by the type the
154
- notifier names.
155
- - **Every delivery method carries a condition.** An ungated one ignores the
156
- person's channels and their window, which is the whole point of the gem.
157
- - **Every channel the catalog offers is delivered by some notifier.** A channel
158
- the catalog offers that no notifier has a delivery method for raises when the
159
- app boots with eager loading on.
160
- - **Only channels the catalog offers for that type may be gated on.** Gating on
161
- a channel the type does not offer makes a condition that is always false, and
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
- - **Notifiers, event mappings and digest schedules all belong in the host's
174
- code**, never in the gem.
175
- - **Out of scope.** Declaring the notification types and their channels, making
176
- a model a recipient, setting the current person and account, storing an
177
- account's addresses, and the pages a person picks their own channels on all
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 — a catalog of notification types, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a mapping from domain events to notifiers
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, a notifier's
56
- declaration, and the channels on offer.
57
- - **Catalog** — the one declaration of which notification types exist, which
58
- channels each offers, and which of those channels apply when a person has
59
- stored nothing. A type that is not in it is unknown to notey, and a channel a
60
- type does not offer cannot be stored against it.
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, for channels addressed at the account rather than at a person.
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
- - **Notifier** — the Noticed class that delivers one notification type. Every
79
- notifier names a type the catalog holds, and every channel the catalog offers
80
- is delivered on by some notifier.
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, declaring the notification catalog, making a model a recipient, setting the current person and account, putting the account on Noticed's rows, supplying a notification url, checking the catalog against the notifiers, and reaching the preferences, inbox and destinations pages.
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 — a catalog of notification types, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a mapping from domain events to notifiers
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 six migrations into the
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.catalog` — declares every notification type, the channels each type may
25
- be delivered on, and the channels a person gets before they have stored
26
- anything.
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::Notifier` — included in each Noticed event class, which registers it
30
- so notey can check it when the app boots.
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.check!`raises when a notifier names a type the catalog does not
37
- hold, or the catalog offers a channel no notifier delivers on, and notey runs
38
- it itself after initialization when the app eager loads.
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 the catalog offers.
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` and `notey_destinations` in
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. Declare the catalog in `config/initializers/notey.rb`:
70
+ 4. Register the channels the application sends on, in
71
+ `config/initializers/notey.rb`:
67
72
 
68
73
  ```ruby
69
- Notey.catalog do
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
- Ask the developer which notification types exist, which channels each one
75
- offers, and which of those channels apply before a person has chosen — there
76
- is no default for any of the three. Declare only channels a notifier in the
77
- app actually delivers on, or the app will refuse to boot at step 10.
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 the catalog's default channels and an empty inbox,
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. Include the notifier concern in each Noticed event class that delivers one of
129
- the catalog's types:
132
+ 8. Define one notification type so there is something to send:
130
133
 
131
134
  ```ruby
132
- class CommentNotifier < Noticed::Event
133
- include Notey::Notifier
135
+ class CommentNotification < Notey::Notification
136
+ notey_type :comment
137
+
138
+ required_params :comment_id
134
139
  end
135
140
  ```
136
141
 
137
- Naming which type the class delivers, and gating each delivery method on what
138
- the person wants, are `notey-develop`'s steps rather than this local's.
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; call it directly in a test or a console
154
- to check the same thing without a boot.
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 up destinations only if a channel in the catalog is addressed at the
163
- account rather than at a person. The stored credential is encrypted, so
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 declared type, `/notey/notifications` lists that person's
172
- notifications for the current account, and `/notey/destinations` lists one
173
- address and credential field per channel the catalog offers.
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 the catalog's default channels when
176
- `Notey::Current.account_id` is nil and a save is refused, so check step 6
177
- before anything else.
178
- - **Boot with eager loading after every catalog change.** The catalog and the
179
- notifiers are checked against each other only then, and an empty catalog
180
- skips the check entirely.
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
- - **Out of scope.** Scheduling the digest runs, tying a notifier to a
190
- notification type, delivering one when a domain event fires, and reading the
191
- inbox from the host's own code all belong to `notey-develop`.
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`.
@@ -1,13 +1,14 @@
1
- scope: notifications — a catalog of notification types, per-person per-account channel preferences, digest windows, an in-app inbox, per-account destinations, and a mapping from domain events to notifiers
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.catalog
6
+ - Notey.channel
7
7
  - Notey::Recipient
8
- - Notey::Notifier
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
- - Notey.wanted
19
- - Notey.deliver_on
20
- - Notey.destination_address
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/concerns/notey/notifier.rb
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.1.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
@@ -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