notey 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +260 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/stylesheets/notey/application.css +15 -0
  6. data/app/controllers/notey/application_controller.rb +7 -0
  7. data/app/controllers/notey/destinations_controller.rb +23 -0
  8. data/app/controllers/notey/notifications_controller.rb +26 -0
  9. data/app/controllers/notey/preferences_controller.rb +26 -0
  10. data/app/delivery_methods/notey/email.rb +9 -0
  11. data/app/delivery_methods/notey/in_app.rb +12 -0
  12. data/app/helpers/notey/application_helper.rb +4 -0
  13. data/app/jobs/notey/application_job.rb +4 -0
  14. data/app/jobs/notey/digest_job.rb +9 -0
  15. data/app/mailers/notey/application_mailer.rb +6 -0
  16. data/app/mailers/notey/digest_mailer.rb +12 -0
  17. data/app/mailers/notey/notification_mailer.rb +11 -0
  18. data/app/models/concerns/notey/notifier.rb +19 -0
  19. data/app/models/concerns/notey/recipient.rb +23 -0
  20. data/app/models/notey/application_record.rb +5 -0
  21. data/app/models/notey/attempt.rb +7 -0
  22. data/app/models/notey/current.rb +7 -0
  23. data/app/models/notey/destination.rb +11 -0
  24. data/app/models/notey/digest.rb +7 -0
  25. data/app/models/notey/kept.rb +9 -0
  26. data/app/models/notey/notification.rb +43 -0
  27. data/app/models/notey/preference.rb +21 -0
  28. data/app/models/notey/refusal.rb +13 -0
  29. data/app/models/notey/save_destination.rb +37 -0
  30. data/app/models/notey/save_my_destination.rb +11 -0
  31. data/app/models/notey/save_preferences.rb +37 -0
  32. data/app/views/notey/_destinations.html.erb +13 -0
  33. data/app/views/notey/_my_destinations.html.erb +12 -0
  34. data/app/views/notey/_preferences.html.erb +25 -0
  35. data/app/views/notey/destinations/show.html.erb +7 -0
  36. data/app/views/notey/digest_mailer/digest.html.erb +8 -0
  37. data/app/views/notey/notification_mailer/notification.html.erb +6 -0
  38. data/app/views/notey/notifications/index.html.erb +17 -0
  39. data/app/views/notey/preferences/show.html.erb +7 -0
  40. data/config/routes.rb +5 -0
  41. data/db/migrate/20260917000002_create_notey_preferences.rb +17 -0
  42. data/db/migrate/20260918020001_add_digest_window_to_notey_preferences.rb +7 -0
  43. data/db/migrate/20260918030002_create_notey_digests.rb +20 -0
  44. data/db/migrate/20260918050001_create_notey_destinations.rb +16 -0
  45. data/db/migrate/20260918060001_index_digest_window_on_notey_preferences.rb +7 -0
  46. data/db/migrate/20260919010001_add_member_to_notey_destinations.rb +11 -0
  47. data/db/migrate/20260921000001_create_notey_attempts.rb +16 -0
  48. data/lib/notey/catalog.rb +51 -0
  49. data/lib/notey/channels.rb +41 -0
  50. data/lib/notey/destinations.rb +11 -0
  51. data/lib/notey/digest_run.rb +73 -0
  52. data/lib/notey/engine.rb +20 -0
  53. data/lib/notey/event_delivery.rb +14 -0
  54. data/lib/notey/inbox.rb +11 -0
  55. data/lib/notey/records_attempt.rb +37 -0
  56. data/lib/notey/version.rb +3 -0
  57. data/lib/notey.rb +166 -0
  58. data/lib/tasks/notey_tasks.rake +4 -0
  59. data/the_local/agents/notey-develop.md +178 -0
  60. data/the_local/agents/notey-info.md +80 -0
  61. data/the_local/agents/notey-install.md +191 -0
  62. data/the_local/interface.yml +52 -0
  63. metadata +159 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bf9f791db654a77800eacc7033b1566c2083b2dd30750d71888f33725b83db33
