activity_notification 2.3.3 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +8 -41
- data/CHANGELOG.md +31 -0
- data/Gemfile +1 -3
- data/README.md +30 -28
- data/activity_notification.gemspec +5 -5
- data/ai-curated-specs/issues/172/design.md +220 -0
- data/ai-curated-specs/issues/172/tasks.md +326 -0
- data/ai-curated-specs/issues/188/design.md +227 -0
- data/ai-curated-specs/issues/188/requirements.md +78 -0
- data/ai-curated-specs/issues/188/tasks.md +203 -0
- data/ai-curated-specs/issues/188/upstream-contributions.md +592 -0
- data/ai-curated-specs/issues/50/design.md +235 -0
- data/ai-curated-specs/issues/50/requirements.md +49 -0
- data/ai-curated-specs/issues/50/tasks.md +232 -0
- data/app/controllers/activity_notification/notifications_api_controller.rb +22 -0
- data/app/controllers/activity_notification/notifications_controller.rb +27 -1
- data/app/mailers/activity_notification/mailer.rb +2 -2
- data/app/views/activity_notification/notifications/default/_index.html.erb +6 -1
- data/app/views/activity_notification/notifications/default/destroy_all.js.erb +6 -0
- data/docs/Functions.md +5 -11
- data/docs/Setup.md +73 -4
- data/docs/Testing.md +12 -1
- data/gemfiles/Gemfile.rails-7.0 +2 -0
- data/gemfiles/Gemfile.rails-8.0 +0 -2
- data/lib/activity_notification/apis/notification_api.rb +51 -2
- data/lib/activity_notification/controllers/concerns/swagger/notifications_api.rb +59 -0
- data/lib/activity_notification/helpers/view_helpers.rb +28 -0
- data/lib/activity_notification/mailers/helpers.rb +14 -7
- data/lib/activity_notification/models/concerns/swagger/subscription_schema.rb +1 -1
- data/lib/activity_notification/models/concerns/target.rb +16 -0
- data/lib/activity_notification/models.rb +1 -1
- data/lib/activity_notification/notification_resilience.rb +115 -0
- data/lib/activity_notification/orm/dynamoid/extension.rb +4 -87
- data/lib/activity_notification/orm/dynamoid/notification.rb +19 -2
- data/lib/activity_notification/orm/dynamoid.rb +42 -6
- data/lib/activity_notification/rails/routes.rb +1 -0
- data/lib/activity_notification/version.rb +1 -1
- data/lib/activity_notification.rb +1 -0
- data/lib/generators/templates/controllers/notifications_api_controller.rb +5 -0
- data/lib/generators/templates/controllers/notifications_api_with_devise_controller.rb +5 -0
- data/lib/generators/templates/controllers/notifications_controller.rb +5 -0
- data/lib/generators/templates/controllers/notifications_with_devise_controller.rb +5 -0
- data/spec/concerns/apis/notification_api_spec.rb +161 -5
- data/spec/concerns/models/target_spec.rb +7 -0
- data/spec/controllers/controller_spec_utility.rb +2 -2
- data/spec/controllers/notifications_api_controller_shared_examples.rb +113 -0
- data/spec/controllers/notifications_controller_shared_examples.rb +150 -0
- data/spec/generators/migration/migration_generator_spec.rb +18 -4
- data/spec/helpers/view_helpers_spec.rb +14 -0
- data/spec/jobs/notification_resilience_job_spec.rb +167 -0
- data/spec/mailers/notification_resilience_spec.rb +263 -0
- data/spec/models/notification_spec.rb +1 -1
- data/spec/models/subscription_spec.rb +1 -1
- data/spec/rails_app/app/helpers/devise_helper.rb +2 -0
- data/spec/rails_app/config/application.rb +1 -0
- data/spec/rails_app/config/initializers/zeitwerk.rb +10 -0
- metadata +35 -22
data/docs/Functions.md
CHANGED
|
@@ -381,8 +381,6 @@ end
|
|
|
381
381
|
|
|
382
382
|
Then, you can access *users/1/subscriptions* and use *[ActivityNotification::SubscriptionsController](/app/controllers/activity_notification/subscriptions_controller.rb)* or *[ActivityNotification::SubscriptionsWithDeviseController](/app/controllers/activity_notification/subscriptions_with_devise_controller.rb)* to manage the subscriptions.
|
|
383
383
|
|
|
384
|
-
You can see sample subscription management view in demo application here: *https://activity-notification-example.herokuapp.com/users/1/subscriptions*
|
|
385
|
-
|
|
386
384
|
If you would like to customize subscription controllers or views, you can use generators like notifications:
|
|
387
385
|
|
|
388
386
|
* Customize subscription controllers
|
|
@@ -436,8 +434,6 @@ You can see [sample single page application](/spec/rails_app/app/javascript/) us
|
|
|
436
434
|
|
|
437
435
|
*activity_notification* provides API reference as [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification).
|
|
438
436
|
|
|
439
|
-
OpenAPI Specification in [online demo](https://activity-notification-example.herokuapp.com/) is published here: **https://activity-notification-example.herokuapp.com/api/v2/apidocs**
|
|
440
|
-
|
|
441
437
|
Public API reference is also hosted in [SwaggerHub](https://swagger.io/tools/swaggerhub/) here: **https://app.swaggerhub.com/apis-docs/simukappu/activity-notification/**
|
|
442
438
|
|
|
443
439
|
You can also publish OpenAPI Specification in your own application using *[ActivityNotification::ApidocsController](/app/controllers/activity_notification/apidocs_controller.rb)* like this:
|
|
@@ -588,7 +584,7 @@ end
|
|
|
588
584
|
To sign in and get *access-token* from Devise Token Auth, call *sign_in* API which you configured by *mount_devise_token_auth_for* method:
|
|
589
585
|
|
|
590
586
|
```console
|
|
591
|
-
$ curl -X POST -H "Content-Type: application/json" -D - -d '{"email": "ichiro@example.com","password": "changeit"}' https://
|
|
587
|
+
$ curl -X POST -H "Content-Type: application/json" -D - -d '{"email": "ichiro@example.com","password": "changeit"}' https://localhost:3000/api/v2/auth/sign_in
|
|
592
588
|
|
|
593
589
|
|
|
594
590
|
HTTP/1.1 200 OK
|
|
@@ -615,7 +611,7 @@ uid: ichiro@example.com
|
|
|
615
611
|
Then, call *activity_notification* API with returned *access-token*, *client* and *uid* as HTTP headers:
|
|
616
612
|
|
|
617
613
|
```console
|
|
618
|
-
$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://
|
|
614
|
+
$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://localhost:3000/api/v2/notifications
|
|
619
615
|
|
|
620
616
|
HTTP/1.1 200 OK
|
|
621
617
|
...
|
|
@@ -631,7 +627,7 @@ HTTP/1.1 200 OK
|
|
|
631
627
|
Without valid *access-token*, API returns *401 Unauthorized*:
|
|
632
628
|
|
|
633
629
|
```console
|
|
634
|
-
$ curl -X GET -H "Content-Type: application/json" -D - https://
|
|
630
|
+
$ curl -X GET -H "Content-Type: application/json" -D - https://localhost:3000/api/v2/notifications
|
|
635
631
|
|
|
636
632
|
HTTP/1.1 401 Unauthorized
|
|
637
633
|
...
|
|
@@ -646,7 +642,7 @@ HTTP/1.1 401 Unauthorized
|
|
|
646
642
|
When you request restricted resources of unauthorized targets, *activity_notification* API returns *403 Forbidden*:
|
|
647
643
|
|
|
648
644
|
```console
|
|
649
|
-
$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://
|
|
645
|
+
$ curl -X GET -H "Content-Type: application/json" -H "access-token: ZiDvw8vJGtbESy5Qpw32Kw" -H "client: W0NkGrTS88xeOx4VDOS-Xg" -H "uid: ichiro@example.com" -D - https://localhost:3000/api/v2/notifications/1
|
|
650
646
|
|
|
651
647
|
HTTP/1.1 403 Forbidden
|
|
652
648
|
...
|
|
@@ -1141,6 +1137,4 @@ user.find_or_create_subscription('comment.reply').subscribe_to_optional_target(:
|
|
|
1141
1137
|
user.find_or_create_subscription('comment.reply').unsubscribe_to_optional_target(:slack)
|
|
1142
1138
|
```
|
|
1143
1139
|
|
|
1144
|
-
You can also manage subscriptions of optional targets by subscriptions REST API. See [REST API backend](#rest-api-backend) for more details.
|
|
1145
|
-
|
|
1146
|
-
You can see sample subscription management view in demo application here: *https://activity-notification-example.herokuapp.com/users/1/subscriptions*
|
|
1140
|
+
You can also manage subscriptions of optional targets by subscriptions REST API. See [REST API backend](#rest-api-backend) for more details.
|
data/docs/Setup.md
CHANGED
|
@@ -22,6 +22,24 @@ $ bin/rails generate activity_notification:install
|
|
|
22
22
|
The generator will install an initializer which describes all configuration options of *activity_notification*.
|
|
23
23
|
It also generates a i18n based translation file which we can configure the presentation of notifications.
|
|
24
24
|
|
|
25
|
+
#### ORM Dependencies
|
|
26
|
+
|
|
27
|
+
By default, *activity_notification* uses **ActiveRecord** as the ORM and no additional ORM gems are required.
|
|
28
|
+
|
|
29
|
+
If you intend to use **Mongoid** support, you need to add the `mongoid` gem separately to your Gemfile:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem 'activity_notification'
|
|
33
|
+
gem 'mongoid', '>= 4.0.0', '< 10.0'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If you intend to use **Dynamoid** support for Amazon DynamoDB, you need to add the `dynamoid` gem separately to your Gemfile:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
gem 'activity_notification'
|
|
40
|
+
gem 'dynamoid', '>= 3.11.0', '< 4.0'
|
|
41
|
+
```
|
|
42
|
+
|
|
25
43
|
### Database setup
|
|
26
44
|
|
|
27
45
|
#### Using ActiveRecord ORM
|
|
@@ -64,7 +82,14 @@ config.yaml_column_permitted_classes << Time
|
|
|
64
82
|
|
|
65
83
|
#### Using Mongoid ORM
|
|
66
84
|
|
|
67
|
-
When you use *activity_notification* with [Mongoid](http://mongoid.org) ORM,
|
|
85
|
+
When you use *activity_notification* with [Mongoid](http://mongoid.org) ORM, you first need to add the `mongoid` gem to your Gemfile:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
gem 'activity_notification'
|
|
89
|
+
gem 'mongoid', '>= 4.0.0', '< 10.0'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Then set **AN_ORM** environment variable to **mongoid**:
|
|
68
93
|
|
|
69
94
|
```console
|
|
70
95
|
$ export AN_ORM=mongoid
|
|
@@ -80,13 +105,14 @@ You need to configure Mongoid in your Rails application for your MongoDB environ
|
|
|
80
105
|
|
|
81
106
|
#### Using Dynamoid ORM
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
When you use *activity_notification* with [Dynamoid](https://github.com/Dynamoid/dynamoid) ORM, you first need to add the `dynamoid` gem to your Gemfile:
|
|
84
109
|
|
|
85
110
|
```ruby
|
|
86
|
-
gem '
|
|
111
|
+
gem 'activity_notification'
|
|
112
|
+
gem 'dynamoid', '>= 3.11.0', '< 4.0'
|
|
87
113
|
```
|
|
88
114
|
|
|
89
|
-
|
|
115
|
+
Then set **AN_ORM** environment variable to **dynamoid**:
|
|
90
116
|
|
|
91
117
|
```console
|
|
92
118
|
$ export AN_ORM=dynamoid
|
|
@@ -762,6 +788,49 @@ notification:
|
|
|
762
788
|
|
|
763
789
|
This structure is valid for notifications with keys *"notification.comment.reply"* or *"comment.reply"*. As mentioned before, *"notification."* part of the key is optional. In addition for above example, `%{notifier_name}` and `%{article_title}` are used from parameter field in the notification record. Pluralization is supported (but optional) for grouped notifications using the `%{group_notification_count}` value.
|
|
764
790
|
|
|
791
|
+
### Managing notifications
|
|
792
|
+
|
|
793
|
+
*activity_notification* provides several methods to manage notifications programmatically. The most common operation is opening notifications to mark them as read.
|
|
794
|
+
|
|
795
|
+
#### Opening notifications
|
|
796
|
+
|
|
797
|
+
You can mark individual notifications as opened (read) using the **open!** method:
|
|
798
|
+
|
|
799
|
+
```ruby
|
|
800
|
+
# Open a single notification
|
|
801
|
+
notification = current_user.notifications.first
|
|
802
|
+
notification.open!
|
|
803
|
+
|
|
804
|
+
# Open notification with specific timestamp
|
|
805
|
+
notification.open!(opened_at: 1.hour.ago)
|
|
806
|
+
|
|
807
|
+
# Open notification with opening group members
|
|
808
|
+
notification.open!(with_members: true)
|
|
809
|
+
|
|
810
|
+
# Open notification skipping validations when the associated notifiable record may have been deleted
|
|
811
|
+
notification.open!(skip_validation: true)
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
The **open!** method accepts the following options:
|
|
815
|
+
|
|
816
|
+
* **:opened_at** (Time) - Time to set as the opened timestamp (defaults to `Time.current`)
|
|
817
|
+
* **:with_members** (Boolean) - Whether to open group member notifications as well (defaults to `false`)
|
|
818
|
+
* **:skip_validation** (Boolean) - Whether to skip ActiveRecord validations when updating (defaults to `false`). Useful when the associated notifiable record may have been deleted but the notification still exists.
|
|
819
|
+
|
|
820
|
+
You can also open all notifications for a target:
|
|
821
|
+
|
|
822
|
+
```ruby
|
|
823
|
+
# Open all unopened notifications for a user
|
|
824
|
+
ActivityNotification::Notification.open_all_of(current_user)
|
|
825
|
+
|
|
826
|
+
# Open notifications with filters
|
|
827
|
+
ActivityNotification::Notification.open_all_of(
|
|
828
|
+
current_user,
|
|
829
|
+
filtered_by_type: 'Comment',
|
|
830
|
+
opened_at: 1.hour.ago
|
|
831
|
+
)
|
|
832
|
+
```
|
|
833
|
+
|
|
765
834
|
### Customizing controllers (optional)
|
|
766
835
|
|
|
767
836
|
If the customization at the views level is not enough, you can customize each controller by following these steps:
|
data/docs/Testing.md
CHANGED
|
@@ -104,6 +104,17 @@ $ bin/rails server
|
|
|
104
104
|
```
|
|
105
105
|
Then, you can access <http://localhost:3000> for the example application.
|
|
106
106
|
|
|
107
|
+
##### Default test users
|
|
108
|
+
|
|
109
|
+
Login as the following test users to experience user activity notifications:
|
|
110
|
+
|
|
111
|
+
| Email | Password | Admin? |
|
|
112
|
+
|:---:|:---:|:---:|
|
|
113
|
+
| ichiro@example.com | changeit | Yes |
|
|
114
|
+
| stephen@example.com | changeit | |
|
|
115
|
+
| klay@example.com | changeit | |
|
|
116
|
+
| kevin@example.com | changeit | |
|
|
117
|
+
|
|
107
118
|
##### Run with your local database
|
|
108
119
|
As default, example Rails application runs with local SQLite database in *spec/rails_app/db/development.sqlite3*.
|
|
109
120
|
This application supports to run with your local MySQL, PostgreSQL, MongoDB.
|
|
@@ -144,5 +155,5 @@ $ cd spec/rails_app
|
|
|
144
155
|
$ # You don't need migration when you use MongoDB only (AN_ORM=mongoid and AN_TEST_DB=mongodb)
|
|
145
156
|
$ bin/rake db:migrate
|
|
146
157
|
$ bin/rake db:seed
|
|
147
|
-
$ bin/rails server
|
|
158
|
+
$ bin/rails server
|
|
148
159
|
```
|
data/gemfiles/Gemfile.rails-7.0
CHANGED
data/gemfiles/Gemfile.rails-8.0
CHANGED
|
@@ -4,8 +4,6 @@ gemspec path: '../'
|
|
|
4
4
|
|
|
5
5
|
gem 'rails', '~> 8.0.0'
|
|
6
6
|
gem 'sprockets-rails'
|
|
7
|
-
# https://github.com/lynndylanhurley/devise_token_auth/pull/1639
|
|
8
|
-
gem 'devise_token_auth', git: 'https://github.com/lynndylanhurley/devise_token_auth.git'
|
|
9
7
|
|
|
10
8
|
group :development do
|
|
11
9
|
gem 'bullet'
|
|
@@ -411,15 +411,63 @@ module ActivityNotification
|
|
|
411
411
|
# @option options [String] :filtered_by_key (nil) Key of the notification for filter
|
|
412
412
|
# @option options [String] :later_than (nil) ISO 8601 format time to filter notification index later than specified time
|
|
413
413
|
# @option options [String] :earlier_than (nil) ISO 8601 format time to filter notification index earlier than specified time
|
|
414
|
+
# @option options [Array] :ids (nil) Array of specific notification IDs to open
|
|
414
415
|
# @return [Array<Notification>] Opened notification records
|
|
415
416
|
def open_all_of(target, options = {})
|
|
416
417
|
opened_at = options[:opened_at] || Time.current
|
|
417
418
|
target_unopened_notifications = target.notifications.unopened_only.filtered_by_options(options)
|
|
419
|
+
# If specific IDs are provided, filter by them
|
|
420
|
+
if options[:ids].present?
|
|
421
|
+
# :nocov:
|
|
422
|
+
case ActivityNotification.config.orm
|
|
423
|
+
when :mongoid
|
|
424
|
+
target_unopened_notifications = target_unopened_notifications.where(id: { '$in' => options[:ids] })
|
|
425
|
+
when :dynamoid
|
|
426
|
+
target_unopened_notifications = target_unopened_notifications.where('id.in': options[:ids])
|
|
427
|
+
else # :active_record
|
|
428
|
+
target_unopened_notifications = target_unopened_notifications.where(id: options[:ids])
|
|
429
|
+
end
|
|
430
|
+
# :nocov:
|
|
431
|
+
end
|
|
418
432
|
opened_notifications = target_unopened_notifications.to_a.map { |n| n.opened_at = opened_at; n }
|
|
419
433
|
target_unopened_notifications.update_all(opened_at: opened_at)
|
|
420
434
|
opened_notifications
|
|
421
435
|
end
|
|
422
436
|
|
|
437
|
+
# Destroys all notifications of the target matching the filter criteria.
|
|
438
|
+
#
|
|
439
|
+
# @param [Object] target Target of the notifications to destroy
|
|
440
|
+
# @param [Hash] options Options for filtering notifications to destroy
|
|
441
|
+
# @option options [String] :filtered_by_type (nil) Notifiable type for filter
|
|
442
|
+
# @option options [Object] :filtered_by_group (nil) Group instance for filter
|
|
443
|
+
# @option options [String] :filtered_by_group_type (nil) Group type for filter, valid with :filtered_by_group_id
|
|
444
|
+
# @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
|
|
445
|
+
# @option options [String] :filtered_by_key (nil) Key of the notification for filter
|
|
446
|
+
# @option options [String] :later_than (nil) ISO 8601 format time to filter notifications later than specified time
|
|
447
|
+
# @option options [String] :earlier_than (nil) ISO 8601 format time to filter notifications earlier than specified time
|
|
448
|
+
# @option options [Array] :ids (nil) Array of specific notification IDs to destroy
|
|
449
|
+
# @return [Array<Notification>] Destroyed notification records
|
|
450
|
+
def destroy_all_of(target, options = {})
|
|
451
|
+
target_notifications = target.notifications.filtered_by_options(options)
|
|
452
|
+
# If specific IDs are provided, filter by them
|
|
453
|
+
if options[:ids].present?
|
|
454
|
+
# :nocov:
|
|
455
|
+
case ActivityNotification.config.orm
|
|
456
|
+
when :mongoid
|
|
457
|
+
target_notifications = target_notifications.where(id: { '$in' => options[:ids] })
|
|
458
|
+
when :dynamoid
|
|
459
|
+
target_notifications = target_notifications.where('id.in': options[:ids])
|
|
460
|
+
else # :active_record
|
|
461
|
+
target_notifications = target_notifications.where(id: options[:ids])
|
|
462
|
+
end
|
|
463
|
+
# :nocov:
|
|
464
|
+
end
|
|
465
|
+
# Get the notifications before destroying them for return value
|
|
466
|
+
destroyed_notifications = target_notifications.to_a
|
|
467
|
+
target_notifications.destroy_all
|
|
468
|
+
destroyed_notifications
|
|
469
|
+
end
|
|
470
|
+
|
|
423
471
|
# Returns if group member of the notifications exists.
|
|
424
472
|
# This method is designed to be called from controllers or views to avoid N+1.
|
|
425
473
|
#
|
|
@@ -517,7 +565,7 @@ module ActivityNotification
|
|
|
517
565
|
# @param [Hash] options Options for notification email
|
|
518
566
|
# @option options [Boolean] :send_later If it sends notification email asynchronously
|
|
519
567
|
# @option options [String, Symbol] :fallback (:default) Fallback template to use when MissingTemplate is raised
|
|
520
|
-
# @return [Mail::Message, ActionMailer::DeliveryJob] Email message
|
|
568
|
+
# @return [Mail::Message, ActionMailer::DeliveryJob, NilClass] Email message, its delivery job, or nil if notification not found
|
|
521
569
|
def send_notification_email(options = {})
|
|
522
570
|
if target.notification_email_allowed?(notifiable, key) &&
|
|
523
571
|
notifiable.notification_email_allowed?(target, key) &&
|
|
@@ -559,6 +607,7 @@ module ActivityNotification
|
|
|
559
607
|
# @param [Hash] options Options for opening notifications
|
|
560
608
|
# @option options [DateTime] :opened_at (Time.current) Time to set to opened_at of the notification record
|
|
561
609
|
# @option options [Boolean] :with_members (true) If it opens notifications including group members
|
|
610
|
+
# @option options [Boolean] :skip_validation (true) If it skips validation of the notification record
|
|
562
611
|
# @return [Integer] Number of opened notification records
|
|
563
612
|
def open!(options = {})
|
|
564
613
|
opened? and return 0
|
|
@@ -566,7 +615,7 @@ module ActivityNotification
|
|
|
566
615
|
with_members = options.has_key?(:with_members) ? options[:with_members] : true
|
|
567
616
|
unopened_member_count = with_members ? group_members.unopened_only.count : 0
|
|
568
617
|
group_members.update_all(opened_at: opened_at) if with_members
|
|
569
|
-
update(opened_at: opened_at)
|
|
618
|
+
options[:skip_validation] ? update_attribute(:opened_at, opened_at) : update(opened_at: opened_at)
|
|
570
619
|
unopened_member_count + 1
|
|
571
620
|
end
|
|
572
621
|
|
|
@@ -92,6 +92,18 @@ module ActivityNotification
|
|
|
92
92
|
extend Swagger::NotificationsParameters::TargetParameters
|
|
93
93
|
extend Swagger::NotificationsParameters::FilterByParameters
|
|
94
94
|
|
|
95
|
+
parameter do
|
|
96
|
+
key :name, :ids
|
|
97
|
+
key :in, :query
|
|
98
|
+
key :description, "Array of specific notification IDs to open"
|
|
99
|
+
key :required, false
|
|
100
|
+
key :type, :array
|
|
101
|
+
items do
|
|
102
|
+
key :type, :string
|
|
103
|
+
end
|
|
104
|
+
key :example, ["1", "2", "3"]
|
|
105
|
+
end
|
|
106
|
+
|
|
95
107
|
response 200 do
|
|
96
108
|
key :description, "Opened notifications"
|
|
97
109
|
content 'application/json' do
|
|
@@ -117,6 +129,53 @@ module ActivityNotification
|
|
|
117
129
|
end
|
|
118
130
|
end
|
|
119
131
|
|
|
132
|
+
swagger_path '/{target_type}/{target_id}/notifications/destroy_all' do
|
|
133
|
+
operation :post do
|
|
134
|
+
key :summary, 'Destroy all notifications'
|
|
135
|
+
key :description, 'Destroys all notifications of the target matching filter criteria.'
|
|
136
|
+
key :operationId, 'destroyAllNotifications'
|
|
137
|
+
key :tags, ['notifications']
|
|
138
|
+
|
|
139
|
+
extend Swagger::NotificationsParameters::TargetParameters
|
|
140
|
+
extend Swagger::NotificationsParameters::FilterByParameters
|
|
141
|
+
|
|
142
|
+
parameter do
|
|
143
|
+
key :name, :ids
|
|
144
|
+
key :in, :query
|
|
145
|
+
key :description, "Array of specific notification IDs to destroy"
|
|
146
|
+
key :required, false
|
|
147
|
+
key :type, :array
|
|
148
|
+
items do
|
|
149
|
+
key :type, :string
|
|
150
|
+
end
|
|
151
|
+
key :example, ["1", "2", "3"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
response 200 do
|
|
155
|
+
key :description, "Destroyed notifications"
|
|
156
|
+
content 'application/json' do
|
|
157
|
+
schema do
|
|
158
|
+
key :type, :object
|
|
159
|
+
property :count do
|
|
160
|
+
key :type, :integer
|
|
161
|
+
key :description, "Number of destroyed notification records"
|
|
162
|
+
key :example, 3
|
|
163
|
+
end
|
|
164
|
+
property :notifications do
|
|
165
|
+
key :type, :array
|
|
166
|
+
items do
|
|
167
|
+
key :'$ref', :Notification
|
|
168
|
+
end
|
|
169
|
+
key :description, "Destroyed notifications"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
extend Swagger::ErrorResponses::InvalidParameterError
|
|
175
|
+
extend Swagger::ErrorResponses::ResourceNotFoundError
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
120
179
|
swagger_path '/{target_type}/{target_id}/notifications/{id}' do
|
|
121
180
|
operation :get do
|
|
122
181
|
key :summary, 'Get notification'
|
|
@@ -145,6 +145,20 @@ module ActivityNotification
|
|
|
145
145
|
send("open_all_#{routing_scope(options)}#{target.to_resource_name}_notifications_path", target, options)
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
# Returns destroy_all_notifications_path for the target
|
|
149
|
+
#
|
|
150
|
+
# @param [Object] target Target instance
|
|
151
|
+
# @param [Hash] params Request parameters
|
|
152
|
+
# @return [String] destroy_all_notifications_path for the target
|
|
153
|
+
# @todo Needs any other better implementation
|
|
154
|
+
# @todo Must handle devise namespace
|
|
155
|
+
def destroy_all_notifications_path_for(target, params = {})
|
|
156
|
+
options = params.dup
|
|
157
|
+
options.delete(:devise_default_routes) ?
|
|
158
|
+
send("destroy_all_#{routing_scope(options)}notifications_path", options) :
|
|
159
|
+
send("destroy_all_#{routing_scope(options)}#{target.to_resource_name}_notifications_path", target, options)
|
|
160
|
+
end
|
|
161
|
+
|
|
148
162
|
# Returns notifications_url for the target
|
|
149
163
|
#
|
|
150
164
|
# @param [Object] target Target instance
|
|
@@ -215,6 +229,20 @@ module ActivityNotification
|
|
|
215
229
|
send("open_all_#{routing_scope(options)}#{target.to_resource_name}_notifications_url", target, options)
|
|
216
230
|
end
|
|
217
231
|
|
|
232
|
+
# Returns destroy_all_notifications_url for the target
|
|
233
|
+
#
|
|
234
|
+
# @param [Object] target Target instance
|
|
235
|
+
# @param [Hash] params Request parameters
|
|
236
|
+
# @return [String] destroy_all_notifications_url for the target
|
|
237
|
+
# @todo Needs any other better implementation
|
|
238
|
+
# @todo Must handle devise namespace
|
|
239
|
+
def destroy_all_notifications_url_for(target, params = {})
|
|
240
|
+
options = params.dup
|
|
241
|
+
options.delete(:devise_default_routes) ?
|
|
242
|
+
send("destroy_all_#{routing_scope(options)}notifications_url", options) :
|
|
243
|
+
send("destroy_all_#{routing_scope(options)}#{target.to_resource_name}_notifications_url", target, options)
|
|
244
|
+
end
|
|
245
|
+
|
|
218
246
|
# Returns subscriptions_path for the target
|
|
219
247
|
#
|
|
220
248
|
# @param [Object] target Target instance
|
|
@@ -5,6 +5,7 @@ module ActivityNotification
|
|
|
5
5
|
# Use to resolve parameters from email configuration and send notification email.
|
|
6
6
|
module Helpers
|
|
7
7
|
extend ActiveSupport::Concern
|
|
8
|
+
include ActivityNotification::NotificationResilience
|
|
8
9
|
|
|
9
10
|
protected
|
|
10
11
|
|
|
@@ -13,10 +14,13 @@ module ActivityNotification
|
|
|
13
14
|
# @param [Notification] notification Notification instance to send email
|
|
14
15
|
# @param [Hash] options Options for notification email
|
|
15
16
|
# @option options [String, Symbol] :fallback (:default) Fallback template to use when MissingTemplate is raised
|
|
17
|
+
# @return [Mail::Message, nil] Email message or nil if notification was not found
|
|
16
18
|
def notification_mail(notification, options = {})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
with_notification_resilience(notification&.id, { target: 'unknown' }) do
|
|
20
|
+
initialize_from_notification(notification)
|
|
21
|
+
headers = headers_for(notification.key, options)
|
|
22
|
+
send_mail(headers, options[:fallback])
|
|
23
|
+
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
# Send batch notification email with configured options.
|
|
@@ -26,11 +30,14 @@ module ActivityNotification
|
|
|
26
30
|
# @param [String] batch_key Key of the batch notification email
|
|
27
31
|
# @param [Hash] options Options for notification email
|
|
28
32
|
# @option options [String, Symbol] :fallback (:batch_default) Fallback template to use when MissingTemplate is raised
|
|
33
|
+
# @return [Mail::Message, nil] Email message or nil if notifications were not found
|
|
29
34
|
def batch_notification_mail(target, notifications, batch_key, options = {})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
with_notification_resilience(notifications&.first&.id, { target: target&.class&.name, batch: true }) do
|
|
36
|
+
initialize_from_notifications(target, notifications)
|
|
37
|
+
headers = headers_for(batch_key, options)
|
|
38
|
+
@notification = nil
|
|
39
|
+
send_mail(headers, options[:fallback])
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
# Initialize instance variables from notification.
|
|
@@ -419,6 +419,22 @@ module ActivityNotification
|
|
|
419
419
|
Notification.open_all_of(self, options)
|
|
420
420
|
end
|
|
421
421
|
|
|
422
|
+
# Destroys all notifications of the target matching the filter criteria.
|
|
423
|
+
#
|
|
424
|
+
# @param [Hash] options Options for filtering notifications to destroy
|
|
425
|
+
# @option options [String] :filtered_by_type (nil) Notifiable type for filter
|
|
426
|
+
# @option options [Object] :filtered_by_group (nil) Group instance for filter
|
|
427
|
+
# @option options [String] :filtered_by_group_type (nil) Group type for filter, valid with :filtered_by_group_id
|
|
428
|
+
# @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
|
|
429
|
+
# @option options [String] :filtered_by_key (nil) Key of the notification for filter
|
|
430
|
+
# @option options [String] :later_than (nil) ISO 8601 format time to filter notifications later than specified time
|
|
431
|
+
# @option options [String] :earlier_than (nil) ISO 8601 format time to filter notifications earlier than specified time
|
|
432
|
+
# @option options [Array] :ids (nil) Array of specific notification IDs to destroy
|
|
433
|
+
# @return [Array<Notification>] Destroyed notification records
|
|
434
|
+
def destroy_all_notifications(options = {})
|
|
435
|
+
Notification.destroy_all_of(self, options)
|
|
436
|
+
end
|
|
437
|
+
|
|
422
438
|
|
|
423
439
|
# Gets automatically arranged notification index of the target with included attributes like target, notifiable, group and notifier.
|
|
424
440
|
# This method is the typical way to get notifications index from controller of view.
|
|
@@ -18,11 +18,11 @@ module ActivityNotification
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
if defined?(ActiveRecord::Base)
|
|
21
|
+
# :nocov:
|
|
21
22
|
ActiveRecord::Base.class_eval { include ActivityNotification::Models }
|
|
22
23
|
|
|
23
24
|
# https://github.com/simukappu/activity_notification/issues/166
|
|
24
25
|
# https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
|
|
25
|
-
# :nocov:
|
|
26
26
|
if (Gem::Version.new("5.2.8.1") <= Rails.gem_version && Rails.gem_version < Gem::Version.new("6.0")) ||
|
|
27
27
|
(Gem::Version.new("6.0.5.1") <= Rails.gem_version && Rails.gem_version < Gem::Version.new("6.1")) ||
|
|
28
28
|
(Gem::Version.new("6.1.6.1") <= Rails.gem_version && Rails.gem_version < Gem::Version.new("7.0"))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module ActivityNotification
|
|
2
|
+
# Provides resilient notification handling across different ORMs
|
|
3
|
+
# Handles missing notification scenarios gracefully without raising exceptions
|
|
4
|
+
module NotificationResilience
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Exception classes for different ORMs
|
|
8
|
+
ORM_EXCEPTIONS = {
|
|
9
|
+
active_record: 'ActiveRecord::RecordNotFound',
|
|
10
|
+
mongoid: 'Mongoid::Errors::DocumentNotFound',
|
|
11
|
+
dynamoid: 'Dynamoid::Errors::RecordNotFound'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
class_methods do
|
|
15
|
+
# Returns the current ORM being used
|
|
16
|
+
# @return [Symbol] The ORM symbol (:active_record, :mongoid, :dynamoid)
|
|
17
|
+
def current_orm
|
|
18
|
+
ActivityNotification.config.orm
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the exception class for the current ORM
|
|
22
|
+
# @return [Class] The exception class for missing records in current ORM
|
|
23
|
+
def record_not_found_exception_class
|
|
24
|
+
exception_name = ORM_EXCEPTIONS[current_orm]
|
|
25
|
+
return nil unless exception_name
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
exception_name.constantize
|
|
29
|
+
rescue NameError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Checks if an exception is a "record not found" exception for any supported ORM
|
|
35
|
+
# @param [Exception] exception The exception to check
|
|
36
|
+
# @return [Boolean] True if the exception indicates a missing record
|
|
37
|
+
def record_not_found_exception?(exception)
|
|
38
|
+
ORM_EXCEPTIONS.values.any? do |exception_name|
|
|
39
|
+
begin
|
|
40
|
+
exception.is_a?(exception_name.constantize)
|
|
41
|
+
rescue NameError
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Module-level methods that delegate to class methods
|
|
49
|
+
def self.current_orm
|
|
50
|
+
ActivityNotification.config.orm
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.record_not_found_exception_class
|
|
54
|
+
exception_name = ORM_EXCEPTIONS[current_orm]
|
|
55
|
+
return nil unless exception_name
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
exception_name.constantize
|
|
59
|
+
rescue NameError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.record_not_found_exception?(exception)
|
|
65
|
+
ORM_EXCEPTIONS.values.any? do |exception_name|
|
|
66
|
+
begin
|
|
67
|
+
exception.is_a?(exception_name.constantize)
|
|
68
|
+
rescue NameError
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Executes a block with resilient notification handling
|
|
75
|
+
# Catches ORM-specific "record not found" exceptions and logs them appropriately
|
|
76
|
+
# @param [String, Integer] notification_id The ID of the notification being processed
|
|
77
|
+
# @param [Hash] context Additional context for logging
|
|
78
|
+
# @yield Block to execute with resilient handling
|
|
79
|
+
# @return [Object, nil] Result of the block, or nil if notification was not found
|
|
80
|
+
def with_notification_resilience(notification_id = nil, context = {})
|
|
81
|
+
yield
|
|
82
|
+
rescue => exception
|
|
83
|
+
if self.class.record_not_found_exception?(exception)
|
|
84
|
+
log_missing_notification(notification_id, exception, context)
|
|
85
|
+
nil
|
|
86
|
+
else
|
|
87
|
+
raise exception
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Logs a warning when a notification is not found
|
|
94
|
+
# @param [String, Integer] notification_id The ID of the missing notification
|
|
95
|
+
# @param [Exception] exception The exception that was caught
|
|
96
|
+
# @param [Hash] context Additional context for logging
|
|
97
|
+
def log_missing_notification(notification_id, exception, context = {})
|
|
98
|
+
orm_name = self.class.current_orm
|
|
99
|
+
exception_class = exception.class.name
|
|
100
|
+
|
|
101
|
+
message = "ActivityNotification: Notification"
|
|
102
|
+
message += " with id #{notification_id}" if notification_id
|
|
103
|
+
message += " not found for email delivery"
|
|
104
|
+
message += " (#{orm_name}/#{exception_class})"
|
|
105
|
+
message += ", likely destroyed before job execution"
|
|
106
|
+
|
|
107
|
+
if context.any?
|
|
108
|
+
context_info = context.map { |k, v| "#{k}: #{v}" }.join(', ')
|
|
109
|
+
message += " [#{context_info}]"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Rails.logger.warn(message)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|