diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..239d6622 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Get Help + url: https://github.com/excid3/noticed/discussions/new?category=help + about: If you can't get something to work the way you expect, open a question in our discussion forums. + - name: Feature Request + url: https://github.com/excid3/noticed/discussions/new?category=ideas + about: 'Suggest any ideas you have using our discussion forums.' + - name: Bug Report + url: https://github.com/excid3/noticed/issues/new?body=%3C%21--%20Please%20provide%20all%20of%20the%20information%20requested%20below.%20We%27re%20a%20small%20team%20and%20without%20all%20of%20this%20information%20it%27s%20not%20possible%20for%20us%20to%20help%20and%20your%20bug%20report%20will%20be%20closed.%20--%3E%0A%0A%2A%2AWhat%20version%20of%20Noticed%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v2.0.4%0A%0A%2A%2AWhat%20version%20of%20Rails%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v7.1.1%0A%0A%2A%2ADescribe%20your%20issue%2A%2A%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead. + about: If you've already asked for help with a problem and confirmed something is broken with Noticed itself, create a bug report. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3785fcf0..148ab106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,29 +7,22 @@ on: push: branches: - main - + jobs: sqlite: runs-on: ubuntu-latest strategy: matrix: - ruby: ['2.7', '3.0', '3.1', '3.2'] + ruby: ['3.0', '3.1', '3.2', '3.3'] gemfile: - - rails_5_2 - - rails_6 - rails_6_1 - rails_7 - rails_7_1 - rails_main exclude: - ruby: '3.0' - gemfile: 'rails_5_2' - - ruby: '3.1' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_6' + gemfile: 'rails_main' + env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile BUNDLE_PATH_RELATIVE_TO_CWD: true @@ -60,23 +53,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: ['2.7', '3.0', '3.1', '3.2'] + ruby: ['3.0', '3.1', '3.2', '3.3'] gemfile: - - rails_5_2 - - rails_6 - rails_6_1 - rails_7 - rails_7_1 - rails_main exclude: - ruby: '3.0' - gemfile: 'rails_5_2' - - ruby: '3.1' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_6' + gemfile: 'rails_main' + env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile BUNDLE_PATH_RELATIVE_TO_CWD: true @@ -116,30 +102,23 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: ['2.7', '3.0', '3.1', '3.2'] + ruby: ['3.0', '3.1', '3.2', '3.3'] gemfile: - - rails_5_2 - - rails_6 - rails_6_1 - rails_7 - rails_7_1 - rails_main exclude: - ruby: '3.0' - gemfile: 'rails_5_2' - - ruby: '3.1' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_5_2' - - ruby: '3.2' - gemfile: 'rails_6' + gemfile: 'rails_main' + env: BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile BUNDLE_PATH_RELATIVE_TO_CWD: true services: postgres: - image: postgres:12 + image: postgres:16 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: password diff --git a/.standard.yml b/.standard.yml index f925a432..3c6e1900 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,4 +1,4 @@ -ruby_version: 2.7 +ruby_version: 3.0 ignore: - '**/*': - Style/HashSyntax diff --git a/Appraisals b/Appraisals index f9b6e6ed..56f3b097 100644 --- a/Appraisals +++ b/Appraisals @@ -1,11 +1,3 @@ -appraise "rails-5-2" do - gem "rails", "~> 5.2.0" -end - -appraise "rails-6" do - gem "rails", "~> 6.0.0" -end - appraise "rails-6-1" do gem "rails", "~> 6.1.0" end diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d04e8bf..c939a0fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ ### Unreleased +### 2.0.0 + +* [Breaking] Noticed now provides its own models for managing notifications. Migrate existing model(s) to use the new Noticed tables. + +TODO - add migration example + +* [Breaking] Noticed::NotificationChannel has been removed. Use an ActionCable channel in your application instead. +* [Breaking] Twilio has been renamed to `twilio_messaging` to provide support for other Twilio services in the future. +* [Breaking] Vonage / Nexmo has been renamed to `vonage_sms` to provide support for other Vonage services in the future. + +```ruby +class NotificationChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end + + def unsubscribed + stop_all_streams + end +end +``` + +* `Notifications` have now been renamed to `Notifiers` and now inherit from the +* Email delivery method now supports args * Support html safe translations for Rails 7+ ### 1.6.3 diff --git a/Gemfile.lock b/Gemfile.lock index 9d7d16d1..0995d582 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,77 +1,77 @@ PATH remote: . specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) + noticed (2.0.0) + rails (>= 6.1.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.1) - actionpack (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activesupport (= 7.1.1) + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.1) - actionview (= 7.1.1) - activesupport (= 7.1.1) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) nokogiri (>= 1.8.5) + racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.1) - actionpack (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.1) - activesupport (= 7.1.1) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.1) - activesupport (= 7.1.1) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (7.1.1) - activesupport (= 7.1.1) - activerecord (7.1.1) - activemodel (= 7.1.1) - activesupport (= 7.1.1) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) timeout (>= 0.4.0) - activestorage (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activesupport (= 7.1.1) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) marcel (~> 1.0) - activesupport (7.1.1) + activesupport (7.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -81,7 +81,7 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) apnotic (1.7.1) connection_pool (~> 2) @@ -91,8 +91,8 @@ GEM rake thor (>= 0.14.0) ast (2.4.2) - base64 (0.1.1) - bigdecimal (3.1.4) + base64 (0.2.0) + bigdecimal (3.1.5) builder (3.2.4) byebug (11.1.3) concurrent-ruby (1.2.2) @@ -100,53 +100,38 @@ GEM crack (0.4.5) rexml crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - drb (2.1.1) + date (3.3.4) + drb (2.2.0) ruby2_keywords erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) + hashdiff (1.1.0) http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.8.1) + io-console (0.7.1) + irb (1.11.1) rdoc - reline (>= 0.3.8) - json (2.6.3) + reline (>= 0.4.2) + json (2.7.1) jwt (2.7.1) language_server-protocol (3.17.0.3) lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -156,38 +141,41 @@ GEM net-smtp marcel (1.0.2) mini_mime (1.1.5) - minitest (5.20.0) + mini_portile2 (2.8.5) + minitest (5.21.1) multi_json (1.15.0) - mutex_m (0.1.2) + mutex_m (0.2.0) mysql2 (0.5.5) + net-http (0.4.1) + uri net-http2 (0.18.5) http-2 (~> 0.11) - net-imap (0.4.1) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) + nio4r (2.7.0) + nokogiri (1.16.0-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.4-x86_64-darwin) + nokogiri (1.16.0-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.4-x86_64-linux) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc pg (1.5.4) - psych (5.1.1) + psych (5.1.2) stringio - public_suffix (5.0.3) - racc (1.7.1) + public_suffix (5.0.4) + racc (1.7.3) rack (3.0.8) rack-session (2.0.0) rack (>= 3.0.0) @@ -196,20 +184,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.1.1) - actioncable (= 7.1.1) - actionmailbox (= 7.1.1) - actionmailer (= 7.1.1) - actionpack (= 7.1.1) - actiontext (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activemodel (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + rails (7.1.2) + actioncable (= 7.1.2) + actionmailbox (= 7.1.2) + actionmailer (= 7.1.2) + actionpack (= 7.1.2) + actiontext (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activemodel (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) bundler (>= 1.15.0) - railties (= 7.1.1) + railties (= 7.1.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -217,39 +205,38 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) irb rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - rdoc (6.5.0) + rake (13.1.0) + rdoc (6.6.2) psych (>= 4.0.0) - regexp_parser (2.8.2) - reline (0.3.9) + regexp_parser (2.9.0) + reline (0.4.2) io-console (~> 0.5) rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) signet (0.18.0) @@ -257,30 +244,27 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sqlite3 (1.6.7-arm64-darwin) - sqlite3 (1.6.7-x86_64-darwin) - sqlite3 (1.6.7-x86_64-linux) - standard (1.31.2) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) + standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.56.4) + rubocop (~> 1.59.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.2) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.2.1) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - stringio (3.0.8) - thor (1.2.2) - timeout (0.4.0) + rubocop-performance (~> 1.20.2) + stringio (3.1.0) + thor (1.3.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.5.0) + uri (0.13.0) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -294,6 +278,7 @@ GEM PLATFORMS arm64-darwin-22 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -310,4 +295,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.20 + 2.5.3 diff --git a/README.md b/README.md index e6c9ec00..edf09c05 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,33 @@ [![Build Status](https://github.com/excid3/noticed/workflows/Tests/badge.svg)](https://github.com/excid3/noticed/actions) [![Gem Version](https://badge.fury.io/rb/noticed.svg)](https://badge.fury.io/rb/noticed) -Currently, we support these notification delivery methods out of the box: +Noticed allows you to send notifications to any number of recipients. You might want a Slack notification with 0 recipients to let your team know when something happens. A notification can also be sent to 1+ recipients -* Database -* Email -* ActionCable channels -* Slack -* Microsoft Teams -* Twilio (SMS) -* Vonage / Nexmo (SMS) -* iOS Apple Push Notifications -* Firebase Cloud Messaging (Android and more) +There are two types of delivery methods: +1. Individual Deliveries - one notification to each recipient. +2. Bulk Deliveries - one notification for all recipients. This is useful for sending a notification to your Slack team, for example. -And you can easily add new notification types for any other delivery methods. +Delivery methods we officially support: + +* [ActionCable](docs/delivery_methods/action_cable.md) +* [Apple Push Notification Service](docs/delivery_methods/ios.md) +* [Email](docs/delivery_methods/email.md) +* [Firebase Cloud Messaging](docs/delivery_methods/fcm.md) (iOS, Android, and web clients) +* [Microsoft Teams](docs/delivery_methods/microsoft_teams.md) +* [Slack](docs/delivery_methods/slack.md) +* [Twilio Messaging](docs/delivery_methods/twilio_messaging.md) - SMS, Whatsapp +* [Vonage SMS](docs/delivery_methods/vonage_sms.md) +* [Test](docs/delivery_methods/test.md) + +Bulk delivery methods we support: + +* [Discord](docs/bulk_delivery_methods/discord.md) +* [Slack](docs/bulk_delivery_methods/slack.md) +* [Webhook](docs/bulk_delivery_methods/webhook.md) ## 🎬 Screencast -
+ [Watch Screencast](https://www.youtube.com/watch?v=Scffi4otlFc) @@ -35,53 +43,64 @@ Run the following command to add Noticed to your Gemfile bundle add "noticed" ``` -To save notifications to your database, use the following command to generate a Notification model. +Add the migraitons -```ruby -rails generate noticed:model +```bash +rails noticed:install:migrations +rails db:migrate ``` -This will generate a Notification model and instructions for associating User models with the notifications table. - ## 📝 Usage To generate a notification object, simply run: -`rails generate noticed:notification CommentNotification` - -#### Sending Notifications +`rails generate noticed:notifier CommentNotifier` -To send a notification to a user: +#### Add Delivery Methods +Then add your delivery methods to the Notifier. ```ruby -# Instantiate a new notification -notification = CommentNotification.with(comment: @comment) +# app/notifiers/comment_notifier.rb +class CommentNotifier < Noticed::Event + bulk_deliver_by :webhook do |config| + config.url = "https://example.org..." + config.json = ->{ text: "New comment: #{record.body}" } + end + + deliver_by :email do |config| + config.mailer = "UserMailer" + config.method = :new_comment + end +end +``` -# Deliver notification in background job -notification.deliver_later(@comment.post.author) +#### Sending Notifications -# Deliver notification immediately -notification.deliver(@comment.post.author) +To send a notification to user(s): -# Deliver notification to multiple recipients -notification.deliver_later(User.all) +```ruby +# Instantiate a new notifier +CommentNotifier.with(record: @comment, foo: "bar").deliver_later(User.all) ``` -This will instantiate a new notification with the `comment` stored in the notification's params. +This instantiates a new `CommentNotifier` with params. Similar to ActiveJob, you can pass any params can be serialized. + +The `record:` param is a special param that gets assigned to the `record` polymorphic association in the database. + +This notification will be delivered to `User.all`. Delivering will create a Noticed::Event record and associated Noticed::Notification records for each recipient. -Each delivery method is able to transform this metadata that's best for the format. For example, the database may simply store the comment so it can be linked when rendering in the navbar. The websocket mechanism may transform this into a browser notification or insert it into the navbar. +After saving, a job will be enqueued for processing this notification and delivering it to all recipients. -#### Notification Objects +Each delivery method also spawns its own job. This allows you to skip email notifications if the user had already opened a push notification, for example. -Notifications inherit from `Noticed::Base`. This provides all their functionality and allows them to be delivered. +#### Notifier Objects -To add delivery methods, simply `include` the module for the delivery methods you would like to use. +Notifiers inherit from `Noticed::Event`. This provides all their functionality and allows them to be delivered. ```ruby -class CommentNotification < Noticed::Base - deliver_by :database +class CommentNotifier < Noticed::Event deliver_by :action_cable - deliver_by :email, mailer: 'CommentMailer', if: :email_notifications? + deliver_by :email, mailer: 'CommentMailer', if: ->(recipient) { !!recipient.preferences[:email] }:email_notifications? # I18n helpers def message @@ -93,27 +112,19 @@ class CommentNotification < Noticed::Base def url post_path(params[:post]) end - - def email_notifications? - !!recipient.preferences[:email] - end - - after_deliver do - # Anything you want - end end ``` **Shared Options** -* `if: :method_name` - Calls `method_name` and cancels delivery method if `false` is returned +* `if: :method_name` - Calls `method_name` and cancels delivery method if `false` is returned. This can also be specified as a lambda. * `unless: :method_name` - Calls `method_name` and cancels delivery method if `true` is returned * `delay: ActiveSupport::Duration` - Delays the delivery for the given duration of time * `delay: :method_name` - Calls `method_name` which should return an `ActiveSupport::Duration` and delays the delivery for the given duration of time ##### Helper Methods -You can define helper methods inside your Notification object to make it easier to render. +You can define helper methods inside your Notifier object to make it easier to render. ##### URL Helpers @@ -125,35 +136,6 @@ Don't forget, you'll need to configure `default_url_options` in order for Rails Rails.application.routes.default_url_options[:host] = 'localhost:3000' ``` -**Callbacks** - -Like ActiveRecord, notifications have several different types of callbacks. - -```ruby -class CommentNotification < Noticed::Base - deliver_by :database - deliver_by :email, mailer: 'CommentMailer' - - # Callbacks for the entire delivery - before_deliver :whatever - around_deliver :whatever - after_deliver :whatever - - # Callbacks for each delivery method - before_database :whatever - around_database :whatever - after_database :whatever - - before_email :whatever - around_email :whatever - after_email :whatever -end -``` - -When using `deliver_later` callbacks will be run around queuing the delivery method jobs (not inside the jobs as they actually execute). - -Defining custom delivery methods allows you to add callbacks that run inside the background job as each individual delivery is executed. See the Custom Delivery Methods section for more information. - ##### Translations We've added `translate` and `t` helpers like Rails has to provide an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under `notifications` and the underscored name of the notification class it is used in. @@ -173,30 +155,20 @@ You can use the `if:` and `unless: ` options on your delivery methods to check t For example: ```ruby -class CommentNotification < Noticed::Base - deliver_by :email, mailer: 'CommentMailer', if: :email_notifications? - - def email_notifications? - recipient.email_notifications? +class CommentNotifier < Noticed::Base + deliver_by :email do |config| + config.mailer = 'CommentMailer' + config.method = :new_comment + config.if = ->{ recipient.email_notifications? } end end ``` -## 🐞 Debugging - -In order to figure out what's up when you run in to errors, you can set the `debug` parameter to `true` in your notification, which will give you a more detailed error message about what went wrong. - -Example: - -```ruby -deliver_by :slack, debug: true -``` - ## ✅ Best Practices ### Creating a notification from an Active Record callback -A common use case is to trigger a notification when a record is created. For example, +Always use `after_commit` hooks to send notifications from ActiveRecord callbacks. For example, to send a notification automatically after a message is created: ```ruby class Message < ApplicationRecord @@ -207,12 +179,10 @@ class Message < ApplicationRecord private def notify_recipient - NewMessageNotification.with(message: self).deliver_later(recipient) + NewMessageNotifier.with(message: self).deliver_later(recipient) end ``` -If you are creating the notification on a background job (i.e. via `#deliver_later`), make sure you use a `commit` hook such as `after_create_commit` or `after_commit`. - Using `after_create` might cause the notification delivery methods to fail. This is because the job was enqueued while inside a database transaction, and the `Message` record might not yet be saved to the database. A common symptom of this problem is undelivered notifications and the following error in your logs. @@ -223,24 +193,24 @@ A common symptom of this problem is undelivered notifications and the following If you rename the class of a notification object your existing queries can break. This is because Noticed serializes the class name and sets it to the `type` column on the `Notification` record. -You can catch these errors at runtime by using `YourNotificationClassName.name` instead of hardcoding the string when performing a query. +You can catch these errors at runtime by using `YourNotifierClassName.name` instead of hardcoding the string when performing a query. ```ruby -Notification.where(type: YourNotificationClassName.name) # good -Notification.where(type: "YourNotificationClassName") # bad +Notification.where(type: YourNotifierClassName.name) # good +Notification.where(type: "YourNotifierClassName") # bad ``` When renaming a notification class you will need to backfill existing notifications to reference the new name. ```ruby -Notification.where(type: "OldNotificationClassName").update_all(type: NewNotificationClassName.name) +Notification.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name) ``` ## 🚛 Delivery Methods The delivery methods are designed to be modular so you can customize the way each type gets delivered. -For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notification and the delivery method will handle the processing of it. +For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notifier and the delivery method will handle the processing of it. * [Database](docs/delivery_methods/database.md) * [Email](docs/delivery_methods/email.md) @@ -258,7 +228,7 @@ For example, emails will require a subject, body, and email address while an SMS A common pattern is to deliver a notification via the database and then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the `delay` option, and the conditional `if` / `unless` option. ```ruby -class CommentNotification < Noticed::Base +class CommentNotifier< Noticed::Base deliver_by :database deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read? end @@ -269,7 +239,7 @@ Here a notification will be created immediately in the database (for display dir You can also configure multiple fallback options: ```ruby -class CriticalSystemNotification < Noticed::Base +class CriticalSystemNotifier < Noticed::Base deliver_by :database deliver_by :slack deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, if: :unread? @@ -307,7 +277,7 @@ end You can use the custom delivery method thus created by adding a `deliver_by` line with a unique name and `class` option in your notification class. ```ruby -class MyNotification < Noticed::Base +class MyNotifier < Noticed::Base deliver_by :discord, class: "DeliveryMethods::Discord" end ``` @@ -343,7 +313,7 @@ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base end end -class CommentNotification < Noticed::Base +class CommentNotifier < Noticed::Base deliver_by :discord, class: 'DeliveryMethods::Discord' end ``` @@ -353,7 +323,7 @@ Now it will raise an error because a required argument is missing. To fix the error, the argument has to be passed correctly. For example: ```ruby -class CommentNotification < Noticed::Base +class CommentNotifier < Noticed::Base deliver_by :discord, class: 'DeliveryMethods::Discord', username: User.admin.username end ``` @@ -399,10 +369,10 @@ user.notifications.mark_as_unread! #### Instance methods -Convert back into a Noticed notification object: +Convert back into a Noticed notifier object: ```ruby -@notification.to_notification +@notification.to_notifier ``` Mark notification as read / unread: @@ -445,11 +415,11 @@ class Post < ApplicationRecord end # Create a CommentNotification with a post param -CommentNotification.with(post: @post).deliver(user) +CommentNotifier.with(post: @post).deliver(user) # Lookup Notifications where params: {post: @post} @post.notifications_as_post -CommentNotification.with(parent: @post).deliver(user) +CommentNotifier.with(parent: @post).deliver(user) @post.notifications_as_parent ``` diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..3bfe6c0f --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,130 @@ +# Noticed Upgrade Guide + +Follow this guide to upgrade your Noticed implementation to the next version + +## Noticed 2.0 + +We've made some major changes to Noticed to simplify and support more delivery methods. + +### Models + +Instead of having models live in your application, Noticed v2 adds models managed by the gem. + +```bash +rails noticed:install:migrations +rails db:migrate +``` + +To migrate your data to the new tables, loop through your existing notifications and create new records for each one + +```ruby +Notification.find_each do |notification| + attributes = notification.attributes.slice("id", "type") + attributes[:type] = attributes[:type].sub("Notification", "Notifier")) + attributes[:params] = Noticed::Coder.load(notification.params) + attributes[:notifications_attributes] = [{recipient_type: notification.recipient_type, recipient_id: notification.recipient_id, seen_at: notification.read_at, read_at: notification.interacted_at}] + Noticed::Event.create!(attributes) +end +``` + +After migrating, you can drop the old notifications table and model. + +### Parent Class + +`Noticed::Base` has been deprecated in favor of `Noticed::Event`. This is an STI model that tracks all Notifier deliveries and recipients. + +```ruby +class CommentNotifier < Noticed::Event +end +``` + +### Database Delivery Method + +The database delivery is now baked into notifications. + +You will need to remove `deliver_by :database` from your notifiers. + +### Notifiers + +For clarity, we've renamed `app/notifications` to `app/notifiers`. + +Notifiers - the class that delivers notifications +Notification - the database record of the notification + +We recommend renaming your existing classes to match. You'll also need to update the `type` column on existing notifications when renaming. + +```ruby +Noticed::Notification.find_each do |notification| + notification.update(type: notification.type.sub("Notification", "Notifier")) +end +``` + +### Delivery Method Configuration + +Configuration for each delivery method can be contained within a block now. This improves organization for delivery method options by defining them in the block. +Procs/Lambdas will be evaluated when needed and symbols can be used to call a method. + +```ruby +class CommentNotifier < Noticed::Event + deliver_by :action_cable do |config| + config.channel = "NotificationChannel" + config.stream = ->{ recipient } + config.message = :to_websocket + end + + def to_websocket + { foo: :bar } + end +``` + +### Required Params + +`param` and `params` have been renamed to `required_param(s)` to be more clear. + +```ruby +class CommentNotifier < Noticed::Event + required_param :comment + required_params :account, :comment +end +``` + +### Has Noticed Notifications + +`has_noticed_notifications` has been removed in favor of the `record` polymorphic relationship that can be directly queried with ActiveRecord. You can add the necessary json query to your model(s) to restore the json query if needed. + +We recommend backfilling the `record` association if your notification params has a primary related record and switching to a has_many association instead. + +```ruby +class Comment < ApplicationRecord + has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event" +end +``` + +If you would like to keep the JSON querying, you can implement a method for querying your model depending on the database you use: + +```ruby +# Define the +param_name = "user" + +# PostgreSQL +model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json) + +# MySQL +model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json) + +# SQLite +model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json) + +# Other +model.where(params: {param_name.to_sym => self}) +``` + +### Receipient Notifications Association + +Recipients can be associated with notifications using the following. This is useful for displaying notifications in your UI. + +```ruby +class User < ApplicationRecord + has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification" +end +``` diff --git a/app/jobs/.keep b/app/jobs/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/jobs/noticed/application_job.rb b/app/jobs/noticed/application_job.rb new file mode 100644 index 00000000..4d94d406 --- /dev/null +++ b/app/jobs/noticed/application_job.rb @@ -0,0 +1,9 @@ +module Noticed + class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + discard_on ActiveJob::DeserializationError + end +end diff --git a/app/jobs/noticed/event_job.rb b/app/jobs/noticed/event_job.rb new file mode 100644 index 00000000..02ee7483 --- /dev/null +++ b/app/jobs/noticed/event_job.rb @@ -0,0 +1,19 @@ +module Noticed + class EventJob < ApplicationJob + queue_as :default + + def perform(event) + # Enqueue bulk deliveries + event.bulk_delivery_methods.each do |_, deliver_by| + deliver_by.perform_later(event) + end + + # Enqueue individual deliveries + event.notifications.each do |notification| + event.delivery_methods.each do |_, deliver_by| + deliver_by.perform_later(notification) + end + end + end + end +end diff --git a/app/models/.keep b/app/models/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/concerns/noticed/deliverable.rb b/app/models/concerns/noticed/deliverable.rb new file mode 100644 index 00000000..99c73e0c --- /dev/null +++ b/app/models/concerns/noticed/deliverable.rb @@ -0,0 +1,149 @@ +module Noticed + module Deliverable + extend ActiveSupport::Concern + + class DeliverBy + attr_reader :name, :config, :bulk + + def initialize(name, config, bulk: false) + @name, @config, @bulk, = name, config, bulk + end + + def constant + namespace = bulk ? "Noticed::BulkDeliveryMethods" : "Noticed::DeliveryMethods" + config.fetch(:class, [namespace, name.to_s.camelize].join("::")).constantize + end + + def validate! + constant.required_option_names.each do |option| + raise ValidationError, "option `#{option}` must be set for `deliver_by :#{name}`" unless config[option].present? + end + end + + def perform_later(event_or_notification, options = {}) + options[:wait] = evaluate_option(:wait, event_or_notification) if config.has_key?(:wait) + options[:wait_until] = evaluate_option(:wait_until, event_or_notification) if config.has_key?(:wait_until) + options[:queue] = evaluate_option(:queue, event_or_notification) if config.has_key?(:queue) + options[:priority] = evaluate_option(:priority, event_or_notification) if config.has_key?(:priority) + + constant.set(options).perform_later(name, event_or_notification) + end + + def evaluate_option(name, context) + option = config[name] + + if option&.respond_to?(:call) + context.instance_exec(&option) + elsif option.is_a?(Symbol) && context.respond_to?(option) + context.send(option) + else + option + end + end + end + + included do + class_attribute :bulk_delivery_methods, instance_writer: false, default: {} + class_attribute :delivery_methods, instance_writer: false, default: {} + class_attribute :required_param_names, instance_writer: false, default: [] + + attribute :params, default: {} + + if Rails.gem_version >= Gem::Version.new("7.1.0.alpha") + serialize :params, coder: Coder + else + serialize :params, Coder + end + end + + class_methods do + def inherited(base) + base.bulk_delivery_methods = bulk_delivery_methods.dup + base.delivery_methods = delivery_methods.dup + base.required_param_names = required_param_names.dup + super + end + + def bulk_deliver_by(name, options = {}) + raise NameError, "#{name} has already been used for this Notifier." if bulk_delivery_methods.has_key?(name) + + config = ActiveSupport::OrderedOptions.new.merge(options) + yield config if block_given? + bulk_delivery_methods[name] = DeliverBy.new(name, config, bulk: true) + end + + def deliver_by(name, options = {}) + raise NameError, "#{name} has already been used for this Notifier." if delivery_methods.has_key?(name) + + config = ActiveSupport::OrderedOptions.new.merge(options) + yield config if block_given? + delivery_methods[name] = DeliverBy.new(name, config) + end + + def required_params(*names) + required_param_names.concat names + end + alias_method :required_param, :required_params + + def with(params) + record = params.delete(:record) + new(params: params, record: record) + end + + def deliver(recipients = nil) + new.deliver(recipients) + end + end + + def deliver(recipients = nil) + validate! + + transaction do + save! + + recipients_attributes = Array.wrap(recipients).map do |recipient| + recipient_attributes_for(recipient) + end + + if Rails.gem_version >= Gem::Version.new("7.0.0.alpha1") + notifications.insert_all!(recipients_attributes, record_timestamps: true) if recipients_attributes.any? + else + time = Time.current + recipients_attributes.each do |attributes| + attributes[:created_at] = time + attributes[:updated_at] = time + end + notifications.insert_all!(recipients_attributes) if recipients_attributes.any? + end + end + + # Enqueue delivery job + EventJob.perform_later(self) + + self + end + + def recipient_attributes_for(recipient) + { + recipient_type: recipient.class.name, + recipient_id: recipient.id + } + end + + def validate! + validate_params! + validate_delivery_methods! + end + + def validate_params! + required_param_names.each do |param_name| + raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params[param_name].present? + end + end + + def validate_delivery_methods! + bulk_delivery_methods.values.each(&:validate!) + delivery_methods.values.each(&:validate!) + end + end +end diff --git a/app/models/concerns/noticed/readable.rb b/app/models/concerns/noticed/readable.rb new file mode 100644 index 00000000..ef2144a3 --- /dev/null +++ b/app/models/concerns/noticed/readable.rb @@ -0,0 +1,62 @@ +module Noticed + module Readable + extend ActiveSupport::Concern + + included do + scope :read, -> { where.not(read_at: nil) } + scope :unread, -> { where(read_at: nil) } + scope :seen, -> { where.not(seen_at: nil) } + scope :unseen, -> { where(seen_at: nil) } + end + + class_methods do + def mark_as_read + update_all(read_at: Time.current) + end + + def mark_as_unread + update_all(read_at: nil) + end + + def mark_as_seen + update_all(seen_at: Time.current) + end + + def mark_as_unseen + update_all(seen_at: nil) + end + end + + def mark_as_read + update(read_at: Time.current) + end + + def mark_as_unread + update(read_at: nil) + end + + def mark_as_seen + update(seen_at: Time.current) + end + + def mark_as_unseen + update(seen_at: nil) + end + + def read? + read_at? + end + + def unread? + !read_at? + end + + def seen? + seen_at? + end + + def unseen? + !seen_at? + end + end +end diff --git a/app/models/noticed/application_record.rb b/app/models/noticed/application_record.rb new file mode 100644 index 00000000..9cd3e459 --- /dev/null +++ b/app/models/noticed/application_record.rb @@ -0,0 +1,6 @@ +module Noticed + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + self.table_name_prefix = "noticed_" + end +end diff --git a/app/models/noticed/event.rb b/app/models/noticed/event.rb new file mode 100644 index 00000000..51d2bc08 --- /dev/null +++ b/app/models/noticed/event.rb @@ -0,0 +1,14 @@ +module Noticed + class Event < ApplicationRecord + include Deliverable + include Translation + include Rails.application.routes.url_helpers + + belongs_to :record, polymorphic: true, optional: true + has_many :notifications, dependent: :delete_all + + accepts_nested_attributes_for :notifications + + scope :newest_first, -> { order(created_at: :desc) } + end +end diff --git a/app/models/noticed/notification.rb b/app/models/noticed/notification.rb new file mode 100644 index 00000000..36d343b8 --- /dev/null +++ b/app/models/noticed/notification.rb @@ -0,0 +1,16 @@ +module Noticed + class Notification < ApplicationRecord + include Rails.application.routes.url_helpers + include Readable + include Translation + + belongs_to :event + belongs_to :recipient, polymorphic: true + + scope :newest_first, -> { order(created_at: :desc) } + + delegate :params, :record, to: :event + + attribute :params, default: {} + end +end diff --git a/app/views/.keep b/app/views/.keep new file mode 100644 index 00000000..e69de29b diff --git a/bin/rails b/bin/rails index de173dd1..c263e360 100755 --- a/bin/rails +++ b/bin/rails @@ -2,12 +2,13 @@ # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. -ENGINE_ROOT = File.expand_path('..', __dir__) -APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) +ENGINE_ROOT = File.expand_path("..", __dir__) +ENGINE_PATH = File.expand_path("../lib/noticed/engine", __dir__) +APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) -require 'rails/all' -require 'rails/engine/commands' +require "rails/all" +require "rails/engine/commands" diff --git a/db/migrate/20231215190233_create_noticed_tables.rb b/db/migrate/20231215190233_create_noticed_tables.rb new file mode 100644 index 00000000..9b966f48 --- /dev/null +++ b/db/migrate/20231215190233_create_noticed_tables.rb @@ -0,0 +1,24 @@ +class CreateNoticedTables < ActiveRecord::Migration[6.1] + def change + create_table :noticed_events do |t| + t.string :type + t.belongs_to :record, polymorphic: true + if t.respond_to?(:jsonb) + t.jsonb :params + else + t.json :params + end + + t.timestamps + end + + create_table :noticed_notifications do |t| + t.belongs_to :event, null: false + t.belongs_to :recipient, polymorphic: true, null: false + t.datetime :read_at + t.datetime :seen_at + + t.timestamps + end + end +end diff --git a/docs/actioncable.md b/docs/actioncable.md deleted file mode 100644 index b7b7616d..00000000 --- a/docs/actioncable.md +++ /dev/null @@ -1,60 +0,0 @@ -# ActionCable Notifications - -ActionCable notifications in noticed are broadcast to the Noticed::NotificationChannel. - -By default, we simply send over the `params` as JSON and subscribe to the `current_user` stream. - -This requires `identified_by :current_user` in your ApplicationCable::Connection. For example, using Devise for authentication: - -```ruby -module ApplicationCable - class Connection < ActionCable::Connection::Base - identified_by :current_user - - def connect - self.current_user = find_verified_user - logger.add_tags "ActionCable", "User #{current_user.id}" - end - - protected - - def find_verified_user - if current_user = env['warden'].user - current_user - else - reject_unauthorized_connection - end - end - end -end -``` - -## Subscribing to the Noticed::NotificationChannel with Javascript - -To receive Noticed notifications client-side, you'll need to subscribe to the Noticed::NotificationChannel. - -```javascript -// app/javascript/channels/notification_channel.js - -import consumer from "./consumer" - -consumer.subscriptions.create("Noticed::NotificationChannel", { - connected() { - // Called when the subscription is ready for use on the server - }, - - disconnected() { - // Called when the subscription has been terminated by the server - }, - - received(data) { - // Called when there's incoming data on the websocket for this channel - console.log(data) - } -}); -``` - -## References - -ActionCable Delivery Method: https://github.com/excid3/noticed/blob/master/lib/noticed/delivery_methods/action_cable.rb -NotificationsChannel: https://github.com/excid3/noticed/blob/master/lib/noticed/notification_channel.rb diff --git a/docs/bulk_delivery_methods/discord.md b/docs/bulk_delivery_methods/discord.md new file mode 100644 index 00000000..7a9207e8 --- /dev/null +++ b/docs/bulk_delivery_methods/discord.md @@ -0,0 +1,20 @@ +# Discord Bulk Delivery Method + +Send a Discord message to builk notify users in a channel. + +We recommend using [Discohook](https://discohook.org) to design your messages. + +## Usage + +```ruby +class CommentNotification + bulk_deliver_by :discord do |config| + config.url = "https://discord.com..." + config.json = -> { + { + # ... + } + } + end +end +``` diff --git a/docs/bulk_delivery_methods/slack.md b/docs/bulk_delivery_methods/slack.md new file mode 100644 index 00000000..4fb1fdbe --- /dev/null +++ b/docs/bulk_delivery_methods/slack.md @@ -0,0 +1,18 @@ +# Slack Bulk Delivery Method + +Send a Slack message to builk notify users in a channel. + +## Usage + +```ruby +class CommentNotification + deliver_by :slackdo |config| + config.url = "https://slack.com..." + config.json = -> { + { + # ... + } + } + end +end +``` diff --git a/docs/bulk_delivery_methods/webhook.md b/docs/bulk_delivery_methods/webhook.md new file mode 100644 index 00000000..258054b1 --- /dev/null +++ b/docs/bulk_delivery_methods/webhook.md @@ -0,0 +1,18 @@ +# Webhook Bulk Delivery Method + +Send a webhook request to builk notify users in a channel. + +## Usage + +```ruby +class CommentNotification + deliver_by :webhook do |config| + config.url = "https://example.org..." + config.json = -> { + { + # ... + } + } + end +end +``` diff --git a/docs/delivery_methods/action_cable.md b/docs/delivery_methods/action_cable.md index 732658dd..a401f1c5 100644 --- a/docs/delivery_methods/action_cable.md +++ b/docs/delivery_methods/action_cable.md @@ -1,10 +1,16 @@ -### ActionCable Delivery Method +# ActionCable Delivery Method Sends a notification to the browser via websockets (ActionCable channel by default). -`deliver_by :action_cable` +```ruby +deliver_by :action_cable do |config| + config.channel = "NotificationsChannel" + config.stream = :custom_stream + config.message = ->{ params.merge( user_id: recipient.id) } +end +``` -##### Options +## Options * `format: :format_for_action_cable` - *Optional* @@ -22,12 +28,43 @@ Sends a notification to the browser via websockets (ActionCable channel by defau Defaults to `recipient` +## Authentication + +To send notifications to individual users, you'll want to use `stream_for current_user` + ```ruby -deliver_by :action_cable, channel: MyChannel, stream: :custom_stream, format: :action_cable_data -def custom_stream - "user_#{recipient.id}" +class NotificationChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end + + def unsubscribed + stop_all_streams + end end -def action_cable_data - { user_id: recipient.id } +``` + +This requires `identified_by :current_user` in your ApplicationCable::Connection. For example, using Devise for authentication: + +```ruby +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + logger.add_tags "ActionCable", "User #{current_user.id}" + end + + protected + + def find_verified_user + if current_user = env['warden'].user + current_user + else + reject_unauthorized_connection + end + end + end end ``` diff --git a/docs/delivery_methods/database.md b/docs/delivery_methods/database.md deleted file mode 100644 index f20d6df5..00000000 --- a/docs/delivery_methods/database.md +++ /dev/null @@ -1,19 +0,0 @@ -### Database Delivery Method - -Writes notification to the database. - -`deliver_by :database` - -**Note:** Database notifications are special in that they will run before the other delivery methods. We do this so you can reference the database record ID in other delivery methods. For that same reason, the delivery can't be delayed (via the `delay` option) or an error will be raised. - -##### Options - -* `association` - *Optional* - - The name of the database association to use. Defaults to `:notifications` - -* `format: :format_for_database` - *Optional* - - Use a custom method to define the attributes saved to the database - - diff --git a/docs/delivery_methods/discord.md b/docs/delivery_methods/discord.md new file mode 100644 index 00000000..69cdd053 --- /dev/null +++ b/docs/delivery_methods/discord.md @@ -0,0 +1,20 @@ +# Discord Bulk Delivery Method + +Send Discord messages to builk notify users in a channel. + +We recommend using [Discohook](https://discohook.org) to design your messages. + +## Usage + +```ruby +class CommentNotification + deliver_by :discord do |config| + config.url = "https://discord.com..." + config.json = -> { + { + # ... + } + } + end +end +``` diff --git a/docs/delivery_methods/email.md b/docs/delivery_methods/email.md index 7bbce97e..90001647 100644 --- a/docs/delivery_methods/email.md +++ b/docs/delivery_methods/email.md @@ -1,8 +1,19 @@ ### Email Delivery Method -Sends an email notification. Emails will always be sent with `deliver_later` +Sends an email to each recipient. -`deliver_by :email, mailer: "UserMailer"` +```ruby +deliver_by :email do |config| + config.mailer = "UserMailer" + config.method = :receipt + config.params = ->{ params } + config.args = ->{ [1, 2, 3] } + + # Enqueues a separate job for sending the email using deliver_later. + # Deliveries already happen in jobs so this is typically unnecessary. + # config.enqueue = false +end +``` ##### Options @@ -14,10 +25,12 @@ Sends an email notification. Emails will always be sent with `deliver_later` Used to customize the method on the mailer that is called -- `format: :format_for_email` - _Optional_ +- `params` - _Optional_ Use a custom method to define the params sent to the mailer. `recipient` will be merged into the params. +- `args` - _Optional_ + - `enqueue: false` - _Optional_ Use `deliver_later` to queue email delivery with ActiveJob. This is `false` by default as each delivery method is already a separate job. diff --git a/docs/delivery_methods/fcm.md b/docs/delivery_methods/fcm.md index f17fd97c..759a1bdd 100644 --- a/docs/delivery_methods/fcm.md +++ b/docs/delivery_methods/fcm.md @@ -1,6 +1,6 @@ # Firebase Cloud Messaging Delivery Method -Send Android Device Notifications using the Google Firebase Cloud Messaging service and the `googleauth` gem. +Send Device Notifications using the Google Firebase Cloud Messaging service and the `googleauth` gem. FCM supports Android, iOS, and web clients. ```bash bundle add "googleauth" @@ -24,19 +24,15 @@ See the below instructions on where to store this information within your applic ```ruby class CommentNotification - deliver_by :fcm, credentials: :fcm_credentials, format: :format_notification - - # This needs to return the path to your FCM credentials - def fcm_credentials - Rails.root.join("config/certs/fcm.json") - end - - def format_notification(device_token) - { - token: device_token, - notification: { - title: "Test Title", - body: "Test body" + deliver_by :fcm do |config| + config.credentials = Rails.root.join("config/certs/fcm.json") + config.json = ->(device_token) { + { + token: device_token, + notification: { + title: "Test Title", + body: "Test body" + } } } end @@ -45,10 +41,10 @@ end ## Options -* `format: :format_notification` - Customize the Firebase Cloud Messaging notification object +* `json` + Customize the Firebase Cloud Messaging notification object. This can be a Lambda or Symbol of a method name on the notifier. The callable object will be given the device token as an argument. -* `credentials: :fcm_credentials` +* `credentials` The location of your Firebase Cloud Messaging credentials. - When a String object: `deliver_by :fcm, credentials: "config/credentials/fcm.json"` * Interally, this string is passed to `Rails.root.join()` as an argument so there is no need to do this beforehand. diff --git a/docs/delivery_methods/twilio.md b/docs/delivery_methods/twilio_messaging.md similarity index 89% rename from docs/delivery_methods/twilio.md rename to docs/delivery_methods/twilio_messaging.md index 30292c8c..e1eb045e 100644 --- a/docs/delivery_methods/twilio.md +++ b/docs/delivery_methods/twilio_messaging.md @@ -1,8 +1,8 @@ -### Twilio SMS Delivery Method +### Twilio Messaaging Delivery Method -Sends an SMS notification via Twilio. +Sends an SMS or Whatsapp message via Twilio Messaging. -`deliver_by :twilio` +`deliver_by :twilio_messaging` ##### Options diff --git a/docs/delivery_methods/vonage.md b/docs/delivery_methods/vonage.md index 0cbf4d14..d56f5943 100644 --- a/docs/delivery_methods/vonage.md +++ b/docs/delivery_methods/vonage.md @@ -2,7 +2,7 @@ Sends an SMS notification via Vonage / Nexmo. -`deliver_by :vonage` +`deliver_by :vonage_sms` ##### Options diff --git a/docs/delivery_methods/vonage_sms.md b/docs/delivery_methods/vonage_sms.md new file mode 100644 index 00000000..0cbf4d14 --- /dev/null +++ b/docs/delivery_methods/vonage_sms.md @@ -0,0 +1,28 @@ +### Vonage SMS + +Sends an SMS notification via Vonage / Nexmo. + +`deliver_by :vonage` + +##### Options + +* `credentials: :get_credentials` - *Optional* + + Use a custom method for retrieving credentials. Method should return a Hash with `:api_key` and `:api_secret` keys. + + Defaults to `Rails.application.credentials.vonage[:api_key]` and `Rails.application.credentials.vonage[:api_secret]` + +* `deliver_by :vonage, format: :format_for_vonage` - *Optional* + + Use a custom method to generate the params sent to Vonage. Method should return a Hash. Defaults to: + + ```ruby + { + api_key: vonage_credentials[:api_key], + api_secret: vonage_credentials[:api_secret], + from: notification.params[:from], + text: notification.params[:body], + to: notification.params[:to], + type: "unicode" + } + ``` diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile deleted file mode 100644 index 459eb9a1..00000000 --- a/gemfiles/rails_5_2.gemfile +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "standard" -gem "webmock" -gem "pg" -gem "mysql2" -gem "sqlite3", "~> 1.6.0.rc2" -gem "byebug", group: [:development, :test] -gem "appraisal" -gem "net-smtp" -gem "apnotic", "~> 1.7" -gem "googleauth", "~> 1.1" -gem "rails", "~> 5.2.0" - -gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile.lock b/gemfiles/rails_5_2.gemfile.lock deleted file mode 100644 index e21f12f6..00000000 --- a/gemfiles/rails_5_2.gemfile.lock +++ /dev/null @@ -1,261 +0,0 @@ -PATH - remote: .. - specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) - globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - apnotic (1.7.1) - connection_pool (~> 2) - net-http2 (>= 0.18.3, < 2) - appraisal (2.5.0) - bundler - rake - thor (>= 0.14.0) - arel (9.0.0) - ast (2.4.2) - base64 (0.1.1) - builder (3.2.4) - byebug (11.1.3) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) - crack (0.4.5) - rexml - crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake - globalid (1.1.0) - activesupport (>= 5.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - hashdiff (1.0.1) - http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - json (2.6.3) - jwt (2.7.1) - language_server-protocol (3.17.0.3) - lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.2) - method_source (1.0.0) - mini_mime (1.1.5) - minitest (5.20.0) - multi_json (1.15.0) - mysql2 (0.5.5) - net-http2 (0.18.5) - http-2 (~> 0.11) - net-imap (0.4.1) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.1) - timeout - net-smtp (0.4.0) - net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) - racc (~> 1.4) - os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) - ast (~> 2.4.1) - racc - pg (1.5.4) - public_suffix (5.0.3) - racc (1.7.1) - rack (2.2.8) - rack-test (2.1.0) - rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) - method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.2) - rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.2.2.3) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - signet (0.18.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.56.4) - standard-custom (~> 1.0.0) - standard-performance (~> 1.2) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.2.1) - lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - thor (1.2.2) - thread_safe (0.3.6) - timeout (0.4.0) - tzinfo (1.2.11) - thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.5.0) - webmock (3.19.1) - addressable (>= 2.8.0) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - -PLATFORMS - arm64-darwin-22 - x86_64-darwin-22 - x86_64-linux - -DEPENDENCIES - apnotic (~> 1.7) - appraisal - byebug - googleauth (~> 1.1) - mysql2 - net-smtp - noticed! - pg - rails (~> 5.2.0) - sqlite3 (~> 1.6.0.rc2) - standard - webmock - -BUNDLED WITH - 2.4.20 diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile deleted file mode 100644 index af45cbfc..00000000 --- a/gemfiles/rails_6.gemfile +++ /dev/null @@ -1,17 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "standard" -gem "webmock" -gem "pg" -gem "mysql2" -gem "sqlite3", "~> 1.6.0.rc2" -gem "byebug", group: [:development, :test] -gem "appraisal" -gem "net-smtp" -gem "apnotic", "~> 1.7" -gem "googleauth", "~> 1.1" -gem "rails", "~> 6.0.0" - -gemspec path: "../" diff --git a/gemfiles/rails_6.gemfile.lock b/gemfiles/rails_6.gemfile.lock deleted file mode 100644 index 3e194cc7..00000000 --- a/gemfiles/rails_6.gemfile.lock +++ /dev/null @@ -1,277 +0,0 @@ -PATH - remote: .. - specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.0.6.1) - actionpack (= 6.0.6.1) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.0.6.1) - actionpack (= 6.0.6.1) - activejob (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) - mail (>= 2.7.1) - actionmailer (6.0.6.1) - actionpack (= 6.0.6.1) - actionview (= 6.0.6.1) - activejob (= 6.0.6.1) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.0.6.1) - actionview (= 6.0.6.1) - activesupport (= 6.0.6.1) - rack (~> 2.0, >= 2.0.8) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.6.1) - actionpack (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) - nokogiri (>= 1.8.5) - actionview (6.0.6.1) - activesupport (= 6.0.6.1) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.6.1) - activesupport (= 6.0.6.1) - globalid (>= 0.3.6) - activemodel (6.0.6.1) - activesupport (= 6.0.6.1) - activerecord (6.0.6.1) - activemodel (= 6.0.6.1) - activesupport (= 6.0.6.1) - activestorage (6.0.6.1) - actionpack (= 6.0.6.1) - activejob (= 6.0.6.1) - activerecord (= 6.0.6.1) - marcel (~> 1.0) - activesupport (6.0.6.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - apnotic (1.7.1) - connection_pool (~> 2) - net-http2 (>= 0.18.3, < 2) - appraisal (2.5.0) - bundler - rake - thor (>= 0.14.0) - ast (2.4.2) - base64 (0.1.1) - builder (3.2.4) - byebug (11.1.3) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) - crack (0.4.5) - rexml - crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake - globalid (1.1.0) - activesupport (>= 5.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - hashdiff (1.0.1) - http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) - i18n (1.14.1) - concurrent-ruby (~> 1.0) - json (2.6.3) - jwt (2.7.1) - language_server-protocol (3.17.0.3) - lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) - crass (~> 1.0.2) - nokogiri (>= 1.12.0) - mail (2.8.1) - mini_mime (>= 0.1.1) - net-imap - net-pop - net-smtp - marcel (1.0.2) - method_source (1.0.0) - mini_mime (1.1.5) - minitest (5.20.0) - multi_json (1.15.0) - mysql2 (0.5.5) - net-http2 (0.18.5) - http-2 (~> 0.11) - net-imap (0.4.1) - date - net-protocol - net-pop (0.1.2) - net-protocol - net-protocol (0.2.1) - timeout - net-smtp (0.4.0) - net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) - racc (~> 1.4) - os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) - ast (~> 2.4.1) - racc - pg (1.5.4) - public_suffix (5.0.3) - racc (1.7.1) - rack (2.2.8) - rack-test (2.1.0) - rack (>= 1.3) - rails (6.0.6.1) - actioncable (= 6.0.6.1) - actionmailbox (= 6.0.6.1) - actionmailer (= 6.0.6.1) - actionpack (= 6.0.6.1) - actiontext (= 6.0.6.1) - actionview (= 6.0.6.1) - activejob (= 6.0.6.1) - activemodel (= 6.0.6.1) - activerecord (= 6.0.6.1) - activestorage (= 6.0.6.1) - activesupport (= 6.0.6.1) - bundler (>= 1.3.0) - railties (= 6.0.6.1) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.2.0) - activesupport (>= 5.0.0) - minitest - nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) - railties (6.0.6.1) - actionpack (= 6.0.6.1) - activesupport (= 6.0.6.1) - method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.2) - rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) - json (~> 2.3) - language_server-protocol (>= 3.17.0) - parallel (~> 1.10) - parser (>= 3.2.2.3) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - signet (0.18.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.56.4) - standard-custom (~> 1.0.0) - standard-performance (~> 1.2) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.2.1) - lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - thor (1.2.2) - thread_safe (0.3.6) - timeout (0.4.0) - tzinfo (1.2.11) - thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.5.0) - webmock (3.19.1) - addressable (>= 2.8.0) - crack (>= 0.3.2) - hashdiff (>= 0.4.0, < 2.0.0) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.6.12) - -PLATFORMS - arm64-darwin-22 - x86_64-darwin-22 - x86_64-linux - -DEPENDENCIES - apnotic (~> 1.7) - appraisal - byebug - googleauth (~> 1.1) - mysql2 - net-smtp - noticed! - pg - rails (~> 6.0.0) - sqlite3 (~> 1.6.0.rc2) - standard - webmock - -BUNDLED WITH - 2.4.20 diff --git a/gemfiles/rails_6_1.gemfile.lock b/gemfiles/rails_6_1.gemfile.lock index 72fb5045..8d4ea682 100644 --- a/gemfiles/rails_6_1.gemfile.lock +++ b/gemfiles/rails_6_1.gemfile.lock @@ -1,9 +1,8 @@ PATH remote: .. specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) + noticed (2.0.0) + rails (>= 6.1.0) GEM remote: https://rubygems.org/ @@ -67,7 +66,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) apnotic (1.7.1) connection_pool (~> 2) @@ -77,7 +76,6 @@ GEM rake thor (>= 0.14.0) ast (2.4.2) - base64 (0.1.1) builder (3.2.4) byebug (11.1.3) concurrent-ruby (1.2.2) @@ -85,47 +83,32 @@ GEM crack (0.4.5) rexml crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + date (3.3.4) erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) + hashdiff (1.1.0) http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - json (2.6.3) + json (2.7.1) jwt (2.7.1) language_server-protocol (3.17.0.3) lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -136,31 +119,38 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.5) - minitest (5.20.0) + mini_portile2 (2.8.5) + minitest (5.21.1) multi_json (1.15.0) mysql2 (0.5.5) + net-http (0.4.1) + uri net-http2 (0.18.5) http-2 (~> 0.11) - net-imap (0.4.1) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) + nio4r (2.7.0) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc pg (1.5.4) - public_suffix (5.0.3) - racc (1.7.1) + public_suffix (5.0.4) + racc (1.7.3) rack (2.2.8) rack-test (2.1.0) rack (>= 1.3) @@ -193,28 +183,26 @@ GEM rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.2) + rake (13.1.0) + regexp_parser (2.9.0) rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -227,27 +215,26 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) + standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.56.4) + rubocop (~> 1.59.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.2) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.2.1) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - thor (1.2.2) - timeout (0.4.0) + rubocop-performance (~> 1.20.2) + thor (1.3.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.5.0) + uri (0.13.0) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -260,6 +247,7 @@ GEM PLATFORMS arm64-darwin-22 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -277,4 +265,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.20 + 2.5.3 diff --git a/gemfiles/rails_7.gemfile.lock b/gemfiles/rails_7.gemfile.lock index 5c214b8f..896acbdb 100644 --- a/gemfiles/rails_7.gemfile.lock +++ b/gemfiles/rails_7.gemfile.lock @@ -1,9 +1,8 @@ PATH remote: .. specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) + noticed (2.0.0) + rails (>= 6.1.0) GEM remote: https://rubygems.org/ @@ -73,7 +72,7 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) apnotic (1.7.1) connection_pool (~> 2) @@ -83,7 +82,6 @@ GEM rake thor (>= 0.14.0) ast (2.4.2) - base64 (0.1.1) builder (3.2.4) byebug (11.1.3) concurrent-ruby (1.2.2) @@ -91,47 +89,32 @@ GEM crack (0.4.5) rexml crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + date (3.3.4) erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) + hashdiff (1.1.0) http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - json (2.6.3) + json (2.7.1) jwt (2.7.1) language_server-protocol (3.17.0.3) lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -142,31 +125,38 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.5) - minitest (5.20.0) + mini_portile2 (2.8.5) + minitest (5.21.1) multi_json (1.15.0) mysql2 (0.5.5) + net-http (0.4.1) + uri net-http2 (0.18.5) http-2 (~> 0.11) - net-imap (0.4.1) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) + nio4r (2.7.0) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc pg (1.5.4) - public_suffix (5.0.3) - racc (1.7.1) + public_suffix (5.0.4) + racc (1.7.3) rack (2.2.8) rack-test (2.1.0) rack (>= 1.3) @@ -199,54 +189,51 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.8.2) + rake (13.1.0) + regexp_parser (2.9.0) rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) + standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.56.4) + rubocop (~> 1.59.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.2) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.2.1) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - thor (1.2.2) - timeout (0.4.0) + rubocop-performance (~> 1.20.2) + thor (1.3.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.5.0) + uri (0.13.0) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -259,6 +246,7 @@ GEM PLATFORMS arm64-darwin-22 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -276,4 +264,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.20 + 2.5.3 diff --git a/gemfiles/rails_7_1.gemfile.lock b/gemfiles/rails_7_1.gemfile.lock index cec75ddc..9c112f6f 100644 --- a/gemfiles/rails_7_1.gemfile.lock +++ b/gemfiles/rails_7_1.gemfile.lock @@ -1,77 +1,77 @@ PATH remote: .. specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) + noticed (2.0.0) + rails (>= 6.1.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.1) - actionpack (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activesupport (= 7.1.1) + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.1) - actionview (= 7.1.1) - activesupport (= 7.1.1) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) nokogiri (>= 1.8.5) + racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.1) - actionpack (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.1) - activesupport (= 7.1.1) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.1) - activesupport (= 7.1.1) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (7.1.1) - activesupport (= 7.1.1) - activerecord (7.1.1) - activemodel (= 7.1.1) - activesupport (= 7.1.1) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) timeout (>= 0.4.0) - activestorage (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activesupport (= 7.1.1) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) marcel (~> 1.0) - activesupport (7.1.1) + activesupport (7.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -81,7 +81,7 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) apnotic (1.7.1) connection_pool (~> 2) @@ -91,8 +91,8 @@ GEM rake thor (>= 0.14.0) ast (2.4.2) - base64 (0.1.1) - bigdecimal (3.1.4) + base64 (0.2.0) + bigdecimal (3.1.5) builder (3.2.4) byebug (11.1.3) concurrent-ruby (1.2.2) @@ -100,53 +100,38 @@ GEM crack (0.4.5) rexml crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - drb (2.1.1) + date (3.3.4) + drb (2.2.0) ruby2_keywords erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) + hashdiff (1.1.0) http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.8.1) + io-console (0.7.1) + irb (1.11.1) rdoc - reline (>= 0.3.8) - json (2.6.3) + reline (>= 0.4.2) + json (2.7.1) jwt (2.7.1) language_server-protocol (3.17.0.3) lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -156,34 +141,41 @@ GEM net-smtp marcel (1.0.2) mini_mime (1.1.5) - minitest (5.20.0) + mini_portile2 (2.8.5) + minitest (5.21.1) multi_json (1.15.0) - mutex_m (0.1.2) + mutex_m (0.2.0) mysql2 (0.5.5) + net-http (0.4.1) + uri net-http2 (0.18.5) http-2 (~> 0.11) - net-imap (0.4.1) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) + nio4r (2.7.0) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc pg (1.5.4) - psych (5.1.1) + psych (5.1.2) stringio - public_suffix (5.0.3) - racc (1.7.1) + public_suffix (5.0.4) + racc (1.7.3) rack (3.0.8) rack-session (2.0.0) rack (>= 3.0.0) @@ -192,20 +184,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.1.1) - actioncable (= 7.1.1) - actionmailbox (= 7.1.1) - actionmailer (= 7.1.1) - actionpack (= 7.1.1) - actiontext (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activemodel (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + rails (7.1.2) + actioncable (= 7.1.2) + actionmailbox (= 7.1.2) + actionmailer (= 7.1.2) + actionpack (= 7.1.2) + actiontext (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activemodel (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) bundler (>= 1.15.0) - railties (= 7.1.1) + railties (= 7.1.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -213,39 +205,38 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) irb rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - rdoc (6.5.0) + rake (13.1.0) + rdoc (6.6.2) psych (>= 4.0.0) - regexp_parser (2.8.2) - reline (0.3.9) + regexp_parser (2.9.0) + reline (0.4.2) io-console (~> 0.5) rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) signet (0.18.0) @@ -253,28 +244,27 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) + standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.56.4) + rubocop (~> 1.59.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.2) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.2.1) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - stringio (3.0.8) - thor (1.2.2) - timeout (0.4.0) + rubocop-performance (~> 1.20.2) + stringio (3.1.0) + thor (1.3.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.5.0) + uri (0.13.0) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -288,6 +278,7 @@ GEM PLATFORMS arm64-darwin-22 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -305,4 +296,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.20 + 2.5.3 diff --git a/gemfiles/rails_main.gemfile.lock b/gemfiles/rails_main.gemfile.lock index 6a497688..30355228 100644 --- a/gemfiles/rails_main.gemfile.lock +++ b/gemfiles/rails_main.gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/rails/rails.git - revision: 90a6f3663b937cf90a7f642a10dfb36f8710ce6d + revision: fc7befc87af05796612d03531b21c691699aeb74 branch: main specs: actioncable (7.2.0.alpha) @@ -15,29 +15,25 @@ GIT activerecord (= 7.2.0.alpha) activestorage (= 7.2.0.alpha) activesupport (= 7.2.0.alpha) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp + mail (>= 2.8.0) actionmailer (7.2.0.alpha) actionpack (= 7.2.0.alpha) actionview (= 7.2.0.alpha) activejob (= 7.2.0.alpha) activesupport (= 7.2.0.alpha) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + mail (>= 2.8.0) rails-dom-testing (~> 2.2) actionpack (7.2.0.alpha) actionview (= 7.2.0.alpha) activesupport (= 7.2.0.alpha) nokogiri (>= 1.8.5) + racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) actiontext (7.2.0.alpha) actionpack (= 7.2.0.alpha) activerecord (= 7.2.0.alpha) @@ -74,8 +70,7 @@ GIT drb i18n (>= 1.6, < 2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + tzinfo (~> 2.0, >= 2.0.5) rails (7.2.0.alpha) actioncable (= 7.2.0.alpha) actionmailbox (= 7.2.0.alpha) @@ -102,14 +97,13 @@ GIT PATH remote: .. specs: - noticed (1.6.3) - http (>= 4.0.0) - rails (>= 5.2.0) + noticed (2.0.0) + rails (>= 6.1.0) GEM remote: https://rubygems.org/ specs: - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) apnotic (1.7.1) connection_pool (~> 2) @@ -119,8 +113,8 @@ GEM rake thor (>= 0.14.0) ast (2.4.2) - base64 (0.1.1) - bigdecimal (3.1.4) + base64 (0.2.0) + bigdecimal (3.1.5) builder (3.2.4) byebug (11.1.3) concurrent-ruby (1.2.2) @@ -128,53 +122,38 @@ GEM crack (0.4.5) rexml crass (1.0.6) - date (3.3.3) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - drb (2.1.1) + date (3.3.4) + drb (2.2.0) ruby2_keywords erubi (1.12.0) - faraday (2.7.11) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.16.3) - ffi-compiler (1.0.1) - ffi (>= 1.0.0) - rake + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http globalid (1.2.1) activesupport (>= 6.1) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - hashdiff (1.0.1) + hashdiff (1.1.0) http-2 (0.11.0) - http (5.1.1) - addressable (~> 2.8) - http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.4.0) - http-cookie (1.0.5) - domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.1) concurrent-ruby (~> 1.0) - io-console (0.6.0) - irb (1.8.1) + io-console (0.7.1) + irb (1.11.1) rdoc - reline (>= 0.3.8) - json (2.6.3) + reline (>= 0.4.2) + json (2.7.1) jwt (2.7.1) language_server-protocol (3.17.0.3) lint_roller (1.1.0) - llhttp-ffi (0.4.0) - ffi-compiler (~> 1.0) - rake (~> 13.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -184,34 +163,40 @@ GEM net-smtp marcel (1.0.2) mini_mime (1.1.5) - minitest (5.20.0) + mini_portile2 (2.8.5) + minitest (5.21.1) multi_json (1.15.0) - mutex_m (0.1.2) mysql2 (0.5.5) + net-http (0.4.1) + uri net-http2 (0.18.5) http-2 (~> 0.11) - net-imap (0.4.1) + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.4-arm64-darwin) + nio4r (2.7.0) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.4) + parallel (1.24.0) + parser (3.3.0.3) ast (~> 2.4.1) racc pg (1.5.4) - psych (5.1.1) + psych (5.1.2) stringio - public_suffix (5.0.3) - racc (1.7.1) + public_suffix (5.0.4) + racc (1.7.3) rack (3.0.8) rack-session (2.0.0) rack (>= 3.0.0) @@ -228,30 +213,29 @@ GEM loofah (~> 2.21) nokogiri (~> 1.14) rainbow (3.1.1) - rake (13.0.6) - rdoc (6.5.0) + rake (13.1.0) + rdoc (6.6.2) psych (>= 4.0.0) - regexp_parser (2.8.2) - reline (0.3.9) + regexp_parser (2.9.0) + reline (0.4.2) io-console (~> 0.5) rexml (3.2.6) - rubocop (1.56.4) - base64 (~> 0.1.1) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.2.4) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) + rubocop-performance (1.20.2) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) signet (0.18.0) @@ -259,28 +243,28 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - sqlite3 (1.6.7-arm64-darwin) - standard (1.31.2) + sqlite3 (1.6.9) + mini_portile2 (~> 2.8.0) + standard (1.33.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.56.4) + rubocop (~> 1.59.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.2) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.2.1) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.19.1) - stringio (3.0.8) - thor (1.2.2) - timeout (0.4.0) + rubocop-performance (~> 1.20.2) + stringio (3.1.0) + thor (1.3.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.5.0) + uri (0.13.0) + useragent (0.16.10) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -294,6 +278,7 @@ GEM PLATFORMS arm64-darwin-22 x86_64-darwin-22 + x86_64-darwin-23 x86_64-linux DEPENDENCIES @@ -311,4 +296,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.20 + 2.5.3 diff --git a/lib/generators/noticed/delivery_method_generator.rb b/lib/generators/noticed/delivery_method_generator.rb index 5edf33ae..2465a86a 100644 --- a/lib/generators/noticed/delivery_method_generator.rb +++ b/lib/generators/noticed/delivery_method_generator.rb @@ -12,7 +12,7 @@ class DeliveryMethodGenerator < Rails::Generators::NamedBase desc "Generates a class for a custom delivery method with the given NAME." def generate_notification - template "delivery_method.rb", "app/notifications/delivery_methods/#{singular_name}.rb" + template "delivery_method.rb", "app/notifiers/delivery_methods/#{singular_name}.rb" end end end diff --git a/lib/generators/noticed/install_generator.rb b/lib/generators/noticed/install_generator.rb new file mode 100644 index 00000000..77b56abc --- /dev/null +++ b/lib/generators/noticed/install_generator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Noticed + module Generators + class ModelGenerator < Rails::Generators::Base + include Rails::Generators::ResourceHelpers + + source_root File.expand_path("../templates", __FILE__) + + def create_migrations + rails_command "railties:install:migrations FROM=noticed", inline: true + end + + def done + readme "README" if behavior == :invoke + end + end + end +end diff --git a/lib/generators/noticed/model_generator.rb b/lib/generators/noticed/model_generator.rb deleted file mode 100644 index 1f4bcd5f..00000000 --- a/lib/generators/noticed/model_generator.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators/named_base" - -module Noticed - module Generators - class ModelGenerator < Rails::Generators::NamedBase - include Rails::Generators::ResourceHelpers - - source_root File.expand_path("../templates", __FILE__) - - desc "Generates a Notification model for storing notifications." - - argument :name, type: :string, default: "Notification", banner: "Notification" - argument :attributes, type: :array, default: [], banner: "field:type field:type" - - def generate_notification - generate :model, name, "recipient:references{polymorphic}", "type", params_column, "read_at:datetime:index", *attributes - end - - def add_noticed_model - inject_into_class model_path, class_name, " include Noticed::Model\n" - end - - def add_not_nullable - migration_path = Dir.glob(Rails.root.join("db/migrate/*")).max_by { |f| File.mtime(f) } - - # Force is required because null: false already exists in the file and Thor isn't smart enough to tell the difference - insert_into_file migration_path, after: "t.string :type", force: true do - ", null: false" - end - end - - def done - readme "README" if behavior == :invoke - end - - private - - def model_path - @model_path ||= File.join("app", "models", "#{file_path}.rb") - end - - def params_column - case current_adapter - when "postgresql", "postgis" - "params:jsonb" - else - # MySQL and SQLite both support json - "params:json" - end - end - - def current_adapter - if ActiveRecord::Base.respond_to?(:connection_db_config) - ActiveRecord::Base.connection_db_config.adapter - else - ActiveRecord::Base.connection_config[:adapter] - end - end - end - end -end diff --git a/lib/generators/noticed/notification_generator.rb b/lib/generators/noticed/notifier_generator.rb similarity index 71% rename from lib/generators/noticed/notification_generator.rb rename to lib/generators/noticed/notifier_generator.rb index d0aad925..eaeace12 100644 --- a/lib/generators/noticed/notification_generator.rb +++ b/lib/generators/noticed/notifier_generator.rb @@ -4,7 +4,7 @@ module Noticed module Generators - class NotificationGenerator < Rails::Generators::NamedBase + class NotifierGenerator < Rails::Generators::NamedBase include Rails::Generators::ResourceHelpers source_root File.expand_path("../templates", __FILE__) @@ -12,7 +12,7 @@ class NotificationGenerator < Rails::Generators::NamedBase desc "Generates a notification with the given NAME." def generate_notification - template "notification.rb", "app/notifications/#{file_path}.rb" + template "notifier.rb", "app/notifiers/#{file_path}.rb" end end end diff --git a/lib/generators/noticed/templates/README b/lib/generators/noticed/templates/README index f8f4c8f5..bb6d1931 100644 --- a/lib/generators/noticed/templates/README +++ b/lib/generators/noticed/templates/README @@ -1,7 +1,8 @@ -🚚 Your notifications database model has been generated! +🚚 You're ready to start sending notifications! Next steps: -1. Run "rails db:migrate" -2. Add "has_many :notifications, as: :recipient, dependent: :destroy" to your User model(s). -3. Generate notifications with "rails g noticed:notification" +1. Run `rails db:migrate` +2. Add `has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"` to your User model(s). +2. Add `has_many :notifications, as: :record, dependent: :destroy, class_name: "Noticed::Event"` to your model(s) that notifications reference. +3. Generate notifiers with "rails g noticed:notifier" diff --git a/lib/generators/noticed/templates/notification.rb.tt b/lib/generators/noticed/templates/notification.rb.tt deleted file mode 100644 index 77131f10..00000000 --- a/lib/generators/noticed/templates/notification.rb.tt +++ /dev/null @@ -1,27 +0,0 @@ -# To deliver this notification: -# -# <%= class_name %>.with(post: @post).deliver_later(current_user) -# <%= class_name %>.with(post: @post).deliver(current_user) - -class <%= class_name %> < Noticed::Base - # Add your delivery methods - # - # deliver_by :database - # deliver_by :email, mailer: "UserMailer" - # deliver_by :slack - # deliver_by :custom, class: "MyDeliveryMethod" - - # Add required params - # - # param :post - - # Define helper methods to make rendering easier. - # - # def message - # t(".message") - # end - # - # def url - # post_path(params[:post]) - # end -end diff --git a/lib/generators/noticed/templates/notifier.rb.tt b/lib/generators/noticed/templates/notifier.rb.tt new file mode 100644 index 00000000..37e0b847 --- /dev/null +++ b/lib/generators/noticed/templates/notifier.rb.tt @@ -0,0 +1,24 @@ +# To deliver this notification: +# +# <%= class_name %>.with(record: @post, message: "New post").deliver(User.all) + +class <%= class_name %> < Noticed::Event + # Add your delivery methods + # + # deliver_by :email do |config| + # config.mailer = "UserMailer" + # config.method = "new_post" + # end + # + # bulk_deliver_by :slack do |config| + # config.url = -> { Rails.application.credentials.slack_webhook_url } + # end + # + # deliver_by :custom do |config| + # config.class = "MyDeliveryMethod" + # end + + # Add required params + # + # required_param :message +end diff --git a/lib/noticed.rb b/lib/noticed.rb index 921598eb..94b5baa4 100644 --- a/lib/noticed.rb +++ b/lib/noticed.rb @@ -1,33 +1,44 @@ -require "active_job/arguments" -require "http" +require "noticed/version" require "noticed/engine" module Noticed - autoload :Base, "noticed/base" + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end + + deprecate_constant :Base, "Noticed::Event", deprecator: deprecator + + autoload :ApiClient, "noticed/api_client" + autoload :BulkDeliveryMethod, "noticed/bulk_delivery_method" autoload :Coder, "noticed/coder" - autoload :HasNotifications, "noticed/has_notifications" - autoload :Model, "noticed/model" - autoload :TextCoder, "noticed/text_coder" + autoload :DeliveryMethod, "noticed/delivery_method" + autoload :RequiredOptions, "noticed/required_options" autoload :Translation, "noticed/translation" - autoload :NotificationChannel, "noticed/notification_channel" + + module BulkDeliveryMethods + autoload :Discord, "noticed/bulk_delivery_methods/discord" + autoload :Slack, "noticed/bulk_delivery_methods/slack" + autoload :Webhook, "noticed/bulk_delivery_methods/webhook" + end module DeliveryMethods + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant :Base, "Noticed::DeliveryMethod", deprecator: Noticed.deprecator + autoload :ActionCable, "noticed/delivery_methods/action_cable" - autoload :Base, "noticed/delivery_methods/base" - autoload :Database, "noticed/delivery_methods/database" autoload :Email, "noticed/delivery_methods/email" autoload :Fcm, "noticed/delivery_methods/fcm" autoload :Ios, "noticed/delivery_methods/ios" autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams" autoload :Slack, "noticed/delivery_methods/slack" autoload :Test, "noticed/delivery_methods/test" - autoload :Twilio, "noticed/delivery_methods/twilio" - autoload :Vonage, "noticed/delivery_methods/vonage" + autoload :TwilioMessaging, "noticed/delivery_methods/twilio_messaging" + autoload :VonageSms, "noticed/delivery_methods/vonage_sms" + autoload :Webhook, "noticed/delivery_methods/webhook" end - mattr_accessor :parent_class - @@parent_class = "ApplicationJob" - class ValidationError < StandardError end @@ -36,6 +47,8 @@ class ResponseUnsuccessful < StandardError def initialize(response) @response = response + + super("Request to returned #{response.code} response") end end end diff --git a/lib/noticed/api_client.rb b/lib/noticed/api_client.rb new file mode 100644 index 00000000..60dbcf52 --- /dev/null +++ b/lib/noticed/api_client.rb @@ -0,0 +1,44 @@ +require "net/http" + +module Noticed + module ApiClient + extend ActiveSupport::Concern + + # Helper method for making POST requests from delivery methods + # + # Usage: + # post_request("http://example.com", basic_auth: {user:, pass:}, headers: {}, json: {}, form: {}) + # + def post_request(url, args = {}) + args.compact! + + uri = URI(url) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true if uri.instance_of? URI::HTTPS + + headers = args.delete(:headers) || {} + headers["Content-Type"] = "application/json" if args.has_key?(:json) + + request = Net::HTTP::Post.new(uri.request_uri, headers) + + if (basic_auth = args.delete(:basic_auth)) + request.basic_auth basic_auth.fetch(:user), basic_auth.fetch(:pass) + end + + if (json = args.delete(:json)) + request.body = json.to_json + elsif (form = args.delete(:form)) + request.set_form(form, "multipart/form-data") + end + + logger.debug("POST #{url}") + logger.debug(request.body) + response = http.request(request) + logger.debug("Response: #{response.code}: #{response.body.inspect}") + + raise ResponseUnsuccessful.new(response) unless response.code.start_with?("20") + + response + end + end +end diff --git a/lib/noticed/base.rb b/lib/noticed/base.rb deleted file mode 100644 index afe01fb5..00000000 --- a/lib/noticed/base.rb +++ /dev/null @@ -1,160 +0,0 @@ -module Noticed - class Base - include Translation - include Rails.application.routes.url_helpers - - extend ActiveModel::Callbacks - define_model_callbacks :deliver - - class_attribute :delivery_methods, instance_writer: false, default: [] - class_attribute :param_names, instance_writer: false, default: [] - - # Gives notifications access to the record and recipient during delivery - attr_accessor :record, :recipient - - delegate :read?, :unread?, to: :record - - class << self - def deliver_by(name, options = {}) - delivery_methods.push(name: name, options: options) - define_model_callbacks(name) - end - - # Copy delivery methods from parent - def inherited(base) # :nodoc: - base.delivery_methods = delivery_methods.dup - base.param_names = param_names.dup - super - end - - def with(params) - new(params) - end - - # Shortcut for delivering without params - def deliver(recipients) - new.deliver(recipients) - end - - # Shortcut for delivering later without params - def deliver_later(recipients) - new.deliver_later(recipients) - end - - def params(*names) - param_names.concat Array.wrap(names) - end - alias_method :param, :params - end - - def initialize(params = {}) - @params = params - end - - def deliver(recipients) - validate! - - run_callbacks :deliver do - Array.wrap(recipients).uniq.each do |recipient| - run_delivery(recipient, enqueue: false) - end - end - end - - def deliver_later(recipients) - validate! - - run_callbacks :deliver do - Array.wrap(recipients).uniq.each do |recipient| - run_delivery(recipient, enqueue: true) - end - end - end - - def params - @params || {} - end - - def clear_recipient - self.recipient = nil - end - - private - - # Runs all delivery methods for a notification - def run_delivery(recipient, enqueue: true) - delivery_methods = self.class.delivery_methods.dup - - self.recipient = recipient - - # Run database delivery inline first if it exists so other methods have access to the record - if (index = delivery_methods.find_index { |m| m[:name] == :database }) - delivery_method = delivery_methods.delete_at(index) - self.record = run_delivery_method(delivery_method, recipient: recipient, enqueue: false, record: nil) - end - - delivery_methods.each do |delivery_method| - run_delivery_method(delivery_method, recipient: recipient, enqueue: enqueue, record: record) - end - end - - # Actually runs an individual delivery - def run_delivery_method(delivery_method, recipient:, enqueue:, record:) - args = { - notification_class: self.class.name, - options: delivery_method[:options], - params: params, - recipient: recipient, - record: record - } - - run_callbacks delivery_method[:name] do - method = delivery_method_for(delivery_method[:name], delivery_method[:options]) - - # If the queue is `nil`, ActiveJob will use a default queue name. - queue = delivery_method.dig(:options, :queue) - - # Always perfrom later if a delay is present - if (delay = delivery_method.dig(:options, :delay)) - # Dynamic delays with metho calls or - delay = send(delay) if delay.is_a? Symbol - - method.set(wait: delay, queue: queue).perform_later(args) - elsif enqueue - method.set(queue: queue).perform_later(args) - else - method.perform_now(args) - end - end - end - - def delivery_method_for(name, options) - if options[:class] - options[:class].constantize - else - "Noticed::DeliveryMethods::#{name.to_s.camelize}".constantize - end - end - - def validate! - validate_params_present! - validate_options_of_delivery_methods! - end - - # Validates that all params are present - def validate_params_present! - self.class.param_names.each do |param_name| - if params[param_name].nil? - raise ValidationError, "#{param_name} is missing." - end - end - end - - def validate_options_of_delivery_methods! - delivery_methods.each do |delivery_method| - method = delivery_method_for(delivery_method[:name], delivery_method[:options]) - method.validate!(delivery_method[:options]) - end - end - end -end diff --git a/lib/noticed/bulk_delivery_method.rb b/lib/noticed/bulk_delivery_method.rb new file mode 100644 index 00000000..1e3de5ad --- /dev/null +++ b/lib/noticed/bulk_delivery_method.rb @@ -0,0 +1,46 @@ +module Noticed + class BulkDeliveryMethod < ApplicationJob + include ApiClient + include RequiredOptions + + class_attribute :logger, default: Rails.logger + + attr_reader :config, :event + + def perform(delivery_method_name, event) + @event = event + @config = event.bulk_delivery_methods.fetch(delivery_method_name).config + + return false if config.has_key?(:if) && !evaluate_option(:if) + return false if config.has_key?(:unless) && evaluate_option(:unless) + + deliver + end + + def deliver + raise NotImplementedError, "Bulk delivery methods must implement the `deliver` method" + end + + def fetch_constant(name) + option = config[name] + option.is_a?(String) ? option.constantize : option + end + + def evaluate_option(name) + option = config[name] + + # Evaluate Proc within the context of the notifier + if option&.respond_to?(:call) + event.instance_exec(&option) + + # Call method if symbol and matching method + elsif option.is_a?(Symbol) && event.respond_to?(option) + event.send(option) + + # Return the value + else + option + end + end + end +end diff --git a/lib/noticed/bulk_delivery_methods/discord.rb b/lib/noticed/bulk_delivery_methods/discord.rb new file mode 100644 index 00000000..c6f9e1c4 --- /dev/null +++ b/lib/noticed/bulk_delivery_methods/discord.rb @@ -0,0 +1,11 @@ +module Noticed + module BulkDeliveryMethods + class Discord < BulkDeliveryMethod + required_options :json, :url + + def deliver + post_request evaluate_option(:url), headers: evaluate_option(:headers), json: evaluate_option(:json) + end + end + end +end diff --git a/lib/noticed/bulk_delivery_methods/slack.rb b/lib/noticed/bulk_delivery_methods/slack.rb new file mode 100644 index 00000000..525b7426 --- /dev/null +++ b/lib/noticed/bulk_delivery_methods/slack.rb @@ -0,0 +1,17 @@ +module Noticed + module BulkDeliveryMethods + class Slack < BulkDeliveryMethod + DEFAULT_URL = "https://slack.com/api/chat.postMessage" + + required_options :json + + def deliver + post_request url, headers: evaluate_option(:headers), json: evaluate_option(:json) + end + + def url + evaluate_option(:url) || DEFAULT_URL + end + end + end +end diff --git a/lib/noticed/bulk_delivery_methods/webhook.rb b/lib/noticed/bulk_delivery_methods/webhook.rb new file mode 100644 index 00000000..c5d006fc --- /dev/null +++ b/lib/noticed/bulk_delivery_methods/webhook.rb @@ -0,0 +1,18 @@ +module Noticed + module BulkDeliveryMethods + class Webhook < BulkDeliveryMethod + required_options :url + + def deliver + Rails.logger.debug(evaluate_option(:json)) + post_request( + evaluate_option(:url), + basic_auth: evaluate_option(:basic_auth), + headers: evaluate_option(:headers), + json: evaluate_option(:json), + form: evaluate_option(:form) + ) + end + end + end +end diff --git a/lib/noticed/delivery_method.rb b/lib/noticed/delivery_method.rb new file mode 100644 index 00000000..6fc704ae --- /dev/null +++ b/lib/noticed/delivery_method.rb @@ -0,0 +1,50 @@ +module Noticed + class DeliveryMethod < ApplicationJob + include ApiClient + include RequiredOptions + + class_attribute :logger, default: Rails.logger + + attr_reader :config, :event, :notification + delegate :recipient, to: :notification + + def perform(delivery_method_name, notification, overrides: {}) + @notification = notification + @event = notification.event + + # Look up config from Notifier and merge overrides + @config = event.delivery_methods.fetch(delivery_method_name).config.merge(overrides) + + return false if config.has_key?(:if) && !evaluate_option(:if) + return false if config.has_key?(:unless) && evaluate_option(:unless) + + deliver + end + + def deliver + raise NotImplementedError, "Delivery methods must implement the `deliver` method" + end + + def fetch_constant(name) + option = config[name] + option.is_a?(String) ? option.constantize : option + end + + def evaluate_option(name) + option = config[name] + + # Evaluate Proc within the context of the Notification + if option&.respond_to?(:call) + notification.instance_exec(&option) + + # Call method if symbol and matching method on Notifier + elsif option.is_a?(Symbol) && event.respond_to?(option) + event.send(option, self) + + # Return the value + else + option + end + end + end +end diff --git a/lib/noticed/delivery_methods/action_cable.rb b/lib/noticed/delivery_methods/action_cable.rb index 7fe40060..d094f240 100644 --- a/lib/noticed/delivery_methods/action_cable.rb +++ b/lib/noticed/delivery_methods/action_cable.rb @@ -1,46 +1,14 @@ module Noticed module DeliveryMethods - class ActionCable < Base - def deliver - channel.broadcast_to stream, format - end + class ActionCable < DeliveryMethod + required_options :channel, :stream, :message - private - - def format - if (method = options[:format]) - notification.send(method) - else - notification.params - end - end - - def channel - @channel ||= begin - value = options[:channel] - case value - when String - value.constantize - when Symbol - notification.send(value) - when Class - value - else - Noticed::NotificationChannel - end - end - end + def deliver + channel = fetch_constant(:channel) + stream = evaluate_option(:stream) + message = evaluate_option(:message) - def stream - value = options[:stream] - case value - when String - value - when Symbol - notification.send(value) - else - recipient - end + channel.broadcast_to stream, message end end end diff --git a/lib/noticed/delivery_methods/base.rb b/lib/noticed/delivery_methods/base.rb deleted file mode 100644 index 3b6793c0..00000000 --- a/lib/noticed/delivery_methods/base.rb +++ /dev/null @@ -1,95 +0,0 @@ -module Noticed - module DeliveryMethods - class Base < Noticed.parent_class.constantize - extend ActiveModel::Callbacks - define_model_callbacks :deliver - - class_attribute :option_names, instance_writer: false, default: [] - - attr_reader :notification, :options, :params, :recipient, :record, :logger - - class << self - # Copy option names from parent - def inherited(base) # :nodoc: - base.option_names = option_names.dup - super - end - - def options(*names) - option_names.concat Array.wrap(names) - end - alias_method :option, :options - - def validate!(delivery_method_options) - option_names.each do |option_name| - unless delivery_method_options.key? option_name - raise ValidationError, "option `#{option_name}` must be set for #{name}" - end - end - end - end - - def assign_args(args) - @notification = args.fetch(:notification_class).constantize.new(args[:params]) - @options = args[:options] || {} - @params = args[:params] - @recipient = args[:recipient] - @record = args[:record] - - # Set the default logger - @logger = @options.fetch(:logger, Rails.logger) - - # Make notification aware of database record and recipient during delivery - @notification.record = args[:record] - @notification.recipient = args[:recipient] - self - end - - def perform(args) - assign_args(args) - - return if (condition = @options[:if]) && !@notification.send(condition) - return if (condition = @options[:unless]) && @notification.send(condition) - - run_callbacks :deliver do - deliver - end - end - - def deliver - raise NotImplementedError, "Delivery methods must implement a deliver method" - end - - private - - # Helper method for making POST requests from delivery methods - # - # Usage: - # post("http://example.com", basic_auth: {user:, pass:}, headers: {}, json: {}, form: {}) - # - def post(url, args = {}) - basic_auth = args.delete(:basic_auth) - headers = args.delete(:headers) - - request = HTTP - request = request.basic_auth(user: basic_auth[:user], pass: basic_auth[:pass]) if basic_auth - request = request.headers(headers) if headers - - response = request.post(url, args) - - if options[:debug] - logger.debug("POST #{url}") - logger.debug("Response: #{response.code}: #{response}") - end - - if !options[:ignore_failure] && !response.status.success? - puts response.status - puts response.body - raise ResponseUnsuccessful.new(response) - end - - response - end - end - end -end diff --git a/lib/noticed/delivery_methods/database.rb b/lib/noticed/delivery_methods/database.rb deleted file mode 100644 index d074e594..00000000 --- a/lib/noticed/delivery_methods/database.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Noticed - module DeliveryMethods - class Database < Base - # Must return the database record - def deliver - recipient.send(association_name).create!(attributes) - end - - def self.validate!(options) - super - - # Must be executed right away so the other deliveries can access the db record - raise ArgumentError, "database delivery cannot be delayed" if options.key?(:delay) - end - - private - - def association_name - options[:association] || :notifications - end - - def attributes - if (method = options[:format]) - notification.send(method) - else - { - type: notification.class.name, - params: notification.params - } - end - end - end - end -end diff --git a/lib/noticed/delivery_methods/discord.rb b/lib/noticed/delivery_methods/discord.rb new file mode 100644 index 00000000..1356ae8c --- /dev/null +++ b/lib/noticed/delivery_methods/discord.rb @@ -0,0 +1,11 @@ +module Noticed + module DeliveryMethods + class Discord < BulkDeliveryMethod + required_options :json, :url + + def deliver + post_request evaluate_option(:url), headers: evaluate_option(:headers), json: evaluate_option(:json) + end + end + end +end diff --git a/lib/noticed/delivery_methods/email.rb b/lib/noticed/delivery_methods/email.rb index a62d3e3e..f1bef99d 100644 --- a/lib/noticed/delivery_methods/email.rb +++ b/lib/noticed/delivery_methods/email.rb @@ -1,54 +1,18 @@ module Noticed module DeliveryMethods - class Email < Base - option :mailer + class Email < DeliveryMethod + required_options :mailer, :method def deliver - if options[:enqueue] - mailer.with(format).send(mailer_method.to_sym).deliver_later - else - mailer.with(format).send(mailer_method.to_sym).deliver_now - end - end - - private + mailer = fetch_constant(:mailer) + email = evaluate_option(:method) + params = (evaluate_option(:params) || notification&.params || {}).merge(record: notification&.record) + args = evaluate_option(:args) - # mailer: "UserMailer" - # mailer: UserMailer - # mailer: :my_method - `my_method` should return Class - def mailer - option = options.fetch(:mailer) - case option - when String - option.constantize - when Symbol - notification.send(option) - else - option - end - end - - # Method should be a symbol - # - # If notification responds to symbol, call that method and use return value - # If notification does not respond to symbol, use the symbol for the mailer method - # Otherwise, use the underscored notification class name as the mailer method - def mailer_method - method_name = options[:method]&.to_sym - if method_name.present? - notification.respond_to?(method_name) ? notification.send(method_name) : method_name - else - notification.class.name.underscore - end - end + mail = mailer.with(params) + mail = args.present? ? mail.send(email, *args) : mail.send(email) - def format - params = if (method = options[:format]) - notification.send(method) - else - notification.params - end - params.merge(recipient: recipient, record: record) + (!!evaluate_option(:enqueue)) ? mail.deliver_later : mail.deliver_now end end end diff --git a/lib/noticed/delivery_methods/fcm.rb b/lib/noticed/delivery_methods/fcm.rb index 14edb34a..ad166637 100644 --- a/lib/noticed/delivery_methods/fcm.rb +++ b/lib/noticed/delivery_methods/fcm.rb @@ -1,94 +1,54 @@ require "googleauth" -# class CommentNotifier -# deliver_by :fcm, credentials: Rails.root.join("config/certs/fcm.json"), format: :format_notification -# -# deliver_by :fcm, credentials: :fcm_credentials -# def fcm_credentials -# { project_id: "api-12345" } -# end -# end - module Noticed module DeliveryMethods - class Fcm < Base - BASE_URI = "https://fcm.googleapis.com/v1/projects/" - - option :format + class Fcm < DeliveryMethod + required_option :credentials, :device_tokens, :json def deliver - device_tokens.each do |device_token| - post("#{BASE_URI}#{project_id}/messages:send", headers: {authorization: "Bearer #{access_token}"}, json: {message: format(device_token)}) - rescue ResponseUnsuccessful => exception - if exception.response.code == 404 - cleanup_invalid_token(device_token) - else - raise - end + evaluate_option(:device_tokens).each do |device_token| + send_notification device_token end end - def cleanup_invalid_token(device_token) - return unless notification.respond_to?(:cleanup_device_token) - notification.send(:cleanup_device_token, token: device_token, platform: "fcm") + def send_notification(device_token) + post_request("https://fcm.googleapis.com/v1/projects/#{credentials[:project_id]}/messages:send", + headers: {authorization: "Bearer #{access_token}"}, + json: notification.instance_exec(device_token, &config[:json])) + rescue Noticed::ResponseUnsuccessful => exception + if exception.response.code == "404" && config[:invalid_token] + notification.instance_exec(device_token, &config[:invalid_token]) + else + raise + end end def credentials @credentials ||= begin - option = options[:credentials] - credentials_to_parse = option.is_a?(Symbol) ? notification.send(option) : option - credentials_hash = case credentials_to_parse + value = evaluate_option(:credentials) + case value when Hash - credentials_to_parse + value when Pathname - load_json(credentials_to_parse) + load_json(value) when String - load_json(Rails.root.join(credentials_to_parse)) + load_json(Rails.root.join(value)) else - Rails.application.credentials.fcm + raise ArgumentError, "FCM credentials must be a Hash, String, Pathname, or Symbol" end - - credentials_hash.symbolize_keys end end def load_json(path) - JSON.parse(File.read(path)) - end - - def project_id - credentials[:project_id] + JSON.parse(File.read(path), symbolize_names: true) end def access_token - token = authorizer.fetch_access_token! - token["access_token"] - end - - def authorizer - @authorizer ||= options.fetch(:authorizer, Google::Auth::ServiceAccountCredentials).make_creds( + @authorizer ||= (evaluate_option(:authorizer) || Google::Auth::ServiceAccountCredentials).make_creds( json_key_io: StringIO.new(credentials.to_json), scope: "https://www.googleapis.com/auth/firebase.messaging" ) - end - - def format(device_token) - notification.send(options[:format], device_token) - end - - def device_tokens - if notification.respond_to?(:fcm_device_tokens) - Array.wrap(notification.fcm_device_tokens(recipient)) - else - raise NoMethodError, <<~MESSAGE - You must implement `fcm_device_tokens` to send Firebase Cloud Messaging notifications - - # This must return an Array of FCM device tokens - def fcm_device_tokens(recipient) - recipient.fcm_device_tokens.pluck(:token) - end - MESSAGE - end + @authorizer.fetch_access_token!["access_token"] end end end diff --git a/lib/noticed/delivery_methods/ios.rb b/lib/noticed/delivery_methods/ios.rb index 42150fe1..70d3e47d 100644 --- a/lib/noticed/delivery_methods/ios.rb +++ b/lib/noticed/delivery_methods/ios.rb @@ -2,26 +2,23 @@ module Noticed module DeliveryMethods - class Ios < Base + class Ios < DeliveryMethod cattr_accessor :connection_pool + required_options :bundle_identifier, :key_id, :team_id, :apns_key, :device_tokens + def deliver - raise ArgumentError, "bundle_identifier is missing" if bundle_identifier.blank? - raise ArgumentError, "key_id is missing" if key_id.blank? - raise ArgumentError, "team_id is missing" if team_id.blank? - raise ArgumentError, "Could not find APN cert at '#{cert_path}'" unless valid_cert_path? + evaluate_option(:device_tokens).each do |device_token| + apn = Apnotic::Notification.new(device_token) + format_notification(apn) - device_tokens.each do |device_token| connection_pool.with do |connection| - apn = Apnotic::Notification.new(device_token) - format_notification(apn) - response = connection.push(apn) raise "Timeout sending iOS push notification" unless response - if bad_token?(response) + if bad_token?(response) && config[:invalid_token] # Allow notification to cleanup invalid iOS device tokens - cleanup_invalid_token(device_token) + notification.instance_exec(device_token, &config[:invalid_token]) elsif !response.ok? raise "Request failed #{response.body}" end @@ -32,41 +29,21 @@ def deliver private def format_notification(apn) - apn.topic = bundle_identifier + apn.topic = evaluate_option(:bundle_identifier) - if (method = options[:format]) - notification.send(method, apn) - elsif params[:message].present? - apn.alert = params[:message] + if (method = config[:format]) + notification.instance_exec(apn, &method) + elsif notification.params.try(:has_key?, :message) + apn.alert = notification.params[:message] else raise ArgumentError, "No message for iOS delivery. Either include message in params or add the 'format' option in 'deliver_by :ios'." end end - def device_tokens - if notification.respond_to?(:ios_device_tokens) - Array.wrap(notification.ios_device_tokens(recipient)) - else - raise NoMethodError, <<~MESSAGE - You must implement `ios_device_tokens` to send iOS notifications - - # This must return an Array of iOS device tokens - def ios_device_tokens(recipient) - recipient.ios_device_tokens.pluck(:token) - end - MESSAGE - end - end - def bad_token?(response) response.status == "410" || (response.status == "400" && response.body["reason"] == "BadDeviceToken") end - def cleanup_invalid_token(token) - return unless notification.respond_to?(:cleanup_device_token) - notification.send(:cleanup_device_token, token: token, platform: "iOS") - end - def connection_pool self.class.connection_pool ||= new_connection_pool end @@ -88,83 +65,18 @@ def new_connection_pool def connection_pool_options { auth_method: :token, - cert_path: cert_path, - key_id: key_id, - team_id: team_id + cert_path: StringIO.new(config.fetch(:apns_key)), + key_id: config.fetch(:key_id), + team_id: config.fetch(:team_id) } end - def bundle_identifier - option = options[:bundle_identifier] - case option - when String - option - when Symbol - notification.send(option) - else - Rails.application.credentials.dig(:ios, :bundle_identifier) - end - end - - def cert_path - option = options[:cert_path] - case option - when String - option - when Symbol - notification.send(option) - else - Rails.root.join("config/certs/ios/apns.p8") - end - end - - def key_id - option = options[:key_id] - case option - when String - option - when Symbol - notification.send(option) - else - Rails.application.credentials.dig(:ios, :key_id) - end - end - - def team_id - option = options[:team_id] - case option - when String - option - when Symbol - notification.send(option) - else - Rails.application.credentials.dig(:ios, :team_id) - end - end - def development? - option = options[:development] - case option - when Symbol - !!notification.send(option) - else - !!option - end - end - - def valid_cert_path? - case cert_path - when File, StringIO - cert_path.size > 0 - else - File.exist?(cert_path) - end + !!evaluate_option(:development) end def pool_options - { - size: options.fetch(:pool_size, 5) - } + {size: evaluate_option(:pool_size) || 5} end end end diff --git a/lib/noticed/delivery_methods/microsoft_teams.rb b/lib/noticed/delivery_methods/microsoft_teams.rb index 02f0c15f..81e64738 100644 --- a/lib/noticed/delivery_methods/microsoft_teams.rb +++ b/lib/noticed/delivery_methods/microsoft_teams.rb @@ -1,31 +1,14 @@ module Noticed module DeliveryMethods - class MicrosoftTeams < Base - def deliver - post(url, json: format) - end + class MicrosoftTeams < DeliveryMethod + required_options :json - private - - def format - if (method = options[:format]) - notification.send(method) - else - { - title: notification.params[:title], - text: notification.params[:text], - sections: notification.params[:sections], - potentialAction: notification.params[:notification_action] - } - end + def deliver + post_request url, headers: evaluate_option(:headers), json: evaluate_option(:json) end def url - if (method = options[:url]) - notification.send(method) - else - Rails.application.credentials.microsoft_teams[:notification_url] - end + evaluate_option(:url) || Rails.application.credentials.dig(:microsoft_teams, :notification_url) end end end diff --git a/lib/noticed/delivery_methods/slack.rb b/lib/noticed/delivery_methods/slack.rb index d8b4d930..2b5045e9 100644 --- a/lib/noticed/delivery_methods/slack.rb +++ b/lib/noticed/delivery_methods/slack.rb @@ -1,26 +1,16 @@ module Noticed module DeliveryMethods - class Slack < Base - def deliver - post(url, json: format) - end + class Slack < DeliveryMethod + DEFAULT_URL = "https://slack.com/api/chat.postMessage" - private + required_options :json - def format - if (method = options[:format]) - notification.send(method) - else - notification.params - end + def deliver + post_request url, headers: evaluate_option(:headers), json: evaluate_option(:json) end def url - if (method = options[:url]) - notification.send(method) - else - Rails.application.credentials.slack[:notification_url] - end + evaluate_option(:url) || DEFAULT_URL end end end diff --git a/lib/noticed/delivery_methods/test.rb b/lib/noticed/delivery_methods/test.rb index bdc9b96e..c3044587 100644 --- a/lib/noticed/delivery_methods/test.rb +++ b/lib/noticed/delivery_methods/test.rb @@ -1,20 +1,10 @@ module Noticed module DeliveryMethods - class Test < Base + class Test < DeliveryMethod class_attribute :delivered, default: [] - class_attribute :callbacks, default: [] - - after_deliver do - self.class.callbacks << :after - end - - def self.clear! - delivered.clear - callbacks.clear - end def deliver - self.class.delivered << notification + delivered << notification end end end diff --git a/lib/noticed/delivery_methods/twilio.rb b/lib/noticed/delivery_methods/twilio.rb deleted file mode 100644 index 61213bee..00000000 --- a/lib/noticed/delivery_methods/twilio.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Noticed - module DeliveryMethods - class Twilio < Base - def deliver - post(url, basic_auth: {user: account_sid, pass: auth_token}, form: format) - end - - private - - def format - if (method = options[:format]) - notification.send(method) - else - { - From: phone_number, - To: recipient.phone_number, - Body: notification.params[:message] - } - end - end - - def url - if (method = options[:url]) - notification.send(method) - else - "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}/Messages.json" - end - end - - def account_sid - credentials.fetch(:account_sid) - end - - def auth_token - credentials.fetch(:auth_token) - end - - def phone_number - credentials.fetch(:phone_number) - end - - def credentials - if (method = options[:credentials]) - notification.send(method) - else - Rails.application.credentials.twilio - end - end - end - end -end diff --git a/lib/noticed/delivery_methods/twilio_messaging.rb b/lib/noticed/delivery_methods/twilio_messaging.rb new file mode 100644 index 00000000..8f802c21 --- /dev/null +++ b/lib/noticed/delivery_methods/twilio_messaging.rb @@ -0,0 +1,37 @@ +module Noticed + module DeliveryMethods + class TwilioMessaging < DeliveryMethod + def deliver + post_request url, basic_auth: {user: account_sid, pass: auth_token}, form: json + end + + def json + evaluate_option(:json) || { + From: phone_number, + To: recipient.phone_number, + Body: params.fetch(:message) + } + end + + def url + evaluate_option(:url) || "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}/Messages.json" + end + + def account_sid + evaluate_option(:account_sid) || credentials.fetch(:account_sid) + end + + def auth_token + evaluate_option(:auth_token) || credentials.fetch(:auth_token) + end + + def phone_number + evaluate_option(:phone_number) || credentials.fetch(:phone_number) + end + + def credentials + evaluate_option(:credentials) || Rails.application.credentials.twilio + end + end + end +end diff --git a/lib/noticed/delivery_methods/vonage.rb b/lib/noticed/delivery_methods/vonage.rb deleted file mode 100644 index 92bfecd7..00000000 --- a/lib/noticed/delivery_methods/vonage.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Noticed - module DeliveryMethods - class Vonage < Base - def deliver - response = post("https://rest.nexmo.com/sms/json", json: format) - status = response.parse.dig("messages", 0, "status") - if !options[:ignore_failure] && status != "0" - raise ResponseUnsuccessful.new(response) - end - - response - end - - private - - def format - if (method = options[:format]) - notification.send(method) - else - { - api_key: credentials[:api_key], - api_secret: credentials[:api_secret], - from: notification.params[:from], - text: notification.params[:body], - to: notification.params[:to], - type: "unicode" - } - end - end - - def credentials - if (method = options[:credentials]) - notification.send(method) - else - Rails.application.credentials.vonage - end - end - end - end -end diff --git a/lib/noticed/delivery_methods/vonage_sms.rb b/lib/noticed/delivery_methods/vonage_sms.rb new file mode 100644 index 00000000..a0cabb8d --- /dev/null +++ b/lib/noticed/delivery_methods/vonage_sms.rb @@ -0,0 +1,18 @@ +module Noticed + module DeliveryMethods + class VonageSms < DeliveryMethod + DEFAULT_URL = "https://rest.nexmo.com/sms/json" + + required_options :json + + def deliver + response = post_request url, headers: evaluate_option(:headers), json: evaluate_option(:json) + raise ResponseUnsuccessful.new(response) if JSON.parse(response.body).dig("messages", 0, "status") != "0" + end + + def url + evaluate_option(:url) || DEFAULT_URL + end + end + end +end diff --git a/lib/noticed/delivery_methods/webhook.rb b/lib/noticed/delivery_methods/webhook.rb new file mode 100644 index 00000000..68a52dbe --- /dev/null +++ b/lib/noticed/delivery_methods/webhook.rb @@ -0,0 +1,17 @@ +module Noticed + module DeliveryMethods + class Webhook < DeliveryMethod + required_options :url + + def deliver + post_request( + evaluate_option(:url), + basic_auth: evaluate_option(:basic_auth), + headers: evaluate_option(:headers), + json: evaluate_option(:json), + form: evaluate_option(:form) + ) + end + end + end +end diff --git a/lib/noticed/engine.rb b/lib/noticed/engine.rb index d2d1c4e0..474a2405 100644 --- a/lib/noticed/engine.rb +++ b/lib/noticed/engine.rb @@ -1,13 +1,5 @@ module Noticed class Engine < ::Rails::Engine - initializer "noticed.has_notifications" do - ActiveSupport.on_load(:active_record) do - include Noticed::HasNotifications - end - end - - initializer "noticed.rails_5_2_support" do - require "rails_6_polyfills/base" if Rails::VERSION::MAJOR < 6 - end + isolate_namespace Noticed end end diff --git a/lib/noticed/has_notifications.rb b/lib/noticed/has_notifications.rb deleted file mode 100644 index fd8d0d23..00000000 --- a/lib/noticed/has_notifications.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Noticed - module HasNotifications - # Defines a method for the association and a before_destroy callback to remove notifications - # where this record is a param - # - # class User < ApplicationRecord - # has_noticed_notifications - # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification" - # end - # - # @user.notifications_as_user - # @user.notifications_as_owner - - extend ActiveSupport::Concern - - class_methods do - def has_noticed_notifications(param_name: model_name.singular, **options) - define_method "notifications_as_#{param_name}" do - model = options.fetch(:model_name, "Notification").constantize - case current_adapter - when "postgresql", "postgis" - model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json) - when "mysql2" - model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json) - when "sqlite3" - model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json) - else - # This will perform an exact match which isn't ideal - model.where(params: {param_name.to_sym => self}) - end - end - - if options.fetch(:destroy, true) - before_destroy do - send("notifications_as_#{param_name}").destroy_all - end - end - end - end - - def current_adapter - if ActiveRecord::Base.respond_to?(:connection_db_config) - ActiveRecord::Base.connection_db_config.adapter - else - ActiveRecord::Base.connection_config[:adapter] - end - end - end -end diff --git a/lib/noticed/model.rb b/lib/noticed/model.rb deleted file mode 100644 index ba70dd35..00000000 --- a/lib/noticed/model.rb +++ /dev/null @@ -1,85 +0,0 @@ -module Noticed - module Model - DATABASE_ERROR_CLASS_NAMES = lambda { - classes = [ActiveRecord::NoDatabaseError] - classes << ActiveRecord::ConnectionNotEstablished - classes << Mysql2::Error if defined?(::Mysql2) - classes << PG::ConnectionBad if defined?(::PG) - classes - }.call.freeze - - extend ActiveSupport::Concern - - included do - self.inheritance_column = nil - - if Rails.gem_version >= Gem::Version.new("7.1.0.alpha") - serialize :params, coder: noticed_coder - else - serialize :params, noticed_coder - end - - belongs_to :recipient, polymorphic: true - - scope :newest_first, -> { order(created_at: :desc) } - scope :unread, -> { where(read_at: nil) } - scope :read, -> { where.not(read_at: nil) } - end - - class_methods do - def mark_as_read! - update_all(read_at: Time.current, updated_at: Time.current) - end - - def mark_as_unread! - update_all(read_at: nil, updated_at: Time.current) - end - - def noticed_coder - return Noticed::TextCoder unless table_exists? - - case attribute_types["params"].type - when :json, :jsonb - Noticed::Coder - else - Noticed::TextCoder - end - rescue *DATABASE_ERROR_CLASS_NAMES => _error - warn("Noticed was unable to bootstrap correctly as the database is unavailable.") - - Noticed::TextCoder - end - end - - # Rehydrate the database notification into the Notification object for rendering - def to_notification - @_notification ||= begin - instance = type.constantize.with(params) - instance.record = self - instance.recipient = recipient - instance - end - end - - def mark_as_read! - update(read_at: Time.current) - end - - def mark_as_unread! - update(read_at: nil) - end - - def unread? - !read? - end - - def read? - read_at? - end - - # If a GlobalID record in params is no longer found, the params will default with a noticed_error key - def deserialize_error? - !!params[:noticed_error] - end - end -end diff --git a/lib/noticed/notification_channel.rb b/lib/noticed/notification_channel.rb deleted file mode 100644 index d3fecb67..00000000 --- a/lib/noticed/notification_channel.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Noticed - class NotificationChannel < ApplicationCable::Channel - def subscribed - stream_for current_user - end - - def unsubscribed - stop_all_streams - end - - def mark_as_read(data) - current_user.notifications.where(id: data["ids"]).mark_as_read! - end - end -end diff --git a/lib/noticed/required_options.rb b/lib/noticed/required_options.rb new file mode 100644 index 00000000..756496b3 --- /dev/null +++ b/lib/noticed/required_options.rb @@ -0,0 +1,21 @@ +module Noticed + module RequiredOptions + extend ActiveSupport::Concern + + included do + class_attribute :required_option_names, instance_writer: false, default: [] + end + + class_methods do + def inherited(base) + base.required_option_names = required_option_names.dup + super + end + + def required_options(*names) + required_option_names.concat names + end + alias_method :required_option, :required_options + end + end +end diff --git a/lib/noticed/text_coder.rb b/lib/noticed/text_coder.rb deleted file mode 100644 index cef44a97..00000000 --- a/lib/noticed/text_coder.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Noticed - class TextCoder - def self.load(data) - return if data.nil? - - # Text columns need JSON parsing - data = JSON.parse(data) - ActiveJob::Arguments.send(:deserialize_argument, data) - end - - def self.dump(data) - return if data.nil? - ActiveJob::Arguments.send(:serialize_argument, data).to_json - end - end -end diff --git a/lib/noticed/translation.rb b/lib/noticed/translation.rb index 0810157b..cad69d5f 100644 --- a/lib/noticed/translation.rb +++ b/lib/noticed/translation.rb @@ -4,7 +4,7 @@ module Translation # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup. def i18n_scope - :notifications + :notifiers end def class_scope @@ -22,7 +22,7 @@ def translate(key, **options) def scope_translation_key(key) if key.to_s.start_with?(".") - "#{i18n_scope}.#{class_scope}#{key}" + [i18n_scope, class_scope].compact.join(".") + key else key end diff --git a/lib/noticed/version.rb b/lib/noticed/version.rb index f7e52b85..9ad721bf 100644 --- a/lib/noticed/version.rb +++ b/lib/noticed/version.rb @@ -1,3 +1,3 @@ module Noticed - VERSION = "1.6.3" + VERSION = "2.0.0" end diff --git a/lib/rails_6_polyfills/actioncable/test_adapter.rb b/lib/rails_6_polyfills/actioncable/test_adapter.rb deleted file mode 100644 index c95641e8..00000000 --- a/lib/rails_6_polyfills/actioncable/test_adapter.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require "action_cable/subscription_adapter/base" -require "action_cable/subscription_adapter/subscriber_map" -require "action_cable/subscription_adapter/async" - -module ActionCable - module SubscriptionAdapter - # == Test adapter for Action Cable - # - # The test adapter should be used only in testing. Along with - # ActionCable::TestHelper it makes a great tool to test your Rails application. - # - # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file. - # - # NOTE: Test adapter extends the ActionCable::SubscriptionsAdapter::Async adapter, - # so it could be used in system tests too. - class Test < Async - def broadcast(channel, payload) - broadcasts(channel) << payload - super - end - - def broadcasts(channel) - channels_data[channel] ||= [] - end - - def clear_messages(channel) - channels_data[channel] = [] - end - - def clear - @channels_data = nil - end - - private - - def channels_data - @channels_data ||= {} - end - end - end - - # Update how broadcast_for determines the channel name so it's consistent with the Rails 6 way - module Channel - module Broadcasting - delegate :broadcast_to, to: :class - module ClassMethods - def broadcast_to(model, message) - ActionCable.server.broadcast(broadcasting_for(model), message) - end - - def broadcasting_for(model) - serialize_broadcasting([channel_name, model]) - end - - def serialize_broadcasting(object) # :nodoc: - case # standard:disable Style/EmptyCaseCondition - when object.is_a?(Array) - object.map { |m| serialize_broadcasting(m) }.join(":") - when object.respond_to?(:to_gid_param) - object.to_gid_param - else - object.to_param - end - end - end - end - end -end diff --git a/lib/rails_6_polyfills/actioncable/test_helper.rb b/lib/rails_6_polyfills/actioncable/test_helper.rb deleted file mode 100644 index 55fd62b9..00000000 --- a/lib/rails_6_polyfills/actioncable/test_helper.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -module ActionCable - # Have ActionCable pick its Test SubscriptionAdapter when it's called for in cable.yml - module Server - class Configuration - def pubsub_adapter - (cable["adapter"] == "test") ? ActionCable::SubscriptionAdapter::Test : super - end - end - end - - # Provides helper methods for testing Action Cable broadcasting - module TestHelper - def before_setup # :nodoc: - server = ActionCable.server - test_adapter = ActionCable::SubscriptionAdapter::Test.new(server) - - @old_pubsub_adapter = server.pubsub - - server.instance_variable_set(:@pubsub, test_adapter) - super - end - - def after_teardown # :nodoc: - super - ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter) - end - - # Asserts that the number of broadcasted messages to the stream matches the given number. - # - # def test_broadcasts - # assert_broadcasts 'messages', 0 - # ActionCable.server.broadcast 'messages', { text: 'hello' } - # assert_broadcasts 'messages', 1 - # ActionCable.server.broadcast 'messages', { text: 'world' } - # assert_broadcasts 'messages', 2 - # end - # - # If a block is passed, that block should cause the specified number of - # messages to be broadcasted. - # - # def test_broadcasts_again - # assert_broadcasts('messages', 1) do - # ActionCable.server.broadcast 'messages', { text: 'hello' } - # end - # - # assert_broadcasts('messages', 2) do - # ActionCable.server.broadcast 'messages', { text: 'hi' } - # ActionCable.server.broadcast 'messages', { text: 'how are you?' } - # end - # end - # - def assert_broadcasts(stream, number) - if block_given? - original_count = broadcasts_size(stream) - yield - new_count = broadcasts_size(stream) - actual_count = new_count - original_count - else - actual_count = broadcasts_size(stream) - end - - assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent" - end - - # Asserts that no messages have been sent to the stream. - # - # def test_no_broadcasts - # assert_no_broadcasts 'messages' - # ActionCable.server.broadcast 'messages', { text: 'hi' } - # assert_broadcasts 'messages', 1 - # end - # - # If a block is passed, that block should not cause any message to be sent. - # - # def test_broadcasts_again - # assert_no_broadcasts 'messages' do - # # No job messages should be sent from this block - # end - # end - # - # Note: This assertion is simply a shortcut for: - # - # assert_broadcasts 'messages', 0, &block - # - def assert_no_broadcasts(stream, &block) - assert_broadcasts stream, 0, &block - end - - # Asserts that the specified message has been sent to the stream. - # - # def test_assert_transmitted_message - # ActionCable.server.broadcast 'messages', text: 'hello' - # assert_broadcast_on('messages', text: 'hello') - # end - # - # If a block is passed, that block should cause a message with the specified data to be sent. - # - # def test_assert_broadcast_on_again - # assert_broadcast_on('messages', text: 'hello') do - # ActionCable.server.broadcast 'messages', text: 'hello' - # end - # end - # - def assert_broadcast_on(stream, data) - # Encode to JSON and back–we want to use this value to compare - # with decoded JSON. - # Comparing JSON strings doesn't work due to the order of the keys. - serialized_msg = - ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data)) - - new_messages = broadcasts(stream) - if block_given? - old_messages = new_messages - clear_messages(stream) - - yield - new_messages = broadcasts(stream) - clear_messages(stream) - - # Restore all sent messages - (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) } - end - - message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg } - - assert message, "No messages sent with #{data} to #{stream}" - end - - def pubsub_adapter # :nodoc: - ActionCable.server.pubsub - end - - delegate :broadcasts, :clear_messages, to: :pubsub_adapter - - private - - def broadcasts_size(channel) - broadcasts(channel).size - end - end -end diff --git a/lib/rails_6_polyfills/activejob/serializers.rb b/lib/rails_6_polyfills/activejob/serializers.rb deleted file mode 100644 index 73c533d6..00000000 --- a/lib/rails_6_polyfills/activejob/serializers.rb +++ /dev/null @@ -1,240 +0,0 @@ -# frozen_string_literal: true - -# First add Rails 6.0 ActiveJob Serializers support, and then the -# DurationSerializer and SymbolSerializer. -module ActiveJob - module Arguments - # :nodoc: - OBJECT_SERIALIZER_KEY = "_aj_serialized" - - def serialize_argument(argument) - case argument - when *TYPE_WHITELIST - argument - when GlobalID::Identification - convert_to_global_id_hash(argument) - when Array - argument.map { |arg| serialize_argument(arg) } - when ActiveSupport::HashWithIndifferentAccess - serialize_indifferent_hash(argument) - when Hash - symbol_keys = argument.each_key.grep(Symbol).map(&:to_s) - result = serialize_hash(argument) - result[SYMBOL_KEYS_KEY] = symbol_keys - result - when ->(arg) { arg.respond_to?(:permitted?) } - serialize_indifferent_hash(argument.to_h) - else # Add Rails 6 support for Serializers - Serializers.serialize(argument) - end - end - - def deserialize_argument(argument) - case argument - when String - argument - when *TYPE_WHITELIST - argument - when Array - argument.map { |arg| deserialize_argument(arg) } - when Hash - if serialized_global_id?(argument) - deserialize_global_id argument - elsif custom_serialized?(argument) - Serializers.deserialize(argument) - else - deserialize_hash(argument) - end - else - raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}" - end - end - - def custom_serialized?(hash) - hash.key?(OBJECT_SERIALIZER_KEY) - end - end - - # The ActiveJob::Serializers module is used to store a list of known serializers - # and to add new ones. It also has helpers to serialize/deserialize objects. - module Serializers # :nodoc: - # Base class for serializing and deserializing custom objects. - # - # Example: - # - # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer - # def serialize(money) - # super("amount" => money.amount, "currency" => money.currency) - # end - # - # def deserialize(hash) - # Money.new(hash["amount"], hash["currency"]) - # end - # - # private - # - # def klass - # Money - # end - # end - class ObjectSerializer - include Singleton - - class << self - delegate :serialize?, :serialize, :deserialize, to: :instance - end - - # Determines if an argument should be serialized by a serializer. - def serialize?(argument) - argument.is_a?(klass) - end - - # Serializes an argument to a JSON primitive type. - def serialize(hash) - {Arguments::OBJECT_SERIALIZER_KEY => self.class.name}.merge!(hash) - end - - # Deserializes an argument from a JSON primitive type. - def deserialize(_argument) - raise NotImplementedError - end - - private - - # The class of the object that will be serialized. - def klass - raise NotImplementedError - end - end - - class DurationSerializer < ObjectSerializer # :nodoc: - def serialize(duration) - super("value" => duration.value, "parts" => Arguments.serialize(duration.parts.each_with_object({}) { |v, s| s[v.first.to_s] = v.last })) - end - - def deserialize(hash) - value = hash["value"] - parts = Arguments.deserialize(hash["parts"]) - - klass.new(value, parts) - end - - private - - def klass - ActiveSupport::Duration - end - end - - class SymbolSerializer < ObjectSerializer # :nodoc: - def serialize(argument) - super("value" => argument.to_s) - end - - def deserialize(argument) - argument["value"].to_sym - end - - private - - def klass - Symbol - end - end - - # ----------------------------- - - mattr_accessor :_additional_serializers - self._additional_serializers = Set.new - - class << self - # Returns serialized representative of the passed object. - # Will look up through all known serializers. - # Raises ActiveJob::SerializationError if it can't find a proper serializer. - def serialize(argument) - serializer = serializers.detect { |s| s.serialize?(argument) } - raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer - serializer.serialize(argument) - end - - # Returns deserialized object. - # Will look up through all known serializers. - # If no serializer found will raise ArgumentError. - def deserialize(argument) - serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY] - raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name - - serializer = serializer_name.safe_constantize - raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer - - serializer.deserialize(argument) - end - - # Returns list of known serializers. - def serializers - self._additional_serializers # standard:disable Style/RedundantSelf - end - - # Adds new serializers to a list of known serializers. - def add_serializers(*new_serializers) - self._additional_serializers += new_serializers.flatten - end - end - - add_serializers DurationSerializer, - SymbolSerializer - # The full set of 6 serializers that Rails 6.0 normally adds here -- feel free to include any others if you wish: - # SymbolSerializer, - # DurationSerializer, # (The one that we've added above in order to support testing) - # DateTimeSerializer, - # DateSerializer, - # TimeWithZoneSerializer, - # TimeSerializer - end - - # Is the updated version of perform_enqueued_jobs from Rails 6.0 missing from ActionJob's TestHelper? - unless TestHelper.private_instance_methods.include?(:flush_enqueued_jobs) - module TestHelper - def perform_enqueued_jobs(only: nil, except: nil, queue: nil) - return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given? - - super - end - - private - - def jobs_with(jobs, only: nil, except: nil, queue: nil) - validate_option(only: only, except: except) - - jobs.count do |job| - job_class = job.fetch(:job) - - if only - next false unless filter_as_proc(only).call(job) - elsif except - next false if filter_as_proc(except).call(job) - end - - if queue - next false unless queue.to_s == job.fetch(:queue, job_class.queue_name) - end - - yield job if block_given? - - true - end - end - - def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block) - jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block) - end - - def flush_enqueued_jobs(only: nil, except: nil, queue: nil) - enqueued_jobs_with(only: only, except: except, queue: queue) do |payload| - instantiate_job(payload).perform_now - queue_adapter.performed_jobs << payload - end - end - end - end -end diff --git a/lib/rails_6_polyfills/base.rb b/lib/rails_6_polyfills/base.rb deleted file mode 100644 index efdef208..00000000 --- a/lib/rails_6_polyfills/base.rb +++ /dev/null @@ -1,18 +0,0 @@ -# The following implements polyfills for Rails < 6.0 -module ActionCable - # If the Rails 6.0 ActionCable::TestHelper is missing then allow it to autoload - unless ActionCable.const_defined? :TestHelper - autoload :TestHelper, "rails_6_polyfills/actioncable/test_helper.rb" - end - # If the Rails 6.0 test SubscriptionAdapter is missing then allow it to autoload - unless ActionCable::SubscriptionAdapter.const_defined? :Test - module SubscriptionAdapter - autoload :Test, "rails_6_polyfills/actioncable/test_adapter.rb" - end - end -end - -# If the Rails 6.0 ActionJob Serializers are missing then load support for them -unless ActiveJob.const_defined?(:Serializers) - require "rails_6_polyfills/activejob/serializers" -end diff --git a/lib/tasks/noticed_tasks.rake b/lib/tasks/noticed_tasks.rake deleted file mode 100644 index a01ede42..00000000 --- a/lib/tasks/noticed_tasks.rake +++ /dev/null @@ -1,4 +0,0 @@ -# desc "Explaining what the task does" -# task :noticed do -# # Task goes here -# end diff --git a/noticed.gemspec b/noticed.gemspec index 83d97d20..df8837aa 100644 --- a/noticed.gemspec +++ b/noticed.gemspec @@ -16,6 +16,5 @@ Gem::Specification.new do |spec| spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] - spec.add_dependency "rails", ">= 5.2.0" - spec.add_dependency "http", ">= 4.0.0" + spec.add_dependency "rails", ">= 6.1.0" end diff --git a/test/bulk_delivery_methods/webhook_test.rb b/test/bulk_delivery_methods/webhook_test.rb new file mode 100644 index 00000000..e1ec9369 --- /dev/null +++ b/test/bulk_delivery_methods/webhook_test.rb @@ -0,0 +1,58 @@ +require "test_helper" + +class WebhookBulkDeliveryMethodTest < ActiveSupport::TestCase + setup do + @delivery_method = Noticed::BulkDeliveryMethods::Webhook.new + end + + test "webhook with json payload" do + set_config( + url: "https://example.org/webhook", + json: {foo: :bar} + ) + stub_request(:post, "https://example.org/webhook").with(body: "{\"foo\":\"bar\"}") + + @delivery_method.deliver + end + + test "webhook with form payload" do + set_config( + url: "https://example.org/webhook", + form: {foo: :bar} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => /multipart\/form-data/}) + @delivery_method.deliver + end + + test "webhook with basic auth" do + set_config( + url: "https://example.org/webhook", + basic_auth: {user: "username", pass: "password"} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="}) + @delivery_method.deliver + end + + test "webhook with headers" do + set_config( + url: "https://example.org/webhook", + headers: {"Content-Type" => "application/json"} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => "application/json"}) + @delivery_method.deliver + end + + test "webhook raises error with unsuccessful status codes" do + set_config(url: "https://example.org/webhook") + stub_request(:post, "https://example.org/webhook").to_return(status: 422) + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end + end + + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) + end +end diff --git a/test/delivery_method_test.rb b/test/delivery_method_test.rb new file mode 100644 index 00000000..f174025c --- /dev/null +++ b/test/delivery_method_test.rb @@ -0,0 +1,42 @@ +require "test_helper" + +class DeliveryMethodTest < ActiveSupport::TestCase + class InheritedDeliveryMethod < Noticed::DeliveryMethods::ActionCable + end + + test "fetch_constant looks up constants" do + @delivery_method = Noticed::DeliveryMethod.new + set_config(mailer: "UserMailer") + assert_equal UserMailer, @delivery_method.fetch_constant(:mailer) + end + + test "delivery methods inhiert required options" do + assert_equal [:channel, :stream, :message], InheritedDeliveryMethod.required_option_names + end + + test "if config" do + event = TestNotifier.deliver(User.first) + notification = event.notifications.first + @delivery_method = Noticed::DeliveryMethods::Test.new + + assert @delivery_method.perform(:test, notification, overrides: {if: true}) + assert @delivery_method.perform(:test, notification, overrides: {if: -> { unread? }}) + refute @delivery_method.perform(:test, notification, overrides: {if: false}) + end + + test "unless overrides" do + event = TestNotifier.deliver(User.first) + notification = event.notifications.first + @delivery_method = Noticed::DeliveryMethods::Test.new + + refute @delivery_method.perform(:test, notification, overrides: {unless: true}) + assert @delivery_method.perform(:test, notification, overrides: {unless: false}) + assert @delivery_method.perform(:test, notification, overrides: {unless: -> { read? }}) + end + + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) + end +end diff --git a/test/delivery_methods/action_cable_test.rb b/test/delivery_methods/action_cable_test.rb index b09905c8..38c8b163 100644 --- a/test/delivery_methods/action_cable_test.rb +++ b/test/delivery_methods/action_cable_test.rb @@ -1,51 +1,30 @@ require "test_helper" -class FakeChannel < ApplicationCable::Channel -end - -class FakeChannelNotification < Noticed::Base - deliver_by :action_cable, channel: :get_channel +class ActionCableDeliveryMethodTest < ActiveSupport::TestCase + include ActionCable::TestHelper - def get_channel - FakeChannel + setup do + @delivery_method = Noticed::DeliveryMethods::ActionCable.new end -end -class ActionCableTest < ActiveSupport::TestCase test "sends websocket message" do - channel = Noticed::NotificationChannel.broadcasting_for(user) - assert_broadcasts(channel, 1) do - CommentNotification.new.deliver(user) - end - end + user = users(:one) + channel = NotificationChannel.broadcasting_for(user) - test "accepts channel as string" do - delivery_method = Noticed::DeliveryMethods::ActionCable.new - delivery_method.instance_variable_set(:@options, {channel: "FakeChannel"}) - assert_equal FakeChannel, delivery_method.send(:channel) - end - - test "accepts channel as object" do - delivery_method = Noticed::DeliveryMethods::ActionCable.new - delivery_method.instance_variable_set(:@options, {channel: FakeChannel}) - assert_equal FakeChannel, delivery_method.send(:channel) - end + set_config( + channel: "NotificationChannel", + stream: user, + message: {foo: :bar} + ) - test "accepts channel as symbol" do - delivery_method = Noticed::DeliveryMethods::ActionCable.new - delivery_method.instance_variable_set(:@notification, FakeChannelNotification.new) - delivery_method.instance_variable_set(:@options, {channel: :get_channel}) - assert_equal FakeChannel, delivery_method.send(:channel) + assert_broadcasts(channel, 1) do + @delivery_method.deliver + end end - test "deliver returns nothing" do - args = { - notification_class: "Noticed::Base", - recipient: user, - options: {} - } - nothing = Noticed::DeliveryMethods::ActionCable.new.perform(args) + private - assert_nil nothing + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/base_test.rb b/test/delivery_methods/base_test.rb deleted file mode 100644 index 4a111484..00000000 --- a/test/delivery_methods/base_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -require "test_helper" - -class CustomDeliveryMethod < Noticed::DeliveryMethods::Base - class_attribute :deliveries, default: [] - - def deliver - self.class.deliveries << params - end -end - -class CustomDeliveryMethodExample < Noticed::Base - deliver_by :example, class: "CustomDeliveryMethod" -end - -class DeliveryMethodWithOptions < Noticed::DeliveryMethods::Test - option :foo -end - -class DeliveryMethodWithOptionsExample < Noticed::Base - deliver_by :example, class: "DeliveryMethodWithOptions" -end - -class DeliveryMethodWithNilOptionsExample < Noticed::Base - deliver_by :example, class: "DeliveryMethodWithOptions", foo: nil -end - -class Noticed::DeliveryMethods::BaseTest < ActiveSupport::TestCase - test "can use custom delivery method with params" do - CustomDeliveryMethodExample.new.deliver(user) - assert_equal 1, CustomDeliveryMethod.deliveries.count - end - - test "validates delivery method options" do - assert_raises Noticed::ValidationError do - DeliveryMethodWithOptionsExample.new.deliver(user) - end - end - - test "nil options are valid" do - assert_difference "Noticed::DeliveryMethods::Test.delivered.count" do - DeliveryMethodWithNilOptionsExample.new.deliver(user) - end - end -end diff --git a/test/delivery_methods/database_test.rb b/test/delivery_methods/database_test.rb deleted file mode 100644 index b0518973..00000000 --- a/test/delivery_methods/database_test.rb +++ /dev/null @@ -1,66 +0,0 @@ -require "test_helper" - -class DatabaseTest < ActiveSupport::TestCase - class JustDatabaseDelivery < Noticed::Base - deliver_by :database - end - - class WithDelayedDatabaseDelivery < Noticed::Base - deliver_by :database, delay: 5.minutes - end - - test "writes to database" do - notification = CommentNotification.with(foo: :bar) - - assert_difference "user.notifications.count" do - assert_difference "Notification.count" do - notification.deliver(user) - end - end - - assert_equal :bar, user.notifications.last.params[:foo] - end - - test "delivery is executed but not enqueued" do - assert_difference "Notification.count" do - JustDatabaseDelivery.new.deliver_later(user) - assert_enqueued_jobs 0 - end - end - - test "writes to custom params database" do - CommentNotification.with(foo: :bar).deliver(user) - assert_equal 1, user.notifications.last.account_id - end - - test "writes to the database before other delivery methods" do - CommentNotification.with(foo: :bar).deliver_later(user) - perform_enqueued_jobs - assert_not_nil Notification.last - assert_equal Notification.last, Noticed::DeliveryMethods::Test.delivered.first.record - end - - test "serializes database attributes like ActiveJob does" do - assert_difference "Notification.count" do - CommentNotification.with(user: user).deliver(user) - end - assert_equal @user, Notification.last.params[:user] - end - - test "deliver returns the created record" do - args = { - notification_class: "Noticed::Base", - recipient: user, - options: {} - } - record = Noticed::DeliveryMethods::Database.new.perform(args) - - assert_kind_of ActiveRecord::Base, record - end - - test "delay option is not provided" do - assert_raises ArgumentError do - WithDelayedDatabaseDelivery.new.deliver(user) - end - end -end diff --git a/test/delivery_methods/email_test.rb b/test/delivery_methods/email_test.rb index e4b73a34..e1e8b18a 100644 --- a/test/delivery_methods/email_test.rb +++ b/test/delivery_methods/email_test.rb @@ -1,46 +1,40 @@ require "test_helper" -class EmailDeliveryWithoutMailer < Noticed::Base - deliver_by :email -end - -class EmailDeliveryWithActiveJob < Noticed::Base - deliver_by :email, mailer: "UserMailer", enqueue: true, method: "comment_notification" -end - class EmailTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + setup do - @user = users(:one) + @delivery_method = Noticed::DeliveryMethods::Email.new end test "sends email" do - assert_emails 1 do - CommentNotification.new.deliver(user) + set_config( + mailer: "UserMailer", + method: "new_comment", + params: -> { {foo: :bar} }, + args: -> { ["hey"] } + ) + + assert_emails(1) do + @delivery_method.deliver end end - test "validates `mailer` is specified for email delivery method" do - assert_raises Noticed::ValidationError do - EmailDeliveryWithoutMailer.new.deliver(user) + test "enqueues email" do + set_config( + mailer: "UserMailer", + method: "receipt", + enqueue: true + ) + + assert_enqueued_emails(1) do + @delivery_method.deliver end end - test "deliver returns the email object" do - args = { - notification_class: "Noticed::Base", - recipient: user, - options: { - mailer: "UserMailer", - method: "comment_notification" - } - } - email = Noticed::DeliveryMethods::Email.new.perform(args) - - assert_kind_of Mail::Message, email - end + private - test "delivery spawns an ActiveJob for email" do - EmailDeliveryWithActiveJob.new.deliver(user) - assert_enqueued_emails 1 + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/fcm_test.rb b/test/delivery_methods/fcm_test.rb index a17940d4..3eb58275 100644 --- a/test/delivery_methods/fcm_test.rb +++ b/test/delivery_methods/fcm_test.rb @@ -1,82 +1,76 @@ require "test_helper" -class FcmExample < Noticed::Base - deliver_by :fcm, credentials: :fcm_credentials, format: :format_notification - - def fcm_credentials - {project_id: "api-12345"} - end - - def fcm_credentials_as_pathname - Rails.root.join("config/credentials/fcm.json") - end - - def fcm_credentials_as_string - "config/credentials/fcm.json" - end - - def format_notification(device_token) - { - token: device_token, - notification: { - title: "Hey Chris", - body: "Am I worky?" - } - } - end -end - -class FakeAuthorizer - def self.make_creds(options = {}) - new - end - - def fetch_access_token! - {"access_token" => "access-token-12341234"} - end -end - class FcmTest < ActiveSupport::TestCase - test "when credentials option is a hash, it returns the hash" do - credentials_hash = {foo: "bar"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: credentials_hash}).credentials + class FakeAuthorizer + def self.make_creds(options = {}) + new + end + + def fetch_access_token! + {"access_token" => "access-token-12341234"} + end end - test "when credentials option is a Pathname object, it returns the file contents" do - credentials_hash = {project_id: "api-12345"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: Rails.root.join("config/credentials/fcm.json")}).credentials + setup do + @delivery_method = Noticed::DeliveryMethods::Fcm.new end - test "when credentials option is a string, it returns the file contents" do - credentials_hash = {project_id: "api-12345"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: "config/credentials/fcm.json"}).credentials - end + test "notifies each device token" do + set_config( + authorizer: FakeAuthorizer, + credentials: { + "type" => "service_account", + "project_id" => "p_1234", + "private_key_id" => "private_key" + }, + device_tokens: [:a, :b], + json: ->(device_token) { + { + message: { + token: device_token, + notification: {title: "Title", body: "Body"} + } + } + } + ) - test "when credentials option is a symbol and return value of a method is a hash, it returns the hash" do - credentials_hash = {project_id: "api-12345"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials}).credentials - end + stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").with(body: "{\"message\":{\"token\":\"a\",\"notification\":{\"title\":\"Title\",\"body\":\"Body\"}}}") + stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").with(body: "{\"message\":{\"token\":\"b\",\"notification\":{\"title\":\"Title\",\"body\":\"Body\"}}}") - test "when credentials option is a symbol and return value of a method is a Pathname object, it returns the file contents" do - credentials_hash = {project_id: "api-12345"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials_as_pathname}).credentials + @delivery_method.deliver end - test "when credentials option is a symbol and return value of a method is a string, it returns the file contents" do - credentials_hash = {project_id: "api-12345"} - assert_equal credentials_hash, Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials_as_string}).credentials + test "notifies of invalid tokens for clean up" do + cleanups = 0 + + set_config( + authorizer: FakeAuthorizer, + credentials: { + "type" => "service_account", + "project_id" => "p_1234", + "private_key_id" => "private_key" + }, + device_tokens: [:a, :b], + json: ->(device_token) { + { + message: { + token: device_token, + notification: {title: "Title", body: "Body"} + } + } + }, + invalid_token: ->(device_token) { cleanups += 1 } + ) + + stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").to_return(status: 404, body: "", headers: {}) + + @delivery_method.deliver + assert_equal 2, cleanups end - test "project_id returns the project id value from the credentials" do - assert_equal "api-12345", Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials}).project_id - end - - test "access token returns a string" do - assert_equal "access-token-12341234", Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials, authorizer: FakeAuthorizer}).access_token - end + private - test "format" do - fcm_message = Noticed::DeliveryMethods::Fcm.new.assign_args(notification_class: "FcmExample", options: {credentials: :fcm_credentials, format: :format_notification}).format("12345") - assert_equal "12345", fcm_message.fetch(:token) + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/ios_test.rb b/test/delivery_methods/ios_test.rb index 8b90f924..506e6b0f 100644 --- a/test/delivery_methods/ios_test.rb +++ b/test/delivery_methods/ios_test.rb @@ -1,80 +1,83 @@ require "test_helper" -class IosExample < Noticed::Base - deliver_by :ios +class IosTest < ActiveSupport::TestCase + class FakeConnectionPool + class_attribute :invalid_tokens, default: [] + attr_reader :deliveries + + def initialize(response) + @response = response + @deliveries = [] + end - def ios_device_tokens(recipient) - [] + def with + yield self + end + + def push(apn) + @deliveries.push(apn) + @response + end end -end -class IosExampleWithoutDeviceTokens < Noticed::Base - deliver_by :ios -end + class FakeResponse + attr_reader :status -class IosTest < ActiveSupport::TestCase - test "raises error when bundle_identifier missing" do - exception = assert_raises ArgumentError do - Noticed::DeliveryMethods::Ios.new.perform(notification_class: "IosExample") + def initialize(status, body = {}) + @status = status end - assert_equal "bundle_identifier is missing", exception.message + def ok? + status.start_with?("20") + end end - test "raises error when key_id missing" do - exception = assert_raises ArgumentError do - Noticed::DeliveryMethods::Ios.new.perform( - notification_class: "IosExample", - options: { - bundle_identifier: "test" - } - ) - end + setup do + FakeConnectionPool.invalid_tokens = [] - assert_equal "key_id is missing", exception.message + @delivery_method = Noticed::DeliveryMethods::Ios.new + @delivery_method.instance_variable_set :@notification, noticed_notifications(:one) + set_config( + bundle_identifier: "bundle_id", + key_id: "key_id", + team_id: "team_id", + apns_key: "apns_key", + device_tokens: [:a, :b], + format: ->(apn) { + apn.alert = "Hello world" + apn.custom_payload = {url: root_url(host: "example.org")} + }, + invalid_token: ->(device_token) { + FakeConnectionPool.invalid_tokens << device_token + } + ) end - test "raises error when team_id missing" do - exception = assert_raises ArgumentError do - Noticed::DeliveryMethods::Ios.new.perform( - notification_class: "IosExample", - options: { - bundle_identifier: "test", - key_id: "test" - } - ) + test "notifies each device token" do + connection_pool = FakeConnectionPool.new(FakeResponse.new("200")) + @delivery_method.stub(:connection_pool, connection_pool) do + @delivery_method.deliver end - assert_equal "team_id is missing", exception.message + assert_equal 2, connection_pool.deliveries.count + assert_equal 0, FakeConnectionPool.invalid_tokens.count end - test "raises error when cert missing" do - exception = assert_raises ArgumentError do - Noticed::DeliveryMethods::Ios.new.perform( - notification_class: "IosExample", - options: { - bundle_identifier: "test", - key_id: "test", - team_id: "test" - } - ) + test "notifies of invalid tokens for cleanup" do + connection_pool = FakeConnectionPool.new(FakeResponse.new("410")) + @delivery_method.stub(:connection_pool, connection_pool) do + @delivery_method.deliver end - assert_match "Could not find APN cert at", exception.message + # Our fake connection pool doesn't understand these wouldn't be delivered in the real world + assert_equal 2, connection_pool.deliveries.count + + assert_equal 2, FakeConnectionPool.invalid_tokens.count end - test "raises error when ios_device_tokens method is missing" do - assert_raises NoMethodError do - File.stub :exist?, true do - Noticed::DeliveryMethods::Ios.new.perform( - notification_class: "IosExampleWithoutDeviceTokens", - options: { - bundle_identifier: "test", - key_id: "test", - team_id: "test" - } - ) - end - end + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/microsoft_teams_test.rb b/test/delivery_methods/microsoft_teams_test.rb index decf0f86..e8eed267 100644 --- a/test/delivery_methods/microsoft_teams_test.rb +++ b/test/delivery_methods/microsoft_teams_test.rb @@ -1,60 +1,29 @@ require "test_helper" class MicrosoftTeamsTest < ActiveSupport::TestCase - class MicrosoftTeamsExample < Noticed::Base - deliver_by :microsoft_teams, debug: true, url: :teams_url, format: :to_teams - - def teams_url - "https://outlook.office.com/webhooks/00000-00000/IncomingWebhook/00000-00000" - end - - def to_teams - { - title: "This is the title", - text: "this is the text", - section: sections, - potentialAction: actions - } - end - - def sections - [{activityTitle: "Section Title", activityText: "Section Text"}] - end - - def actions - [{ - "@type": "OpenUri", - name: "View on Foo.Com", - targets: [{os: "default", uri: "https://foo.example.com"}] - }] - end + setup do + @delivery_method = Noticed::DeliveryMethods::MicrosoftTeams.new + set_config( + json: {foo: :bar}, + url: "https://teams.microsoft.com" + ) end - test "sends a POST to Teams" do - stub_delivery_method_request(delivery_method: :microsoft_teams, matcher: /outlook.office.com/) - MicrosoftTeamsExample.new.deliver(user) + test "sends a message" do + stub_request(:post, "https://teams.microsoft.com").with(body: "{\"foo\":\"bar\"}") + @delivery_method.deliver end - test "raises an error when http request fails" do - stub_delivery_method_request(delivery_method: :microsoft_teams, matcher: /outlook.office.com/, type: :failure) - - e = assert_raises(::Noticed::ResponseUnsuccessful) { - MicrosoftTeamsExample.new.deliver(user) - } - - assert_equal HTTP::Response, e.response.class + test "raises error on failure" do + stub_request(:post, "https://teams.microsoft.com").to_return(status: 422) + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end end - test "deliver returns an http response" do - stub_delivery_method_request(delivery_method: :microsoft_teams, matcher: /outlook.office.com/) - - args = { - notification_class: "::MicrosoftTeamsTest::MicrosoftTeamsExample", - recipient: user, - options: {url: :teams_url, format: :to_teams} - } - response = Noticed::DeliveryMethods::MicrosoftTeams.new.perform(args) + private - assert_kind_of HTTP::Response, response + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/slack_test.rb b/test/delivery_methods/slack_test.rb index 3ceb0ab1..fdd671e2 100644 --- a/test/delivery_methods/slack_test.rb +++ b/test/delivery_methods/slack_test.rb @@ -1,58 +1,26 @@ require "test_helper" class SlackTest < ActiveSupport::TestCase - class TestLogger - attr_reader :logs - - def debug(msg) - @logs ||= [] - @logs << msg - end - end - - class SlackExample < Noticed::Base - deliver_by :slack, debug: true, url: :slack_url, logger: TestLogger.new - - def slack_url - "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" - end + setup do + @delivery_method = Noticed::DeliveryMethods::Slack.new + set_config(json: {foo: :bar}) end - test "sends a POST to Slack" do - stub_delivery_method_request(delivery_method: :slack, matcher: /hooks.slack.com/) - SlackExample.new.deliver(user) + test "sends a slack message" do + stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}") + @delivery_method.deliver end - test "raises an error when http request fails" do - stub_delivery_method_request(delivery_method: :slack, matcher: /hooks.slack.com/, type: :failure) - e = assert_raises(::Noticed::ResponseUnsuccessful) { - SlackExample.new.deliver(user) - } - assert_equal HTTP::Response, e.response.class - end - - test "deliver returns an http response" do - stub_delivery_method_request(delivery_method: :slack, matcher: /hooks.slack.com/) - - args = { - notification_class: "::SlackTest::SlackExample", - recipient: user, - options: {url: :slack_url} - } - response = Noticed::DeliveryMethods::Slack.new.perform(args) - - assert_kind_of HTTP::Response, response + test "raises error on failure" do + stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).to_return(status: 422) + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end end - test "logs verbosely in debug mode" do - stub_delivery_method_request(delivery_method: :slack, matcher: /hooks.slack.com/) - - SlackExample.new.deliver(user) + private - logger = SlackExample.delivery_methods.find { |m| m[:name] == :slack }.dig(:options, :logger) - assert_equal logger.logs[-2..], [ - "POST https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX", - "Response: 200: ok\r\n" - ] + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) end end diff --git a/test/delivery_methods/twilio_messaging_test.rb b/test/delivery_methods/twilio_messaging_test.rb new file mode 100644 index 00000000..7f6a7bda --- /dev/null +++ b/test/delivery_methods/twilio_messaging_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +class TwilioMessagingTest < ActiveSupport::TestCase + setup do + @delivery_method = Noticed::DeliveryMethods::TwilioMessaging.new + set_config( + account_sid: "acct_1234", + auth_token: "token", + json: -> { + { + From: "+1234567890", + To: "+1234567890", + Body: "Hello world" + } + } + ) + end + + test "sends sms" do + stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/acct_1234/Messages.json").with( + headers: { + "Authorization" => "Basic YWNjdF8xMjM0OnRva2Vu", + "Content-Type" => "multipart/form-data" + } + ).to_return(status: 200) + @delivery_method.deliver + end + + test "raises error on failure" do + stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/acct_1234/Messages.json").to_return(status: 422) + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end + end + + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) + end +end diff --git a/test/delivery_methods/twilio_test.rb b/test/delivery_methods/twilio_test.rb deleted file mode 100644 index 49047293..00000000 --- a/test/delivery_methods/twilio_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "test_helper" - -class TwilioTest < ActiveSupport::TestCase - class TwilioExample < Noticed::Base - deliver_by :twilio, credentials: :twilio_creds, debug: true # , ignore_failure: true - - def twilio_creds - { - account_sid: "a", - auth_token: "b", - phone_number: "c" - } - end - end - - test "sends a POST to Twilio" do - stub_delivery_method_request(delivery_method: :twilio, matcher: /api.twilio.com/) - TwilioExample.new.deliver(user) - end - - test "raises an error when http request fails" do - stub_delivery_method_request(delivery_method: :twilio, matcher: /api.twilio.com/, type: :failure) - e = assert_raises(::Noticed::ResponseUnsuccessful) { - TwilioExample.new.deliver(user) - } - assert_equal HTTP::Response, e.response.class - end - - test "deliver returns an http response" do - stub_delivery_method_request(delivery_method: :twilio, matcher: /api.twilio.com/) - - args = { - notification_class: "::TwilioTest::TwilioExample", - recipient: user, - options: {credentials: :twilio_creds} - } - response = Noticed::DeliveryMethods::Twilio.new.perform(args) - - assert_kind_of HTTP::Response, response - end -end diff --git a/test/delivery_methods/vonage_sms_test.rb b/test/delivery_methods/vonage_sms_test.rb new file mode 100644 index 00000000..dd49c48e --- /dev/null +++ b/test/delivery_methods/vonage_sms_test.rb @@ -0,0 +1,27 @@ +require "test_helper" + +class VonageSmsTest < ActiveSupport::TestCase + setup do + @delivery_method = Noticed::DeliveryMethods::VonageSms.new + end + + test "sends sms" do + set_config(json: {foo: :bar}) + stub_request(:post, Noticed::DeliveryMethods::VonageSms::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}").to_return(status: 200, body: "{\"messages\":[{\"status\":\"0\"}]}") + @delivery_method.deliver + end + + test "raises error on failure" do + set_config(json: {foo: :bar}) + stub_request(:post, Noticed::DeliveryMethods::VonageSms::DEFAULT_URL).to_return(status: 200, body: "{\"messages\":[{\"status\":\"1\"}]}") + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end + end + + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) + end +end diff --git a/test/delivery_methods/vonage_test.rb b/test/delivery_methods/vonage_test.rb deleted file mode 100644 index a7819a2e..00000000 --- a/test/delivery_methods/vonage_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -require "test_helper" - -class VonageTest < ActiveSupport::TestCase - class VonageExample < Noticed::Base - deliver_by :vonage, format: :to_vonage, debug: true - - def to_vonage - { - api_key: "a", - api_secret: "b", - from: "c", - text: "d", - to: "e", - type: "unicode" - } - end - end - - test "sends a POST to Vonage" do - stub_delivery_method_request(delivery_method: :vonage, matcher: /rest.nexmo.com/) - VonageExample.new.deliver(user) - end - - test "raises an error when http request fails" do - stub_delivery_method_request(delivery_method: :vonage, matcher: /rest.nexmo.com/, type: :failure) - e = assert_raises(::Noticed::ResponseUnsuccessful) { - VonageExample.new.deliver(user) - } - assert_equal HTTP::Response, e.response.class - end - - test "deliver returns an http response" do - stub_delivery_method_request(delivery_method: :vonage, matcher: /rest.nexmo.com/) - - args = { - notification_class: "::VonageTest::VonageExample", - recipient: user, - options: {format: :to_vonage} - } - response = Noticed::DeliveryMethods::Vonage.new.perform(args) - - assert_kind_of HTTP::Response, response - end -end diff --git a/test/delivery_methods/webhook_test.rb b/test/delivery_methods/webhook_test.rb new file mode 100644 index 00000000..3e6240e7 --- /dev/null +++ b/test/delivery_methods/webhook_test.rb @@ -0,0 +1,58 @@ +require "test_helper" + +class WebhookDeliveryMethodTest < ActiveSupport::TestCase + setup do + @delivery_method = Noticed::DeliveryMethods::Webhook.new + end + + test "webhook with json payload" do + set_config( + url: "https://example.org/webhook", + json: {foo: :bar} + ) + stub_request(:post, "https://example.org/webhook").with(body: "{\"foo\":\"bar\"}") + + @delivery_method.deliver + end + + test "webhook with form payload" do + set_config( + url: "https://example.org/webhook", + form: {foo: :bar} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => /multipart\/form-data/}) + @delivery_method.deliver + end + + test "webhook with basic auth" do + set_config( + url: "https://example.org/webhook", + basic_auth: {user: "username", pass: "password"} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="}) + @delivery_method.deliver + end + + test "webhook with headers" do + set_config( + url: "https://example.org/webhook", + headers: {"Content-Type" => "application/json"} + ) + stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => "application/json"}) + @delivery_method.deliver + end + + test "webhook raises error with unsuccessful status codes" do + set_config(url: "https://example.org/webhook") + stub_request(:post, "https://example.org/webhook").to_return(status: 422) + assert_raises Noticed::ResponseUnsuccessful do + @delivery_method.deliver + end + end + + private + + def set_config(config) + @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config) + end +end diff --git a/test/dummy/.ruby-version b/test/dummy/.ruby-version deleted file mode 100644 index 2eb2fe97..00000000 --- a/test/dummy/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -ruby-2.7.2 diff --git a/test/dummy/app/channels/application_cable/connection.rb b/test/dummy/app/channels/application_cable/connection.rb index 0ff5442f..6bf0c201 100644 --- a/test/dummy/app/channels/application_cable/connection.rb +++ b/test/dummy/app/channels/application_cable/connection.rb @@ -1,4 +1,18 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + def connect + self.current_user = find_verified_user + logger.add_tags "ActionCable", "User #{current_user.id}" + end + + protected + + def find_verified_user + if (current_user = env["warden"].user(:user)) + current_user + else + reject_unauthorized_connection + end + end end end diff --git a/test/dummy/app/channels/notification_channel.rb b/test/dummy/app/channels/notification_channel.rb new file mode 100644 index 00000000..66cdb454 --- /dev/null +++ b/test/dummy/app/channels/notification_channel.rb @@ -0,0 +1,9 @@ +class NotificationChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end + + def unsubscribed + stop_all_streams + end +end diff --git a/test/dummy/app/javascript/packs/application.js b/test/dummy/app/javascript/packs/application.js deleted file mode 100644 index 67ce4675..00000000 --- a/test/dummy/app/javascript/packs/application.js +++ /dev/null @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. JavaScript code in this file should be added after the last require_* statement. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require rails-ujs -//= require activestorage -//= require_tree . diff --git a/test/dummy/app/mailers/user_mailer.rb b/test/dummy/app/mailers/user_mailer.rb index cba5fb63..dee30036 100644 --- a/test/dummy/app/mailers/user_mailer.rb +++ b/test/dummy/app/mailers/user_mailer.rb @@ -1,5 +1,9 @@ class UserMailer < ApplicationMailer - def comment_notification - mail(body: "") + def new_comment(args) + mail(body: "new comment") + end + + def receipt + mail(body: "receipt") end end diff --git a/test/dummy/app/models/account.rb b/test/dummy/app/models/account.rb index c17a8747..9a5b0294 100644 --- a/test/dummy/app/models/account.rb +++ b/test/dummy/app/models/account.rb @@ -1,2 +1,4 @@ class Account < ApplicationRecord + has_many :notifications, as: :record, dependent: :destroy, class_name: "Noticed::Notification" + has_many :notifiers, as: :record, dependent: :destroy, class_name: "Noticed::Event" end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb index 10a4cba8..022a777d 100644 --- a/test/dummy/app/models/application_record.rb +++ b/test/dummy/app/models/application_record.rb @@ -1,3 +1,7 @@ class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true + if respond_to?(:primary_abstract_class) + primary_abstract_class + else + self.abstract_class = true + end end diff --git a/test/dummy/app/models/discord_notification.rb b/test/dummy/app/models/discord_notification.rb deleted file mode 100644 index 9f24f8fd..00000000 --- a/test/dummy/app/models/discord_notification.rb +++ /dev/null @@ -1,2 +0,0 @@ -class DiscordNotification < Noticed::DeliveryMethods::Test -end diff --git a/test/dummy/app/models/json_notification.rb b/test/dummy/app/models/json_notification.rb deleted file mode 100644 index 4fe80f19..00000000 --- a/test/dummy/app/models/json_notification.rb +++ /dev/null @@ -1,4 +0,0 @@ -class JsonNotification < ApplicationRecord - include Noticed::Model - self.table_name = "json_notifications" -end diff --git a/test/dummy/app/models/jsonb_notification.rb b/test/dummy/app/models/jsonb_notification.rb deleted file mode 100644 index 3c174ff8..00000000 --- a/test/dummy/app/models/jsonb_notification.rb +++ /dev/null @@ -1,4 +0,0 @@ -class JsonbNotification < ApplicationRecord - include Noticed::Model - self.table_name = "jsonb_notifications" -end diff --git a/test/dummy/app/models/notification.rb b/test/dummy/app/models/notification.rb deleted file mode 100644 index 15dcdf6d..00000000 --- a/test/dummy/app/models/notification.rb +++ /dev/null @@ -1,3 +0,0 @@ -class Notification < ApplicationRecord - include Noticed::Model -end diff --git a/test/dummy/app/models/text_notification.rb b/test/dummy/app/models/text_notification.rb deleted file mode 100644 index 64605aaf..00000000 --- a/test/dummy/app/models/text_notification.rb +++ /dev/null @@ -1,3 +0,0 @@ -class TextNotification < ApplicationRecord - include Noticed::Model -end diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb index 76370be5..68f955ee 100644 --- a/test/dummy/app/models/user.rb +++ b/test/dummy/app/models/user.rb @@ -1,10 +1,3 @@ class User < ApplicationRecord - has_many :notifications, as: :recipient - - has_noticed_notifications - has_noticed_notifications param_name: :owner, destroy: false - - def phone_number - "8675309" - end + has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification" end diff --git a/test/dummy/app/notifications/comment_notification.rb b/test/dummy/app/notifications/comment_notification.rb deleted file mode 100644 index 66971165..00000000 --- a/test/dummy/app/notifications/comment_notification.rb +++ /dev/null @@ -1,18 +0,0 @@ -class CommentNotification < Noticed::Base - deliver_by :database, format: :attributes_for_database - deliver_by :action_cable - deliver_by :email, mailer: "UserMailer" - deliver_by :discord, class: "DiscordNotification" - - def attributes_for_database - { - account_id: 1, - type: self.class.name, - params: params - } - end - - def url - root_url - end -end diff --git a/test/dummy/app/notifiers/bulk_notifier.rb b/test/dummy/app/notifiers/bulk_notifier.rb new file mode 100644 index 00000000..42cc3c05 --- /dev/null +++ b/test/dummy/app/notifiers/bulk_notifier.rb @@ -0,0 +1,3 @@ +class BulkNotifier < Noticed::Event + bulk_deliver_by :webhook, url: "https://example.org/bulk" +end diff --git a/test/dummy/app/notifiers/comment_notifier.rb b/test/dummy/app/notifiers/comment_notifier.rb new file mode 100644 index 00000000..7e80c635 --- /dev/null +++ b/test/dummy/app/notifiers/comment_notifier.rb @@ -0,0 +1,58 @@ +class CommentNotifier < Noticed::Event + required_params :message + + deliver_by :test + + # delivery_by :email, mailer: "UserMailer", method: "new_comment" + deliver_by :email do |config| + config.mailer = "UserMailer" + config.method = :new_comment + # config.params = -> { params } + # config.args = -> { recipient } + end + + deliver_by :action_cable do |config| + config.channel = "NotificationChannel" + config.stream = -> { recipient } + config.message = -> { params } + end + + deliver_by :twilio_messaging do |config| + config.phone_number = "+1234567890" + config.account_sid = "abcd1234" + config.auth_token = "secret" + end + + deliver_by :microsoft_teams do |config| + config.url = "https://example.org" + config.json = -> { params } + end + + deliver_by :slack do |config| + config.headers = {"Authorization" => "Bearer xoxb-xxxxxxxxx-xxxxxxxxxx"} + config.json = -> { params } + end + + deliver_by :fcm do |config| + config.credentials = Rails.root.join("config/certs/fcm.json") + # Or store them in the Rails credentials + # config.credentials = Rails.application.credentials.fcm + config.device_tokens = -> { recipient.device_tokens } + + config.json = ->(device_token) { + { + token: device_token, + notification: { + title: "Test Title", + body: "Test body" + } + } + } + + # Clean up invalid tokens that are no longer usable + config.invalid_token = ->(token:, platform:) { recipient.device_tokens.where(id: token.id).destroy_all } + end + + deliver_by :ios do |config| + end +end diff --git a/test/dummy/app/notifiers/inherited_notifier.rb b/test/dummy/app/notifiers/inherited_notifier.rb new file mode 100644 index 00000000..6c4acd8b --- /dev/null +++ b/test/dummy/app/notifiers/inherited_notifier.rb @@ -0,0 +1,2 @@ +class InheritedNotifier < SimpleNotifier +end diff --git a/test/dummy/app/notifiers/priority_notifier.rb b/test/dummy/app/notifiers/priority_notifier.rb new file mode 100644 index 00000000..1bd89234 --- /dev/null +++ b/test/dummy/app/notifiers/priority_notifier.rb @@ -0,0 +1,3 @@ +class PriorityNotifier < Noticed::Event + deliver_by :test, priority: 2 +end diff --git a/test/dummy/app/notifiers/queue_notifier.rb b/test/dummy/app/notifiers/queue_notifier.rb new file mode 100644 index 00000000..6bd99b03 --- /dev/null +++ b/test/dummy/app/notifiers/queue_notifier.rb @@ -0,0 +1,3 @@ +class QueueNotifier < Noticed::Event + deliver_by :test, queue: :example_queue +end diff --git a/test/dummy/app/notifiers/receipt_notifier.rb b/test/dummy/app/notifiers/receipt_notifier.rb new file mode 100644 index 00000000..8dfd7eb7 --- /dev/null +++ b/test/dummy/app/notifiers/receipt_notifier.rb @@ -0,0 +1,9 @@ +class ReceiptNotifier < Noticed::Event + deliver_by :test + + deliver_by :email do |config| + config.mailer = "UserMailer" + config.method = :receipt + config.params = -> { params } + end +end diff --git a/test/dummy/app/notifiers/simple_notifier.rb b/test/dummy/app/notifiers/simple_notifier.rb new file mode 100644 index 00000000..baf3026a --- /dev/null +++ b/test/dummy/app/notifiers/simple_notifier.rb @@ -0,0 +1,8 @@ +class SimpleNotifier < Noticed::Event + deliver_by :test + required_params :message + + def url + root_url(host: "example.org") + end +end diff --git a/test/dummy/app/notifiers/test_notifier.rb b/test/dummy/app/notifiers/test_notifier.rb new file mode 100644 index 00000000..4103c5a0 --- /dev/null +++ b/test/dummy/app/notifiers/test_notifier.rb @@ -0,0 +1,3 @@ +class TestNotifier < Noticed::Event + deliver_by :test +end diff --git a/test/dummy/app/notifiers/wait_notifier.rb b/test/dummy/app/notifiers/wait_notifier.rb new file mode 100644 index 00000000..38f82150 --- /dev/null +++ b/test/dummy/app/notifiers/wait_notifier.rb @@ -0,0 +1,3 @@ +class WaitNotifier < Noticed::Event + deliver_by :test, wait: 5.minutes +end diff --git a/test/dummy/app/notifiers/wait_until_notifier.rb b/test/dummy/app/notifiers/wait_until_notifier.rb new file mode 100644 index 00000000..eab4bd42 --- /dev/null +++ b/test/dummy/app/notifiers/wait_until_notifier.rb @@ -0,0 +1,3 @@ +class WaitUntilNotifier < Noticed::Event + deliver_by :test, wait_until: -> { 1.hour.from_now } +end diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb index 24307d38..f72b4ef0 100644 --- a/test/dummy/app/views/layouts/application.html.erb +++ b/test/dummy/app/views/layouts/application.html.erb @@ -2,10 +2,11 @@Hello world
" - notifications: + notifiers: noticed: i18n_example: message: "This is a notification" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb index 0064140a..afa809b4 100644 --- a/test/dummy/config/puma.rb +++ b/test/dummy/config/puma.rb @@ -1,38 +1,35 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. -# -max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) -min_threads_count = ENV.fetch("RAILS_MIN_THREADS", max_threads_count) +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count +# Specifies that the worker count should equal the number of processors in production. +if ENV["RAILS_ENV"] == "production" + require "concurrent-ruby" + worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) + workers worker_count if worker_count > 1 +end + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port ENV.fetch("PORT", 3000) +port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("RAILS_ENV", "development") +environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked web server processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -# preload_app! +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } -# Allow puma to be restarted by `rails restart` command. +# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 130fd76f..ad060d79 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,4 +1,10 @@ Rails.application.routes.draw do - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html - root to: "main#index" + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", :as => :rails_health_check + + # Defines the root path route ("/") + root "posts#index" end diff --git a/test/dummy/config/spring.rb b/test/dummy/config/spring.rb deleted file mode 100644 index db5bf130..00000000 --- a/test/dummy/config/spring.rb +++ /dev/null @@ -1,6 +0,0 @@ -Spring.watch( - ".ruby-version", - ".rbenv-vars", - "tmp/restart.txt", - "tmp/caching-dev.txt" -) diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml index d32f76e8..4942ab66 100644 --- a/test/dummy/config/storage.yml +++ b/test/dummy/config/storage.yml @@ -6,27 +6,27 @@ local: service: Disk root: <%= Rails.root.join("storage") %> -# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 -# bucket: your_own_bucket +# bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket +# bucket: your_own_bucket-<%= Rails.env %> -# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name +# container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror diff --git a/test/dummy/db/migrate/20231215202921_create_users.rb b/test/dummy/db/migrate/20231215202921_create_users.rb new file mode 100644 index 00000000..1098860c --- /dev/null +++ b/test/dummy/db/migrate/20231215202921_create_users.rb @@ -0,0 +1,9 @@ +class CreateUsers < ActiveRecord::Migration[7.1] + def change + create_table :users do |t| + t.string :email + + t.timestamps + end + end +end diff --git a/test/dummy/db/migrate/20231215202924_create_accounts.rb b/test/dummy/db/migrate/20231215202924_create_accounts.rb new file mode 100644 index 00000000..528f2b98 --- /dev/null +++ b/test/dummy/db/migrate/20231215202924_create_accounts.rb @@ -0,0 +1,9 @@ +class CreateAccounts < ActiveRecord::Migration[7.1] + def change + create_table :accounts do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 8636f94f..b921920a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -2,89 +2,46 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# This file is the source Rails uses to define your schema when running `rails -# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_08_03_191250) do - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "notifications", force: :cascade do |t| - t.integer "account_id" - t.string "recipient_type", null: false - t.bigint "recipient_id", null: false - t.string "type" - if t.respond_to? :jsonb - t.jsonb "params" - else - t.json "params" - end - t.datetime "read_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["account_id"], name: "index_notifications_on_account_id" - t.index ["recipient_type", "recipient_id"], name: "index_notifications_on_recipient_type_and_recipient_id" +ActiveRecord::Schema.define(version: 2023_12_15_202924) do + create_table "accounts", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table "json_notifications", force: :cascade do |t| - t.integer "account_id" - t.string "recipient_type", null: false - t.bigint "recipient_id", null: false + create_table "noticed_events", force: :cascade do |t| t.string "type" + t.string "record_type" + t.integer "record_id" t.json "params" - t.datetime "read_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["account_id"], name: "index_with_json_on_account_id" - t.index ["recipient_type", "recipient_id"], name: "index_with_json_on_recipient_type_and_recipient_id" - end - - create_table "jsonb_notifications", force: :cascade do |t| - t.integer "account_id" - t.string "recipient_type", null: false - t.bigint "recipient_id", null: false - t.string "type" - if t.respond_to? :jsonb - t.jsonb "params" - else - t.json "params" - end - t.datetime "read_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["account_id"], name: "index_with_jsonb_on_account_id" - t.index ["recipient_type", "recipient_id"], name: "index_with_jsonb_on_recipient_type_and_recipient_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["record_type", "record_id"], name: "index_noticed_events_on_record" end - create_table "text_notifications", force: :cascade do |t| - t.integer "account_id" + create_table "noticed_notifications", force: :cascade do |t| + t.integer "event_id", null: false t.string "recipient_type", null: false - t.bigint "recipient_id", null: false - t.string "type" - t.text "params" + t.integer "recipient_id", null: false t.datetime "read_at" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["account_id"], name: "index_text_notifications_on_account_id" - t.index ["recipient_type", "recipient_id"], name: "index_text_on_recipient_type_and_recipient_id" - end - - create_table "accounts", force: :cascade do |t| - t.string "name" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "seen_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["event_id"], name: "index_noticed_notifications_on_event_id" + t.index ["recipient_type", "recipient_id"], name: "index_noticed_notifications_on_recipient" end create_table "users", force: :cascade do |t| - t.string "first_name" - t.string "last_name" t.string "email" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end end diff --git a/test/dummy/noticed_test b/test/dummy/noticed_test deleted file mode 100644 index 22868a11..00000000 Binary files a/test/dummy/noticed_test and /dev/null differ diff --git a/test/dummy/test/fixtures/accounts.yml b/test/dummy/test/fixtures/accounts.yml new file mode 100644 index 00000000..7d412240 --- /dev/null +++ b/test/dummy/test/fixtures/accounts.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + +two: + name: MyString diff --git a/test/dummy/test/fixtures/users.yml b/test/dummy/test/fixtures/users.yml new file mode 100644 index 00000000..6c868efa --- /dev/null +++ b/test/dummy/test/fixtures/users.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + email: MyString + +two: + email: MyString diff --git a/test/dummy/test/models/account_test.rb b/test/dummy/test/models/account_test.rb new file mode 100644 index 00000000..b6de6a15 --- /dev/null +++ b/test/dummy/test/models/account_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class AccountTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/dummy/test/models/user_test.rb b/test/dummy/test/models/user_test.rb new file mode 100644 index 00000000..5c07f490 --- /dev/null +++ b/test/dummy/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml index c742788d..a44e54ad 100644 --- a/test/fixtures/accounts.yml +++ b/test/fixtures/accounts.yml @@ -1,2 +1,5 @@ -primary: - name: "Primary" +one: + name: Account One + +two: + name: Account Two diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/files/microsoft_teams/failure.txt b/test/fixtures/files/microsoft_teams/failure.txt deleted file mode 100644 index 01ea11d0..00000000 --- a/test/fixtures/files/microsoft_teams/failure.txt +++ /dev/null @@ -1,19 +0,0 @@ -HTTP/1.1 403 -cache-control: no-cache -pragma: no-cache -content-length: 1 -content-type: text/plain; charset=utf-8 -expires: -1 -request-id: ea4f4a2e-4077-4e46-908b-c4dcf1c5cf78 -strict-transport-security: max-age=31536000; includeSubDomains; preload -x-calculatedbetarget: AM9PR07MB7138.eurprd07.prod.outlook.com -x-backendhttpstatus: 200 -x-aspnet-version: 4.0.30319 -x-cafeserver: AM8P191CA0028.EURP191.PROD.OUTLOOK.COM -x-beserver: AM9PR07MB7138 -x-proxy-routingcorrectness: 1 -x-proxy-backendserverstatus: 200 -x-powered-by: ASP.NET -x-feserver: AM8P191CA0028 -x-msedge-ref: Ref A: 75193BF48B8E459C89D7DE54777A06AA Ref B: MAD30EDGE0716 Ref C: 2020-11-09T19:00:35Z -date: Mon, 09 Nov 2020 19:00:35 GMT diff --git a/test/fixtures/files/microsoft_teams/success.txt b/test/fixtures/files/microsoft_teams/success.txt deleted file mode 100644 index 3ecd94f2..00000000 --- a/test/fixtures/files/microsoft_teams/success.txt +++ /dev/null @@ -1,21 +0,0 @@ -HTTP/1.1 200 -cache-control: no-cache -pragma: no-cache -content-length: 1 -content-type: text/plain; charset=utf-8 -expires: -1 -request-id: ea4f4a2e-4077-4e46-908b-c4dcf1c5cf78 -strict-transport-security: max-age=31536000; includeSubDomains; preload -x-calculatedbetarget: AM9PR07MB7138.eurprd07.prod.outlook.com -x-backendhttpstatus: 200 -x-aspnet-version: 4.0.30319 -x-cafeserver: AM8P191CA0028.EURP191.PROD.OUTLOOK.COM -x-beserver: AM9PR07MB7138 -x-proxy-routingcorrectness: 1 -x-proxy-backendserverstatus: 200 -x-powered-by: ASP.NET -x-feserver: AM8P191CA0028 -x-msedge-ref: Ref A: 75193BF48B8E459C89D7DE54777A06AA Ref B: MAD30EDGE0716 Ref C: 2020-11-09T19:00:35Z -date: Mon, 09 Nov 2020 19:00:35 GMT - -1 \ No newline at end of file diff --git a/test/fixtures/files/slack/failure.txt b/test/fixtures/files/slack/failure.txt deleted file mode 100644 index 06eb0306..00000000 --- a/test/fixtures/files/slack/failure.txt +++ /dev/null @@ -1,11 +0,0 @@ -HTTP/1.1 403 -date: Mon, 09 Nov 2020 12:14:30 GMT -server: Apache -strict-transport-security: max-age=31536000; includeSubDomains; preload -x-slack-backend: r -access-control-allow-origin: * -x-frame-options: SAMEORIGIN -referrer-policy: no-referrer -vary: Accept-Encoding -content-type: text/html -x-via: haproxy-www-2n6w,haproxy-edge-fra-9k3b diff --git a/test/fixtures/files/slack/success.txt b/test/fixtures/files/slack/success.txt deleted file mode 100644 index edb11430..00000000 --- a/test/fixtures/files/slack/success.txt +++ /dev/null @@ -1,13 +0,0 @@ -HTTP/1.1 200 -date: Mon, 09 Nov 2020 12:14:30 GMT -server: Apache -strict-transport-security: max-age=31536000; includeSubDomains; preload -x-slack-backend: r -access-control-allow-origin: * -x-frame-options: SAMEORIGIN -referrer-policy: no-referrer -vary: Accept-Encoding -content-type: text/html -x-via: haproxy-www-2n6w,haproxy-edge-fra-9k3b - -ok diff --git a/test/fixtures/files/twilio/failure.txt b/test/fixtures/files/twilio/failure.txt deleted file mode 100644 index 678c8dff..00000000 --- a/test/fixtures/files/twilio/failure.txt +++ /dev/null @@ -1,18 +0,0 @@ -HTTP/1.1 403 -Date: Mon, 09 Nov 2020 17:49:49 GMT -Content-Type: application/json -Content-Length: 821 -Connection: keep-alive -Twilio-Concurrent-Requests: 1 -Twilio-Request-Id: rcfgo3MAij1kMIb50j8mQBQ9kIp0vcsp -Twilio-Request-Duration: 0.113 -Access-Control-Allow-Origin: * -Access-Control-Allow-Headers: Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since -Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS -Access-Control-Expose-Headers: ETag -Access-Control-Allow-Credentials: true -X-Powered-By: AT-5000 -X-Shenanigans: none -X-Home-Region: us1 -X-API-Domain: api.twilio.com -Strict-Transport-Security: max-age=31536000 diff --git a/test/fixtures/files/twilio/success.txt b/test/fixtures/files/twilio/success.txt deleted file mode 100644 index b1de4d91..00000000 --- a/test/fixtures/files/twilio/success.txt +++ /dev/null @@ -1,20 +0,0 @@ -HTTP/1.1 201 CREATED -Date: Mon, 09 Nov 2020 17:49:49 GMT -Content-Type: application/json -Content-Length: 821 -Connection: keep-alive -Twilio-Concurrent-Requests: 1 -Twilio-Request-Id: rcfgo3MAij1kMIb50j8mQBQ9kIp0vcsp -Twilio-Request-Duration: 0.113 -Access-Control-Allow-Origin: * -Access-Control-Allow-Headers: Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since -Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS -Access-Control-Expose-Headers: ETag -Access-Control-Allow-Credentials: true -X-Powered-By: AT-5000 -X-Shenanigans: none -X-Home-Region: us1 -X-API-Domain: api.twilio.com -Strict-Transport-Security: max-age=31536000 - -{"sid": "um2w7iplBZAe6Ogi7o8YVCNIE2YeOfAP", "date_created": "Mon, 09 Nov 2020 17:49:49 +0000", "date_updated": "Mon, 09 Nov 2020 17:49:49 +0000", "date_sent": null, "account_sid": "a", "to": "8675309", "from": "c", "messaging_service_sid": null, "body": "Noticed", "status": "queued", "num_segments": "1", "num_media": "0", "direction": "outbound-api", "api_version": "2010-04-01", "price": null, "price_unit": "USD", "error_code": null, "error_message": null, "uri": "/2010-04-01/Accounts/a/Messages/um2w7iplBZAe6Ogi7o8YVCNIE2YeOfAP.json", "subresource_uris": {"media": "/2010-04-01/Accounts/a/Messages/um2w7iplBZAe6Ogi7o8YVCNIE2YeOfAP/Media.json"}} \ No newline at end of file diff --git a/test/fixtures/files/vonage/failure.txt b/test/fixtures/files/vonage/failure.txt deleted file mode 100644 index 42cd6331..00000000 --- a/test/fixtures/files/vonage/failure.txt +++ /dev/null @@ -1,10 +0,0 @@ -HTTP/1.1 403 -server: nginx -date: Mon, 09 Nov 2020 18:24:47 GMT -content-type: application/json -cache-control: max-age=1 -x-frame-options: deny -x-xss-protection: 1; mode=block; -strict-transport-security: max-age=31536000; includeSubdomains -content-disposition: attachment; filename="api.txt" -x-nexmo-trace-id: cNvzs9rIF6DymshFf63xsi85UiiOQRYl diff --git a/test/fixtures/files/vonage/success.txt b/test/fixtures/files/vonage/success.txt deleted file mode 100644 index 949ebe8b..00000000 --- a/test/fixtures/files/vonage/success.txt +++ /dev/null @@ -1,22 +0,0 @@ -HTTP/1.1 200 -server: nginx -date: Mon, 09 Nov 2020 18:24:47 GMT -content-type: application/json -cache-control: max-age=1 -x-frame-options: deny -x-xss-protection: 1; mode=block; -strict-transport-security: max-age=31536000; includeSubdomains -content-disposition: attachment; filename="api.txt" -x-nexmo-trace-id: cNvzs9rIF6DymshFf63xsi85UiiOQRYl - -{ - "message-count": "1", - "messages": [{ - "to": "e", - "message-id": "FFQ4KtfvqqpSpye8", - "status": "0", - "remaining-balance": "1.86220000", - "message-price": "0.06890000", - "network": "21407" - }] -} \ No newline at end of file diff --git a/test/fixtures/noticed/events.yml b/test/fixtures/noticed/events.yml new file mode 100644 index 00000000..8313e405 --- /dev/null +++ b/test/fixtures/noticed/events.yml @@ -0,0 +1,22 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + type: CommentNotifier + record: one + record_type: User + params: + foo: bar + +two: + type: CommentNotifier + record: two + record_type: User + params: + foo: bar + +three: + type: ReceiptNotifier + record: two + record_type: User + params: + foo: bar diff --git a/test/fixtures/noticed/notifications.yml b/test/fixtures/noticed/notifications.yml new file mode 100644 index 00000000..46e354f8 --- /dev/null +++ b/test/fixtures/noticed/notifications.yml @@ -0,0 +1,25 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + event: one + recipient: one (User) + read_at: 2023-12-15 13:03:19 + seen_at: 2023-12-15 13:03:19 + +two: + event: two + recipient: two (User) + read_at: 2023-12-15 13:03:19 + seen_at: 2023-12-15 13:03:19 + +three: + event: three + recipient: one (Account) + read_at: 2023-12-15 13:03:19 + seen_at: 2023-12-15 13:03:19 + +four: + event: three + recipient: two (Account) + read_at: 2023-12-15 13:03:19 + seen_at: 2023-12-15 13:03:19 diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml deleted file mode 100644 index 37427f71..00000000 --- a/test/fixtures/notifications.yml +++ /dev/null @@ -1,21 +0,0 @@ -one: - type: CommentNotification - recipient: one (User) - params: - foo: bar - account: - _aj_globalid: gid://dummy/Account/<%= ActiveRecord::FixtureSet.identify(:primary) %> - _aj_symbol_keys: - - account - read_at: <%= Time.current %> - -missing_account: - type: CommentNotification - recipient: one (User) - params: - foo: bar - account: - _aj_globalid: gid://dummy/Account/100000 - _aj_symbol_keys: - - account - read_at: <%= Time.current %> diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 38000f54..584e4641 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,9 +1,5 @@ one: - first_name: "First" - last_name: "User" - email: "first@example.com" + email: one@example.org two: - first_name: "Second" - last_name: "User" - email: "second@example.com" + email: two@example.org diff --git a/test/generators/model_generator_test.rb b/test/generators/model_generator_test.rb deleted file mode 100644 index bd80d62e..00000000 --- a/test/generators/model_generator_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "generators/noticed/model_generator" - -class Noticed::ModelGeneratorTest < ::Rails::Generators::TestCase - tests ::Noticed::Generators::ModelGenerator - - destination Rails.root - - teardown do - remove_if_exists("app/models/test_notification.rb") - remove_if_exists("db/migrate") - remove_if_exists("test") - end - - test "Active Record model and migration are built" do - run_generator ["TestNotification"] - assert_file "app/models/test_notification.rb" - assert_migration "db/migrate/create_test_notifications.rb" - end - - def remove_if_exists(path) - full_path = Rails.root.join(path) - FileUtils.rm_rf(full_path) - end -end diff --git a/test/jobs/event_job_test.rb b/test/jobs/event_job_test.rb new file mode 100644 index 00000000..72c16f24 --- /dev/null +++ b/test/jobs/event_job_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class EventJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 00000000..e69de29b diff --git a/test/models/noticed/event_test.rb b/test/models/noticed/event_test.rb new file mode 100644 index 00000000..aeb18824 --- /dev/null +++ b/test/models/noticed/event_test.rb @@ -0,0 +1,40 @@ +require "test_helper" + +class Noticed::EventTest < ActiveSupport::TestCase + class ExampleNotifier < Noticed::Event + deliver_by :test + required_params :message + end + + test "validates required params" do + assert_raises Noticed::ValidationError do + ExampleNotifier.deliver + end + end + + test "deliver saves event" do + assert_difference "Noticed::Event.count" do + ExampleNotifier.with(message: "test").deliver + end + end + + test "deliver saves notifications" do + assert_no_difference "Noticed::Notification.count" do + ExampleNotifier.with(message: "test").deliver + end + + assert_difference "Noticed::Notification.count" do + ExampleNotifier.with(message: "test").deliver(users(:one)) + end + + assert_difference "Noticed::Notification.count", User.count do + ExampleNotifier.with(message: "test").deliver(User.all) + end + end + + test "deliver extracts record from params" do + account = accounts(:one) + event = ExampleNotifier.with(message: "test", record: account).deliver + assert_equal account, event.record + end +end diff --git a/test/models/noticed/notification_test.rb b/test/models/noticed/notification_test.rb new file mode 100644 index 00000000..b2690dee --- /dev/null +++ b/test/models/noticed/notification_test.rb @@ -0,0 +1,77 @@ +require "test_helper" + +class Noticed::NotificationTest < ActiveSupport::TestCase + test "delegates params to event" do + notification = noticed_notifications(:one) + assert_equal notification.event.params, notification.params + end + + test "delegates record to event" do + notification = noticed_notifications(:one) + assert_equal notification.event.record, notification.record + end + + test "notification associations" do + assert_equal 1, users(:one).notifications.count + end + + test "read scope" do + assert_equal 4, Noticed::Notification.read.count + end + + test "unread scope" do + assert_equal 0, Noticed::Notification.unread.count + end + + test "seen scope" do + assert_equal 4, Noticed::Notification.seen.count + end + + test "unseen scope" do + assert_equal 0, Noticed::Notification.unseen.count + end + + test "mark_as_read" do + Noticed::Notification.update_all(read_at: nil) + assert_equal 0, Noticed::Notification.read.count + Noticed::Notification.mark_as_read + assert_equal 4, Noticed::Notification.read.count + end + + test "mark_as_unread" do + Noticed::Notification.update_all(read_at: Time.current) + assert_equal 4, Noticed::Notification.read.count + Noticed::Notification.mark_as_unread + assert_equal 0, Noticed::Notification.read.count + end + + test "mark_as_seen" do + Noticed::Notification.update_all(seen_at: nil) + assert_equal 0, Noticed::Notification.seen.count + Noticed::Notification.mark_as_seen + assert_equal 4, Noticed::Notification.seen.count + end + + test "mark_as_unseen" do + Noticed::Notification.update_all(seen_at: Time.current) + assert_equal 4, Noticed::Notification.seen.count + Noticed::Notification.mark_as_unseen + assert_equal 0, Noticed::Notification.seen.count + end + + test "read?" do + assert noticed_notifications(:one).read? + end + + test "unread?" do + assert_not noticed_notifications(:one).unread? + end + + test "seen?" do + assert noticed_notifications(:one).seen? + end + + test "unseen?" do + assert_not noticed_notifications(:one).unseen? + end +end diff --git a/test/noticed/coder_test.rb b/test/noticed/coder_test.rb deleted file mode 100644 index a827d254..00000000 --- a/test/noticed/coder_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "test_helper" - -class CoderTest < ActiveSupport::TestCase - test "uses TextCoder for text columns" do - assert_equal Noticed::TextCoder, TextNotification.noticed_coder - end - - test "uses Coder for json columns" do - assert_equal Noticed::Coder, JsonNotification.noticed_coder - end - - test "uses Coder for jsonb columns" do - assert_equal Noticed::Coder, JsonbNotification.noticed_coder - end - - test "serializes globalid objects with text column" do - notification = Notification.create!(recipient: user, type: "Example", params: {user: user}) - assert_equal({user: user}, notification.params) - end - - test "serializes globalid objects with json column" do - notification = JsonNotification.create!(recipient: user, type: "Example", params: {user: user}) - assert_equal({user: user}, notification.params) - end - - test "serializes globalid objects with jsonb column" do - notification = JsonbNotification.create!(recipient: user, type: "Example", params: {user: user}) - assert_equal({user: user}, notification.params) - end -end diff --git a/test/noticed/has_notifications_test.rb b/test/noticed/has_notifications_test.rb deleted file mode 100644 index 3a27db19..00000000 --- a/test/noticed/has_notifications_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -require "test_helper" - -class HasNotificationsTest < ActiveSupport::TestCase - class DatabaseDelivery < Noticed::Base - deliver_by :database - end - - test "has_noticed_notifications" do - assert User.respond_to?(:has_noticed_notifications) - end - - test "noticed notifications association" do - assert user.respond_to?(:notifications_as_user) - end - - test "noticed notifications with custom name" do - assert user.respond_to?(:notifications_as_owner) - end - - test "association returns notifications" do - assert_difference "user.notifications_as_user.count" do - DatabaseDelivery.with(user: user, foo: :bar).deliver(user) - end - end - - test "association with custom name returns notifications" do - assert_difference "user.notifications_as_owner.count" do - DatabaseDelivery.with(owner: user, foo: :bar).deliver(user) - end - end - - test "deletes notifications with matching param" do - DatabaseDelivery.with(user: user, foo: :bar).deliver(users(:two)) - - assert_difference "Notification.count", -1 do - user.destroy - end - end - - test "doesn't delete notifications when disabled" do - DatabaseDelivery.with(owner: user, foo: :bar).deliver(users(:two)) - - assert_no_difference "Notification.count" do - user.destroy - end - end -end diff --git a/test/noticed/model_test.rb b/test/noticed/model_test.rb deleted file mode 100644 index 0e44ef22..00000000 --- a/test/noticed/model_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require "test_helper" - -class ModelTest < ActiveSupport::TestCase - test "can mark notifications as read" do - notification = make_notification - Notification.mark_as_read! - assert_not_nil notification.reload.read_at - end - - test "can mark notifications as unread" do - notification = make_notification(read: true) - Notification.mark_as_unread! - assert_nil notification.reload.read_at - end - - test "unread scope" do - assert_difference "Notification.unread.count" do - make_notification - end - end - - test "read scope" do - assert_difference "Notification.read.count" do - make_notification(read: true) - end - end - - test "safely handles missing GlobalID records in params" do - notification = notifications(:missing_account) - assert_nothing_raised do - notification.params - assert notification.deserialize_error? - end - end - - def make_notification(read: false) - CommentNotification.with(foo: :bar).deliver(users(:one)) - notification = Notification.last - notification.mark_as_read! if read - notification - end -end diff --git a/test/noticed_test.rb b/test/noticed_test.rb index 0c5c27c5..1ec72b5e 100644 --- a/test/noticed_test.rb +++ b/test/noticed_test.rb @@ -1,236 +1,7 @@ require "test_helper" -class IfExample < Noticed::Base - deliver_by :test, if: :falsey - def falsey - false - end -end - -class UnlessExample < Noticed::Base - deliver_by :test, unless: :truthy - def truthy - true - end -end - -class RecipientExample < Noticed::Base - deliver_by :database - - def message - recipient.id - end -end - -class IfRecipientExample < Noticed::Base - deliver_by :test, if: :falsey - def falsey - raise ArgumentError unless recipient - end -end - -class UnlessRecipientExample < Noticed::Base - deliver_by :test, unless: :truthy - def truthy - raise ArgumentError unless recipient - end -end - -class AttributeExample < Noticed::Base - param :user_id -end - -class MultipleParamsExample < Noticed::Base - params :foo, :bar -end - -class CallbackExample < Noticed::Base - class_attribute :callbacks, default: [] - - deliver_by :database - - before_database do - raise ArgumentError unless recipient - - self.class.callbacks << :before_database - end - - around_database do - raise ArgumentError unless recipient - - self.class.callbacks << :around_database - end - - after_database do - raise ArgumentError unless recipient - - self.class.callbacks << :after_database - end - - after_deliver do - self.class.callbacks << :after_everything - end -end - -class RequiredOption < Noticed::DeliveryMethods::Base - def deliver - end - - def self.validate!(options) - unless options.key?(:a_required_option) - raise Noticed::ValidationError, "the `a_required_option` attribute is missing" - end - end -end - -class NotificationWithValidOptions < Noticed::Base - deliver_by :custom, class: "RequiredOption", a_required_option: true -end - -class NotificationWithoutValidOptions < Noticed::Base - deliver_by :custom, class: "RequiredOption" -end - -class With5MinutesDelay < Noticed::Base - deliver_by :test, delay: 5.minutes -end - -class WithDynamicDelay < Noticed::Base - deliver_by :test, delay: :dynamic_delay - - def dynamic_delay - (recipient.email == "first@example.com") ? 1.minute : 2.minutes - end -end - -class WithCustomQueue < Noticed::Base - deliver_by :test, queue: "custom" -end - -class Noticed::Test < ActiveSupport::TestCase - test "stores data in params" do - notification = make_notification(foo: :bar, user: user) - assert_equal :bar, notification.params[:foo] - assert_equal user, notification.params[:user] - end - - test "can deliver a notification" do - assert make_notification(foo: :bar).deliver(user) - end - - test "enqueues notification jobs (skipping database)" do - assert_enqueued_jobs CommentNotification.delivery_methods.length - 1 do - CommentNotification.deliver_later(user) - end - end - - test "cancels delivery when if clause is falsey" do - IfExample.deliver(user) - assert_empty Noticed::DeliveryMethods::Test.delivered - end - - test "cancels delivery when unless clause is truthy" do - UnlessExample.deliver(user) - assert_empty Noticed::DeliveryMethods::Test.delivered - end - - test "has access to recipient in if clause" do - assert_nothing_raised do - IfRecipientExample.deliver(user) - end - end - - test "has access to recipient in unless clause" do - assert_nothing_raised do - UnlessRecipientExample.deliver(user) - end - end - - test "has access to recipient in notification instance" do - RecipientExample.deliver(user) - assert_equal user.id, Notification.last.to_notification.message - end - - test "validates attributes for params" do - assert_raises Noticed::ValidationError do - AttributeExample.deliver(users(:one)) - end - end - - test "allows to pass multiple params" do - assert_equal [:foo, :bar], MultipleParamsExample.with(foo: true, bar: false).param_names - end - - test "runs callbacks on notifications" do - CallbackExample.deliver(user) - assert_equal [:before_database, :around_database, :after_database, :after_everything], CallbackExample.callbacks - end - - test "runs callbacks on delivery methods" do - assert_difference "Noticed::DeliveryMethods::Test.callbacks.count" do - make_notification(foo: :bar).deliver(user) - end - end - - test "can send notifications to multiple recipients" do - assert User.count >= 2 - assert_difference "Notification.count", User.count do - make_notification(foo: :bar).deliver(User.all) - end - end - - test "assigns record to notification when delivering" do - notification = make_notification(foo: :bar) - notification.deliver(user) - assert_equal Notification.last, Noticed::DeliveryMethods::Test.delivered.last.record - assert_equal notification.record, Noticed::DeliveryMethods::Test.delivered.last.record - end - - test "assigns recipient to notification when delivering" do - make_notification(foo: :bar).deliver(user) - assert_equal user, Noticed::DeliveryMethods::Test.delivered.last.recipient - end - - test "validates options of delivery methods when options are valid" do - assert_nothing_raised do - NotificationWithValidOptions.deliver(user) - end - end - - test "validates options of delivery methods when options are invalid" do - assert_raises Noticed::ValidationError do - NotificationWithoutValidOptions.deliver(user) - end - end - - test "asserts delivery is delayed" do - freeze_time do - assert_enqueued_with(at: 5.minutes.from_now) do - With5MinutesDelay.deliver(user) - end - end - end - - test "asserts dynamic delay" do - freeze_time do - assert_enqueued_with(at: 1.minutes.from_now) do - WithDynamicDelay.deliver(users(:one)) - end - - assert_enqueued_with(at: 2.minutes.from_now) do - WithDynamicDelay.deliver(users(:two)) - end - end - end - - test "asserts delivery is queued with different queue" do - assert_enqueued_with(queue: "custom") do - WithCustomQueue.deliver_later(user) - end - end - - test "loading notification from fixture" do - notification = notifications(:one) - assert_equal accounts(:primary), notification.params[:account] +class NoticedTest < ActiveSupport::TestCase + test "it has a version number" do + assert Noticed::VERSION end end diff --git a/test/notifier_test.rb b/test/notifier_test.rb new file mode 100644 index 00000000..17cd1cbe --- /dev/null +++ b/test/notifier_test.rb @@ -0,0 +1,99 @@ +require "test_helper" + +class NotifierTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + test "includes Rails urls" do + assert_equal "http://example.org/", SimpleNotifier.new.url + end + + test "notifiers inherit required params" do + assert_equal [:message], InheritedNotifier.required_params + end + + test "serializes globalid objects with text column" do + user = users(:one) + notification = Noticed::Event.create!(type: "SimpleNotifier", params: {user: user}) + assert_equal({user: user}, notification.params) + end + + test "deliver creates an event" do + assert_difference "Noticed::Event.count" do + ReceiptNotifier.deliver(User.first) + end + end + + test "deliver creates notifications for each recipient" do + assert_no_difference "Noticed::Notification.count" do + ReceiptNotifier.deliver + end + + assert_difference "Noticed::Notification.count" do + ReceiptNotifier.deliver(User.first) + end + + assert_difference "Noticed::Notification.count", User.count do + ReceiptNotifier.deliver(User.all) + end + end + + test "creates jobs for deliveries" do + # Delivering a notification creates records + assert_enqueued_jobs 1, only: Noticed::EventJob do + ReceiptNotifier.deliver(User.first) + end + + # Run the Event Job + assert_enqueued_jobs 1, only: Noticed::DeliveryMethods::Test do + perform_enqueued_jobs + end + + # Run the individual deliveries + perform_enqueued_jobs + + assert_equal Noticed::Notification.last, Noticed::DeliveryMethods::Test.delivered.last + end + + test "creates jobs for bulk deliveries" do + assert_enqueued_jobs 1, only: Noticed::EventJob do + BulkNotifier.deliver + end + + assert_enqueued_jobs 1, only: Noticed::BulkDeliveryMethods::Webhook do + perform_enqueued_jobs + end + end + + test "wait delivery method option" do + freeze_time + event = WaitNotifier.deliver(User.first) + assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], at: 5.minutes.from_now) do + perform_enqueued_jobs + end + end + + test "wait_until delivery method option" do + freeze_time + event = WaitUntilNotifier.deliver(User.first) + assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], at: 1.hour.from_now) do + perform_enqueued_jobs + end + end + + test "queue delivery method option" do + event = QueueNotifier.deliver(User.first) + assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], queue: "example_queue") do + perform_enqueued_jobs + end + end + + # assert_enqeued_with doesn't support priority before Rails 7 + if Rails.gem_version >= Gem::Version.new("7.0.0.alpha1") + test "priority delivery method option" do + event = PriorityNotifier.deliver(User.first) + assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], priority: 2) do + perform_enqueued_jobs + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index d200d50e..9d1ac1cc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,55 +4,18 @@ require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] require "rails/test_help" -require "byebug" - -# Filter out the backtrace from minitest while preserving the one from other libraries. -Minitest.backtrace_filter = Minitest::BacktraceFilter.new - -require "rails/test_unit/reporter" -Rails::TestUnitReporter.executable = "bin/test" +require "minitest/mock" +require "webmock/minitest" # Load fixtures from the engine -if ActiveSupport::TestCase.respond_to?(:fixture_path=) +if ActiveSupport::TestCase.respond_to?(:fixture_paths=) + ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)] + ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths + ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files" + ActiveSupport::TestCase.fixtures :all +elsif ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" ActiveSupport::TestCase.fixtures :all end - -require "minitest/unit" -require "webmock/minitest" - -class ExampleNotification < Noticed::Base - class_attribute :callback_responses, default: [] - - deliver_by :test, foo: :bar - deliver_by :database - - # after_deliver do - # self.class.callback_reponses << "delivered" - # end -end - -class ActiveSupport::TestCase - include ActionCable::TestHelper - include ActionMailer::TestHelper - - teardown do - Noticed::DeliveryMethods::Test.clear! - end - - private - - def user - @user ||= users(:one) - end - - def make_notification(params) - ExampleNotification.with(params) - end - - def stub_delivery_method_request(delivery_method:, matcher:, method: :post, type: :success) - stub_request(method, matcher).to_return(File.new(file_fixture("#{delivery_method}/#{type}.txt"))) - end -end diff --git a/test/translation_test.rb b/test/translation_test.rb index 8be0dfdb..391e88b5 100644 --- a/test/translation_test.rb +++ b/test/translation_test.rb @@ -1,7 +1,11 @@ require "test_helper" class TranslationTest < ActiveSupport::TestCase - class I18nExample < Noticed::Base + class I18nExample < Noticed::Event + deliver_by :test do |config| + config.message = -> { t("hello") } + end + def message t("hello") end @@ -11,13 +15,13 @@ def html_message end end - class Noticed::I18nExample < Noticed::Base + class Noticed::I18nExample < Noticed::Event def message t(".message") end end - class ::ScopedI18nExample < Noticed::Base + class ::ScopedI18nExample < Noticed::Event def i18n_scope :noticed end @@ -33,7 +37,7 @@ def message end test "I18n supports namespaces" do - assert_equal "notifications.noticed.i18n_example.message", Noticed::I18nExample.new.send(:scope_translation_key, ".message") + assert_equal "notifiers.noticed.i18n_example.message", Noticed::I18nExample.new.send(:scope_translation_key, ".message") assert_equal "This is a notification", Noticed::I18nExample.new.message end @@ -49,4 +53,9 @@ def message assert message.html_safe? end end + + test "delivery method blocks can use translations" do + block = I18nExample.delivery_methods[:test].config[:message] + assert_equal "Hello world", noticed_notifications(:one).instance_exec(&block) + end end