4
+ data.tar.gz: d942224462d599deb0525be5443b3b81a8f9a901a23edbaa730378cbed4ce9f8
5
+ SHA512:
6
+ metadata.gz: 0e764a54cababf319b8d8259ed775fd6dfdca0346c98281d6f08971d8b6ee3169d9804dc82d6a178712b914afb736d368ccc3e26060acb811520c7a544b2ed49
7
+ data.tar.gz: cc0023e05f0cb136f7a3cfb5c395afed39210135348470d4fec84105b9e108f47830bf29bc15d6eedab841ebf51d77a48c92a7589ab18b51774a936e1a18cb94
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright TODO: Write your name
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # Notey
2
+
3
+ Notey is the notification layer for multi-tenant Rails apps. It sits on top of
4
+ [Noticed](https://github.com/excid3/noticed) and supplies the parts Noticed
5
+ leaves to the host.
6
+
7
+ ## What Noticed already does
8
+
9
+ - **Recipients.** A notifier declares `recipients`, and delivery inserts one
10
+ `noticed_notifications` row per recipient.
11
+ - **In-app records.** Every notification is written to the database, with
12
+ read/unread and seen/unseen state.
13
+ - **Channels.** Email, SMS through Twilio or Vonage, iOS, FCM, Action Push,
14
+ Slack, Discord, Microsoft Teams, webhooks, and Action Cable.
15
+
16
+ ## What Notey adds
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.
20
+ - **Preferences.** Which channels a person wants for each type, per account,
21
+ with a page they set them on.
22
+ - **Digest windows.** A type set to daily or weekly is held back and sent as one
23
+ email covering the window.
24
+ - **An inbox.** In-app notification pages scoped to the account the person is in.
25
+ - **Destinations.** An address and an encrypted credential per account per
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.
29
+
30
+ ## Installation
31
+
32
+ ```ruby
33
+ gem "notey"
34
+ ```
35
+
36
+ ```bash
37
+ bundle install
38
+ bin/rails notey:install:migrations
39
+ bin/rails db:migrate
40
+ ```
41
+
42
+ Mount the engine:
43
+
44
+ ```ruby
45
+ # config/routes.rb
46
+ mount Notey::Engine => "/notey"
47
+ ```
48
+
49
+ That gives you `/notey/preferences`, `/notey/notifications` and
50
+ `/notey/destinations`.
51
+
52
+ ## Wiring
53
+
54
+ Notey owns its own tables and never owns your person or account records. Six
55
+ things connect it to yours.
56
+
57
+ ### 1. Declare the catalog
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.
62
+
63
+ ```ruby
64
+ # 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
69
+ ```
70
+
71
+ ### 2. Make your person model a recipient
72
+
73
+ ```ruby
74
+ class User < ApplicationRecord
75
+ include Notey::Recipient
76
+ end
77
+ ```
78
+
79
+ ### 3. Set the current person and account per request
80
+
81
+ ```ruby
82
+ class ApplicationController < ActionController::Base
83
+ before_action do
84
+ Notey::Current.member = current_user
85
+ Notey::Current.account_id = current_account&.id
86
+ end
87
+ end
88
+ ```
89
+
90
+ A read with no account set returns the declared defaults and an empty inbox,
91
+ never another account's rows.
92
+
93
+ ### 4. Put the account on Noticed's rows
94
+
95
+ Notey reads the account from the Noticed event, because a delivery runs in a job
96
+ where the current account is gone. Noticed does not add that column, so your app
97
+ does.
98
+
99
+ ```ruby
100
+ # a migration
101
+ add_column :noticed_events, :account_id, :bigint
102
+ add_column :noticed_notifications, :account_id, :bigint
103
+ ```
104
+
105
+ ```ruby
106
+ # config/initializers/noticed.rb
107
+ ActiveSupport.on_load :noticed_event do
108
+ after_initialize { self.account_id ||= Notey::Current.account_id }
109
+
110
+ def recipient_attributes_for(recipient)
111
+ super.merge(account_id: account_id)
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### 5. Point your notifiers at the catalog
117
+
118
+ Each notifier names its type, and each delivery method asks whether the
119
+ recipient wants that channel.
120
+
121
+ ```ruby
122
+ class CommentNotifier < Noticed::Event
123
+ include Notey::Notifier
124
+ notey_type :comment
125
+
126
+ deliver_by :email do |config|
127
+ config.mailer = "CommentMailer"
128
+ config.if = Notey.wanted(:comment, on: :email)
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### 6. Tell Notey where a notification lives
134
+
135
+ Digest emails link to each notification through a lambda you supply, so the link
136
+ points at your own page rather than one this engine picks.
137
+
138
+ ```ruby
139
+ # config/initializers/notey.rb
140
+ Notey.notification_url = lambda do |notification|
141
+ Rails.application.routes.url_helpers.notification_url(notification)
142
+ end
143
+ ```
144
+
145
+ ## Settings sections
146
+
147
+ Notey ships its two pages as partials and their saving as action objects, so a
148
+ settings shell renders them inside its own chrome rather than linking away.
149
+ With [`bureau`](https://github.com/DYB-Development/bureau):
150
+
151
+ ```ruby
152
+ # config/initializers/bureau.rb
153
+ Bureau.section :notifications, area: :user, title: "Notifications",
154
+ renders: "notey/preferences", runs: "Notey::SavePreferences"
155
+
156
+ Bureau.section :notification_destinations, area: :account, title: "Notification destinations",
157
+ renders: "notey/destinations", runs: "Notey::SaveDestination", capability: :configure_site
158
+ ```
159
+
160
+ A section registered this way is served by the settings shell, so the shell's
161
+ own capability check guards it. A section that only links to a mounted path is
162
+ not guarded, because the shell never renders it.
163
+
164
+ `Notey::SavePreferences` and `Notey::SaveDestination` take `person:`, `account:`
165
+ and `values:`, and answer with an object responding to `ok?` and `message`. The
166
+ engine's own pages call the same two actions.
167
+
168
+ ## Sending digests
169
+
170
+ A type set to daily or weekly sends nothing when it happens. Run the window on a
171
+ schedule — Notey does not register one:
172
+
173
+ ```ruby
174
+ Notey::DigestRun.new(window: "daily").call
175
+ Notey::DigestRun.new(window: "weekly").call
176
+ ```
177
+
178
+ Each run enqueues one job per person, so one failing send does not stop the
179
+ rest. A window is sent once even if the run overlaps itself; a send that fails
180
+ releases the window so it can be sent again.
181
+
182
+ ## Destinations
183
+
184
+ A channel like email reaches a person at an address the app already holds. A
185
+ 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:
187
+
188
+ ```ruby
189
+ Notey.catalog do
190
+ notification :comment, channels: %w[email sms], default: %w[email]
191
+ addressed :sms
192
+ end
193
+ ```
194
+
195
+ **A person sets their own.** Their phone number, their endpoint. A notification
196
+ addressed to them goes only to an address they set — never to one the account
197
+ set.
198
+
199
+ ```ruby
200
+ Bureau.section :my_notification_addresses, area: :user, title: "Where notifications reach me",
201
+ renders: "notey/my_destinations", runs: "Notey::SaveMyDestination"
202
+ ```
203
+
204
+ **An account sets its own**, for notifications that belong to the whole account
205
+ rather than to one person — a team Slack or Discord channel.
206
+
207
+ ```ruby
208
+ Bureau.section :notification_destinations, area: :account, title: "Notification destinations",
209
+ renders: "notey/destinations", runs: "Notey::SaveDestination", capability: :configure_site
210
+ ```
211
+
212
+ A notifier reads the address through the options Noticed already evaluates:
213
+
214
+ ```ruby
215
+ deliver_by :webhook do |config|
216
+ config.url = Notey.destination_address(:webhook)
217
+ config.if = Notey.addressed(:webhook)
218
+ end
219
+ ```
220
+
221
+ A credential is encrypted at rest, which needs Active Record encryption keys
222
+ configured in your app. A channel with no address set sends nothing on it.
223
+
224
+ **Nothing in this engine restricts who may set an account's address.** Register
225
+ it as a settings section so the shell's capability check guards it.
226
+
227
+ ## Domain events
228
+
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
+ ```
236
+
237
+ ```ruby
238
+ # app/subscribers/notey_notifications.rb
239
+ class NoteyNotifications < EventEngine::Subscribers::Base
240
+ subscribes_to :comment_posted
241
+
242
+ def handle(event)
243
+ Notey::EventDelivery.call(event)
244
+ end
245
+ end
246
+ ```
247
+
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.
251
+
252
+ ## Development
253
+
254
+ After checking out the repo, run `bin/setup` to install dependencies, then
255
+ `bundle exec rake test` to run the tests and `bin/rubocop` for the linter.
256
+
257
+ ## License
258
+
259
+ The gem is available as open source under the terms of the
260
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ require "bundler/gem_tasks"
7
+
8
+ task test: "app:test"
9
+ task default: :test
10
+
11
+ require "the_local/rake"
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class ApplicationController < ::ApplicationController
5
+ helper KeystoneUiHelper, Notey::Engine.routes.url_helpers
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class DestinationsController < ApplicationController
5
+ def show
6
+ end
7
+
8
+ def update
9
+ result = SaveDestination.new(person: Current.member, account: Current.account_id, values: submitted).call
10
+
11
+ return redirect_to destinations_path if result.ok?
12
+
13
+ show
14
+ render :show, status: :unprocessable_content
15
+ end
16
+
17
+ private
18
+
19
+ def submitted
20
+ { destinations: params.fetch(:destinations, {}).permit!.to_h }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class NotificationsController < ApplicationController
5
+ PER_PAGE = 25
6
+
7
+ def index
8
+ @notifications = inbox.includes(:event).order(created_at: :desc).limit(PER_PAGE)
9
+ @unread_count = inbox.unread.count
10
+
11
+ Noticed::Notification.where(id: @notifications.map(&:id)).unseen.mark_as_seen
12
+ end
13
+
14
+ def update
15
+ inbox.find(params[:id]).mark_as_read!
16
+
17
+ redirect_to notifications_path
18
+ end
19
+
20
+ private
21
+
22
+ def inbox
23
+ Inbox.for(Current.member, account_id: Current.account_id)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class PreferencesController < ApplicationController
5
+ def show
6
+ end
7
+
8
+ def update
9
+ result = SavePreferences.new(person: Current.member, account: Current.account_id, values: submitted).call
10
+
11
+ return redirect_to preferences_path if result.ok?
12
+
13
+ show
14
+ render :show, status: :unprocessable_content
15
+ end
16
+
17
+ private
18
+
19
+ def submitted
20
+ {
21
+ preferences: params.fetch(:preferences, {}).permit!.to_h,
22
+ windows: params.fetch(:windows, {}).permit!.to_h
23
+ }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Email < Noticed::DeliveryMethod
5
+ def deliver
6
+ NotificationMailer.with(notification: notification, recipient: recipient).notification.deliver_now
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class InApp < Noticed::DeliveryMethod
5
+ def deliver
6
+ end
7
+
8
+ def outbound?
9
+ false
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Notey
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Notey
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class DigestJob < ApplicationJob
5
+ def perform(member, account_id, window)
6
+ DigestRun.new(window: window).deliver_to_member(member, account_id)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Notey
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: -> { Notey.mailer_sender }
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class DigestMailer < ApplicationMailer
5
+ def digest(member, notifications, window)
6
+ @notifications = notifications
7
+ @window = window
8
+
9
+ mail(to: member.email, subject: "Your #{window} notifications")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class NotificationMailer < ApplicationMailer
5
+ def notification
6
+ @notification = params[:notification]
7
+
8
+ mail(to: params[:recipient].email, subject: @notification.event.title)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ module Recipient
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :notey_preferences, as: :member, class_name: "Notey::Preference", dependent: :destroy
9
+ end
10
+
11
+ def wants?(notification_type, on:, account_id: Current.account_id)
12
+ channels_for(notification_type, account_id: account_id).include?(on.to_s)
13
+ end
14
+
15
+ def digest_window_for(notification_type, account_id: Current.account_id)
16
+ Channels.window_for(self, notification_type, account_id: account_id)
17
+ end
18
+
19
+ def channels_for(notification_type, account_id: Current.account_id)
20
+ Channels.for(self, notification_type, account_id: account_id)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module Notey
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Attempt < ApplicationRecord
5
+ belongs_to :notification, class_name: "Noticed::Notification"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Current < ActiveSupport::CurrentAttributes
5
+ attribute :account_id, :member
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Destination < ApplicationRecord
5
+ belongs_to :member, polymorphic: true, optional: true
6
+
7
+ encrypts :credential
8
+
9
+ validates :channel, :address, presence: true
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Digest < ApplicationRecord
5
+ belongs_to :member, polymorphic: true
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Kept
5
+ def ok? = true
6
+
7
+ def message = nil
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Notification < Noticed::Event
5
+ class_attribute :notey_notification_type, instance_writer: false
6
+
7
+ def self.inherited(subclass)
8
+ super
9
+ Notey.register_notifier(subclass)
10
+ end
11
+
12
+ def self.notey_type(name)
13
+ self.notey_notification_type = name.to_s
14
+ end
15
+
16
+ def self.notify(recipients, **information)
17
+ with(information).deliver(recipients)
18
+ end
19
+
20
+ def self.delivery_methods
21
+ Notey.registered_channels.to_h do |channel|
22
+ [ channel.name.to_sym, Noticed::Deliverable::DeliverBy.new(channel.name.to_sym, delivery_config(channel)) ]
23
+ end
24
+ end
25
+
26
+ def self.delivery_config(channel)
27
+ config = ActiveSupport::OrderedOptions.new
28
+ config[:class] = channel.delivery_method if channel.delivery_method
29
+ config[:notey_channel] = channel.name
30
+ config[:if] = Notey.sends(notey_notification_type, on: channel.name, addressed: channel.addressed?)
31
+ config
32
+ end
33
+ private_class_method :delivery_config
34
+
35
+ def title
36
+ notey_notification_type.humanize
37
+ end
38
+
39
+ def body
40
+ nil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Preference < ApplicationRecord
5
+ belongs_to :member, polymorphic: true
6
+
7
+ WINDOWS = %w[immediate daily weekly].freeze
8
+
9
+ validates :digest_window, inclusion: { in: WINDOWS, message: "is not a window notey sends on" }
10
+
11
+ validate :channels_offered_for_notification_type
12
+
13
+ private
14
+
15
+ def channels_offered_for_notification_type
16
+ (Array(channels).map(&:to_s) - Notey.channels).each do |unoffered|
17
+ errors.add(:channels, "is not a channel this application has: #{unoffered}")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notey
4
+ class Refusal
5
+ attr_reader :message
6
+
7
+ def initialize(message)
8
+ @message = message
9
+ end
10
+
11
+ def ok? = false
12
+ end
13
+ end