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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
class SaveDestination
|
|
5
|
+
def initialize(values:, person: nil, account: nil)
|
|
6
|
+
@person = person
|
|
7
|
+
@account_id = account.respond_to?(:id) ? account.id : account
|
|
8
|
+
@values = values
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
refusal = submitted.filter_map { |channel, attributes| store(channel, attributes) }.first
|
|
13
|
+
|
|
14
|
+
refusal || Kept.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def owner
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def submitted
|
|
24
|
+
@values.fetch(:destinations, {}).slice(*Notey.addressed_channels)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def store(channel, attributes)
|
|
28
|
+
destination = Destination.find_or_initialize_by(
|
|
29
|
+
account_id: @account_id, channel: channel.to_s, member: owner
|
|
30
|
+
)
|
|
31
|
+
destination.address = attributes["address"]
|
|
32
|
+
destination.credential = attributes["credential"] if attributes["credential"].present?
|
|
33
|
+
|
|
34
|
+
Refusal.new(destination.errors.full_messages.first) unless destination.save
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
class SavePreferences
|
|
5
|
+
def initialize(values:, person: nil, account: nil)
|
|
6
|
+
@person = person
|
|
7
|
+
@account_id = account.respond_to?(:id) ? account.id : account
|
|
8
|
+
@values = values
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
refusal = chosen.filter_map { |notification_type, channels| store(notification_type, channels) }.first
|
|
13
|
+
|
|
14
|
+
refusal || Kept.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def chosen
|
|
20
|
+
@values.fetch(:preferences, {}).select { |type, _| Notey.notification_types.include?(type.to_s) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def windows
|
|
24
|
+
@values.fetch(:windows, {})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def store(notification_type, channels)
|
|
28
|
+
preference = Preference.find_or_initialize_by(
|
|
29
|
+
member: @person, account_id: @account_id, notification_type: notification_type.to_s
|
|
30
|
+
)
|
|
31
|
+
preference.channels = Array(channels).map(&:to_s).reject(&:blank?)
|
|
32
|
+
preference.digest_window = windows.fetch(notification_type.to_s, "immediate")
|
|
33
|
+
|
|
34
|
+
Refusal.new(preference.errors.full_messages.first) unless preference.save
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<% stored = Notey::Destination.where(account_id: account, member_id: nil).index_by(&:channel) %>
|
|
2
|
+
|
|
3
|
+
<%= ui_form(action: submit_url, method: :patch) do %>
|
|
4
|
+
<% Notey.addressed_channels.each do |channel| %>
|
|
5
|
+
<%= ui_panel do %>
|
|
6
|
+
<%= ui_section(title: channel.humanize) do %>
|
|
7
|
+
<%= ui_input(name: "destinations[#{channel}][address]", value: stored[channel]&.address) %>
|
|
8
|
+
<%= ui_input(name: "destinations[#{channel}][credential]", type: :password) %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<%= ui_button(label: "Save") %>
|
|
13
|
+
<% end %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% stored = Notey::Destination.where(account_id: account, member: person).index_by(&:channel) %>
|
|
2
|
+
|
|
3
|
+
<%= ui_form(action: submit_url, method: :patch) do %>
|
|
4
|
+
<% Notey.addressed_channels.each do |channel| %>
|
|
5
|
+
<%= ui_panel do %>
|
|
6
|
+
<%= ui_section(title: channel.humanize) do %>
|
|
7
|
+
<%= ui_input(name: "destinations[#{channel}][address]", value: stored[channel]&.address) %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<%= ui_button(label: "Save") %>
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<% stored = Notey::Preference.where(member: person, account_id: account).index_by(&:notification_type) %>
|
|
2
|
+
|
|
3
|
+
<%= ui_form(action: submit_url, method: :patch) do %>
|
|
4
|
+
<% Notey.notification_types.each do |name| %>
|
|
5
|
+
<%= ui_panel do %>
|
|
6
|
+
<%= ui_section(title: name.humanize) do %>
|
|
7
|
+
<%= hidden_field_tag "preferences[#{name}][]", "", id: nil %>
|
|
8
|
+
<% Notey.channels.each do |channel| %>
|
|
9
|
+
<%= ui_checkbox_row(
|
|
10
|
+
name: "preferences[#{name}][]",
|
|
11
|
+
value: channel,
|
|
12
|
+
label: channel.humanize,
|
|
13
|
+
checked: Notey::Channels.channels_of(stored[name], name).include?(channel)
|
|
14
|
+
) %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<%= ui_select(
|
|
17
|
+
name: "windows[#{name}]",
|
|
18
|
+
options: [ [ "Immediately", "immediate" ], [ "Once a day", "daily" ], [ "Once a week", "weekly" ] ],
|
|
19
|
+
selected: stored[name]&.digest_window || "immediate"
|
|
20
|
+
) %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<%= ui_button(label: "Save") %>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= ui_page(max_width: :lg) do %>
|
|
2
|
+
<%= ui_section(title: "Destinations") do %>
|
|
3
|
+
<%= render partial: "notey/destinations",
|
|
4
|
+
locals: { person: Notey::Current.member, account: Notey::Current.account_id,
|
|
5
|
+
selection: {}, submit_url: destinations_path } %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<h1>Your <%= @window %> notifications</h1>
|
|
2
|
+
|
|
3
|
+
<ul>
|
|
4
|
+
<% @notifications.each do |notification| %>
|
|
5
|
+
<% url = Notey.notification_url.call(notification) %>
|
|
6
|
+
<li><%= url ? link_to(notification.event.class.name, url) : notification.event.class.name %></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
</ul>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%= ui_page(max_width: :lg) do %>
|
|
2
|
+
<%= ui_section(title: "Notifications", subtitle: "#{@unread_count} unread") do %>
|
|
3
|
+
<% @notifications.each do |notification| %>
|
|
4
|
+
<%= ui_panel do %>
|
|
5
|
+
<div data-notification-id="<%= notification.id %>">
|
|
6
|
+
<%= notification.event.class.name %>
|
|
7
|
+
<%= ui_badge(label: notification.read? ? "Read" : "Unread") %>
|
|
8
|
+
<% unless notification.read? %>
|
|
9
|
+
<%= ui_form(action: notification_path(notification), method: :patch) do %>
|
|
10
|
+
<%= ui_button(label: "Mark read") %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= ui_page(max_width: :lg) do %>
|
|
2
|
+
<%= ui_section(title: "Notifications") do %>
|
|
3
|
+
<%= render partial: "notey/preferences",
|
|
4
|
+
locals: { person: Notey::Current.member, account: Notey::Current.account_id,
|
|
5
|
+
selection: {}, submit_url: preferences_path } %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNoteyPreferences < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :notey_preferences do |t|
|
|
6
|
+
t.references :member, polymorphic: true, null: false
|
|
7
|
+
t.bigint :account_id, null: false
|
|
8
|
+
t.string :notification_type, null: false
|
|
9
|
+
t.json :channels, null: false, default: []
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :notey_preferences, %i[member_type member_id account_id notification_type],
|
|
15
|
+
unique: true, name: "index_notey_preferences_unique"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNoteyDigests < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :notey_digests do |t|
|
|
6
|
+
t.references :member, polymorphic: true, null: false
|
|
7
|
+
t.bigint :account_id, null: false
|
|
8
|
+
t.string :digest_window, null: false
|
|
9
|
+
t.datetime :period_start, null: false
|
|
10
|
+
t.integer :notifications_count, null: false, default: 0
|
|
11
|
+
t.datetime :sent_at
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :notey_digests,
|
|
17
|
+
%i[member_type member_id account_id digest_window period_start],
|
|
18
|
+
unique: true, name: "index_notey_digests_unique"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNoteyDestinations < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :notey_destinations do |t|
|
|
6
|
+
t.bigint :account_id, null: false
|
|
7
|
+
t.string :channel, null: false
|
|
8
|
+
t.string :address, null: false
|
|
9
|
+
t.text :credential
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :notey_destinations, %i[account_id channel], unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMemberToNoteyDestinations < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
add_reference :notey_destinations, :member, polymorphic: true, null: true
|
|
6
|
+
|
|
7
|
+
remove_index :notey_destinations, %i[account_id channel]
|
|
8
|
+
add_index :notey_destinations, %i[member_type member_id account_id channel],
|
|
9
|
+
unique: true, name: "index_notey_destinations_unique"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateNoteyAttempts < ActiveRecord::Migration[8.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :notey_attempts do |t|
|
|
6
|
+
t.references :notification, null: false, foreign_key: { to_table: :noticed_notifications, on_delete: :cascade }
|
|
7
|
+
t.string :channel, null: false
|
|
8
|
+
t.string :state, null: false, default: "claimed"
|
|
9
|
+
t.text :failure
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :notey_attempts, %i[notification_id channel], unique: true, name: "index_notey_attempts_unique"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
class Channels
|
|
5
|
+
Decision = Struct.new(:channels, :window, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def self.decision_for(member, notification_type, account_id:)
|
|
8
|
+
return Decision.new(channels: [], window: "immediate") unless Notey.notification_types.include?(notification_type.to_s)
|
|
9
|
+
|
|
10
|
+
stored = stored_for(member, notification_type, account_id)
|
|
11
|
+
|
|
12
|
+
Decision.new(
|
|
13
|
+
channels: stored ? Array(stored.channels).map(&:to_s) : Notey.default_channels,
|
|
14
|
+
window: stored&.digest_window || "immediate"
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.for(member, notification_type, account_id:)
|
|
19
|
+
decision_for(member, notification_type, account_id: account_id).channels
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.window_for(member, notification_type, account_id:)
|
|
23
|
+
decision_for(member, notification_type, account_id: account_id).window
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.channels_of(preference, _notification_type)
|
|
27
|
+
return Array(preference.channels).map(&:to_s) if preference
|
|
28
|
+
|
|
29
|
+
Notey.default_channels
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.window_of(preference)
|
|
33
|
+
preference.digest_window || "immediate"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.stored_for(member, notification_type, account_id)
|
|
37
|
+
member.notey_preferences.find_by(account_id: account_id, notification_type: notification_type.to_s)
|
|
38
|
+
end
|
|
39
|
+
private_class_method :stored_for
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
class DigestRun
|
|
5
|
+
PERIODS = { "daily" => :day, "weekly" => :week }.freeze
|
|
6
|
+
|
|
7
|
+
def initialize(window:, now: Time.current)
|
|
8
|
+
@window = window.to_s
|
|
9
|
+
@now = now
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
preferences.group_by { |preference| [ preference.member, preference.account_id ] }
|
|
14
|
+
.each { |(member, account_id), _| DigestJob.perform_later(member, account_id, window) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deliver_to_member(member, account_id)
|
|
18
|
+
deliver_to(member, account_id, preferences.where(member: member, account_id: account_id))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :window, :now
|
|
24
|
+
|
|
25
|
+
def preferences
|
|
26
|
+
Preference.where(digest_window: window)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def deliver_to(member, account_id, grouped)
|
|
30
|
+
notifications = gathered(member, account_id, types_in_window(grouped))
|
|
31
|
+
return if notifications.empty?
|
|
32
|
+
|
|
33
|
+
digest = claim(member, account_id)
|
|
34
|
+
return if digest.nil?
|
|
35
|
+
|
|
36
|
+
send_digest(digest, member, notifications)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_digest(digest, member, notifications)
|
|
40
|
+
DigestMailer.digest(member, notifications, window).deliver_now
|
|
41
|
+
digest.update!(notifications_count: notifications.size, sent_at: Time.current)
|
|
42
|
+
rescue StandardError
|
|
43
|
+
digest.destroy
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def claim(member, account_id)
|
|
48
|
+
Digest.create!(member: member, account_id: account_id, digest_window: window, period_start: period_start)
|
|
49
|
+
rescue ActiveRecord::RecordNotUnique
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def types_in_window(grouped)
|
|
54
|
+
grouped.select { |preference| Channels.window_of(preference) == window }
|
|
55
|
+
.map(&:notification_type)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def gathered(member, account_id, notification_types)
|
|
59
|
+
Inbox.for(member, account_id: account_id)
|
|
60
|
+
.includes(:event)
|
|
61
|
+
.where(created_at: period_start..now)
|
|
62
|
+
.select { |notification| notification_types.include?(type_of(notification)) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def type_of(notification)
|
|
66
|
+
notification.event.class.try(:notey_notification_type)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def period_start
|
|
70
|
+
@period_start ||= now.public_send("beginning_of_#{PERIODS.fetch(window)}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/notey/engine.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "noticed"
|
|
2
|
+
require "keystone_ui"
|
|
3
|
+
|
|
4
|
+
module Notey
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Notey
|
|
7
|
+
|
|
8
|
+
initializer "notey.filter_parameters" do |app|
|
|
9
|
+
app.config.filter_parameters += [ :credential ]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "notey.forget_notifiers_on_reload" do
|
|
13
|
+
ActiveSupport::Reloader.before_class_unload { Notey.forget_notifiers }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
config.to_prepare { Noticed::DeliveryMethod.include(Notey::RecordsAttempt) }
|
|
17
|
+
|
|
18
|
+
config.after_initialize { |app| Notey.check! if app.config.eager_load }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
data/lib/notey/inbox.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notey
|
|
4
|
+
module RecordsAttempt
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
around_deliver :record_attempt
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def outbound?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record_attempt
|
|
16
|
+
channel = config[:notey_channel]
|
|
17
|
+
return yield if channel.blank? || !outbound?
|
|
18
|
+
|
|
19
|
+
attempt = claim_attempt(channel)
|
|
20
|
+
return if attempt.nil?
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
yield
|
|
24
|
+
attempt.update!(state: "sent")
|
|
25
|
+
rescue StandardError => error
|
|
26
|
+
attempt.update!(state: "failed", failure: error.message)
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def claim_attempt(channel)
|
|
32
|
+
Attempt.create!(notification: notification, channel: channel)
|
|
33
|
+
rescue ActiveRecord::RecordNotUnique
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|