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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +260 -0
- data/Rakefile +11 -0
- data/app/assets/stylesheets/notey/application.css +15 -0
- data/app/controllers/notey/application_controller.rb +7 -0
- data/app/controllers/notey/destinations_controller.rb +23 -0
- data/app/controllers/notey/notifications_controller.rb +26 -0
- data/app/controllers/notey/preferences_controller.rb +26 -0
- data/app/delivery_methods/notey/email.rb +9 -0
- data/app/delivery_methods/notey/in_app.rb +12 -0
- data/app/helpers/notey/application_helper.rb +4 -0
- data/app/jobs/notey/application_job.rb +4 -0
- data/app/jobs/notey/digest_job.rb +9 -0
- data/app/mailers/notey/application_mailer.rb +6 -0
- data/app/mailers/notey/digest_mailer.rb +12 -0
- data/app/mailers/notey/notification_mailer.rb +11 -0
- data/app/models/concerns/notey/notifier.rb +19 -0
- data/app/models/concerns/notey/recipient.rb +23 -0
- data/app/models/notey/application_record.rb +5 -0
- data/app/models/notey/attempt.rb +7 -0
- data/app/models/notey/current.rb +7 -0
- data/app/models/notey/destination.rb +11 -0
- data/app/models/notey/digest.rb +7 -0
- data/app/models/notey/kept.rb +9 -0
- data/app/models/notey/notification.rb +43 -0
- data/app/models/notey/preference.rb +21 -0
- data/app/models/notey/refusal.rb +13 -0
- data/app/models/notey/save_destination.rb +37 -0
- data/app/models/notey/save_my_destination.rb +11 -0
- data/app/models/notey/save_preferences.rb +37 -0
- data/app/views/notey/_destinations.html.erb +13 -0
- data/app/views/notey/_my_destinations.html.erb +12 -0
- data/app/views/notey/_preferences.html.erb +25 -0
- data/app/views/notey/destinations/show.html.erb +7 -0
- data/app/views/notey/digest_mailer/digest.html.erb +8 -0
- data/app/views/notey/notification_mailer/notification.html.erb +6 -0
- data/app/views/notey/notifications/index.html.erb +17 -0
- data/app/views/notey/preferences/show.html.erb +7 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260917000002_create_notey_preferences.rb +17 -0
- data/db/migrate/20260918020001_add_digest_window_to_notey_preferences.rb +7 -0
- data/db/migrate/20260918030002_create_notey_digests.rb +20 -0
- data/db/migrate/20260918050001_create_notey_destinations.rb +16 -0
- data/db/migrate/20260918060001_index_digest_window_on_notey_preferences.rb +7 -0
- data/db/migrate/20260919010001_add_member_to_notey_destinations.rb +11 -0
- data/db/migrate/20260921000001_create_notey_attempts.rb +16 -0
- data/lib/notey/catalog.rb +51 -0
- data/lib/notey/channels.rb +41 -0
- data/lib/notey/destinations.rb +11 -0
- data/lib/notey/digest_run.rb +73 -0
- data/lib/notey/engine.rb +20 -0
- data/lib/notey/event_delivery.rb +14 -0
- data/lib/notey/inbox.rb +11 -0
- data/lib/notey/records_attempt.rb +37 -0
- data/lib/notey/version.rb +3 -0
- data/lib/notey.rb +166 -0
- data/lib/tasks/notey_tasks.rake +4 -0
- data/the_local/agents/notey-develop.md +178 -0
- data/the_local/agents/notey-info.md +80 -0
- data/the_local/agents/notey-install.md +191 -0
- data/the_local/interface.yml +52 -0
- 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,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,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,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,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,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
|