├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── issue_template.md
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ └── publish_gem.yml
├── .gitignore
├── .standard.yml
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── Rakefile
├── UPGRADE.md
├── app
├── jobs
│ ├── .keep
│ └── noticed
│ │ ├── application_job.rb
│ │ └── event_job.rb
└── models
│ ├── .keep
│ ├── concerns
│ ├── .keep
│ └── noticed
│ │ ├── deliverable.rb
│ │ ├── notification_methods.rb
│ │ └── readable.rb
│ └── noticed
│ ├── application_record.rb
│ ├── deliverable
│ └── deliver_by.rb
│ ├── ephemeral.rb
│ ├── event.rb
│ └── notification.rb
├── bin
├── rails
└── test
├── db
└── migrate
│ ├── 20231215190233_create_noticed_tables.rb
│ └── 20240129184740_add_notifications_count_to_noticed_event.rb
├── docs
├── bulk_delivery_methods
│ ├── bluesky.md
│ ├── discord.md
│ ├── slack.md
│ └── webhook.md
├── delivery_methods
│ ├── action_cable.md
│ ├── discord.md
│ ├── email.md
│ ├── fcm.md
│ ├── ios.md
│ ├── microsoft_teams.md
│ ├── slack.md
│ ├── test.md
│ ├── twilio_messaging.md
│ ├── vonage.md
│ └── vonage_sms.md
├── extending-noticed.md
└── images
│ ├── fcm-credentials-json.png
│ └── fcm-project-settings.png
├── gemfiles
├── rails_6_1.gemfile
├── rails_6_1.gemfile.lock
├── rails_7_0.gemfile
├── rails_7_0.gemfile.lock
├── rails_7_1.gemfile
├── rails_7_1.gemfile.lock
├── rails_7_2.gemfile
├── rails_7_2.gemfile.lock
├── rails_8_0.gemfile
├── rails_8_0.gemfile.lock
├── rails_main.gemfile
└── rails_main.gemfile.lock
├── lib
├── generators
│ └── noticed
│ │ ├── delivery_method_generator.rb
│ │ ├── install_generator.rb
│ │ ├── notifier_generator.rb
│ │ └── templates
│ │ ├── README
│ │ ├── application_delivery_method.rb.tt
│ │ ├── application_notifier.rb.tt
│ │ ├── delivery_method.rb.tt
│ │ └── notifier.rb.tt
├── noticed.rb
└── noticed
│ ├── api_client.rb
│ ├── bulk_delivery_method.rb
│ ├── bulk_delivery_methods
│ ├── bluesky.rb
│ ├── discord.rb
│ ├── slack.rb
│ ├── test.rb
│ └── webhook.rb
│ ├── coder.rb
│ ├── delivery_method.rb
│ ├── delivery_methods
│ ├── action_cable.rb
│ ├── discord.rb
│ ├── email.rb
│ ├── fcm.rb
│ ├── ios.rb
│ ├── microsoft_teams.rb
│ ├── slack.rb
│ ├── test.rb
│ ├── twilio_messaging.rb
│ ├── vonage_sms.rb
│ └── webhook.rb
│ ├── engine.rb
│ ├── has_notifications.rb
│ ├── notification_channel.rb
│ ├── required_options.rb
│ ├── translation.rb
│ └── version.rb
├── noticed.gemspec
└── test
├── bulk_delivery_methods
└── webhook_test.rb
├── delivery_method_test.rb
├── delivery_methods
├── action_cable_test.rb
├── email_test.rb
├── fcm_test.rb
├── ios_test.rb
├── microsoft_teams_test.rb
├── slack_test.rb
├── twilio_messaging_test.rb
├── vonage_sms_test.rb
└── webhook_test.rb
├── dummy
├── Rakefile
├── app
│ ├── assets
│ │ ├── config
│ │ │ └── manifest.js
│ │ ├── images
│ │ │ └── .keep
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── concerns
│ │ │ └── .keep
│ ├── helpers
│ │ └── application_helper.rb
│ ├── jobs
│ │ └── application_job.rb
│ ├── mailers
│ │ ├── application_mailer.rb
│ │ └── user_mailer.rb
│ ├── models
│ │ ├── account.rb
│ │ ├── admin.rb
│ │ ├── application_record.rb
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── user.rb
│ ├── notifiers
│ │ ├── application_delivery_method.rb
│ │ ├── application_notifier.rb
│ │ ├── bulk_notifier.rb
│ │ ├── comment_notifier.rb
│ │ ├── deprecated_notifier.rb
│ │ ├── ephemeral_notifier.rb
│ │ ├── inherited_notifier.rb
│ │ ├── priority_notifier.rb
│ │ ├── queue_notifier.rb
│ │ ├── receipt_notifier.rb
│ │ ├── record_notifier.rb
│ │ ├── simple_notifier.rb
│ │ ├── test_notifier.rb
│ │ ├── wait_notifier.rb
│ │ └── wait_until_notifier.rb
│ └── views
│ │ └── layouts
│ │ ├── application.html.erb
│ │ ├── mailer.html.erb
│ │ └── mailer.text.erb
├── bin
│ ├── rails
│ ├── rake
│ └── setup
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── cable.yml
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── assets.rb
│ │ ├── content_security_policy.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── inflections.rb
│ │ └── permissions_policy.rb
│ ├── locales
│ │ └── en.yml
│ ├── puma.rb
│ ├── routes.rb
│ └── storage.yml
├── db
│ ├── migrate
│ │ ├── 20231215202921_create_users.rb
│ │ └── 20231215202924_create_accounts.rb
│ └── schema.rb
├── lib
│ └── assets
│ │ └── .keep
├── log
│ └── .keep
└── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ └── favicon.ico
├── ephemeral_notifier_test.rb
├── fixtures
├── accounts.yml
├── files
│ └── .keep
├── noticed
│ ├── events.yml
│ └── notifications.yml
└── users.yml
├── has_notifications_test.rb
├── jobs
└── event_job_test.rb
├── models
├── .keep
└── noticed
│ ├── deliverable
│ └── deliver_by_test.rb
│ ├── event_test.rb
│ └── notification_test.rb
├── noticed_test.rb
├── notifier_test.rb
├── test_helper.rb
└── translation_test.rb
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [excid3] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Get Help
4 | url: https://github.com/excid3/noticed/discussions/new?category=help
5 | about: If you can't get something to work the way you expect, open a question in our discussion forums.
6 | - name: Feature Request
7 | url: https://github.com/excid3/noticed/discussions/new?category=ideas
8 | about: 'Suggest any ideas you have using our discussion forums.'
9 | - name: Bug Report
10 | 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.
11 | about: If you've already asked for help with a problem and confirmed something is broken with Noticed itself, create a bug report.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐞 Bug
3 | about: File a bug/issue
4 | title: '[BUG]
'
5 | labels: Bug, Needs Triage
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Bug Report
11 |
12 | **Describe the Bug:**
13 |
14 |
15 | **To Reproduce:**
16 |
17 |
18 | 1. Step 1
19 | 2. Step 2
20 | 3. ...
21 |
22 | **Expected Behavior:**
23 |
24 |
25 | **Actual Behavior:**
26 |
27 |
28 | **Screenshots (if applicable):**
29 |
30 |
31 | **Environment:**
32 | - Noticed gem version:
33 | - Ruby version:
34 | - Rails version:
35 | - Operating System:
36 |
37 | **Additional Context:**
38 |
39 |
40 | **Possible Fix:**
41 |
42 |
43 | **Steps to Reproduce with Fix (if available):**
44 |
45 |
46 | **Related Issues:**
47 |
48 |
49 | **Labels to Apply:**
50 |
51 |
52 | **Checklist:**
53 |
54 |
55 | - [ ] I have searched for similar issues and couldn't find any
56 | - [ ] I have checked the documentation for relevant information
57 | - [ ] I have included all the required information
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Pull Request
2 |
3 | **Summary:**
4 |
5 |
6 | **Related Issue:**
7 |
8 |
9 | **Description:**
10 |
11 |
12 | **Testing:**
13 |
14 |
15 | **Screenshots (if applicable):**
16 |
17 |
18 | **Checklist:**
19 |
20 |
21 | - [ ] Code follows the project's coding standards
22 | - [ ] Tests have been added or updated to cover the changes
23 | - [ ] Documentation has been updated (if applicable)
24 | - [ ] All existing tests pass
25 | - [ ] Conforms to the contributing guidelines
26 |
27 | **Additional Notes:**
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - main
10 | workflow_call:
11 |
12 | jobs:
13 | sqlite:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | ruby: ['3.2', '3.3', '3.4']
18 | gemfile:
19 | - rails_6_1
20 | - rails_7_0
21 | - rails_7_1
22 | - rails_7_2
23 | - rails_8_0
24 | - rails_main
25 | exclude:
26 | # sqlite3 ~> 1.7 is not compatible with Ruby 3.4+
27 | - gemfile: rails_6_1
28 | ruby: '3.4'
29 | - gemfile: rails_7_0
30 | ruby: '3.4'
31 | - gemfile: rails_7_1
32 | ruby: '3.4'
33 | - gemfile: rails_7_2
34 | ruby: '3.4'
35 |
36 | env:
37 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
38 | BUNDLE_PATH_RELATIVE_TO_CWD: true
39 |
40 | steps:
41 | - uses: actions/checkout@v4
42 |
43 | - name: Set up Ruby
44 | uses: ruby/setup-ruby@v1
45 | with:
46 | ruby-version: ${{ matrix.ruby }}
47 | bundler: default
48 | bundler-cache: true
49 | rubygems: latest
50 |
51 | - name: StandardRb check
52 | run: bundle exec standardrb
53 |
54 | - name: Run tests
55 | env:
56 | DATABASE_URL: "sqlite3:noticed_test"
57 | RAILS_ENV: test
58 | run: |
59 | bundle exec rails db:test:prepare
60 | bundle exec rails test
61 |
62 | mysql:
63 | runs-on: ubuntu-latest
64 | strategy:
65 | matrix:
66 | ruby: ['3.2', '3.3', '3.4']
67 | gemfile:
68 | - rails_6_1
69 | - rails_7_0
70 | - rails_7_1
71 | - rails_7_2
72 | - rails_8_0
73 | - rails_main
74 | exclude:
75 | # sqlite3 ~> 1.7 is not compatible with Ruby 3.4+
76 | - gemfile: rails_6_1
77 | ruby: '3.4'
78 | - gemfile: rails_7_0
79 | ruby: '3.4'
80 | - gemfile: rails_7_1
81 | ruby: '3.4'
82 | - gemfile: rails_7_2
83 | ruby: '3.4'
84 |
85 | env:
86 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
87 | BUNDLE_PATH_RELATIVE_TO_CWD: true
88 |
89 | services:
90 | mysql:
91 | image: mysql:8
92 | env:
93 | MYSQL_ALLOW_EMPTY_PASSWORD: true
94 | MYSQL_DATABASE: test
95 | ports: ['3306:3306']
96 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
97 |
98 | steps:
99 | - uses: actions/checkout@v4
100 |
101 | - name: Set up Ruby
102 | uses: ruby/setup-ruby@v1
103 | with:
104 | ruby-version: ${{ matrix.ruby }}
105 | bundler: default
106 | bundler-cache: true
107 | rubygems: latest
108 |
109 | - name: StandardRb check
110 | run: bundle exec standardrb
111 |
112 | - name: Run tests
113 | env:
114 | DATABASE_URL: trilogy://root:@127.0.0.1:3306/test
115 | RAILS_ENV: test
116 | run: |
117 | bundle exec rails db:test:prepare
118 | bundle exec rails test
119 |
120 | postgres:
121 | runs-on: ubuntu-latest
122 | strategy:
123 | matrix:
124 | ruby: ['3.2', '3.3', '3.4']
125 | gemfile:
126 | - rails_6_1
127 | - rails_7_0
128 | - rails_7_1
129 | - rails_7_2
130 | - rails_8_0
131 | - rails_main
132 | exclude:
133 | # sqlite3 ~> 1.7 is not compatible with Ruby 3.4+
134 | - gemfile: rails_6_1
135 | ruby: '3.4'
136 | - gemfile: rails_7_0
137 | ruby: '3.4'
138 | - gemfile: rails_7_1
139 | ruby: '3.4'
140 | - gemfile: rails_7_2
141 | ruby: '3.4'
142 |
143 | env:
144 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
145 | BUNDLE_PATH_RELATIVE_TO_CWD: true
146 |
147 | services:
148 | postgres:
149 | image: postgres:16
150 | env:
151 | POSTGRES_USER: postgres
152 | POSTGRES_PASSWORD: password
153 | POSTGRES_DB: test
154 | ports: ['5432:5432']
155 |
156 | steps:
157 | - uses: actions/checkout@v4
158 |
159 | - name: Set up Ruby
160 | uses: ruby/setup-ruby@v1
161 | with:
162 | ruby-version: ${{ matrix.ruby }}
163 | bundler: default
164 | bundler-cache: true
165 | rubygems: latest
166 |
167 | - name: StandardRb check
168 | run: bundle exec standardrb
169 |
170 | - name: Run tests
171 | env:
172 | DATABASE_URL: postgres://postgres:password@localhost:5432/test
173 | RAILS_ENV: test
174 | run: |
175 | bundle exec rails db:test:prepare
176 | bundle exec rails test
177 |
--------------------------------------------------------------------------------
/.github/workflows/publish_gem.yml:
--------------------------------------------------------------------------------
1 | name: Publish Gem
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version:
6 | description: "Version"
7 | required: true
8 | type: string
9 |
10 | jobs:
11 | test:
12 | uses: ./.github/workflows/ci.yml
13 |
14 | push:
15 | needs: test
16 | runs-on: ubuntu-latest
17 |
18 | permissions:
19 | contents: write
20 | id-token: write
21 |
22 | steps:
23 | # Set up
24 | - uses: actions/checkout@v4
25 | - name: Set up Ruby
26 | uses: ruby/setup-ruby@v1
27 | with:
28 | bundler-cache: true
29 | ruby-version: ruby
30 |
31 | - name: Update version
32 | run: |
33 | sed -i 's/".*"/"${{ inputs.version }}"/' lib/noticed/version.rb
34 | bundle config set --local deployment 'false'
35 | bundle
36 | bundle exec appraisal
37 | git config user.name 'GitHub Actions'
38 | git config user.email github-actions@github.com
39 | git add Gemfile.lock gemfiles lib
40 | git commit -m "Version bump"
41 | git push
42 |
43 | # Release
44 | - uses: rubygems/release-gem@v1
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | test/dummy/db/*.sqlite3
5 | test/dummy/db/*.sqlite3-journal
6 | test/dummy/db/*.sqlite3-*
7 | test/dummy/log/*.log
8 | test/dummy/storage/
9 | test/dummy/tmp/
10 | .byebug_history
11 | .DS_Store
12 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ruby_version: 3.0
2 | ignore:
3 | - '**/*':
4 | - Style/HashSyntax
5 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-6-1" do
2 | gem "rails", "~> 6.1.0"
3 | gem "sqlite3", "~> 1.7"
4 | gem "activerecord-trilogy-adapter"
5 |
6 | # Ruby 3.4 drops these default gems
7 | gem "bigdecimal"
8 | gem "drb"
9 | gem "mutex_m"
10 |
11 | # Fixes uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError)
12 | gem "concurrent-ruby", "< 1.3.5"
13 | end
14 |
15 | appraise "rails-7-0" do
16 | gem "rails", "~> 7.0.0"
17 | gem "sqlite3", "~> 1.7"
18 | gem "activerecord-trilogy-adapter"
19 |
20 | # Ruby 3.4 drops these default gems
21 | gem "bigdecimal"
22 | gem "drb"
23 | gem "mutex_m"
24 |
25 | # Fixes uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError)
26 | gem "concurrent-ruby", "< 1.3.5"
27 | end
28 |
29 | appraise "rails-7-1" do
30 | gem "rails", "~> 7.1.0"
31 | gem "sqlite3", "~> 1.7"
32 | gem "trilogy"
33 | end
34 |
35 | appraise "rails-7-2" do
36 | gem "rails", "~> 7.2.0"
37 | gem "sqlite3", "~> 1.7"
38 | gem "trilogy"
39 | end
40 |
41 | appraise "rails-8-0" do
42 | gem "rails", "~> 8.0.0"
43 | gem "sqlite3", "~> 2.0"
44 | gem "trilogy"
45 | end
46 |
47 | appraise "rails-main" do
48 | gem "rails", github: "rails/rails", branch: "main"
49 | gem "sqlite3", "~> 2.0"
50 | gem "trilogy"
51 | end
52 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Declare your gem's dependencies in noticed.gemspec.
5 | # Bundler will treat runtime dependencies like base dependencies, and
6 | # development dependencies will be added by default to the :development group.
7 | gemspec
8 |
9 | # Declare any dependencies that are still in development here instead of in
10 | # your gemspec. These might include edge Rails or gems from your path or
11 | # Git. Remember to move these dependencies to your gemspec before releasing
12 | # your gem to rubygems.org.
13 |
14 | gem "appraisal"
15 | gem "pg"
16 | gem "sqlite3"
17 | gem "standard"
18 | gem "webmock"
19 |
20 | # iOS notifications
21 | gem "apnotic", "~> 1.7"
22 |
23 | # firebase notifications
24 | gem "googleauth", "~> 1.1"
25 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | noticed (2.7.0)
5 | rails (>= 6.1.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actioncable (8.0.1)
11 | actionpack (= 8.0.1)
12 | activesupport (= 8.0.1)
13 | nio4r (~> 2.0)
14 | websocket-driver (>= 0.6.1)
15 | zeitwerk (~> 2.6)
16 | actionmailbox (8.0.1)
17 | actionpack (= 8.0.1)
18 | activejob (= 8.0.1)
19 | activerecord (= 8.0.1)
20 | activestorage (= 8.0.1)
21 | activesupport (= 8.0.1)
22 | mail (>= 2.8.0)
23 | actionmailer (8.0.1)
24 | actionpack (= 8.0.1)
25 | actionview (= 8.0.1)
26 | activejob (= 8.0.1)
27 | activesupport (= 8.0.1)
28 | mail (>= 2.8.0)
29 | rails-dom-testing (~> 2.2)
30 | actionpack (8.0.1)
31 | actionview (= 8.0.1)
32 | activesupport (= 8.0.1)
33 | nokogiri (>= 1.8.5)
34 | rack (>= 2.2.4)
35 | rack-session (>= 1.0.1)
36 | rack-test (>= 0.6.3)
37 | rails-dom-testing (~> 2.2)
38 | rails-html-sanitizer (~> 1.6)
39 | useragent (~> 0.16)
40 | actiontext (8.0.1)
41 | actionpack (= 8.0.1)
42 | activerecord (= 8.0.1)
43 | activestorage (= 8.0.1)
44 | activesupport (= 8.0.1)
45 | globalid (>= 0.6.0)
46 | nokogiri (>= 1.8.5)
47 | actionview (8.0.1)
48 | activesupport (= 8.0.1)
49 | builder (~> 3.1)
50 | erubi (~> 1.11)
51 | rails-dom-testing (~> 2.2)
52 | rails-html-sanitizer (~> 1.6)
53 | activejob (8.0.1)
54 | activesupport (= 8.0.1)
55 | globalid (>= 0.3.6)
56 | activemodel (8.0.1)
57 | activesupport (= 8.0.1)
58 | activerecord (8.0.1)
59 | activemodel (= 8.0.1)
60 | activesupport (= 8.0.1)
61 | timeout (>= 0.4.0)
62 | activestorage (8.0.1)
63 | actionpack (= 8.0.1)
64 | activejob (= 8.0.1)
65 | activerecord (= 8.0.1)
66 | activesupport (= 8.0.1)
67 | marcel (~> 1.0)
68 | activesupport (8.0.1)
69 | base64
70 | benchmark (>= 0.3)
71 | bigdecimal
72 | concurrent-ruby (~> 1.0, >= 1.3.1)
73 | connection_pool (>= 2.2.5)
74 | drb
75 | i18n (>= 1.6, < 2)
76 | logger (>= 1.4.2)
77 | minitest (>= 5.1)
78 | securerandom (>= 0.3)
79 | tzinfo (~> 2.0, >= 2.0.5)
80 | uri (>= 0.13.1)
81 | addressable (2.8.7)
82 | public_suffix (>= 2.0.2, < 7.0)
83 | apnotic (1.7.2)
84 | connection_pool (~> 2)
85 | net-http2 (>= 0.18.3, < 2)
86 | appraisal (2.5.0)
87 | bundler
88 | rake
89 | thor (>= 0.14.0)
90 | ast (2.4.2)
91 | base64 (0.2.0)
92 | benchmark (0.4.0)
93 | bigdecimal (3.1.9)
94 | builder (3.3.0)
95 | concurrent-ruby (1.3.5)
96 | connection_pool (2.5.0)
97 | crack (1.0.0)
98 | bigdecimal
99 | rexml
100 | crass (1.0.6)
101 | date (3.4.1)
102 | drb (2.2.1)
103 | erubi (1.13.1)
104 | faraday (2.12.2)
105 | faraday-net_http (>= 2.0, < 3.5)
106 | json
107 | logger
108 | faraday-net_http (3.4.0)
109 | net-http (>= 0.5.0)
110 | globalid (1.2.1)
111 | activesupport (>= 6.1)
112 | google-cloud-env (2.2.1)
113 | faraday (>= 1.0, < 3.a)
114 | google-logging-utils (0.1.0)
115 | googleauth (1.12.2)
116 | faraday (>= 1.0, < 3.a)
117 | google-cloud-env (~> 2.2)
118 | google-logging-utils (~> 0.1)
119 | jwt (>= 1.4, < 3.0)
120 | multi_json (~> 1.11)
121 | os (>= 0.9, < 2.0)
122 | signet (>= 0.16, < 2.a)
123 | hashdiff (1.1.2)
124 | http-2 (0.12.0)
125 | base64
126 | i18n (1.14.6)
127 | concurrent-ruby (~> 1.0)
128 | io-console (0.8.0)
129 | irb (1.14.3)
130 | rdoc (>= 4.0.0)
131 | reline (>= 0.4.2)
132 | json (2.9.1)
133 | jwt (2.10.1)
134 | base64
135 | language_server-protocol (3.17.0.3)
136 | lint_roller (1.1.0)
137 | logger (1.6.5)
138 | loofah (2.24.0)
139 | crass (~> 1.0.2)
140 | nokogiri (>= 1.12.0)
141 | mail (2.8.1)
142 | mini_mime (>= 0.1.1)
143 | net-imap
144 | net-pop
145 | net-smtp
146 | marcel (1.0.4)
147 | mini_mime (1.1.5)
148 | minitest (5.25.4)
149 | multi_json (1.15.0)
150 | net-http (0.6.0)
151 | uri
152 | net-http2 (0.18.5)
153 | http-2 (~> 0.11)
154 | net-imap (0.5.5)
155 | date
156 | net-protocol
157 | net-pop (0.1.2)
158 | net-protocol
159 | net-protocol (0.2.2)
160 | timeout
161 | net-smtp (0.5.0)
162 | net-protocol
163 | nio4r (2.7.4)
164 | nokogiri (1.18.1-aarch64-linux-gnu)
165 | racc (~> 1.4)
166 | nokogiri (1.18.1-aarch64-linux-musl)
167 | racc (~> 1.4)
168 | nokogiri (1.18.1-arm-linux-gnu)
169 | racc (~> 1.4)
170 | nokogiri (1.18.1-arm-linux-musl)
171 | racc (~> 1.4)
172 | nokogiri (1.18.1-arm64-darwin)
173 | racc (~> 1.4)
174 | nokogiri (1.18.1-x86_64-darwin)
175 | racc (~> 1.4)
176 | nokogiri (1.18.1-x86_64-linux-gnu)
177 | racc (~> 1.4)
178 | nokogiri (1.18.1-x86_64-linux-musl)
179 | racc (~> 1.4)
180 | os (1.1.4)
181 | parallel (1.26.3)
182 | parser (3.3.7.0)
183 | ast (~> 2.4.1)
184 | racc
185 | pg (1.5.9)
186 | psych (5.2.2)
187 | date
188 | stringio
189 | public_suffix (6.0.1)
190 | racc (1.8.1)
191 | rack (3.1.8)
192 | rack-session (2.1.0)
193 | base64 (>= 0.1.0)
194 | rack (>= 3.0.0)
195 | rack-test (2.2.0)
196 | rack (>= 1.3)
197 | rackup (2.2.1)
198 | rack (>= 3)
199 | rails (8.0.1)
200 | actioncable (= 8.0.1)
201 | actionmailbox (= 8.0.1)
202 | actionmailer (= 8.0.1)
203 | actionpack (= 8.0.1)
204 | actiontext (= 8.0.1)
205 | actionview (= 8.0.1)
206 | activejob (= 8.0.1)
207 | activemodel (= 8.0.1)
208 | activerecord (= 8.0.1)
209 | activestorage (= 8.0.1)
210 | activesupport (= 8.0.1)
211 | bundler (>= 1.15.0)
212 | railties (= 8.0.1)
213 | rails-dom-testing (2.2.0)
214 | activesupport (>= 5.0.0)
215 | minitest
216 | nokogiri (>= 1.6)
217 | rails-html-sanitizer (1.6.2)
218 | loofah (~> 2.21)
219 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
220 | railties (8.0.1)
221 | actionpack (= 8.0.1)
222 | activesupport (= 8.0.1)
223 | irb (~> 1.13)
224 | rackup (>= 1.0.0)
225 | rake (>= 12.2)
226 | thor (~> 1.0, >= 1.2.2)
227 | zeitwerk (~> 2.6)
228 | rainbow (3.1.1)
229 | rake (13.2.1)
230 | rdoc (6.11.0)
231 | psych (>= 4.0.0)
232 | regexp_parser (2.10.0)
233 | reline (0.6.0)
234 | io-console (~> 0.5)
235 | rexml (3.4.0)
236 | rubocop (1.70.0)
237 | json (~> 2.3)
238 | language_server-protocol (>= 3.17.0)
239 | parallel (~> 1.10)
240 | parser (>= 3.3.0.2)
241 | rainbow (>= 2.2.2, < 4.0)
242 | regexp_parser (>= 2.9.3, < 3.0)
243 | rubocop-ast (>= 1.36.2, < 2.0)
244 | ruby-progressbar (~> 1.7)
245 | unicode-display_width (>= 2.4.0, < 4.0)
246 | rubocop-ast (1.37.0)
247 | parser (>= 3.3.1.0)
248 | rubocop-performance (1.23.1)
249 | rubocop (>= 1.48.1, < 2.0)
250 | rubocop-ast (>= 1.31.1, < 2.0)
251 | ruby-progressbar (1.13.0)
252 | securerandom (0.4.1)
253 | signet (0.19.0)
254 | addressable (~> 2.8)
255 | faraday (>= 0.17.5, < 3.a)
256 | jwt (>= 1.5, < 3.0)
257 | multi_json (~> 1.10)
258 | sqlite3 (2.5.0-aarch64-linux-gnu)
259 | sqlite3 (2.5.0-aarch64-linux-musl)
260 | sqlite3 (2.5.0-arm-linux-gnu)
261 | sqlite3 (2.5.0-arm-linux-musl)
262 | sqlite3 (2.5.0-arm64-darwin)
263 | sqlite3 (2.5.0-x86_64-darwin)
264 | sqlite3 (2.5.0-x86_64-linux-gnu)
265 | sqlite3 (2.5.0-x86_64-linux-musl)
266 | standard (1.44.0)
267 | language_server-protocol (~> 3.17.0.2)
268 | lint_roller (~> 1.0)
269 | rubocop (~> 1.70.0)
270 | standard-custom (~> 1.0.0)
271 | standard-performance (~> 1.6)
272 | standard-custom (1.0.2)
273 | lint_roller (~> 1.0)
274 | rubocop (~> 1.50)
275 | standard-performance (1.6.0)
276 | lint_roller (~> 1.1)
277 | rubocop-performance (~> 1.23.0)
278 | stringio (3.1.2)
279 | thor (1.3.2)
280 | timeout (0.4.3)
281 | tzinfo (2.0.6)
282 | concurrent-ruby (~> 1.0)
283 | unicode-display_width (3.1.4)
284 | unicode-emoji (~> 4.0, >= 4.0.4)
285 | unicode-emoji (4.0.4)
286 | uri (1.0.2)
287 | useragent (0.16.11)
288 | webmock (3.24.0)
289 | addressable (>= 2.8.0)
290 | crack (>= 0.3.2)
291 | hashdiff (>= 0.4.0, < 2.0.0)
292 | websocket-driver (0.7.7)
293 | base64
294 | websocket-extensions (>= 0.1.0)
295 | websocket-extensions (0.1.5)
296 | zeitwerk (2.7.1)
297 |
298 | PLATFORMS
299 | aarch64-linux-gnu
300 | aarch64-linux-musl
301 | arm-linux-gnu
302 | arm-linux-musl
303 | arm64-darwin
304 | x86_64-darwin
305 | x86_64-linux
306 | x86_64-linux-gnu
307 | x86_64-linux-musl
308 |
309 | DEPENDENCIES
310 | apnotic (~> 1.7)
311 | appraisal
312 | googleauth (~> 1.1)
313 | noticed!
314 | pg
315 | sqlite3
316 | standard
317 | webmock
318 |
319 | BUNDLED WITH
320 | 2.6.8
321 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Chris Oliver
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require "bundler/setup"
3 | rescue LoadError
4 | puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5 | end
6 |
7 | require "rdoc/task"
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = "rdoc"
11 | rdoc.title = "Noticed"
12 | rdoc.options << "--line-numbers"
13 | rdoc.rdoc_files.include("README.md")
14 | rdoc.rdoc_files.include("lib/**/*.rb")
15 | end
16 |
17 | require "bundler/gem_tasks"
18 |
19 | require "rake/testtask"
20 |
21 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
22 | load "rails/tasks/engine.rake"
23 | load "rails/tasks/statistics.rake"
24 |
25 | Rake::TestTask.new(:test) do |t|
26 | t.libs << "test"
27 | t.pattern = "test/**/*_test.rb"
28 | t.verbose = false
29 | end
30 |
31 | task default: :test
32 |
--------------------------------------------------------------------------------
/app/jobs/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/app/jobs/.keep
--------------------------------------------------------------------------------
/app/jobs/noticed/application_job.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class ApplicationJob < ActiveJob::Base
3 | # Automatically retry jobs that encountered a deadlock
4 | # retry_on ActiveRecord::Deadlocked
5 |
6 | # Most jobs are safe to ignore if the underlying records are no longer available
7 | discard_on ActiveJob::DeserializationError
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/noticed/event_job.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class EventJob < Noticed.parent_class.constantize
3 | def perform(event)
4 | # Enqueue bulk deliveries
5 | event.bulk_delivery_methods.each_value do |deliver_by|
6 | deliver_by.perform_later(event) if deliver_by.perform?(event)
7 | end
8 |
9 | # Enqueue individual deliveries
10 | event.notifications.each do |notification|
11 | event.delivery_methods.each_value do |deliver_by|
12 | deliver_by.perform_later(notification) if deliver_by.perform?(notification)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/app/models/.keep
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/concerns/noticed/deliverable.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module Deliverable
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | class_attribute :bulk_delivery_methods, instance_writer: false, default: {}
7 | class_attribute :delivery_methods, instance_writer: false, default: {}
8 | class_attribute :required_param_names, instance_writer: false, default: []
9 | class_attribute :_recipients, instance_writer: false
10 | end
11 |
12 | class_methods do
13 | def inherited(base)
14 | base.bulk_delivery_methods = bulk_delivery_methods.dup
15 | base.delivery_methods = delivery_methods.dup
16 | base.required_param_names = required_param_names.dup
17 | super
18 | end
19 |
20 | def bulk_deliver_by(name, options = {})
21 | raise NameError, "#{name} has already been used for this Notifier." if bulk_delivery_methods.has_key?(name)
22 |
23 | config = ActiveSupport::OrderedOptions.new.merge(options)
24 | yield config if block_given?
25 | bulk_delivery_methods[name] = DeliverBy.new(name, config, bulk: true)
26 | end
27 |
28 | def deliver_by(name, options = {})
29 | raise NameError, "#{name} has already been used for this Notifier." if delivery_methods.has_key?(name)
30 |
31 | if name == :database
32 | Noticed.deprecator.warn <<-WARNING.squish
33 | The :database delivery method has been deprecated and does nothing. Notifiers automatically save to the database now.
34 | WARNING
35 | return
36 | end
37 |
38 | config = ActiveSupport::OrderedOptions.new.merge(options)
39 | yield config if block_given?
40 | delivery_methods[name] = DeliverBy.new(name, config)
41 | end
42 |
43 | def recipients(option = nil, &block)
44 | self._recipients = block || option
45 | end
46 |
47 | def required_params(*names)
48 | required_param_names.concat names
49 | end
50 | alias_method :required_param, :required_params
51 |
52 | def params(*names)
53 | Noticed.deprecator.warn <<-WARNING.squish
54 | `params` is deprecated and has been renamed to `required_params`
55 | WARNING
56 | required_params(*names)
57 | end
58 |
59 | def param(*names)
60 | Noticed.deprecator.warn <<-WARNING.squish
61 | `param :name` is deprecated and has been renamed to `required_param :name`
62 | WARNING
63 | required_params(*names)
64 | end
65 |
66 | def with(params)
67 | if self < Ephemeral
68 | new(params: params)
69 | else
70 | record = params.delete(:record)
71 | new(params: params, record: record)
72 | end
73 | end
74 |
75 | def deliver(recipients = nil, **options)
76 | new.deliver(recipients, **options)
77 | end
78 | alias_method :deliver_later, :deliver
79 | end
80 |
81 | # CommentNotifier.deliver(User.all)
82 | # CommentNotifier.deliver(User.all, priority: 10)
83 | # CommentNotifier.deliver(User.all, queue: :low_priority)
84 | # CommentNotifier.deliver(User.all, wait: 5.minutes)
85 | # CommentNotifier.deliver(User.all, wait_until: 1.hour.from_now)
86 | def deliver(recipients = nil, enqueue_job: true, **options)
87 | recipients ||= evaluate_recipients
88 |
89 | validate!
90 |
91 | transaction do
92 | recipients_attributes = Array.wrap(recipients).map do |recipient|
93 | recipient_attributes_for(recipient)
94 | end
95 |
96 | self.notifications_count = recipients_attributes.size
97 | save!
98 |
99 | if Rails.gem_version >= Gem::Version.new("7.0.0.alpha1")
100 | notifications.insert_all!(recipients_attributes, record_timestamps: true) if recipients_attributes.any?
101 | else
102 | time = Time.current
103 | recipients_attributes.each do |attributes|
104 | attributes[:created_at] = time
105 | attributes[:updated_at] = time
106 | end
107 | notifications.insert_all!(recipients_attributes) if recipients_attributes.any?
108 | end
109 | end
110 |
111 | # Enqueue delivery job
112 | EventJob.set(options).perform_later(self) if enqueue_job
113 |
114 | self
115 | end
116 | alias_method :deliver_later, :deliver
117 |
118 | def evaluate_recipients
119 | return unless _recipients
120 |
121 | if _recipients.respond_to?(:call)
122 | instance_exec(&_recipients)
123 | elsif _recipients.is_a?(Symbol) && respond_to?(_recipients)
124 | send(_recipients)
125 | end
126 | end
127 |
128 | def recipient_attributes_for(recipient)
129 | {
130 | type: "#{self.class.name}::Notification",
131 | recipient_type: recipient.class.base_class.name,
132 | recipient_id: recipient.id
133 | }
134 | end
135 |
136 | def validate!
137 | validate_params!
138 | validate_delivery_methods!
139 | end
140 |
141 | def validate_params!
142 | required_param_names.each do |param_name|
143 | raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params.has_key?(param_name)
144 | end
145 | end
146 |
147 | def validate_delivery_methods!
148 | bulk_delivery_methods.values.each(&:validate!)
149 | delivery_methods.values.each(&:validate!)
150 | end
151 |
152 | # If a GlobalID record in params is no longer found, the params will default with a noticed_error key
153 | def deserialize_error?
154 | !!params[:noticed_error]
155 | end
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/app/models/concerns/noticed/notification_methods.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module NotificationMethods
3 | extend ActiveSupport::Concern
4 |
5 | class_methods do
6 | # Generate a Notification class each time a Notifier is defined
7 | def inherited(notifier)
8 | super
9 | notifier.const_set :Notification, Class.new(const_defined?(:Notification) ? const_get(:Notification) : Noticed::Notification)
10 | end
11 |
12 | def notification_methods(&block)
13 | const_get(:Notification).class_eval(&block)
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/concerns/noticed/readable.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module Readable
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | scope :read, -> { where.not(read_at: nil) }
7 | scope :unread, -> { where(read_at: nil) }
8 | scope :seen, -> { where.not(seen_at: nil) }
9 | scope :unseen, -> { where(seen_at: nil) }
10 | end
11 |
12 | class_methods do
13 | def mark_as_read_and_seen(**kwargs)
14 | update_all(**kwargs.with_defaults(read_at: Time.current, seen_at: Time.current, updated_at: Time.current))
15 | end
16 |
17 | def mark_as_unread_and_unseen(**kwargs)
18 | update_all(**kwargs.with_defaults(read_at: nil, seen_at: nil, updated_at: Time.current))
19 | end
20 |
21 | def mark_as_read(**kwargs)
22 | update_all(**kwargs.with_defaults(read_at: Time.current, updated_at: Time.current))
23 | end
24 |
25 | def mark_as_unread(**kwargs)
26 | update_all(**kwargs.with_defaults(read_at: nil, updated_at: Time.current))
27 | end
28 |
29 | def mark_as_seen(**kwargs)
30 | update_all(**kwargs.with_defaults(seen_at: Time.current, updated_at: Time.current))
31 | end
32 |
33 | def mark_as_unseen(**kwargs)
34 | update_all(**kwargs.with_defaults(seen_at: nil, updated_at: Time.current))
35 | end
36 | end
37 |
38 | def mark_as_read
39 | update(read_at: Time.current)
40 | end
41 |
42 | def mark_as_read!
43 | update!(read_at: Time.current)
44 | end
45 |
46 | def mark_as_unread
47 | update(read_at: nil)
48 | end
49 |
50 | def mark_as_unread!
51 | update!(read_at: nil)
52 | end
53 |
54 | def mark_as_seen
55 | update(seen_at: Time.current)
56 | end
57 |
58 | def mark_as_seen!
59 | update!(seen_at: Time.current)
60 | end
61 |
62 | def mark_as_unseen
63 | update(seen_at: nil)
64 | end
65 |
66 | def mark_as_unseen!
67 | update!(seen_at: nil)
68 | end
69 |
70 | def read?
71 | read_at?
72 | end
73 |
74 | def unread?
75 | !read_at?
76 | end
77 |
78 | def seen?
79 | seen_at?
80 | end
81 |
82 | def unseen?
83 | !seen_at?
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/app/models/noticed/application_record.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class ApplicationRecord < ActiveRecord::Base
3 | self.abstract_class = true
4 | self.table_name_prefix = "noticed_"
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/noticed/deliverable/deliver_by.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module Deliverable
3 | class DeliverBy
4 | attr_reader :name, :config, :bulk
5 |
6 | def initialize(name, config, bulk: false)
7 | @name, @config, @bulk, = name, config, bulk
8 | end
9 |
10 | def constant
11 | namespace = bulk ? "Noticed::BulkDeliveryMethods" : "Noticed::DeliveryMethods"
12 | config.fetch(:class, [namespace, name.to_s.camelize].join("::")).constantize
13 | end
14 |
15 | def validate!
16 | constant.required_option_names.each do |option|
17 | raise ValidationError, "option `#{option}` must be set for `deliver_by :#{name}`" unless config[option].present?
18 | end
19 | end
20 |
21 | def perform_later(event_or_notification, options = {})
22 | constant.set(computed_options(options, event_or_notification)).perform_later(name, event_or_notification)
23 | end
24 |
25 | def ephemeral_perform_later(notifier, recipient, params, options = {})
26 | constant.set(computed_options(options, recipient))
27 | .perform_later(name, "#{notifier}::Notification", recipient: recipient, params: params)
28 | end
29 |
30 | def evaluate_option(name, context)
31 | option = config[name]
32 |
33 | if option.respond_to?(:call)
34 | context.instance_exec(&option)
35 | elsif option.is_a?(Symbol) && context.respond_to?(option, true)
36 | context.send(option)
37 | else
38 | option
39 | end
40 | end
41 |
42 | def perform?(notification)
43 | return true unless config.key?(:before_enqueue)
44 |
45 | perform = false
46 | catch(:abort) {
47 | evaluate_option(:before_enqueue, notification)
48 | perform = true
49 | }
50 | perform
51 | end
52 |
53 | private
54 |
55 | def computed_options(options, recipient)
56 | options[:wait] ||= evaluate_option(:wait, recipient) if config.has_key?(:wait)
57 | options[:wait_until] ||= evaluate_option(:wait_until, recipient) if config.has_key?(:wait_until)
58 | options[:queue] ||= evaluate_option(:queue, recipient) if config.has_key?(:queue)
59 | options[:priority] ||= evaluate_option(:priority, recipient) if config.has_key?(:priority)
60 | options
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/app/models/noticed/ephemeral.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class Ephemeral
3 | include ActiveModel::Model
4 | include ActiveModel::Attributes
5 | include Noticed::Deliverable
6 | include Noticed::Translation
7 | include Rails.application.routes.url_helpers
8 |
9 | attribute :record
10 | attribute :params, default: {}
11 |
12 | class Notification
13 | include ActiveModel::Model
14 | include ActiveModel::Attributes
15 | include Noticed::Translation
16 | include Rails.application.routes.url_helpers
17 |
18 | attribute :recipient
19 | attribute :event
20 |
21 | delegate :params, :record, to: :event
22 |
23 | def self.new_with_params(recipient, params)
24 | instance = new(recipient: recipient)
25 | instance.event = module_parent.new(params: params)
26 | instance
27 | end
28 | end
29 |
30 | # Dynamically define Notification on each Ephemeral Notifier
31 | def self.inherited(notifier)
32 | super
33 | notifier.const_set :Notification, Class.new(Noticed::Ephemeral::Notification)
34 | end
35 |
36 | def self.notification_methods(&block)
37 | const_get(:Notification).class_eval(&block)
38 | end
39 |
40 | def deliver(recipients = nil)
41 | recipients ||= evaluate_recipients
42 | recipients = Array.wrap(recipients)
43 |
44 | bulk_delivery_methods.each do |_, deliver_by|
45 | deliver_by.ephemeral_perform_later(self.class.name, recipients, params)
46 | end
47 |
48 | recipients.each do |recipient|
49 | delivery_methods.each do |_, deliver_by|
50 | deliver_by.ephemeral_perform_later(self.class.name, recipient, params)
51 | end
52 | end
53 |
54 | self
55 | end
56 |
57 | def record
58 | params[:record]
59 | end
60 | end
61 | end
62 |
63 | ActiveSupport.run_load_hooks :noticed_ephemeral, Noticed::Ephemeral
64 |
--------------------------------------------------------------------------------
/app/models/noticed/event.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class Event < ApplicationRecord
3 | include Deliverable
4 | include NotificationMethods
5 | include Translation
6 | include Rails.application.routes.url_helpers
7 |
8 | belongs_to :record, polymorphic: true, optional: true
9 | has_many :notifications, dependent: :delete_all
10 |
11 | accepts_nested_attributes_for :notifications
12 |
13 | scope :newest_first, -> { order(created_at: :desc) }
14 |
15 | attribute :params, :json, default: {}
16 |
17 | # Ephemeral notifiers cannot serialize params since they aren't ActiveRecord backed
18 | if respond_to? :serialize
19 | if Rails.gem_version >= Gem::Version.new("7.1.0.alpha")
20 | serialize :params, coder: Coder
21 | else
22 | serialize :params, Coder
23 | end
24 | end
25 | end
26 | end
27 |
28 | ActiveSupport.run_load_hooks :noticed_event, Noticed::Event
29 |
--------------------------------------------------------------------------------
/app/models/noticed/notification.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class Notification < ApplicationRecord
3 | include Rails.application.routes.url_helpers
4 | include Readable
5 | include Translation
6 |
7 | belongs_to :event, counter_cache: true
8 | belongs_to :recipient, polymorphic: true
9 |
10 | scope :newest_first, -> { order(created_at: :desc) }
11 |
12 | delegate :params, :record, to: :event
13 | end
14 | end
15 |
16 | ActiveSupport.run_load_hooks :noticed_notification, Noticed::Notification
17 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path("..", __dir__)
6 | ENGINE_PATH = File.expand_path("../lib/noticed/engine", __dir__)
7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
12 |
13 | require "rails/all"
14 | require "rails/engine/commands"
15 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | $: << File.expand_path("../test", __dir__)
3 |
4 | require "bundler/setup"
5 | require "rails/plugin/test"
6 |
--------------------------------------------------------------------------------
/db/migrate/20231215190233_create_noticed_tables.rb:
--------------------------------------------------------------------------------
1 | class CreateNoticedTables < ActiveRecord::Migration[6.1]
2 | def change
3 | primary_key_type, foreign_key_type = primary_and_foreign_key_types
4 | create_table :noticed_events, id: primary_key_type do |t|
5 | t.string :type
6 | t.belongs_to :record, polymorphic: true, type: foreign_key_type
7 | if t.respond_to?(:jsonb)
8 | t.jsonb :params
9 | else
10 | t.json :params
11 | end
12 |
13 | t.timestamps
14 | end
15 |
16 | create_table :noticed_notifications, id: primary_key_type do |t|
17 | t.string :type
18 | t.belongs_to :event, null: false, type: foreign_key_type
19 | t.belongs_to :recipient, polymorphic: true, null: false, type: foreign_key_type
20 | t.datetime :read_at
21 | t.datetime :seen_at
22 |
23 | t.timestamps
24 | end
25 | end
26 |
27 | private
28 |
29 | def primary_and_foreign_key_types
30 | config = Rails.configuration.generators
31 | setting = config.options[config.orm][:primary_key_type]
32 | primary_key_type = setting || :primary_key
33 | foreign_key_type = setting || :bigint
34 | [primary_key_type, foreign_key_type]
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/db/migrate/20240129184740_add_notifications_count_to_noticed_event.rb:
--------------------------------------------------------------------------------
1 | class AddNotificationsCountToNoticedEvent < ActiveRecord::Migration[6.1]
2 | def change
3 | add_column :noticed_events, :notifications_count, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/docs/bulk_delivery_methods/bluesky.md:
--------------------------------------------------------------------------------
1 | # Bluesky Bulk Delivery Method
2 |
3 | Create a Bluesky post.
4 |
5 | ## Usage
6 |
7 | ```ruby
8 | class CommentNotification
9 | bulk_deliver_by :bluesky do |config|
10 | config.identifier = "username"
11 | config.password = "password"
12 | config.json = -> {
13 | {
14 | text: "Hello world!",
15 | createdAt: Time.current.iso8601
16 | # ...
17 | }
18 | }
19 | end
20 | end
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/bulk_delivery_methods/discord.md:
--------------------------------------------------------------------------------
1 | # Discord Bulk Delivery Method
2 |
3 | Send a Discord message to builk notify users in a channel.
4 |
5 | We recommend using [Discohook](https://discohook.org) to design your messages.
6 |
7 | ## Usage
8 |
9 | ```ruby
10 | class CommentNotification
11 | bulk_deliver_by :discord do |config|
12 | config.url = "https://discord.com..."
13 | config.json = -> {
14 | {
15 | # ...
16 | }
17 | }
18 | end
19 | end
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/bulk_delivery_methods/slack.md:
--------------------------------------------------------------------------------
1 | # Slack Bulk Delivery Method
2 |
3 | Send a Slack message to bulk notify users in a channel.
4 |
5 | ## Usage
6 |
7 | ```ruby
8 | class CommentNotification
9 | bulk_deliver_by :slack do |config|
10 | config.url = "https://slack.com..."
11 | config.json = -> {
12 | {
13 | # ...
14 | }
15 | }
16 |
17 | # Slack's chat.postMessage endpoint returns a 200 with {ok: true/false}. Disable this check by setting to false
18 | # config.raise_if_not_ok = true
19 | end
20 | end
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/bulk_delivery_methods/webhook.md:
--------------------------------------------------------------------------------
1 | # Webhook Bulk Delivery Method
2 |
3 | Send a webhook request to bulk notify users in a channel.
4 |
5 | ## Usage
6 |
7 | ```ruby
8 | class CommentNotification
9 | bulk_deliver_by :webhook do |config|
10 | config.url = "https://example.org..."
11 | config.json = -> {
12 | {
13 | # ...
14 | }
15 | }
16 | end
17 | end
18 | ```
19 |
--------------------------------------------------------------------------------
/docs/delivery_methods/action_cable.md:
--------------------------------------------------------------------------------
1 | # ActionCable Delivery Method
2 |
3 | Sends a notification to the browser via websockets (ActionCable channel by default).
4 |
5 | ```ruby
6 | deliver_by :action_cable do |config|
7 | config.channel = "Noticed::NotificationChannel"
8 | config.stream = ->{ recipient }
9 | config.message = ->{ params.merge( user_id: recipient.id) }
10 | end
11 | ```
12 |
13 | ## Options
14 |
15 | * `message`
16 |
17 | Should return a Hash to be sent as the ActionCable message
18 |
19 | * `channel`
20 |
21 | Override the ActionCable channel used to send notifications. Defaults to `Noticed::NotificationChannel`
22 |
23 | * `stream`
24 |
25 | Should return the stream the message is broadcasted to. Defaults to `recipient`
26 |
27 | ## Authentication
28 |
29 | To send notifications to individual users, you'll want to use `stream_for current_user`. This requires `identified_by :current_user` in your ApplicationCable::Connection. For example, using Devise for authentication:
30 |
31 | ```ruby
32 | module ApplicationCable
33 | class Connection < ActionCable::Connection::Base
34 | identified_by :current_user
35 |
36 | def connect
37 | self.current_user = find_verified_user
38 | logger.add_tags "ActionCable", "User #{current_user.id}"
39 | end
40 |
41 | protected
42 |
43 | def find_verified_user
44 | if current_user = env['warden'].user
45 | current_user
46 | else
47 | reject_unauthorized_connection
48 | end
49 | end
50 | end
51 | end
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/delivery_methods/discord.md:
--------------------------------------------------------------------------------
1 | # Discord Bulk Delivery Method
2 |
3 | Send Discord messages to builk notify users in a channel.
4 |
5 | We recommend using [Discohook](https://discohook.org) to design your messages.
6 |
7 | ## Usage
8 |
9 | ```ruby
10 | class CommentNotification
11 | deliver_by :discord do |config|
12 | config.url = "https://discord.com..."
13 | config.json = -> {
14 | {
15 | # ...
16 | }
17 | }
18 | end
19 | end
20 | ```
21 |
--------------------------------------------------------------------------------
/docs/delivery_methods/email.md:
--------------------------------------------------------------------------------
1 | ### Email Delivery Method
2 |
3 | Sends an email to each recipient.
4 |
5 | ```ruby
6 | deliver_by :email do |config|
7 | config.mailer = "UserMailer"
8 | config.method = :receipt
9 | config.params = ->{ params }
10 | config.args = ->{ [1, 2, 3] }
11 |
12 | # Enqueues a separate job for sending the email using deliver_later.
13 | # Deliveries already happen in jobs so this is typically unnecessary.
14 | # config.enqueue = false
15 | end
16 | ```
17 |
18 | ##### Options
19 |
20 | - `mailer` - **Required**
21 |
22 | The mailer that should send the email
23 |
24 | - `method: :invoice_paid` - **Required**
25 |
26 | Used to customize the method on the mailer that is called
27 |
28 | - `params` - _Optional_
29 |
30 | Use a custom method to define the params sent to the mailer. `recipient` will be merged into the params.
31 |
32 | - `args` - _Optional_
33 |
34 | - `enqueue: false` - _Optional_
35 |
36 | Use `deliver_later` to queue email delivery with ActiveJob. This is `false` by default as each delivery method is already a separate job.
37 |
38 | ##### ActionMailer::Preview
39 |
40 | Use `YourMailer.with({ recipient: user }).mailer_method_name` to set up a `ActionMailer::Preview`. And you can pass any number of params into the `Hash` but you will need the recipient key.
41 |
42 | ```ruby
43 | # test/mailers/previews/comment_mailer_preview.rb
44 | class CommentMailerPreview < ActionMailer::Preview
45 | def mailer_method_name
46 | CommentMailer.with({ recipient: User.first }).mailer_method_name
47 | end
48 | end
49 | ```
50 |
--------------------------------------------------------------------------------
/docs/delivery_methods/fcm.md:
--------------------------------------------------------------------------------
1 | # Firebase Cloud Messaging Delivery Method
2 |
3 | Send Device Notifications using the Google Firebase Cloud Messaging service and the `googleauth` gem. FCM supports Android, iOS, and web clients.
4 |
5 | ```bash
6 | bundle add "googleauth"
7 | ```
8 |
9 | ## Google Firebase Cloud Messaging Notification Service
10 |
11 | To generate your Firebase Cloud Messaging credentials, you'll need to create your project if you have not already. See https://console.firebase.google.com/u/1/
12 | Once you have created your project, visit the project dashboard and click the settings cog in the top of the left sidebar menu, then click project settings.
13 |
14 | 
15 |
16 | In the project settings screen click on the Service accounts tab in the top navigation menu, then click the Generate new private key button.
17 |
18 | 
19 |
20 | This json file will contain the necessary credentials in order to send notifications via Google Firebase Cloud Messaging.
21 | See the below instructions on where to store this information within your application.
22 |
23 | ## Usage
24 |
25 | ```ruby
26 | class CommentNotification
27 | deliver_by :fcm do |config|
28 | config.credentials = Rails.root.join("config/certs/fcm.json")
29 | config.device_tokens = -> { recipient.notification_tokens.where(platform: "fcm").pluck(:token) }
30 | config.json = ->(device_token) {
31 | {
32 | message: {
33 | token: device_token,
34 | notification: {
35 | title: "Test Title",
36 | body: "Test body"
37 | }
38 | }
39 | }
40 | }
41 | config.if = -> { recipient.android_notifications? }
42 | end
43 | end
44 | ```
45 |
46 | ## Options
47 |
48 | ### `json`
49 | Customize the Firebase Cloud Messaging notification object. This can be a Lambda or Symbol of a method name on the notifier.
50 |
51 | The callable object will be given the device token as an argument.
52 |
53 | There are lots of options of how to structure a FCM notification message. See https://firebase.google.com/docs/cloud-messaging/concept-options for more details.
54 |
55 | ### `credentials`
56 | The location of your Firebase Cloud Messaging credentials.
57 |
58 | #### When a String Object
59 |
60 | Internally, this string is passed to `Rails.root.join()` as an argument so there is no need to do this beforehand.
61 |
62 | ```ruby
63 | deliver_by :fcm do |config|
64 | config.credentials = "config/credentials/fcm.json"
65 | end
66 | ```
67 |
68 | #### When a Pathname object
69 |
70 | The Pathname object can point to any location where you are storing your credentials.
71 |
72 | ```ruby
73 | deliver_by :fcm do |config|
74 | config.credentials = Rails.root.join("config/credentials/fcm.json")
75 | end
76 | ```
77 |
78 | #### When a Hash object
79 |
80 | A Hash which contains your credentials
81 |
82 | ```ruby
83 | deliver_by :fcm do |config|
84 | config.credentials = credentials_hash
85 | end
86 |
87 | credentials_hash = {
88 | "type": "service_account",
89 | "project_id": "test-project-1234",
90 | "private_key_id": ...,
91 | etc.....
92 | }
93 | ```
94 |
95 | #### When a Symbol
96 |
97 | Points to a method which can return a Hash of your credentials, Pathname, or String to your credentials like the examples above.
98 |
99 | We pass the notification object as an argument to the method. If you don't need to use it you can use the splat operator `(*)` to ignore it.
100 |
101 | ```ruby
102 | deliver_by :fcm do |config|
103 | config.credentials = :fcm_credentials
104 | config.json = :format_notification
105 | end
106 |
107 | def fcm_credentials(*)
108 | Rails.root.join("config/certs/fcm.json")
109 | end
110 | ```
111 |
112 | #### Otherwise
113 |
114 | If the credentials option is left out, it will look for your credentials in: `Rails.application.credentials.fcm`
115 |
116 | ## Gathering Notification Tokens
117 |
118 | A recipient can have multiple tokens (i.e. multiple Android devices), so make sure to return them all.
119 |
120 | Here, the recipient `has_many :notification_tokens` with columns `platform` and `token`.
121 |
122 | ```ruby
123 | def fcm_device_tokens(recipient)
124 | recipient.notification_tokens.where(platform: "fcm").pluck(:token)
125 | end
126 | ```
127 |
128 | ## Handling Failures
129 |
130 | Firebase Cloud Messaging Notifications may fail delivery if the user has removed the app from their device.
131 |
132 | ```ruby
133 | class CommentNotification
134 | deliver_by :fcm do |config|
135 | config.invalid_token = ->(device_token) { NotificationToken.find_by(token: device_token).destroy }
136 | end
137 | end
138 | ```
139 |
--------------------------------------------------------------------------------
/docs/delivery_methods/ios.md:
--------------------------------------------------------------------------------
1 | # iOS Notification Delivery Method
2 |
3 | Send Apple Push Notifications with HTTP2 using the `apnotic` gem. The benefit of HTTP2 is that we can receive feedback for invalid device tokens without running a separate feedback service like RPush does.
4 |
5 | ```bash
6 | bundle add "apnotic"
7 | ```
8 |
9 | ## Apple Push Notification Service (APNS) Authentication
10 |
11 | Token-based authentication is used for APNS.
12 | * A single key can be used for every app in your developer account.
13 | * Token authentication never expires, unlike certificate authentication which must be renewed annually.
14 |
15 | Follow these docs for setting up Token-based authentication.
16 | https://github.com/ostinelli/apnotic#token-based-authentication
17 | https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns
18 |
19 | ## Usage
20 |
21 | ```ruby
22 | class CommentNotifier < ApplicationNotifier
23 | deliver_by :ios do |config|
24 | config.device_tokens = -> { recipient.notification_tokens.where(platform: :iOS).pluck(:token) }
25 | config.format = ->(apn) {
26 | apn.alert = "Hello world"
27 | apn.custom_payload = {url: root_url(host: "example.org")}
28 | }
29 | config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_id)
30 | config.key_id = Rails.application.credentials.dig(:ios, :key_id)
31 | config.team_id = Rails.application.credentials.dig(:ios, :team_id)
32 | config.apns_key = Rails.application.credentials.dig(:ios, :apns_key)
33 | config.error_handler = ->(exception) { ... }
34 | end
35 | end
36 | ```
37 |
38 | ## Options
39 |
40 | * `format`
41 |
42 | Customize the Apnotic notification object
43 |
44 | See https://github.com/ostinelli/apnotic#apnoticnotification
45 |
46 | * `bundle_identifier`
47 |
48 | The APN bundle identifier
49 |
50 | * `apns_key`
51 |
52 | The contents of your p8 apns key file.
53 |
54 | * `key_id`
55 |
56 | Your APN Key ID
57 |
58 | * `team_id`
59 |
60 | Your APN Team ID
61 |
62 | * `pool_size: 5` - *Optional*
63 |
64 | The connection pool size for Apnotic
65 |
66 | * `development` - *Optional*
67 |
68 | Set this to `true` to use the APNS sandbox environment for sending notifications. This is required when running the app to your device via Xcode. Running the app via TestFlight or the App Store should not use development.
69 |
70 | * `error_handler` - *Optional*
71 | A lambda to allow your app to handle Apnotic errors.
72 |
73 | ## Gathering Notification Tokens
74 |
75 | A recipient can have multiple tokens (i.e. multiple iOS devices), so make sure to return them all.
76 |
77 | Here, the recipient `has_many :notification_tokens` with columns `platform` and `token`.
78 |
79 | ```ruby
80 | deliver_by :ios do |config|
81 | config.device_tokens = -> { recipient.notification_tokens.where(platform: :iOS).pluck(:token) }
82 | end
83 | ```
84 |
85 | ## Handling Failures
86 |
87 | Apple Push Notifications may fail delivery if the user has removed the app from their device. Noticed allows you
88 |
89 | ```ruby
90 | class CommentNotifier < ApplicationNotifier
91 | deliver_by :ios do |config|
92 | config.invalid_token = ->(token) { NotificationToken.where(token: token).destroy_all }
93 | end
94 | end
95 | ```
96 |
97 | ## Updating the iOS app badge
98 |
99 | If you're managing the iOS app badge, you can pass it along in the format
100 |
101 | ```ruby
102 | class CommentNotifier < ApplicationNotifier
103 | deliver_by :ios do |config|
104 | config.format = ->(apn) {
105 | apn.alert = "Hello world"
106 | apn.custom_payload = {url: root_url(host: "example.org")}
107 | apn.badge = recipient.notifications.unread.count
108 | }
109 | end
110 | end
111 | ```
112 |
113 | Another common action is to update the badge after a user reads a notification.
114 |
115 | This is a great use of the Noticed::Ephemeral class. Since it's all in-memory, it will perform the job and not touch the database.
116 |
117 | ```ruby
118 | class NativeBadgeNotifier < Noticed::Ephemeral
119 | deliver_by :ios do |config|
120 | config.format = ->(apn) {
121 | # Setting the alert text to nil will deliver the notification in
122 | # the background. This is used to update the app badge on the iOS home screen
123 | apn.alert = nil
124 | apn.custom_payload = {}
125 | apn.badge = recipient.notifications.unread.count
126 | }
127 | end
128 | end
129 | ```
130 |
131 | Then you can simply deliver this notifier to update the badge when you mark the notification as read
132 |
133 | ```ruby
134 | notification.mark_as_read!
135 | NativeBadgeNotifier.with(record: notification).deliver(notification.recipient)
136 | ```
137 |
138 | ## Delivering to Sandboxes and real devices
139 |
140 | If you wish to send notifications to both sandboxed and real devices from the same application, you can configure two iOS delivery methods
141 | A user has_many tokens that can be generated from both development (sandboxed devices), or production (not sandboxed devices) and is unrelated to the rails environment or endpoint being used. I
142 |
143 | ```ruby
144 | deliver_by :ios do |config|
145 | config.device_tokens = -> { recipient.notification_tokens.where(environment: :production, platform: :iOS).pluck(:token) }
146 | end
147 |
148 | deliver_by :ios_development, class: "Noticed::DeliveryMethods::Ios" do |config|
149 | config.development = true
150 | config.device_tokens = ->{ recipient.notification_tokens.where(environment: :development, platform: :iOS).pluck(:token) }
151 | end
152 | ```
153 |
--------------------------------------------------------------------------------
/docs/delivery_methods/microsoft_teams.md:
--------------------------------------------------------------------------------
1 | ### Microsoft Teams Delivery Method
2 |
3 | Sends a Teams notification via webhook.
4 |
5 | `deliver_by :microsoft_teams`
6 |
7 | #### Options
8 |
9 | * `format: :format_for_teams` - *Optional*
10 |
11 | Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
12 | Documentation for posting via Webhooks available at: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
13 |
14 | ```ruby
15 | {
16 | title: "This is the title for the card",
17 | text: "This is the body text for the card",
18 | sections: [{activityTitle: "Section Title", activityText: "Section Text"}],
19 | "potentialAction": [{
20 | "@type": "OpenUri",
21 | name: "Button Text",
22 | targets: [{
23 | os: "default",
24 | uri: "https://example.com/foo/action"
25 | }]
26 | }]
27 |
28 | }
29 | ```
30 |
31 | * `url: :url_for_teams_channel`: - *Optional*
32 |
33 | Use a custom method to retrieve the MS Teams Webhook URL. Method should return a string.
34 |
35 | Defaults to `Rails.application.credentials.microsoft_teams[:notification_url]`
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/delivery_methods/slack.md:
--------------------------------------------------------------------------------
1 | # Slack Delivery Method
2 |
3 | Send a Slack message to notify users in a channel.
4 |
5 | ## Usage
6 |
7 | ```ruby
8 | class CommentNotification
9 | deliver_by :slack do |config|
10 | config.url = "https://slack.com..."
11 | config.json = -> {
12 | {
13 | # ...
14 | }
15 | }
16 |
17 | # Slack's chat.postMessage endpoint returns a 200 with {ok: true/false}. Disable this check by setting to false
18 | # config.raise_if_not_ok = true
19 | end
20 | end
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/delivery_methods/test.md:
--------------------------------------------------------------------------------
1 | # Test Delivery Method
2 |
3 | Saves deliveries for testing.
4 |
5 | ## Usage
6 |
7 | ```ruby
8 | class CommentNotification
9 | deliver_by :test
10 | end
11 | ```
12 |
13 | ```ruby
14 | Noticed::DeliveryMethods::Test.delivered #=> []
15 | ```
16 |
--------------------------------------------------------------------------------
/docs/delivery_methods/twilio_messaging.md:
--------------------------------------------------------------------------------
1 | # Twilio Messaging Delivery Method
2 |
3 | Sends an SMS or Whatsapp message via Twilio Messaging.
4 |
5 | ```ruby
6 | deliver_by :twilio_messaging do |config|
7 | config.json = ->{
8 | {
9 | From: phone_number,
10 | To: recipient.phone_number,
11 | Body: params.fetch(:message)
12 | }
13 | }
14 |
15 | config.credentials = {
16 | phone_number: Rails.application.credentials.dig(:twilio, :phone_number),
17 | account_sid: Rails.application.credentials.dig(:twilio, :account_sid),
18 | auth_token: Rails.application.credentials.dig(:twilio, :auth_token)
19 | }
20 | # config.credentials = Rails.application.credentials.twilio
21 | # config.phone = "+1234567890"
22 | # config.url = "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}/Messages.json"
23 | end
24 | ```
25 |
26 | ## Content Templates
27 |
28 | ```ruby
29 | deliver_by :twilio_messaging do |config|
30 | config.json = -> {
31 | {
32 | From: "+1234567890",
33 | To: recipient.phone_number,
34 | ContentSid: "value", # Template SID
35 | ContentVariables: {1: recipient.first_name}
36 | }
37 | }
38 | end
39 | ```
40 |
41 | ## Error Handling
42 |
43 | Twilio provides a full list of error codes that can be handled as needed. See https://www.twilio.com/docs/api/errors
44 |
45 | ```ruby
46 | deliver_by :twilio_messaging do |config|
47 | config.error_handler = lambda do |twilio_error_response|
48 | error_hash = JSON.parse(twilio_error_response.body)
49 | case error_hash["code"]
50 | when 21211
51 | # The 'To' number is not a valid phone number.
52 | # Write your error handling code
53 | else
54 | raise "Unhandled Twilio error: #{error_hash}"
55 | end
56 | end
57 | end
58 | ```
59 |
60 | ## Options
61 |
62 | * `json` - *Optional*
63 |
64 | Use a custom method to define the payload sent to Twilio. Method should return a Hash.
65 |
66 | Defaults to:
67 |
68 | ```ruby
69 | {
70 | Body: params[:message], # From notification.params
71 | From: Rails.application.credentials.twilio[:phone_number],
72 | To: recipient.phone_number
73 | }
74 | ```
75 |
76 | * `credentials` - *Optional*
77 |
78 | Retrieve the credentials for Twilio. Should return a Hash with `:account_sid`, `:auth_token` and `:phone_number` keys.
79 |
80 | Defaults to `Rails.application.credentials.twilio[:account_sid]` and `Rails.application.credentials.twilio[:auth_token]`
81 |
82 | * `url` - *Optional*
83 |
84 | Retrieve the Twilio URL. Should return the Twilio API url as a string.
85 |
86 | Defaults to `"https://api.twilio.com/2010-04-01/Accounts/#{twilio_credentials(recipient)[:account_sid]}/Messages.json"`
87 |
--------------------------------------------------------------------------------
/docs/delivery_methods/vonage.md:
--------------------------------------------------------------------------------
1 | ### Vonage SMS
2 |
3 | Sends an SMS notification via Vonage / Nexmo.
4 |
5 | `deliver_by :vonage_sms`
6 |
7 | ##### Options
8 |
9 | * `credentials: :get_credentials` - *Optional*
10 |
11 | Use a custom method for retrieving credentials. Method should return a Hash with `:api_key` and `:api_secret` keys.
12 |
13 | Defaults to `Rails.application.credentials.vonage[:api_key]` and `Rails.application.credentials.vonage[:api_secret]`
14 |
15 | * `deliver_by :vonage, format: :format_for_vonage` - *Optional*
16 |
17 | Use a custom method to generate the params sent to Vonage. Method should return a Hash. Defaults to:
18 |
19 | ```ruby
20 | {
21 | api_key: vonage_credentials[:api_key],
22 | api_secret: vonage_credentials[:api_secret],
23 | from: notification.params[:from],
24 | text: notification.params[:body],
25 | to: notification.params[:to],
26 | type: "unicode"
27 | }
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/delivery_methods/vonage_sms.md:
--------------------------------------------------------------------------------
1 | ### Vonage SMS
2 |
3 | Sends an SMS notification via Vonage / Nexmo.
4 |
5 | `deliver_by :vonage`
6 |
7 | ##### Options
8 |
9 | * `credentials: :get_credentials` - *Optional*
10 |
11 | Use a custom method for retrieving credentials. Method should return a Hash with `:api_key` and `:api_secret` keys.
12 |
13 | Defaults to `Rails.application.credentials.vonage[:api_key]` and `Rails.application.credentials.vonage[:api_secret]`
14 |
15 | * `deliver_by :vonage, format: :format_for_vonage` - *Optional*
16 |
17 | Use a custom method to generate the params sent to Vonage. Method should return a Hash. Defaults to:
18 |
19 | ```ruby
20 | {
21 | api_key: vonage_credentials[:api_key],
22 | api_secret: vonage_credentials[:api_secret],
23 | from: notification.params[:from],
24 | text: notification.params[:body],
25 | to: notification.params[:to],
26 | type: "unicode"
27 | }
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/extending-noticed.md:
--------------------------------------------------------------------------------
1 | # Extending Noticed
2 |
3 | Noticed includes lazy load hooks that can be used to extend it's models and functionality.
4 |
5 | ### Example: Multitenancy
6 |
7 | This example adds multitenancy support to Noticed models so they have an `account:belongs_to` association.
8 |
9 | ```ruby
10 | ActiveSupport.on_load :noticed_event do
11 | belongs_to :account
12 |
13 | # Set account association from params
14 | def self.with(params)
15 | account = params.delete(:account) || Current.account
16 | record = params.delete(:record)
17 |
18 | # Instantiate Noticed::Event with account:belongs_to
19 | new(account: account, params: params, record: record)
20 | end
21 |
22 | def recipient_attributes_for(recipient)
23 | super.merge(account_id: account&.id || recipient.personal_account&.id)
24 | end
25 | end
26 |
27 | ActiveSupport.on_load :noticed_notification do
28 | belongs_to :account
29 | delegate :message, to: :event
30 | end
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/images/fcm-credentials-json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/docs/images/fcm-credentials-json.png
--------------------------------------------------------------------------------
/docs/images/fcm-project-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/docs/images/fcm-project-settings.png
--------------------------------------------------------------------------------
/gemfiles/rails_6_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 1.7"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", "~> 6.1.0"
13 | gem "activerecord-trilogy-adapter"
14 | gem "bigdecimal"
15 | gem "drb"
16 | gem "mutex_m"
17 | gem "concurrent-ruby", "< 1.3.5"
18 |
19 | gemspec path: "../"
20 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 1.7"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", "~> 7.0.0"
13 | gem "activerecord-trilogy-adapter"
14 | gem "bigdecimal"
15 | gem "drb"
16 | gem "mutex_m"
17 | gem "concurrent-ruby", "< 1.3.5"
18 |
19 | gemspec path: "../"
20 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_0.gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ..
3 | specs:
4 | noticed (2.7.0)
5 | rails (>= 6.1.0)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | actioncable (7.0.8.7)
11 | actionpack (= 7.0.8.7)
12 | activesupport (= 7.0.8.7)
13 | nio4r (~> 2.0)
14 | websocket-driver (>= 0.6.1)
15 | actionmailbox (7.0.8.7)
16 | actionpack (= 7.0.8.7)
17 | activejob (= 7.0.8.7)
18 | activerecord (= 7.0.8.7)
19 | activestorage (= 7.0.8.7)
20 | activesupport (= 7.0.8.7)
21 | mail (>= 2.7.1)
22 | net-imap
23 | net-pop
24 | net-smtp
25 | actionmailer (7.0.8.7)
26 | actionpack (= 7.0.8.7)
27 | actionview (= 7.0.8.7)
28 | activejob (= 7.0.8.7)
29 | activesupport (= 7.0.8.7)
30 | mail (~> 2.5, >= 2.5.4)
31 | net-imap
32 | net-pop
33 | net-smtp
34 | rails-dom-testing (~> 2.0)
35 | actionpack (7.0.8.7)
36 | actionview (= 7.0.8.7)
37 | activesupport (= 7.0.8.7)
38 | rack (~> 2.0, >= 2.2.4)
39 | rack-test (>= 0.6.3)
40 | rails-dom-testing (~> 2.0)
41 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
42 | actiontext (7.0.8.7)
43 | actionpack (= 7.0.8.7)
44 | activerecord (= 7.0.8.7)
45 | activestorage (= 7.0.8.7)
46 | activesupport (= 7.0.8.7)
47 | globalid (>= 0.6.0)
48 | nokogiri (>= 1.8.5)
49 | actionview (7.0.8.7)
50 | activesupport (= 7.0.8.7)
51 | builder (~> 3.1)
52 | erubi (~> 1.4)
53 | rails-dom-testing (~> 2.0)
54 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
55 | activejob (7.0.8.7)
56 | activesupport (= 7.0.8.7)
57 | globalid (>= 0.3.6)
58 | activemodel (7.0.8.7)
59 | activesupport (= 7.0.8.7)
60 | activerecord (7.0.8.7)
61 | activemodel (= 7.0.8.7)
62 | activesupport (= 7.0.8.7)
63 | activerecord-trilogy-adapter (3.1.2)
64 | activerecord (>= 6.0.a, < 7.1.a)
65 | trilogy (>= 2.4.0)
66 | activestorage (7.0.8.7)
67 | actionpack (= 7.0.8.7)
68 | activejob (= 7.0.8.7)
69 | activerecord (= 7.0.8.7)
70 | activesupport (= 7.0.8.7)
71 | marcel (~> 1.0)
72 | mini_mime (>= 1.1.0)
73 | activesupport (7.0.8.7)
74 | concurrent-ruby (~> 1.0, >= 1.0.2)
75 | i18n (>= 1.6, < 2)
76 | minitest (>= 5.1)
77 | tzinfo (~> 2.0)
78 | addressable (2.8.7)
79 | public_suffix (>= 2.0.2, < 7.0)
80 | apnotic (1.7.2)
81 | connection_pool (~> 2)
82 | net-http2 (>= 0.18.3, < 2)
83 | appraisal (2.5.0)
84 | bundler
85 | rake
86 | thor (>= 0.14.0)
87 | ast (2.4.3)
88 | base64 (0.2.0)
89 | bigdecimal (3.1.9)
90 | builder (3.3.0)
91 | concurrent-ruby (1.3.4)
92 | connection_pool (2.5.3)
93 | crack (1.0.0)
94 | bigdecimal
95 | rexml
96 | crass (1.0.6)
97 | date (3.4.1)
98 | drb (2.2.1)
99 | erubi (1.13.1)
100 | faraday (2.13.1)
101 | faraday-net_http (>= 2.0, < 3.5)
102 | json
103 | logger
104 | faraday-net_http (3.4.0)
105 | net-http (>= 0.5.0)
106 | globalid (1.2.1)
107 | activesupport (>= 6.1)
108 | google-cloud-env (2.3.0)
109 | base64 (~> 0.2)
110 | faraday (>= 1.0, < 3.a)
111 | google-logging-utils (0.2.0)
112 | googleauth (1.14.0)
113 | faraday (>= 1.0, < 3.a)
114 | google-cloud-env (~> 2.2)
115 | google-logging-utils (~> 0.1)
116 | jwt (>= 1.4, < 3.0)
117 | multi_json (~> 1.11)
118 | os (>= 0.9, < 2.0)
119 | signet (>= 0.16, < 2.a)
120 | hashdiff (1.1.2)
121 | http-2 (1.1.1)
122 | i18n (1.14.7)
123 | concurrent-ruby (~> 1.0)
124 | json (2.12.0)
125 | jwt (2.10.1)
126 | base64
127 | language_server-protocol (3.17.0.5)
128 | lint_roller (1.1.0)
129 | logger (1.7.0)
130 | loofah (2.24.1)
131 | crass (~> 1.0.2)
132 | nokogiri (>= 1.12.0)
133 | mail (2.8.1)
134 | mini_mime (>= 0.1.1)
135 | net-imap
136 | net-pop
137 | net-smtp
138 | marcel (1.0.4)
139 | method_source (1.1.0)
140 | mini_mime (1.1.5)
141 | mini_portile2 (2.8.9)
142 | minitest (5.25.5)
143 | multi_json (1.15.0)
144 | mutex_m (0.3.0)
145 | net-http (0.6.0)
146 | uri
147 | net-http2 (0.19.0)
148 | http-2 (>= 1.0)
149 | net-imap (0.5.8)
150 | date
151 | net-protocol
152 | net-pop (0.1.2)
153 | net-protocol
154 | net-protocol (0.2.2)
155 | timeout
156 | net-smtp (0.5.1)
157 | net-protocol
158 | nio4r (2.7.4)
159 | nokogiri (1.18.8)
160 | mini_portile2 (~> 2.8.2)
161 | racc (~> 1.4)
162 | nokogiri (1.18.8-aarch64-linux-gnu)
163 | racc (~> 1.4)
164 | nokogiri (1.18.8-aarch64-linux-musl)
165 | racc (~> 1.4)
166 | nokogiri (1.18.8-arm-linux-gnu)
167 | racc (~> 1.4)
168 | nokogiri (1.18.8-arm-linux-musl)
169 | racc (~> 1.4)
170 | nokogiri (1.18.8-arm64-darwin)
171 | racc (~> 1.4)
172 | nokogiri (1.18.8-x86_64-darwin)
173 | racc (~> 1.4)
174 | nokogiri (1.18.8-x86_64-linux-gnu)
175 | racc (~> 1.4)
176 | nokogiri (1.18.8-x86_64-linux-musl)
177 | racc (~> 1.4)
178 | os (1.1.4)
179 | parallel (1.27.0)
180 | parser (3.3.8.0)
181 | ast (~> 2.4.1)
182 | racc
183 | pg (1.5.9)
184 | prism (1.4.0)
185 | public_suffix (6.0.2)
186 | racc (1.8.1)
187 | rack (2.2.14)
188 | rack-test (2.2.0)
189 | rack (>= 1.3)
190 | rails (7.0.8.7)
191 | actioncable (= 7.0.8.7)
192 | actionmailbox (= 7.0.8.7)
193 | actionmailer (= 7.0.8.7)
194 | actionpack (= 7.0.8.7)
195 | actiontext (= 7.0.8.7)
196 | actionview (= 7.0.8.7)
197 | activejob (= 7.0.8.7)
198 | activemodel (= 7.0.8.7)
199 | activerecord (= 7.0.8.7)
200 | activestorage (= 7.0.8.7)
201 | activesupport (= 7.0.8.7)
202 | bundler (>= 1.15.0)
203 | railties (= 7.0.8.7)
204 | rails-dom-testing (2.2.0)
205 | activesupport (>= 5.0.0)
206 | minitest
207 | nokogiri (>= 1.6)
208 | rails-html-sanitizer (1.6.2)
209 | loofah (~> 2.21)
210 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
211 | railties (7.0.8.7)
212 | actionpack (= 7.0.8.7)
213 | activesupport (= 7.0.8.7)
214 | method_source
215 | rake (>= 12.2)
216 | thor (~> 1.0)
217 | zeitwerk (~> 2.5)
218 | rainbow (3.1.1)
219 | rake (13.2.1)
220 | regexp_parser (2.10.0)
221 | rexml (3.4.1)
222 | rubocop (1.75.6)
223 | json (~> 2.3)
224 | language_server-protocol (~> 3.17.0.2)
225 | lint_roller (~> 1.1.0)
226 | parallel (~> 1.10)
227 | parser (>= 3.3.0.2)
228 | rainbow (>= 2.2.2, < 4.0)
229 | regexp_parser (>= 2.9.3, < 3.0)
230 | rubocop-ast (>= 1.44.0, < 2.0)
231 | ruby-progressbar (~> 1.7)
232 | unicode-display_width (>= 2.4.0, < 4.0)
233 | rubocop-ast (1.44.1)
234 | parser (>= 3.3.7.2)
235 | prism (~> 1.4)
236 | rubocop-performance (1.25.0)
237 | lint_roller (~> 1.1)
238 | rubocop (>= 1.75.0, < 2.0)
239 | rubocop-ast (>= 1.38.0, < 2.0)
240 | ruby-progressbar (1.13.0)
241 | signet (0.20.0)
242 | addressable (~> 2.8)
243 | faraday (>= 0.17.5, < 3.a)
244 | jwt (>= 1.5, < 3.0)
245 | multi_json (~> 1.10)
246 | sqlite3 (1.7.3)
247 | mini_portile2 (~> 2.8.0)
248 | sqlite3 (1.7.3-aarch64-linux)
249 | sqlite3 (1.7.3-arm-linux)
250 | sqlite3 (1.7.3-arm64-darwin)
251 | sqlite3 (1.7.3-x86-linux)
252 | sqlite3 (1.7.3-x86_64-darwin)
253 | sqlite3 (1.7.3-x86_64-linux)
254 | standard (1.50.0)
255 | language_server-protocol (~> 3.17.0.2)
256 | lint_roller (~> 1.0)
257 | rubocop (~> 1.75.5)
258 | standard-custom (~> 1.0.0)
259 | standard-performance (~> 1.8)
260 | standard-custom (1.0.2)
261 | lint_roller (~> 1.0)
262 | rubocop (~> 1.50)
263 | standard-performance (1.8.0)
264 | lint_roller (~> 1.1)
265 | rubocop-performance (~> 1.25.0)
266 | thor (1.3.2)
267 | timeout (0.4.3)
268 | trilogy (2.9.0)
269 | tzinfo (2.0.6)
270 | concurrent-ruby (~> 1.0)
271 | unicode-display_width (3.1.4)
272 | unicode-emoji (~> 4.0, >= 4.0.4)
273 | unicode-emoji (4.0.4)
274 | uri (1.0.3)
275 | webmock (3.25.1)
276 | addressable (>= 2.8.0)
277 | crack (>= 0.3.2)
278 | hashdiff (>= 0.4.0, < 2.0.0)
279 | websocket-driver (0.7.7)
280 | base64
281 | websocket-extensions (>= 0.1.0)
282 | websocket-extensions (0.1.5)
283 | zeitwerk (2.7.2)
284 |
285 | PLATFORMS
286 | aarch64-linux
287 | aarch64-linux-gnu
288 | aarch64-linux-musl
289 | arm-linux
290 | arm-linux-gnu
291 | arm-linux-musl
292 | arm64-darwin
293 | ruby
294 | x86-linux
295 | x86_64-darwin
296 | x86_64-linux
297 | x86_64-linux-gnu
298 | x86_64-linux-musl
299 |
300 | DEPENDENCIES
301 | activerecord-trilogy-adapter
302 | apnotic (~> 1.7)
303 | appraisal
304 | bigdecimal
305 | concurrent-ruby (< 1.3.5)
306 | drb
307 | googleauth (~> 1.1)
308 | mutex_m
309 | noticed!
310 | pg
311 | rails (~> 7.0.0)
312 | sqlite3 (~> 1.7)
313 | standard
314 | webmock
315 |
316 | BUNDLED WITH
317 | 2.6.8
318 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 1.7"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", "~> 7.1.0"
13 | gem "trilogy"
14 |
15 | gemspec path: "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 1.7"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", "~> 7.2.0"
13 | gem "trilogy"
14 |
15 | gemspec path: "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails_8_0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 2.0"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", "~> 8.0.0"
13 | gem "trilogy"
14 |
15 | gemspec path: "../"
16 |
--------------------------------------------------------------------------------
/gemfiles/rails_main.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "pg"
7 | gem "sqlite3", "~> 2.0"
8 | gem "standard"
9 | gem "webmock"
10 | gem "apnotic", "~> 1.7"
11 | gem "googleauth", "~> 1.1"
12 | gem "rails", branch: "main", git: "https://github.com/rails/rails.git"
13 | gem "trilogy"
14 |
15 | gemspec path: "../"
16 |
--------------------------------------------------------------------------------
/lib/generators/noticed/delivery_method_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/named_base"
4 |
5 | module Noticed
6 | module Generators
7 | class DeliveryMethodGenerator < Rails::Generators::NamedBase
8 | include Rails::Generators::ResourceHelpers
9 |
10 | source_root File.expand_path("../templates", __FILE__)
11 |
12 | desc "Generates a class for a custom delivery method with the given NAME."
13 |
14 | def generate_notification
15 | template "application_delivery_method.rb", "app/notifiers/application_delivery_method.rb"
16 | template "delivery_method.rb", "app/notifiers/delivery_methods/#{singular_name}.rb"
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/generators/noticed/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Noticed
4 | module Generators
5 | class ModelGenerator < Rails::Generators::Base
6 | include Rails::Generators::ResourceHelpers
7 |
8 | source_root File.expand_path("../templates", __FILE__)
9 |
10 | def create_migrations
11 | rails_command "railties:install:migrations FROM=noticed", inline: true
12 | end
13 |
14 | def done
15 | readme "README" if behavior == :invoke
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/generators/noticed/notifier_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rails/generators/named_base"
4 |
5 | module Noticed
6 | module Generators
7 | class NotifierGenerator < Rails::Generators::NamedBase
8 | include Rails::Generators::ResourceHelpers
9 |
10 | check_class_collision suffix: "Notifier"
11 |
12 | source_root File.expand_path("../templates", __FILE__)
13 |
14 | desc "Generates a notification with the given NAME."
15 |
16 | def generate_abstract_class
17 | return if File.exist?("app/notifiers/application_notifier.rb")
18 | template "application_notifier.rb", "app/notifiers/application_notifier.rb"
19 | end
20 |
21 | def generate_notification
22 | template "notifier.rb", "app/notifiers/#{file_path}_notifier.rb"
23 | end
24 |
25 | private
26 |
27 | def file_name # :doc:
28 | @_file_name ||= super.sub(/_notifier\z/i, "")
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/generators/noticed/templates/README:
--------------------------------------------------------------------------------
1 |
2 | 🚚 You're ready to start sending notifications!
3 |
4 | Next steps:
5 | 1. Run `rails db:migrate`
6 | 2. Add `has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"` to your User model(s).
7 | 2. Add `has_many :notifications, as: :record, dependent: :destroy, class_name: "Noticed::Event"` to your model(s) that notifications reference.
8 | 3. Generate notifiers with "rails g noticed:notifier"
9 |
--------------------------------------------------------------------------------
/lib/generators/noticed/templates/application_delivery_method.rb.tt:
--------------------------------------------------------------------------------
1 | class ApplicationDeliveryMethod < Noticed::DeliveryMethod
2 | end
3 |
--------------------------------------------------------------------------------
/lib/generators/noticed/templates/application_notifier.rb.tt:
--------------------------------------------------------------------------------
1 | class ApplicationNotifier < Noticed::Event
2 | end
3 |
--------------------------------------------------------------------------------
/lib/generators/noticed/templates/delivery_method.rb.tt:
--------------------------------------------------------------------------------
1 | class DeliveryMethods::<%= class_name %> < ApplicationDeliveryMethod
2 | # Specify the config options your delivery method requires in its config block
3 | required_options # :foo, :bar
4 |
5 | def deliver
6 | # Logic for sending the notification
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/generators/noticed/templates/notifier.rb.tt:
--------------------------------------------------------------------------------
1 | # To deliver this notification:
2 | #
3 | # <%= class_name %>Notifier.with(record: @post, message: "New post").deliver(User.all)
4 |
5 | class <%= class_name %>Notifier < ApplicationNotifier
6 | # Add your delivery methods
7 | #
8 | # deliver_by :email do |config|
9 | # config.mailer = "UserMailer"
10 | # config.method = "new_post"
11 | # end
12 | #
13 | # bulk_deliver_by :slack do |config|
14 | # config.url = -> { Rails.application.credentials.slack_webhook_url }
15 | # end
16 | #
17 | # deliver_by :custom do |config|
18 | # config.class = "MyDeliveryMethod"
19 | # end
20 |
21 | # Add required params
22 | #
23 | # required_param :message
24 | end
25 |
--------------------------------------------------------------------------------
/lib/noticed.rb:
--------------------------------------------------------------------------------
1 | require "noticed/version"
2 | require "noticed/engine"
3 |
4 | module Noticed
5 | include ActiveSupport::Deprecation::DeprecatedConstantAccessor
6 |
7 | def self.deprecator # :nodoc:
8 | @deprecator ||= ActiveSupport::Deprecation.new
9 | end
10 |
11 | deprecate_constant :Base, "Noticed::Event", deprecator: deprecator
12 |
13 | autoload :ApiClient, "noticed/api_client"
14 | autoload :BulkDeliveryMethod, "noticed/bulk_delivery_method"
15 | autoload :Coder, "noticed/coder"
16 | autoload :DeliveryMethod, "noticed/delivery_method"
17 | autoload :HasNotifications, "noticed/has_notifications"
18 | autoload :NotificationChannel, "noticed/notification_channel"
19 | autoload :RequiredOptions, "noticed/required_options"
20 | autoload :Translation, "noticed/translation"
21 |
22 | module BulkDeliveryMethods
23 | autoload :Bluesky, "noticed/bulk_delivery_methods/bluesky"
24 | autoload :Discord, "noticed/bulk_delivery_methods/discord"
25 | autoload :Slack, "noticed/bulk_delivery_methods/slack"
26 | autoload :Test, "noticed/bulk_delivery_methods/test"
27 | autoload :Webhook, "noticed/bulk_delivery_methods/webhook"
28 | end
29 |
30 | module DeliveryMethods
31 | include ActiveSupport::Deprecation::DeprecatedConstantAccessor
32 | deprecate_constant :Base, "Noticed::DeliveryMethod", deprecator: Noticed.deprecator
33 |
34 | autoload :ActionCable, "noticed/delivery_methods/action_cable"
35 | autoload :Email, "noticed/delivery_methods/email"
36 | autoload :Fcm, "noticed/delivery_methods/fcm"
37 | autoload :Ios, "noticed/delivery_methods/ios"
38 | autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
39 | autoload :Slack, "noticed/delivery_methods/slack"
40 | autoload :Test, "noticed/delivery_methods/test"
41 | autoload :TwilioMessaging, "noticed/delivery_methods/twilio_messaging"
42 | autoload :VonageSms, "noticed/delivery_methods/vonage_sms"
43 | autoload :Webhook, "noticed/delivery_methods/webhook"
44 | end
45 |
46 | mattr_accessor :parent_class
47 | @@parent_class = "Noticed::ApplicationJob"
48 |
49 | class ValidationError < StandardError
50 | end
51 |
52 | class ResponseUnsuccessful < StandardError
53 | attr_reader :response
54 |
55 | def initialize(response, url, args)
56 | @response = response
57 | @url = url
58 | @args = args
59 |
60 | super("POST request to #{url} returned #{response.code} response:\n#{response.body.inspect}")
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/noticed/api_client.rb:
--------------------------------------------------------------------------------
1 | require "net/http"
2 |
3 | module Noticed
4 | module ApiClient
5 | extend ActiveSupport::Concern
6 |
7 | # Helper method for making POST requests from delivery methods
8 | #
9 | # Usage:
10 | # post_request("http://example.com", basic_auth: {user:, pass:}, headers: {}, json: {}, form: {})
11 | #
12 | def post_request(url, args = {})
13 | args.compact!
14 |
15 | uri = URI(url)
16 | http = Net::HTTP.new(uri.host, uri.port)
17 | http.use_ssl = true if uri.instance_of? URI::HTTPS
18 |
19 | headers = args.delete(:headers) || {}
20 | headers["Content-Type"] = "application/json" if args.has_key?(:json)
21 |
22 | request = Net::HTTP::Post.new(uri.request_uri, headers)
23 |
24 | if (basic_auth = args.delete(:basic_auth))
25 | request.basic_auth basic_auth.fetch(:user), basic_auth.fetch(:pass)
26 | end
27 |
28 | if (json = args.delete(:json))
29 | request.body = json.to_json
30 | elsif (form = args.delete(:form))
31 | request.form_data = form
32 | end
33 |
34 | logger.debug("POST #{url}")
35 | logger.debug(request.body)
36 | response = http.request(request)
37 | logger.debug("Response: #{response.code}: #{response.body.inspect}")
38 |
39 | raise ResponseUnsuccessful.new(response, url, args) unless response.code.start_with?("20")
40 |
41 | response
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_method.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class BulkDeliveryMethod < Noticed.parent_class.constantize
3 | include ApiClient
4 | include RequiredOptions
5 |
6 | extend ActiveModel::Callbacks
7 | define_model_callbacks :deliver
8 |
9 | class_attribute :logger, default: Rails.logger
10 |
11 | attr_reader :config, :event
12 |
13 | def perform(delivery_method_name, event, recipient: nil, params: {}, overrides: {})
14 | # Ephemeral notifications
15 | if event.is_a? String
16 | @event = event.constantize.new_with_params(recipient, params)
17 | @config = overrides
18 | else
19 | @event = event
20 | @config = event.bulk_delivery_methods.fetch(delivery_method_name).config.merge(overrides)
21 | end
22 |
23 | return false if config.has_key?(:if) && !evaluate_option(:if)
24 | return false if config.has_key?(:unless) && evaluate_option(:unless)
25 |
26 | run_callbacks :deliver do
27 | deliver
28 | end
29 | end
30 |
31 | def deliver
32 | raise NotImplementedError, "Bulk delivery methods must implement the `deliver` method"
33 | end
34 |
35 | def fetch_constant(name)
36 | option = config[name]
37 | option.is_a?(String) ? option.constantize : evaluate_option(option)
38 | end
39 |
40 | def evaluate_option(name)
41 | option = config[name]
42 |
43 | # Evaluate Proc within the context of the notifier
44 | if option&.respond_to?(:call)
45 | event.instance_exec(&option)
46 |
47 | # Call method if symbol and matching method
48 | elsif option.is_a?(Symbol) && event.respond_to?(option, true)
49 | event.send(option)
50 |
51 | # Return the value
52 | else
53 | option
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_methods/bluesky.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module BulkDeliveryMethods
3 | class Bluesky < BulkDeliveryMethod
4 | required_options :identifier, :password, :json
5 |
6 | # bulk_deliver_by :bluesky do |config|
7 | # config.identifier = ENV["BLUESKY_ID"]
8 | # config.password = ENV["BLUESKY_PASSWORD"]
9 | # config.json = {text: "...", createdAt: "..."}
10 | # end
11 |
12 | def deliver
13 | Rails.logger.debug(evaluate_option(:json))
14 | post_request(
15 | "https://#{host}/xrpc/com.atproto.repo.createRecord",
16 | headers: {"Authorization" => "Bearer #{token}"},
17 | json: {
18 | repo: identifier,
19 | collection: "app.bsky.feed.post",
20 | record: evaluate_option(:json)
21 | }
22 | )
23 | end
24 |
25 | def token
26 | start_session.dig("accessJwt")
27 | end
28 |
29 | def start_session
30 | response = post_request(
31 | "https://#{host}/xrpc/com.atproto.server.createSession",
32 | json: {
33 | identifier: identifier,
34 | password: evaluate_option(:password)
35 | }
36 | )
37 | JSON.parse(response.body)
38 | end
39 |
40 | def host
41 | @host ||= evaluate_option(:host) || "bsky.social"
42 | end
43 |
44 | def identifier
45 | @identifier ||= evaluate_option(:identifier)
46 | end
47 | end
48 | end
49 | end
50 |
51 | ActiveSupport.run_load_hooks :noticed_bulk_delivery_methods_bluesky, Noticed::BulkDeliveryMethods::Bluesky
52 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_methods/discord.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module BulkDeliveryMethods
3 | class Discord < BulkDeliveryMethod
4 | required_options :json, :url
5 |
6 | def deliver
7 | post_request evaluate_option(:url), headers: evaluate_option(:headers), json: evaluate_option(:json)
8 | end
9 | end
10 | end
11 | end
12 |
13 | ActiveSupport.run_load_hooks :noticed_bulk_delivery_methods_discord, Noticed::BulkDeliveryMethods::Discord
14 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_methods/slack.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module BulkDeliveryMethods
3 | class Slack < BulkDeliveryMethod
4 | DEFAULT_URL = "https://slack.com/api/chat.postMessage"
5 |
6 | required_options :json
7 |
8 | def deliver
9 | headers = evaluate_option(:headers)
10 | json = evaluate_option(:json)
11 | response = post_request url, headers: headers, json: json
12 |
13 | if raise_if_not_ok? && !success?(response)
14 | raise ResponseUnsuccessful.new(response, url, {headers: headers, json: json})
15 | end
16 |
17 | response
18 | end
19 |
20 | def url
21 | evaluate_option(:url) || DEFAULT_URL
22 | end
23 |
24 | def raise_if_not_ok?
25 | value = evaluate_option(:raise_if_not_ok)
26 | value.nil? || value
27 | end
28 |
29 | def success?(response)
30 | if response.content_type == "application/json"
31 | JSON.parse(response.body).dig("ok")
32 | else
33 | # https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks
34 | response.is_a?(Net::HTTPSuccess)
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
41 | ActiveSupport.run_load_hooks :noticed_bulk_delivery_methods_slack, Noticed::BulkDeliveryMethods::Slack
42 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_methods/test.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module BulkDeliveryMethods
3 | class Test < BulkDeliveryMethod
4 | class_attribute :delivered, default: []
5 |
6 | def deliver
7 | delivered << event
8 | end
9 | end
10 | end
11 | end
12 |
13 | ActiveSupport.run_load_hooks :noticed_bulk_delivery_methods_test, Noticed::BulkDeliveryMethods::Test
14 |
--------------------------------------------------------------------------------
/lib/noticed/bulk_delivery_methods/webhook.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module BulkDeliveryMethods
3 | class Webhook < BulkDeliveryMethod
4 | required_options :url
5 |
6 | def deliver
7 | Rails.logger.debug(evaluate_option(:json))
8 | post_request(
9 | evaluate_option(:url),
10 | basic_auth: evaluate_option(:basic_auth),
11 | headers: evaluate_option(:headers),
12 | json: evaluate_option(:json),
13 | form: evaluate_option(:form)
14 | )
15 | end
16 | end
17 | end
18 | end
19 |
20 | ActiveSupport.run_load_hooks :noticed_bulk_delivery_methods_webhook, Noticed::BulkDeliveryMethods::Webhook
21 |
--------------------------------------------------------------------------------
/lib/noticed/coder.rb:
--------------------------------------------------------------------------------
1 | require "active_job/arguments"
2 |
3 | module Noticed
4 | class Coder
5 | def self.load(data)
6 | return if data.nil?
7 | ActiveJob::Arguments.send(:deserialize_argument, data)
8 | rescue ActiveRecord::RecordNotFound => error
9 | {noticed_error: error.message, original_params: data}
10 | end
11 |
12 | def self.dump(data)
13 | return if data.nil?
14 | ActiveJob::Arguments.send(:serialize_argument, data)
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_method.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class DeliveryMethod < Noticed.parent_class.constantize
3 | include ApiClient
4 | include RequiredOptions
5 |
6 | extend ActiveModel::Callbacks
7 | define_model_callbacks :deliver
8 |
9 | class_attribute :logger, default: Rails.logger
10 |
11 | attr_reader :config, :event, :notification
12 | delegate :recipient, to: :notification
13 | delegate :record, :params, to: :event
14 |
15 | def perform(delivery_method_name, notification, recipient: nil, params: {}, overrides: {})
16 | # Ephemeral notifications
17 | if notification.is_a? String
18 | @notification = notification.constantize.new_with_params(recipient, params)
19 | @event = @notification.event
20 | else
21 | @notification = notification
22 | @event = notification.event
23 | end
24 |
25 | # Look up config from Notifier and merge overrides
26 | @config = event.delivery_methods.fetch(delivery_method_name).config.merge(overrides)
27 |
28 | return false if config.has_key?(:if) && !evaluate_option(:if)
29 | return false if config.has_key?(:unless) && evaluate_option(:unless)
30 |
31 | run_callbacks :deliver do
32 | deliver
33 | end
34 | end
35 |
36 | def deliver
37 | raise NotImplementedError, "Delivery methods must implement the `deliver` method"
38 | end
39 |
40 | def fetch_constant(name)
41 | option = evaluate_option(name)
42 | option.is_a?(String) ? option.constantize : option
43 | end
44 |
45 | def evaluate_option(name)
46 | option = config[name]
47 |
48 | # Evaluate Proc within the context of the Notification
49 | if option&.respond_to?(:call)
50 | notification.instance_exec(&option)
51 |
52 | # Call method if symbol and matching method on Notifier
53 | elsif option.is_a?(Symbol) && event.respond_to?(option, true)
54 | event.send(option, notification)
55 |
56 | # Return the value
57 | else
58 | option
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/action_cable.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class ActionCable < DeliveryMethod
4 | required_options :message
5 |
6 | def deliver
7 | channel.broadcast_to stream, evaluate_option(:message)
8 | end
9 |
10 | def channel
11 | fetch_constant(:channel) || Noticed::NotificationChannel
12 | end
13 |
14 | def stream
15 | evaluate_option(:stream) || recipient
16 | end
17 | end
18 | end
19 | end
20 |
21 | ActiveSupport.run_load_hooks :noticed_delivery_methods_action_cable, Noticed::DeliveryMethods::ActionCable
22 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/discord.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class Discord < BulkDeliveryMethod
4 | required_options :json, :url
5 |
6 | def deliver
7 | post_request evaluate_option(:url), headers: evaluate_option(:headers), json: evaluate_option(:json)
8 | end
9 | end
10 | end
11 | end
12 |
13 | ActiveSupport.run_load_hooks :noticed_delivery_methods_discord, Noticed::DeliveryMethods::Discord
14 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/email.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class Email < DeliveryMethod
4 | required_options :mailer, :method
5 |
6 | def deliver
7 | mailer = fetch_constant(:mailer)
8 | email = evaluate_option(:method)
9 | args = evaluate_option(:args) || []
10 | mail = mailer.with(params).public_send(email, *args)
11 | (!!evaluate_option(:enqueue)) ? mail.deliver_later : mail.deliver_now
12 | end
13 |
14 | def params
15 | (evaluate_option(:params) || notification&.params || {}).merge(
16 | notification: notification,
17 | record: notification&.record,
18 | recipient: notification&.recipient
19 | )
20 | end
21 | end
22 | end
23 | end
24 |
25 | ActiveSupport.run_load_hooks :noticed_delivery_methods_email, Noticed::DeliveryMethods::Email
26 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/fcm.rb:
--------------------------------------------------------------------------------
1 | require "googleauth"
2 |
3 | module Noticed
4 | module DeliveryMethods
5 | class Fcm < DeliveryMethod
6 | required_option :credentials, :device_tokens, :json
7 |
8 | def deliver
9 | evaluate_option(:device_tokens).each do |device_token|
10 | send_notification device_token
11 | end
12 | end
13 |
14 | def send_notification(device_token)
15 | post_request("https://fcm.googleapis.com/v1/projects/#{credentials[:project_id]}/messages:send",
16 | headers: {authorization: "Bearer #{access_token}"},
17 | json: format_notification(device_token))
18 | rescue Noticed::ResponseUnsuccessful => exception
19 | if bad_token?(exception.response) && config[:invalid_token]
20 | notification.instance_exec(device_token, &config[:invalid_token])
21 | else
22 | raise
23 | end
24 | end
25 |
26 | def format_notification(device_token)
27 | method = config[:json]
28 | if method.is_a?(Symbol) && event.respond_to?(method, true)
29 | event.send(method, device_token)
30 | else
31 | notification.instance_exec(device_token, &method)
32 | end
33 | end
34 |
35 | def bad_token?(response)
36 | response.code == "404" || response.code == "400"
37 | end
38 |
39 | def credentials
40 | @credentials ||= begin
41 | value = evaluate_option(:credentials)
42 | case value
43 | when Hash
44 | value
45 | when Pathname
46 | load_json(value)
47 | when String
48 | load_json(Rails.root.join(value))
49 | else
50 | raise ArgumentError, "FCM credentials must be a Hash, String, Pathname, or Symbol"
51 | end
52 | end
53 | end
54 |
55 | def load_json(path)
56 | JSON.parse(File.read(path), symbolize_names: true)
57 | end
58 |
59 | def access_token
60 | @authorizer ||= (evaluate_option(:authorizer) || Google::Auth::ServiceAccountCredentials).make_creds(
61 | json_key_io: StringIO.new(credentials.to_json),
62 | scope: "https://www.googleapis.com/auth/firebase.messaging"
63 | )
64 | @authorizer.fetch_access_token!["access_token"]
65 | end
66 | end
67 | end
68 | end
69 |
70 | ActiveSupport.run_load_hooks :noticed_delivery_methods_fcm, Noticed::DeliveryMethods::Fcm
71 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/ios.rb:
--------------------------------------------------------------------------------
1 | require "apnotic"
2 |
3 | module Noticed
4 | module DeliveryMethods
5 | class Ios < DeliveryMethod
6 | cattr_accessor :development_connection_pool, :production_connection_pool
7 |
8 | required_options :bundle_identifier, :key_id, :team_id, :apns_key, :device_tokens
9 |
10 | def deliver
11 | evaluate_option(:device_tokens).each do |device_token|
12 | apn = Apnotic::Notification.new(device_token)
13 | format_notification(apn)
14 |
15 | connection_pool = (!!evaluate_option(:development)) ? development_pool : production_pool
16 | connection_pool.with do |connection|
17 | response = connection.push(apn)
18 | raise "Timeout sending iOS push notification" unless response
19 | connection.close
20 |
21 | if bad_token?(response) && config[:invalid_token]
22 | # Allow notification to cleanup invalid iOS device tokens
23 | notification.instance_exec(device_token, &config[:invalid_token])
24 | elsif !response.ok?
25 | raise "Request failed #{response.body}"
26 | end
27 | end
28 | end
29 | end
30 |
31 | private
32 |
33 | def format_notification(apn)
34 | apn.topic = evaluate_option(:bundle_identifier)
35 |
36 | method = config[:format]
37 | # Call method on Notifier if defined
38 | if method&.is_a?(Symbol) && event.respond_to?(method, true)
39 | event.send(method, notification, apn)
40 | # If Proc, evaluate it on the Notification
41 | elsif method&.respond_to?(:call)
42 | notification.instance_exec(apn, &method)
43 | elsif notification.params.try(:has_key?, :message)
44 | apn.alert = notification.params[:message]
45 | else
46 | raise ArgumentError, "No message for iOS delivery. Either include message in params or add the 'format' option in 'deliver_by :ios'."
47 | end
48 | end
49 |
50 | def bad_token?(response)
51 | response.status == "410" || (response.status == "400" && response.body["reason"] == "BadDeviceToken")
52 | end
53 |
54 | def development_pool
55 | self.class.development_connection_pool ||= new_connection_pool(development: true)
56 | end
57 |
58 | def production_pool
59 | self.class.production_connection_pool ||= new_connection_pool(development: false)
60 | end
61 |
62 | def new_connection_pool(development:)
63 | handler = proc do |connection|
64 | connection.on(:error) do |exception|
65 | Rails.logger.info "Apnotic exception raised: #{exception}"
66 | if config[:error_handler].respond_to?(:call)
67 | notification.instance_exec(exception, &config[:error_handler])
68 | end
69 | end
70 | end
71 |
72 | if development
73 | Apnotic::ConnectionPool.development(connection_pool_options, pool_options, &handler)
74 | else
75 | Apnotic::ConnectionPool.new(connection_pool_options, pool_options, &handler)
76 | end
77 | end
78 |
79 | def connection_pool_options
80 | {
81 | auth_method: :token,
82 | cert_path: StringIO.new(evaluate_option(:apns_key)),
83 | key_id: evaluate_option(:key_id),
84 | team_id: evaluate_option(:team_id)
85 | }
86 | end
87 |
88 | def pool_options
89 | {size: evaluate_option(:pool_size) || 5}
90 | end
91 | end
92 | end
93 | end
94 |
95 | ActiveSupport.run_load_hooks :noticed_delivery_methods_ios, Noticed::DeliveryMethods::Ios
96 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/microsoft_teams.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class MicrosoftTeams < DeliveryMethod
4 | required_options :json
5 |
6 | def deliver
7 | post_request url, headers: evaluate_option(:headers), json: evaluate_option(:json)
8 | end
9 |
10 | def url
11 | evaluate_option(:url) || Rails.application.credentials.dig(:microsoft_teams, :notification_url)
12 | end
13 | end
14 | end
15 | end
16 |
17 | ActiveSupport.run_load_hooks :noticed_delivery_methods_microsoft_teams, Noticed::DeliveryMethods::MicrosoftTeams
18 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/slack.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class Slack < DeliveryMethod
4 | DEFAULT_URL = "https://slack.com/api/chat.postMessage"
5 |
6 | required_options :json
7 |
8 | def deliver
9 | headers = evaluate_option(:headers)
10 | json = evaluate_option(:json)
11 | response = post_request url, headers: headers, json: json
12 |
13 | if raise_if_not_ok? && !success?(response)
14 | raise ResponseUnsuccessful.new(response, url, {headers: headers, json: json})
15 | end
16 |
17 | response
18 | end
19 |
20 | def url
21 | evaluate_option(:url) || DEFAULT_URL
22 | end
23 |
24 | def raise_if_not_ok?
25 | value = evaluate_option(:raise_if_not_ok)
26 | value.nil? || value
27 | end
28 |
29 | def success?(response)
30 | if response.content_type == "application/json"
31 | JSON.parse(response.body).dig("ok")
32 | else
33 | # https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks
34 | response.is_a?(Net::HTTPSuccess)
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
41 | ActiveSupport.run_load_hooks :noticed_delivery_methods_slack, Noticed::DeliveryMethods::Slack
42 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/test.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class Test < DeliveryMethod
4 | class_attribute :delivered, default: []
5 |
6 | def deliver
7 | delivered << notification
8 | end
9 | end
10 | end
11 | end
12 |
13 | ActiveSupport.run_load_hooks :noticed_delivery_methods_test, Noticed::DeliveryMethods::Test
14 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/twilio_messaging.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class TwilioMessaging < DeliveryMethod
4 | def deliver
5 | post_request url, basic_auth: {user: account_sid, pass: auth_token}, form: json.stringify_keys
6 | rescue Noticed::ResponseUnsuccessful => exception
7 | if exception.response.code.start_with?("4") && config[:error_handler]
8 | notification.instance_exec(exception.response, &config[:error_handler])
9 | else
10 | raise
11 | end
12 | end
13 |
14 | def json
15 | evaluate_option(:json) || {
16 | From: phone_number,
17 | To: recipient.phone_number,
18 | Body: params.fetch(:message)
19 | }
20 | end
21 |
22 | def url
23 | evaluate_option(:url) || "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}/Messages.json"
24 | end
25 |
26 | def account_sid
27 | evaluate_option(:account_sid) || credentials.fetch(:account_sid)
28 | end
29 |
30 | def auth_token
31 | evaluate_option(:auth_token) || credentials.fetch(:auth_token)
32 | end
33 |
34 | def phone_number
35 | evaluate_option(:phone_number) || credentials.fetch(:phone_number)
36 | end
37 |
38 | def credentials
39 | evaluate_option(:credentials) || Rails.application.credentials.twilio
40 | end
41 | end
42 | end
43 | end
44 |
45 | ActiveSupport.run_load_hooks :noticed_delivery_methods_twilio_messaging, Noticed::DeliveryMethods::TwilioMessaging
46 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/vonage_sms.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class VonageSms < DeliveryMethod
4 | DEFAULT_URL = "https://rest.nexmo.com/sms/json"
5 |
6 | required_options :json
7 |
8 | def deliver
9 | headers = evaluate_option(:headers)
10 | json = evaluate_option(:json)
11 | response = post_request url, headers: headers, json: json
12 | raise ResponseUnsuccessful.new(response, url, headers: headers, json: json) if JSON.parse(response.body).dig("messages", 0, "status") != "0"
13 | end
14 |
15 | def url
16 | evaluate_option(:url) || DEFAULT_URL
17 | end
18 | end
19 | end
20 | end
21 |
22 | ActiveSupport.run_load_hooks :noticed_delivery_methods_vonage_sms, Noticed::DeliveryMethods::VonageSms
23 |
--------------------------------------------------------------------------------
/lib/noticed/delivery_methods/webhook.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module DeliveryMethods
3 | class Webhook < DeliveryMethod
4 | required_options :url
5 |
6 | def deliver
7 | post_request(
8 | evaluate_option(:url),
9 | basic_auth: evaluate_option(:basic_auth),
10 | headers: evaluate_option(:headers),
11 | json: evaluate_option(:json),
12 | form: evaluate_option(:form)
13 | )
14 | end
15 | end
16 | end
17 | end
18 |
19 | ActiveSupport.run_load_hooks :noticed_delivery_methods_webhook, Noticed::DeliveryMethods::Webhook
20 |
--------------------------------------------------------------------------------
/lib/noticed/engine.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class Engine < ::Rails::Engine
3 | isolate_namespace Noticed
4 |
5 | initializer "noticed.has_notifications" do
6 | ActiveSupport.on_load(:active_record) do
7 | include Noticed::HasNotifications
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/noticed/has_notifications.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module HasNotifications
3 | # Defines a method for the association and a before_destroy callback to remove notifications
4 | # where this record is a param
5 | #
6 | # class User < ApplicationRecord
7 | # has_noticed_notifications
8 | # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
9 | # end
10 | #
11 | # @user.notifications_as_user
12 | # @user.notifications_as_owner
13 |
14 | extend ActiveSupport::Concern
15 |
16 | class_methods do
17 | def has_noticed_notifications(param_name: model_name.singular, **options)
18 | define_method :"notifications_as_#{param_name}" do
19 | model = options.fetch(:model_name, "Noticed::Event").constantize
20 | case current_adapter
21 | when "postgresql", "postgis"
22 | model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
23 | when "mysql2", "trilogy"
24 | model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
25 | when "sqlite3"
26 | model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
27 | else
28 | # This will perform an exact match which isn't ideal
29 | model.where(params: {param_name.to_sym => self})
30 | end
31 | end
32 |
33 | if options.fetch(:destroy, true)
34 | before_destroy do
35 | send(:"notifications_as_#{param_name}").destroy_all
36 | end
37 | end
38 | end
39 | end
40 |
41 | def current_adapter
42 | if ActiveRecord::Base.respond_to?(:connection_db_config)
43 | ActiveRecord::Base.connection_db_config.adapter
44 | else
45 | ActiveRecord::Base.connection_config[:adapter]
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/noticed/notification_channel.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | class NotificationChannel < ApplicationCable::Channel
3 | def subscribed
4 | stream_for current_user
5 | end
6 |
7 | def unsubscribed
8 | stop_all_streams
9 | end
10 |
11 | def mark_as_seen(data)
12 | current_user.notifications.where(id: data["ids"]).mark_as_seen
13 | end
14 |
15 | def mark_as_read(data)
16 | current_user.notifications.where(id: data["ids"]).mark_as_read
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/noticed/required_options.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module RequiredOptions
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 | class_attribute :required_option_names, instance_writer: false, default: []
7 | end
8 |
9 | class_methods do
10 | def inherited(base)
11 | base.required_option_names = required_option_names.dup
12 | super
13 | end
14 |
15 | def required_options(*names)
16 | required_option_names.concat names
17 | end
18 | alias_method :required_option, :required_options
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/noticed/translation.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | module Translation
3 | extend ActiveSupport::Concern
4 |
5 | # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
6 | def i18n_scope
7 | :notifiers
8 | end
9 |
10 | def class_scope
11 | self.class.name.underscore.tr("/", ".")
12 | end
13 |
14 | def translate(key, **options)
15 | if defined?(::ActiveSupport::HtmlSafeTranslation)
16 | ActiveSupport::HtmlSafeTranslation.translate scope_translation_key(key), **options
17 | else
18 | I18n.translate scope_translation_key(key), **options
19 | end
20 | end
21 | alias_method :t, :translate
22 |
23 | def scope_translation_key(key)
24 | if key.to_s.start_with?(".")
25 | [i18n_scope, class_scope].compact.join(".") + key
26 | else
27 | key
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/noticed/version.rb:
--------------------------------------------------------------------------------
1 | module Noticed
2 | VERSION = "2.7.0"
3 | end
4 |
--------------------------------------------------------------------------------
/noticed.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("lib", __dir__)
2 |
3 | # Maintain your gem's version:
4 | require "noticed/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |spec|
8 | spec.name = "noticed"
9 | spec.version = Noticed::VERSION
10 | spec.authors = ["Chris Oliver"]
11 | spec.email = ["excid3@gmail.com"]
12 | spec.homepage = "https://github.com/excid3/noticed"
13 | spec.summary = "Notifications for Ruby on Rails applications"
14 | spec.description = "Database, browser, realtime ActionCable, Email, SMS, Slack notifications, and more for Rails apps"
15 | spec.license = "MIT"
16 |
17 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
18 |
19 | spec.add_dependency "rails", ">= 6.1.0"
20 | end
21 |
--------------------------------------------------------------------------------
/test/bulk_delivery_methods/webhook_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class WebhookBulkDeliveryMethodTest < ActiveSupport::TestCase
4 | include ActiveJob::TestHelper
5 |
6 | setup do
7 | @delivery_method = Noticed::BulkDeliveryMethods::Webhook.new
8 | end
9 |
10 | test "end to end" do
11 | stub_request(:post, "https://example.org/bulk")
12 | perform_enqueued_jobs do
13 | BulkNotifier.deliver
14 | end
15 | end
16 |
17 | test "webhook with json payload" do
18 | set_config(
19 | url: "https://example.org/webhook",
20 | json: {foo: :bar}
21 | )
22 | stub_request(:post, "https://example.org/webhook").with(body: "{\"foo\":\"bar\"}")
23 |
24 | assert_nothing_raised do
25 | @delivery_method.deliver
26 | end
27 | end
28 |
29 | test "webhook with form payload" do
30 | set_config(
31 | url: "https://example.org/webhook",
32 | form: {foo: :bar}
33 | )
34 | stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => /application\/x-www-form-urlencoded/})
35 | assert_nothing_raised do
36 | @delivery_method.deliver
37 | end
38 | end
39 |
40 | test "webhook with basic auth" do
41 | set_config(
42 | url: "https://example.org/webhook",
43 | basic_auth: {user: "username", pass: "password"}
44 | )
45 | stub_request(:post, "https://example.org/webhook").with(headers: {"Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="})
46 | assert_nothing_raised do
47 | @delivery_method.deliver
48 | end
49 | end
50 |
51 | test "webhook with headers" do
52 | set_config(
53 | url: "https://example.org/webhook",
54 | headers: {"Content-Type" => "application/json"}
55 | )
56 | stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => "application/json"})
57 |
58 | assert_nothing_raised do
59 | @delivery_method.deliver
60 | end
61 | end
62 |
63 | test "webhook raises error with unsuccessful status codes" do
64 | set_config(url: "https://example.org/webhook")
65 | stub_request(:post, "https://example.org/webhook").to_return(status: 422)
66 | assert_raises Noticed::ResponseUnsuccessful do
67 | @delivery_method.deliver
68 | end
69 | end
70 |
71 | private
72 |
73 | def set_config(config)
74 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/test/delivery_method_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class DeliveryMethodTest < ActiveSupport::TestCase
4 | class InheritedDeliveryMethod < Noticed::DeliveryMethods::ActionCable
5 | end
6 |
7 | test "fetch_constant looks up constants from String" do
8 | @delivery_method = Noticed::DeliveryMethod.new
9 | set_config(mailer: "UserMailer")
10 | assert_equal UserMailer, @delivery_method.fetch_constant(:mailer)
11 | end
12 |
13 | test "fetch_constant looks up constants from proc that returns String" do
14 | @delivery_method = Noticed::DeliveryMethod.new
15 | set_config(mailer: -> { "UserMailer" })
16 | assert_equal UserMailer, @delivery_method.fetch_constant(:mailer)
17 | end
18 |
19 | test "delivery methods inherit required options" do
20 | assert_equal [:message], InheritedDeliveryMethod.required_option_names
21 | end
22 |
23 | test "if config" do
24 | event = TestNotifier.deliver(User.first)
25 | notification = event.notifications.first
26 | delivery_method = Noticed::DeliveryMethods::Test.new
27 |
28 | assert delivery_method.perform(:test, notification, overrides: {if: true})
29 | assert delivery_method.perform(:test, notification, overrides: {if: -> { unread? }})
30 | refute delivery_method.perform(:test, notification, overrides: {if: false})
31 | end
32 |
33 | test "unless overrides" do
34 | event = TestNotifier.deliver(User.first)
35 | notification = event.notifications.first
36 | delivery_method = Noticed::DeliveryMethods::Test.new
37 |
38 | refute delivery_method.perform(:test, notification, overrides: {unless: true})
39 | assert delivery_method.perform(:test, notification, overrides: {unless: false})
40 | assert delivery_method.perform(:test, notification, overrides: {unless: -> { read? }})
41 | end
42 |
43 | test "passes notification when calling methods on Event" do
44 | notification = noticed_notifications(:one)
45 | event = notification.event
46 |
47 | def event.example_method(notification)
48 | @example = notification
49 | end
50 |
51 | delivery_method = Noticed::DeliveryMethods::Test.new
52 | delivery_method.instance_variable_set :@notification, notification
53 | delivery_method.instance_variable_set :@event, event
54 | delivery_method.instance_variable_set :@config, {if: :example_method}
55 | delivery_method.evaluate_option(:if)
56 |
57 | assert_equal notification, event.instance_variable_get(:@example)
58 | end
59 |
60 | class CallbackDeliveryMethod < Noticed::DeliveryMethod
61 | before_deliver :set_message
62 | attr_reader :message
63 |
64 | def set_message
65 | @message = "new message"
66 | end
67 |
68 | def deliver
69 | end
70 | end
71 |
72 | class CallbackBulkDeliveryMethod < Noticed::BulkDeliveryMethod
73 | before_deliver :set_message
74 | attr_reader :message
75 |
76 | def set_message
77 | @message = "new message"
78 | end
79 |
80 | def deliver
81 | end
82 | end
83 |
84 | class CallbackNotifier < Noticed::Event
85 | deliver_by :test
86 | end
87 |
88 | class CallbackBulkNotifier < Noticed::Event
89 | bulk_deliver_by :test
90 | end
91 |
92 | test "calls callbacks" do
93 | event = CallbackNotifier.with(message: "test")
94 | notification = Noticed::Notification.create(recipient: User.first, event: event)
95 | delivery_method = CallbackDeliveryMethod.new
96 | delivery_method.perform(:test, notification)
97 | assert_equal delivery_method.message, "new message"
98 | end
99 |
100 | test "calls callbacks for bulk delivery" do
101 | event = CallbackBulkNotifier.with(message: "test")
102 | delivery_method = CallbackBulkDeliveryMethod.new
103 | delivery_method.perform(:test, event)
104 | assert_equal delivery_method.message, "new message"
105 | end
106 |
107 | private
108 |
109 | def set_config(config)
110 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
111 | end
112 | end
113 |
--------------------------------------------------------------------------------
/test/delivery_methods/action_cable_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ActionCableDeliveryMethodTest < ActiveSupport::TestCase
4 | include ActionCable::TestHelper
5 |
6 | setup do
7 | @delivery_method = Noticed::DeliveryMethods::ActionCable.new
8 | end
9 |
10 | test "sends websocket message" do
11 | user = users(:one)
12 | channel = Noticed::NotificationChannel.broadcasting_for(user)
13 |
14 | set_config(
15 | channel: "Noticed::NotificationChannel",
16 | stream: user,
17 | message: {foo: :bar}
18 | )
19 |
20 | assert_broadcasts(channel, 1) do
21 | @delivery_method.deliver
22 | end
23 | end
24 |
25 | test "default channel" do
26 | set_config({})
27 | assert_equal Noticed::NotificationChannel, @delivery_method.channel
28 | end
29 |
30 | test "default stream" do
31 | notification = noticed_notifications(:one)
32 | set_config({})
33 | @delivery_method.instance_variable_set :@notification, notification
34 | assert_equal notification.recipient, @delivery_method.stream
35 | end
36 |
37 | private
38 |
39 | def set_config(config)
40 | @delivery_method.instance_variable_set :@config, ActiveSupport::OrderedOptions.new.merge(config)
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/delivery_methods/email_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class EmailTest < ActiveSupport::TestCase
4 | include ActionMailer::TestHelper
5 |
6 | setup do
7 | @delivery_method = Noticed::DeliveryMethods::Email.new
8 | @notification = noticed_notifications(:one)
9 | end
10 |
11 | test "sends email" do
12 | set_config(
13 | mailer: "UserMailer",
14 | method: "new_comment",
15 | params: -> { {foo: :bar} },
16 | args: -> { ["hey"] }
17 | )
18 |
19 | assert_emails(1) do
20 | @delivery_method.deliver
21 | end
22 | end
23 |
24 | test "enqueues email" do
25 | set_config(
26 | mailer: "UserMailer",
27 | method: "receipt",
28 | enqueue: true
29 | )
30 |
31 | assert_enqueued_emails(1) do
32 | @delivery_method.deliver
33 | end
34 | end
35 |
36 | test "includes notification in params" do
37 | set_config(mailer: "UserMailer", method: "new_comment")
38 | assert_equal @notification, @delivery_method.params.fetch(:notification)
39 | end
40 |
41 | test "includes record in params" do
42 | set_config(mailer: "UserMailer", method: "new_comment")
43 | assert_equal @notification.record, @delivery_method.params.fetch(:record)
44 | end
45 |
46 | test "includes recipient in params" do
47 | set_config(mailer: "UserMailer", method: "new_comment")
48 | assert_equal @notification.recipient, @delivery_method.params.fetch(:recipient)
49 | end
50 |
51 | private
52 |
53 | def set_config(config)
54 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
55 | @delivery_method.instance_variable_set :@notification, @notification
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/test/delivery_methods/fcm_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class FcmTest < ActiveSupport::TestCase
4 | class FakeAuthorizer
5 | def self.make_creds(options = {})
6 | new
7 | end
8 |
9 | def fetch_access_token!
10 | {"access_token" => "access-token-12341234"}
11 | end
12 | end
13 |
14 | setup do
15 | @delivery_method = Noticed::DeliveryMethods::Fcm.new
16 | end
17 |
18 | test "notifies each device token" do
19 | set_config(
20 | authorizer: FakeAuthorizer,
21 | credentials: {
22 | "type" => "service_account",
23 | "project_id" => "p_1234",
24 | "private_key_id" => "private_key"
25 | },
26 | device_tokens: [:a, :b],
27 | json: ->(device_token) {
28 | {
29 | message: {
30 | token: device_token,
31 | notification: {title: "Title", body: "Body"}
32 | }
33 | }
34 | }
35 | )
36 |
37 | stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").with(body: "{\"message\":{\"token\":\"a\",\"notification\":{\"title\":\"Title\",\"body\":\"Body\"}}}")
38 | stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").with(body: "{\"message\":{\"token\":\"b\",\"notification\":{\"title\":\"Title\",\"body\":\"Body\"}}}")
39 |
40 | assert_nothing_raised do
41 | @delivery_method.deliver
42 | end
43 | end
44 |
45 | test "notifies of invalid tokens for clean up" do
46 | cleanups = 0
47 |
48 | set_config(
49 | authorizer: FakeAuthorizer,
50 | credentials: {
51 | "type" => "service_account",
52 | "project_id" => "p_1234",
53 | "private_key_id" => "private_key"
54 | },
55 | device_tokens: [:a, :b],
56 | json: ->(device_token) {
57 | {
58 | message: {
59 | token: device_token,
60 | notification: {title: "Title", body: "Body"}
61 | }
62 | }
63 | },
64 | invalid_token: ->(device_token) { cleanups += 1 }
65 | )
66 |
67 | stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").to_return(status: 404, body: "", headers: {})
68 |
69 | @delivery_method.deliver
70 | assert_equal 2, cleanups
71 | end
72 |
73 | test "notifies of unregistered tokens for clean up" do
74 | cleanups = 0
75 |
76 | set_config(
77 | authorizer: FakeAuthorizer,
78 | credentials: {
79 | "type" => "service_account",
80 | "project_id" => "p_1234",
81 | "private_key_id" => "private_key"
82 | },
83 | device_tokens: [:a, :b],
84 | json: ->(device_token) {
85 | {
86 | message: {
87 | token: device_token,
88 | notification: {title: "Title", body: "Body"}
89 | }
90 | }
91 | },
92 | invalid_token: ->(device_token) { cleanups += 1 }
93 | )
94 |
95 | stub_request(:post, "https://fcm.googleapis.com/v1/projects/p_1234/messages:send").to_return(status: 400, body: "", headers: {})
96 |
97 | @delivery_method.deliver
98 | assert_equal 2, cleanups
99 | end
100 |
101 | private
102 |
103 | def set_config(config)
104 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/test/delivery_methods/ios_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class IosTest < ActiveSupport::TestCase
4 | class FakeConnectionPool
5 | class_attribute :invalid_tokens, default: []
6 | attr_reader :deliveries
7 |
8 | def initialize(response)
9 | @response = response
10 | @deliveries = []
11 | end
12 |
13 | def with
14 | yield self
15 | end
16 |
17 | def push(apn)
18 | @deliveries.push(apn)
19 | @response
20 | end
21 |
22 | def close
23 | end
24 | end
25 |
26 | class FakeResponse
27 | attr_reader :status
28 |
29 | def initialize(status, body = {})
30 | @status = status
31 | end
32 |
33 | def ok?
34 | status.start_with?("20")
35 | end
36 | end
37 |
38 | setup do
39 | FakeConnectionPool.invalid_tokens = []
40 |
41 | @delivery_method = Noticed::DeliveryMethods::Ios.new
42 | @delivery_method.instance_variable_set :@notification, noticed_notifications(:one)
43 | set_config(
44 | bundle_identifier: "bundle_id",
45 | key_id: "key_id",
46 | team_id: "team_id",
47 | apns_key: "apns_key",
48 | device_tokens: [:a, :b],
49 | format: ->(apn) {
50 | apn.alert = "Hello world"
51 | apn.custom_payload = {url: root_url(host: "example.org")}
52 | },
53 | invalid_token: ->(device_token) {
54 | FakeConnectionPool.invalid_tokens << device_token
55 | }
56 | )
57 | end
58 |
59 | test "notifies each device token" do
60 | connection_pool = FakeConnectionPool.new(FakeResponse.new("200"))
61 | @delivery_method.stub(:production_pool, connection_pool) do
62 | @delivery_method.deliver
63 | end
64 |
65 | assert_equal 2, connection_pool.deliveries.count
66 | assert_equal 0, FakeConnectionPool.invalid_tokens.count
67 | end
68 |
69 | test "notifies of invalid tokens for cleanup" do
70 | connection_pool = FakeConnectionPool.new(FakeResponse.new("410"))
71 | @delivery_method.stub(:production_pool, connection_pool) do
72 | @delivery_method.deliver
73 | end
74 |
75 | # Our fake connection pool doesn't understand these wouldn't be delivered in the real world
76 | assert_equal 2, connection_pool.deliveries.count
77 | assert_equal 2, FakeConnectionPool.invalid_tokens.count
78 | end
79 |
80 | private
81 |
82 | def set_config(config)
83 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/delivery_methods/microsoft_teams_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class MicrosoftTeamsTest < ActiveSupport::TestCase
4 | setup do
5 | @delivery_method = Noticed::DeliveryMethods::MicrosoftTeams.new
6 | set_config(
7 | json: {foo: :bar},
8 | url: "https://teams.microsoft.com"
9 | )
10 | end
11 |
12 | test "sends a message" do
13 | stub_request(:post, "https://teams.microsoft.com").with(body: "{\"foo\":\"bar\"}")
14 | assert_nothing_raised do
15 | @delivery_method.deliver
16 | end
17 | end
18 |
19 | test "raises error on failure" do
20 | stub_request(:post, "https://teams.microsoft.com").to_return(status: 422)
21 | assert_raises Noticed::ResponseUnsuccessful do
22 | @delivery_method.deliver
23 | end
24 | end
25 |
26 | private
27 |
28 | def set_config(config)
29 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/delivery_methods/slack_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SlackTest < ActiveSupport::TestCase
4 | setup do
5 | @delivery_method = Noticed::DeliveryMethods::Slack.new
6 | set_config(json: {foo: :bar})
7 | end
8 |
9 | test "sends a slack message with application/json content type" do
10 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL)
11 | .with(
12 | body: "{\"foo\":\"bar\"}",
13 | headers: {"Content-Type" => "application/json"}
14 | )
15 | .to_return(
16 | status: 200,
17 | body: {ok: true}.to_json,
18 | headers: {"Content-Type" => "application/json"}
19 | )
20 |
21 | assert_nothing_raised do
22 | @delivery_method.deliver
23 | end
24 | end
25 |
26 | test "sends a slack message with text/html content type" do
27 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL)
28 | .with(
29 | body: "{\"foo\":\"bar\"}",
30 | headers: {"Content-Type" => "application/json"}
31 | )
32 | .to_return(
33 | status: 200,
34 | body: "ok",
35 | headers: {"Content-Type" => "text/html"}
36 | )
37 |
38 | assert_nothing_raised do
39 | @delivery_method.deliver
40 | end
41 | end
42 |
43 | test "raises error on failure" do
44 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).to_return(status: 422)
45 | assert_raises Noticed::ResponseUnsuccessful do
46 | @delivery_method.deliver
47 | end
48 | end
49 |
50 | test "doesnt raise error on failed 200 status code request with raise_if_not_ok false" do
51 | @delivery_method.config[:raise_if_not_ok] = false
52 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}").to_return(status: 200, body: {ok: false}.to_json, headers: {"Content-Type" => "application/json"})
53 | assert_nothing_raised do
54 | @delivery_method.deliver
55 | end
56 | end
57 |
58 | test "raises error on 200 status code request with raise_if_not_ok true" do
59 | @delivery_method.config[:raise_if_not_ok] = true
60 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}").to_return(status: 200, body: {ok: false}.to_json, headers: {"Content-Type" => "application/json"})
61 | assert_raises Noticed::ResponseUnsuccessful do
62 | @delivery_method.deliver
63 | end
64 | end
65 |
66 | test "raises error on 400 status code request with raise_if_not_ok true" do
67 | @delivery_method.config[:raise_if_not_ok] = true
68 | stub_request(:post, Noticed::DeliveryMethods::Slack::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}").to_return(status: 403, headers: {"Content-Type" => "text/html"})
69 | assert_raises Noticed::ResponseUnsuccessful do
70 | @delivery_method.deliver
71 | end
72 | end
73 |
74 | private
75 |
76 | def set_config(config)
77 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/test/delivery_methods/twilio_messaging_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class TwilioMessagingTest < ActiveSupport::TestCase
4 | setup do
5 | @delivery_method = Noticed::DeliveryMethods::TwilioMessaging.new
6 | @config = {
7 | account_sid: "acct_1234",
8 | auth_token: "token",
9 | json: -> {
10 | {
11 | From: "+1234567890",
12 | To: "+1234567890",
13 | Body: "Hello world"
14 | }
15 | }
16 | }
17 | set_config(@config)
18 | end
19 |
20 | test "sends sms" do
21 | stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/acct_1234/Messages.json").with(
22 | headers: {
23 | "Authorization" => "Basic YWNjdF8xMjM0OnRva2Vu",
24 | "Content-Type" => "application/x-www-form-urlencoded"
25 | },
26 | body: {
27 | From: "+1234567890",
28 | To: "+1234567890",
29 | Body: "Hello world"
30 | }
31 | ).to_return(status: 200)
32 |
33 | assert_nothing_raised do
34 | @delivery_method.deliver
35 | end
36 | end
37 |
38 | test "raises error on failure" do
39 | stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/acct_1234/Messages.json").to_return(status: 422)
40 | assert_raises Noticed::ResponseUnsuccessful do
41 | @delivery_method.deliver
42 | end
43 | end
44 |
45 | test "passes error to notification instance if error_handler is configured" do
46 | @delivery_method = Noticed::DeliveryMethods::TwilioMessaging.new(
47 | "delivery_method_name",
48 | noticed_notifications(:one)
49 | )
50 |
51 | error_handler_called = false
52 | @config[:error_handler] = lambda do |twilio_error_message|
53 | error_handler_called = true
54 | end
55 | set_config(@config)
56 |
57 | stub_request(:post, "https://api.twilio.com/2010-04-01/Accounts/acct_1234/Messages.json").to_return(status: 422)
58 | assert_nothing_raised do
59 | @delivery_method.deliver
60 | end
61 |
62 | assert(error_handler_called, "Handler is called if status is 4xx")
63 | end
64 |
65 | private
66 |
67 | def set_config(config)
68 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test/delivery_methods/vonage_sms_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class VonageSmsTest < ActiveSupport::TestCase
4 | setup do
5 | @delivery_method = Noticed::DeliveryMethods::VonageSms.new
6 | end
7 |
8 | test "sends sms" do
9 | set_config(json: {foo: :bar})
10 | stub_request(:post, Noticed::DeliveryMethods::VonageSms::DEFAULT_URL).with(body: "{\"foo\":\"bar\"}").to_return(status: 200, body: "{\"messages\":[{\"status\":\"0\"}]}")
11 | assert_nothing_raised do
12 | @delivery_method.deliver
13 | end
14 | end
15 |
16 | test "raises error on failure" do
17 | set_config(json: {foo: :bar})
18 | stub_request(:post, Noticed::DeliveryMethods::VonageSms::DEFAULT_URL).to_return(status: 200, body: "{\"messages\":[{\"status\":\"1\"}]}")
19 | assert_raises Noticed::ResponseUnsuccessful do
20 | @delivery_method.deliver
21 | end
22 | end
23 |
24 | private
25 |
26 | def set_config(config)
27 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/delivery_methods/webhook_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class WebhookDeliveryMethodTest < ActiveSupport::TestCase
4 | setup do
5 | @delivery_method = Noticed::DeliveryMethods::Webhook.new
6 | end
7 |
8 | test "webhook with json payload" do
9 | set_config(
10 | url: "https://example.org/webhook",
11 | json: {foo: :bar}
12 | )
13 | stub_request(:post, "https://example.org/webhook").with(body: "{\"foo\":\"bar\"}")
14 |
15 | assert_nothing_raised do
16 | @delivery_method.deliver
17 | end
18 | end
19 |
20 | test "webhook with form payload" do
21 | set_config(
22 | url: "https://example.org/webhook",
23 | form: {foo: :bar}
24 | )
25 | stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => /application\/x-www-form-urlencoded/})
26 |
27 | assert_nothing_raised do
28 | @delivery_method.deliver
29 | end
30 | end
31 |
32 | test "webhook with basic auth" do
33 | set_config(
34 | url: "https://example.org/webhook",
35 | basic_auth: {user: "username", pass: "password"}
36 | )
37 | stub_request(:post, "https://example.org/webhook").with(headers: {"Authorization" => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="})
38 |
39 | assert_nothing_raised do
40 | @delivery_method.deliver
41 | end
42 | end
43 |
44 | test "webhook with headers" do
45 | set_config(
46 | url: "https://example.org/webhook",
47 | headers: {"Content-Type" => "application/json"}
48 | )
49 | stub_request(:post, "https://example.org/webhook").with(headers: {"Content-Type" => "application/json"})
50 |
51 | assert_nothing_raised do
52 | @delivery_method.deliver
53 | end
54 | end
55 |
56 | test "webhook raises error with unsuccessful status codes" do
57 | set_config(url: "https://example.org/webhook")
58 | stub_request(:post, "https://example.org/webhook").to_return(status: 422)
59 | assert_raises Noticed::ResponseUnsuccessful do
60 | @delivery_method.deliver
61 | end
62 | end
63 |
64 | private
65 |
66 | def set_config(config)
67 | @delivery_method.instance_variable_set :@config, ActiveSupport::HashWithIndifferentAccess.new(config)
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/app/assets/images/.keep
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | def connect
4 | self.current_user = find_verified_user
5 | logger.add_tags "ActionCable", "User #{current_user.id}"
6 | end
7 |
8 | protected
9 |
10 | def find_verified_user
11 | if (current_user = env["warden"].user(:user))
12 | current_user
13 | else
14 | reject_unauthorized_connection
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com", to: "to@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | class UserMailer < ApplicationMailer
2 | def new_comment(*args)
3 | mail(body: "new comment")
4 | end
5 |
6 | def receipt
7 | mail(body: "receipt")
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/app/models/account.rb:
--------------------------------------------------------------------------------
1 | class Account < ApplicationRecord
2 | has_many :notifications, as: :record, dependent: :destroy, class_name: "Noticed::Notification"
3 | has_many :notifiers, as: :record, dependent: :destroy, class_name: "Noticed::Event"
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/admin.rb:
--------------------------------------------------------------------------------
1 | class Admin < User
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | if respond_to?(:primary_abstract_class)
3 | primary_abstract_class
4 | else
5 | self.abstract_class = true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/app/models/concerns/.keep
--------------------------------------------------------------------------------
/test/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
3 |
4 | # Used for querying Noticed::Event where params[:user] is a User instance
5 | has_noticed_notifications
6 | has_noticed_notifications param_name: :owner, destroy: false
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/application_delivery_method.rb:
--------------------------------------------------------------------------------
1 | class ApplicationDeliveryMethod < Noticed::DeliveryMethod
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/application_notifier.rb:
--------------------------------------------------------------------------------
1 | class ApplicationNotifier < Noticed::Event
2 | notification_methods do
3 | def inherited_method
4 | end
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/bulk_notifier.rb:
--------------------------------------------------------------------------------
1 | class BulkNotifier < ApplicationNotifier
2 | bulk_deliver_by :webhook, url: "https://example.org/bulk"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/comment_notifier.rb:
--------------------------------------------------------------------------------
1 | class CommentNotifier < ApplicationNotifier
2 | recipients -> { params[:recipients] }
3 |
4 | deliver_by :test
5 |
6 | # delivery_by :email, mailer: "UserMailer", method: "new_comment"
7 | deliver_by :email do |config|
8 | config.mailer = "UserMailer"
9 | config.method = :new_comment
10 | end
11 |
12 | deliver_by :action_cable do |config|
13 | config.channel = "Noticed::NotificationChannel"
14 | config.stream = -> { recipient }
15 | config.message = -> { params }
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/deprecated_notifier.rb:
--------------------------------------------------------------------------------
1 | class DeprecatedNotifier < Noticed::Base
2 | param :message
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/ephemeral_notifier.rb:
--------------------------------------------------------------------------------
1 | class EphemeralNotifier < Noticed::Ephemeral
2 | bulk_deliver_by :test
3 |
4 | deliver_by :test do |config|
5 | config.wait = 5.minutes
6 | end
7 |
8 | deliver_by :email do |config|
9 | config.mailer = "UserMailer"
10 | config.method = "new_comment"
11 | config.args = :email_args
12 | config.params = -> { {recipient: recipient} }
13 | end
14 |
15 | def email_args(recipient)
16 | [recipient]
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/inherited_notifier.rb:
--------------------------------------------------------------------------------
1 | class InheritedNotifier < SimpleNotifier
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/priority_notifier.rb:
--------------------------------------------------------------------------------
1 | class PriorityNotifier < ApplicationNotifier
2 | deliver_by :test, priority: 2
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/queue_notifier.rb:
--------------------------------------------------------------------------------
1 | class QueueNotifier < ApplicationNotifier
2 | deliver_by :test, queue: :example_queue
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/receipt_notifier.rb:
--------------------------------------------------------------------------------
1 | class ReceiptNotifier < ApplicationNotifier
2 | deliver_by :test
3 |
4 | deliver_by :email do |config|
5 | config.mailer = "UserMailer"
6 | config.method = :receipt
7 | config.params = -> { params }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/record_notifier.rb:
--------------------------------------------------------------------------------
1 | class RecordNotifier < ApplicationNotifier
2 | validates :record, presence: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/simple_notifier.rb:
--------------------------------------------------------------------------------
1 | class SimpleNotifier < ApplicationNotifier
2 | deliver_by :test
3 | required_params :message
4 |
5 | def url
6 | root_url
7 | end
8 |
9 | notification_methods do
10 | def message
11 | "hello #{recipient.email}"
12 | end
13 |
14 | def url
15 | root_url
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/test_notifier.rb:
--------------------------------------------------------------------------------
1 | class TestNotifier < ApplicationNotifier
2 | deliver_by :test
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/wait_notifier.rb:
--------------------------------------------------------------------------------
1 | class WaitNotifier < ApplicationNotifier
2 | deliver_by :test, wait: 5.minutes
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/notifiers/wait_until_notifier.rb:
--------------------------------------------------------------------------------
1 | class WaitUntilNotifier < ApplicationNotifier
2 | deliver_by :test, wait_until: -> { 1.hour.from_now }
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 |
6 | <%= csrf_meta_tags %>
7 | <%= csp_meta_tag %>
8 |
9 | <%= stylesheet_link_tag "application" %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Dummy
10 | class Application < Rails::Application
11 | config.load_defaults Rails::VERSION::STRING.to_f
12 |
13 | # For compatibility with applications that use this config
14 | config.action_controller.include_all_helpers = false
15 |
16 | # Please, add to the `ignore` list any other `lib` subdirectories that do
17 | # not contain `.rb` files, or that should not be reloaded or eager loaded.
18 | # Common ones are `templates`, `generators`, or `middleware`, for example.
19 | # config.autoload_lib(ignore: %w[assets tasks])
20 |
21 | # Configuration for the application, engines, and railties goes here.
22 | #
23 | # These settings can be overridden in specific environments using the files
24 | # in config/environments, which are processed later.
25 | #
26 | # config.time_zone = "Central Time (US & Canada)"
27 | # config.eager_load_paths << Rails.root.join("extras")
28 |
29 | config.active_job.queue_adapter = :test
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: storage/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: storage/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: storage/production.sqlite3
26 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.enable_reloading = true
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | # Print deprecation notices to the Rails logger.
45 | config.active_support.deprecation = :log
46 |
47 | # Raise exceptions for disallowed deprecations.
48 | config.active_support.disallowed_deprecation = :raise
49 |
50 | # Tell Active Support which deprecation messages to disallow.
51 | config.active_support.disallowed_deprecation_warnings = []
52 |
53 | # Raise an error on page load if there are pending migrations.
54 | config.active_record.migration_error = :page_load
55 |
56 | # Highlight code that triggered database queries in logs.
57 | config.active_record.verbose_query_logs = true
58 |
59 | # Highlight code that enqueued background job in logs.
60 | config.active_job.verbose_enqueue_logs = true
61 |
62 | # Suppress logger output for asset requests.
63 | # config.assets.quiet = true
64 |
65 | # Raises error for missing translations.
66 | # config.i18n.raise_on_missing_translations = true
67 |
68 | # Annotate rendered view with file names.
69 | # config.action_view.annotate_rendered_view_with_filenames = true
70 |
71 | # Uncomment if you wish to allow Action Cable access from any origin.
72 | # config.action_cable.disable_request_forgery_protection = true
73 |
74 | # Raise error when a before_action's only/except options reference missing actions
75 | config.action_controller.raise_on_missing_callback_actions = true
76 |
77 | routes.default_url_options[:host] = "localhost:3000"
78 | end
79 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.enable_reloading = false
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment
20 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
24 | # config.public_file_server.enabled = false
25 |
26 | # Compress CSS using a preprocessor.
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = false
31 |
32 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
33 | # config.asset_host = "http://assets.example.com"
34 |
35 | # Specifies the header that your server uses for sending files.
36 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
37 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
38 |
39 | # Store uploaded files on the local file system (see config/storage.yml for options).
40 | config.active_storage.service = :local
41 |
42 | # Mount Action Cable outside main process or domain.
43 | # config.action_cable.mount_path = nil
44 | # config.action_cable.url = "wss://example.com/cable"
45 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
46 |
47 | # Assume all access to the app is happening through a SSL-terminating reverse proxy.
48 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
49 | # config.assume_ssl = true
50 |
51 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
52 | config.force_ssl = true
53 |
54 | # Log to STDOUT by default
55 | config.logger = ActiveSupport::Logger.new($stdout)
56 | .tap { |logger| logger.formatter = ::Logger::Formatter.new }
57 | .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
58 |
59 | # Prepend all log lines with the following tags.
60 | config.log_tags = [:request_id]
61 |
62 | # Info include generic and useful information about system operation, but avoids logging too much
63 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you
64 | # want to log everything, set the level to "debug".
65 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
66 |
67 | # Use a different cache store in production.
68 | # config.cache_store = :mem_cache_store
69 |
70 | # Use a real queuing backend for Active Job (and separate queues per environment).
71 | # config.active_job.queue_adapter = :resque
72 | # config.active_job.queue_name_prefix = "dummy_production"
73 |
74 | config.action_mailer.perform_caching = false
75 |
76 | # Ignore bad email addresses and do not raise email delivery errors.
77 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
78 | # config.action_mailer.raise_delivery_errors = false
79 |
80 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
81 | # the I18n.default_locale when a translation cannot be found).
82 | config.i18n.fallbacks = true
83 |
84 | # Don't log any deprecations.
85 | config.active_support.report_deprecations = false
86 |
87 | # Do not dump schema after migrations.
88 | config.active_record.dump_schema_after_migration = false
89 |
90 | # Enable DNS rebinding protection and other `Host` header attacks.
91 | # config.hosts = [
92 | # "example.com", # Allow requests from example.com
93 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com`
94 | # ]
95 | # Skip DNS rebinding protection for the default health check endpoint.
96 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
97 | end
98 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # While tests run files are not watched, reloading is not necessary.
12 | config.enable_reloading = false
13 |
14 | # Eager loading loads your entire application. When running a single test locally,
15 | # this is usually not necessary, and can slow down your test suite. However, it's
16 | # recommended that you enable it in continuous integration systems to ensure eager
17 | # loading is working properly before deploying your code.
18 | config.eager_load = ENV["CI"].present?
19 |
20 | # Configure public file server for tests with Cache-Control for performance.
21 | config.public_file_server.enabled = true
22 | config.public_file_server.headers = {
23 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
24 | }
25 |
26 | # Show full error reports and disable caching.
27 | config.consider_all_requests_local = true
28 | config.action_controller.perform_caching = false
29 | config.cache_store = :null_store
30 |
31 | # Render exception templates for rescuable exceptions and raise for other exceptions.
32 | config.action_dispatch.show_exceptions = :rescuable
33 |
34 | # Disable request forgery protection in test environment.
35 | config.action_controller.allow_forgery_protection = false
36 |
37 | # Store uploaded files on the local file system in a temporary directory.
38 | config.active_storage.service = :test
39 |
40 | config.action_mailer.perform_caching = false
41 |
42 | # Tell Action Mailer not to deliver emails to the real world.
43 | # The :test delivery method accumulates sent emails in the
44 | # ActionMailer::Base.deliveries array.
45 | config.action_mailer.delivery_method = :test
46 |
47 | # Print deprecation notices to the stderr.
48 | config.active_support.deprecation = :stderr
49 |
50 | # Raise exceptions for disallowed deprecations.
51 | config.active_support.disallowed_deprecation = :raise
52 |
53 | # Tell Active Support which deprecation messages to disallow.
54 | config.active_support.disallowed_deprecation_warnings = []
55 |
56 | # Raises error for missing translations.
57 | # config.i18n.raise_on_missing_translations = true
58 |
59 | # Annotate rendered view with file names.
60 | # config.action_view.annotate_rendered_view_with_filenames = true
61 |
62 | # Raise error when a before_action's only/except options reference missing actions
63 | # config.action_controller.raise_on_missing_callback_actions = true
64 |
65 | routes.default_url_options[:host] = "localhost:3000"
66 | end
67 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | # Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src style-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
4 | # Use this to limit dissemination of sensitive information.
5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide HTTP permissions policy. For further
4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy
5 |
6 | # Rails.application.config.permissions_policy do |policy|
7 | # policy.camera :none
8 | # policy.gyroscope :none
9 | # policy.microphone :none
10 | # policy.usb :none
11 | # policy.fullscreen :self
12 | # policy.payment :self, "https://secure.example.com"
13 | # end
14 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization and
2 | # are automatically loaded by Rails. If you want to use locales other than
3 | # English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more about the API, please read the Rails Internationalization guide
20 | # at https://guides.rubyonrails.org/i18n.html.
21 | #
22 | # Be aware that YAML interprets the following case-insensitive strings as
23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24 | # must be quoted to be interpreted as strings. For example:
25 | #
26 | # en:
27 | # "yes": yup
28 | # enabled: "ON"
29 |
30 | en:
31 | hello: "Hello world"
32 | message_html: "Hello world
"
33 | notifiers:
34 | noticed:
35 | i18n_example:
36 | message: "This is a notification"
37 | noticed:
38 | scoped_i18n_example:
39 | message: "This is a custom scoped translation"
40 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # This configuration file will be evaluated by Puma. The top-level methods that
2 | # are invoked here are part of Puma's configuration DSL. For more information
3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
4 |
5 | # Puma can serve each request in a thread from an internal thread pool.
6 | # The `threads` method setting takes two numbers: a minimum and maximum.
7 | # Any libraries that use thread pools should be configured to match
8 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
9 | # and maximum; this matches the default thread size of Active Record.
10 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
11 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
12 | threads min_threads_count, max_threads_count
13 |
14 | # Specifies that the worker count should equal the number of processors in production.
15 | if ENV["RAILS_ENV"] == "production"
16 | require "concurrent-ruby"
17 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count })
18 | workers worker_count if worker_count > 1
19 | end
20 |
21 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
22 | # terminating a worker in development environments.
23 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
24 |
25 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
26 | port ENV.fetch("PORT") { 3000 }
27 |
28 | # Specifies the `environment` that Puma will run in.
29 | environment ENV.fetch("RAILS_ENV") { "development" }
30 |
31 | # Specifies the `pidfile` that Puma will use.
32 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
33 |
34 | # Allow puma to be restarted by `bin/rails restart` command.
35 | plugin :tmp_restart
36 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
3 |
4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
5 | # Can be used by load balancers and uptime monitors to verify that the app is live.
6 | get "up" => "rails/health#show", :as => :rails_health_check
7 |
8 | # Defines the root path route ("/")
9 | root "posts#index"
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20231215202921_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :users do |t|
4 | t.string :type
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20231215202924_create_accounts.rb:
--------------------------------------------------------------------------------
1 | class CreateAccounts < ActiveRecord::Migration[7.1]
2 | def change
3 | create_table :accounts do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 2024_01_29_184740) do
14 | create_table "accounts", force: :cascade do |t|
15 | t.string "name"
16 | t.datetime "created_at", null: false
17 | t.datetime "updated_at", null: false
18 | end
19 |
20 | create_table "noticed_events", force: :cascade do |t|
21 | t.string "type"
22 | t.string "record_type"
23 | t.integer "record_id"
24 | if t.respond_to?(:jsonb)
25 | t.jsonb "params"
26 | else
27 | t.json "params"
28 | end
29 | t.integer "notifications_count"
30 | t.datetime "created_at", null: false
31 | t.datetime "updated_at", null: false
32 | t.index ["record_type", "record_id"], name: "index_noticed_events_on_record"
33 | end
34 |
35 | create_table "noticed_notifications", force: :cascade do |t|
36 | t.string "type"
37 | t.integer "event_id", null: false
38 | t.string "recipient_type", null: false
39 | t.integer "recipient_id", null: false
40 | t.datetime "read_at"
41 | t.datetime "seen_at"
42 | t.datetime "created_at", null: false
43 | t.datetime "updated_at", null: false
44 | t.index ["event_id"], name: "index_noticed_notifications_on_event_id"
45 | t.index ["recipient_type", "recipient_id"], name: "index_noticed_notifications_on_recipient"
46 | end
47 |
48 | create_table "users", force: :cascade do |t|
49 | t.string "type"
50 | t.string "email"
51 | t.datetime "created_at", null: false
52 | t.datetime "updated_at", null: false
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/lib/assets/.keep
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/log/.keep
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/dummy/public/favicon.ico
--------------------------------------------------------------------------------
/test/ephemeral_notifier_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class EphemeralNotifierTest < ActiveSupport::TestCase
4 | include ActionMailer::TestHelper
5 | include ActiveJob::TestHelper
6 |
7 | test "can enqueue delivery methods" do
8 | assert_enqueued_jobs 3 do
9 | EphemeralNotifier.new(params: {foo: :bar}).deliver(User.last)
10 | end
11 |
12 | assert_emails 1 do
13 | perform_enqueued_jobs
14 | end
15 | end
16 |
17 | test "ephemeral has record shortcut" do
18 | assert_equal :foo, EphemeralNotifier.with(record: :foo).record
19 | end
20 |
21 | test "ephemeral notifier includes Rails urls" do
22 | assert_equal "http://localhost:3000/", EphemeralNotifier.new.root_url
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/fixtures/accounts.yml:
--------------------------------------------------------------------------------
1 | one:
2 | name: Account One
3 |
4 | two:
5 | name: Account Two
6 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/fixtures/noticed/events.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | type: CommentNotifier
5 | record: one
6 | record_type: User
7 | params:
8 | foo: bar
9 | notifications_count: 1
10 |
11 | two:
12 | type: CommentNotifier
13 | record: two
14 | record_type: User
15 | params:
16 | foo: bar
17 | notifications_count: 1
18 |
19 | three:
20 | type: ReceiptNotifier
21 | record: two
22 | record_type: User
23 | params:
24 | foo: bar
25 | notifications_count: 2
26 |
27 | account:
28 | type: ReceiptNotifier
29 | record: two
30 | record_type: User
31 | params:
32 | foo: bar
33 | account:
34 | _aj_globalid: gid://dummy/Account/<%= ActiveRecord::FixtureSet.identify(:primary) %>
35 | _aj_symbol_keys:
36 | - account
37 |
38 | missing_account:
39 | type: ReceiptNotifier
40 | record: two
41 | record_type: User
42 | params:
43 | foo: bar
44 | account:
45 | _aj_globalid: gid://dummy/Account/100000
46 | _aj_symbol_keys:
47 | - account
48 |
--------------------------------------------------------------------------------
/test/fixtures/noticed/notifications.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | event: one
5 | type: CommentNotifier::Notification
6 | recipient: one (User)
7 | read_at: 2023-12-15 13:03:19
8 | seen_at: 2023-12-15 13:03:19
9 |
10 | two:
11 | event: two
12 | type: CommentNotifier::Notification
13 | recipient: two (User)
14 | read_at: 2023-12-15 13:03:19
15 | seen_at: 2023-12-15 13:03:19
16 |
17 | three:
18 | event: three
19 | type: ReceiptNotifier::Notification
20 | recipient: one (Account)
21 | read_at: 2023-12-15 13:03:19
22 | seen_at: 2023-12-15 13:03:19
23 |
24 | four:
25 | event: three
26 | type: ReceiptNotifier::Notification
27 | recipient: two (Account)
28 | read_at: 2023-12-15 13:03:19
29 | seen_at: 2023-12-15 13:03:19
30 |
--------------------------------------------------------------------------------
/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | one:
2 | email: one@example.org
3 |
4 | two:
5 | email: two@example.org
6 |
7 | admin:
8 | type: Admin
9 | email: admin@example.org
10 |
--------------------------------------------------------------------------------
/test/has_notifications_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class HasNotificationsTest < ActiveSupport::TestCase
4 | test "has_noticed_notifications" do
5 | assert User.respond_to?(:has_noticed_notifications)
6 | end
7 |
8 | test "noticed notifications association" do
9 | assert user.respond_to?(:notifications_as_user)
10 | end
11 |
12 | test "noticed notifications with custom name" do
13 | assert user.respond_to?(:notifications_as_owner)
14 | end
15 |
16 | test "association returns notifications" do
17 | assert_difference "user.notifications_as_user.count" do
18 | SimpleNotifier.with(user: user, message: "test").deliver(user)
19 | end
20 | end
21 |
22 | test "association with custom name returns notifications" do
23 | assert_difference "user.notifications_as_owner.count" do
24 | SimpleNotifier.with(owner: user, message: "test").deliver(user)
25 | end
26 | end
27 |
28 | test "deletes notifications with matching param" do
29 | SimpleNotifier.with(user: user, message: "test").deliver(users(:two))
30 |
31 | assert_difference "Noticed::Event.count", -1 do
32 | user.destroy
33 | end
34 | end
35 |
36 | test "doesn't delete notifications when disabled" do
37 | SimpleNotifier.with(owner: user, message: "test").deliver(users(:two))
38 |
39 | assert_no_difference "Noticed::Event.count" do
40 | user.destroy
41 | end
42 | end
43 |
44 | def user
45 | @user ||= users(:one)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/jobs/event_job_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class EventJobTest < ActiveJob::TestCase
4 | module ::Noticed
5 | class DeliveryMethods::Test1 < DeliveryMethod; end
6 |
7 | class DeliveryMethods::Test2 < DeliveryMethod; end
8 |
9 | class BulkDeliveryMethods::Test1 < BulkDeliveryMethod; end
10 |
11 | class BulkDeliveryMethods::Test2 < BulkDeliveryMethod; end
12 | end
13 |
14 | test "enqueues jobs for each notification and delivery method" do
15 | Noticed::EventJob.perform_now(noticed_notifications(:one).event)
16 | assert_enqueued_jobs 3
17 | end
18 |
19 | test "skips enqueueing jobs if before_enqueue raises an error" do
20 | notification = noticed_notifications(:one)
21 | event = notification.event
22 | event.class.deliver_by :test1 do |config|
23 | config.before_enqueue = -> { false }
24 | end
25 | event.class.deliver_by :test2 do |config|
26 | config.before_enqueue = -> { throw :abort }
27 | end
28 |
29 | Noticed::EventJob.perform_now(event)
30 | assert_enqueued_jobs 4
31 |
32 | event.class.delivery_methods.delete(:test1)
33 | event.class.delivery_methods.delete(:test2)
34 | end
35 |
36 | test "skips enqueueing bulk delivery job if before_enqueue raises an error" do
37 | notification = noticed_notifications(:one)
38 | event = notification.event
39 | event.class.bulk_deliver_by :test1 do |config|
40 | config.before_enqueue = -> { false }
41 | end
42 | event.class.bulk_deliver_by :test2 do |config|
43 | config.before_enqueue = -> { throw :abort }
44 | end
45 |
46 | Noticed::EventJob.perform_now(event)
47 | assert_enqueued_jobs 4
48 |
49 | event.class.bulk_delivery_methods.delete(:test1)
50 | event.class.bulk_delivery_methods.delete(:test2)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/excid3/noticed/d63ddeef0e6561e1c2eb39581f384cea321db415/test/models/.keep
--------------------------------------------------------------------------------
/test/models/noticed/deliverable/deliver_by_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class Noticed::Deliverable::DeliverByTest < ActiveSupport::TestCase
6 | class TestDelivery < Noticed::Deliverable::DeliverBy; end
7 |
8 | test "#perform? returns true when before_enqueue is missing" do
9 | config = ActiveSupport::OrderedOptions.new.merge({})
10 | assert_equal true, TestDelivery.new(:test, config).perform?({})
11 | end
12 |
13 | test "#perform? returns false when before_enqueue throws" do
14 | config = ActiveSupport::OrderedOptions.new.merge({})
15 | config.before_enqueue = -> { throw :abort }
16 | assert_equal false, TestDelivery.new(:test, config).perform?({})
17 | end
18 |
19 | test "#perform? returns true when before_enqueue does not throw" do
20 | config = ActiveSupport::OrderedOptions.new.merge({})
21 | config.before_enqueue = -> { false }
22 | assert_equal true, TestDelivery.new(:test, config).perform?({})
23 | end
24 |
25 | test "#perform? takes context into account" do
26 | config = ActiveSupport::OrderedOptions.new.merge({})
27 | config.before_enqueue = -> { throw :abort if key?(:test_value) }
28 | assert_equal false, TestDelivery.new(:test, config).perform?({test_value: true})
29 | assert_equal true, TestDelivery.new(:test, config).perform?({other_value: true})
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/models/noticed/event_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Noticed::EventTest < ActiveSupport::TestCase
4 | class ExampleNotifier < Noticed::Event
5 | deliver_by :test
6 | required_params :message
7 | end
8 |
9 | test "validates required params" do
10 | assert_raises Noticed::ValidationError do
11 | ExampleNotifier.deliver
12 | end
13 | end
14 |
15 | test "deliver saves event" do
16 | assert_difference "Noticed::Event.count" do
17 | ExampleNotifier.with(message: "test").deliver
18 | end
19 | end
20 |
21 | test "deliver saves notifications" do
22 | assert_no_difference "Noticed::Notification.count" do
23 | ExampleNotifier.with(message: "test").deliver
24 | end
25 |
26 | assert_difference "Noticed::Notification.count" do
27 | ExampleNotifier.with(message: "test").deliver(users(:one))
28 | end
29 |
30 | assert_difference "Noticed::Notification.count", User.count do
31 | ExampleNotifier.with(message: "test").deliver(User.all)
32 | end
33 | end
34 |
35 | test "deliver extracts record from params" do
36 | account = accounts(:one)
37 | event = ExampleNotifier.with(message: "test", record: account).deliver
38 | assert_equal account, event.record
39 | end
40 |
41 | test "deserialize_error?" do
42 | assert noticed_events(:missing_account).deserialize_error?
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/models/noticed/notification_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class Noticed::NotificationTest < ActiveSupport::TestCase
4 | test "delegates params to event" do
5 | notification = noticed_notifications(:one)
6 | assert_equal notification.event.params, notification.params
7 | end
8 |
9 | test "delegates record to event" do
10 | notification = noticed_notifications(:one)
11 | assert_equal notification.event.record, notification.record
12 | end
13 |
14 | test "notification associations" do
15 | assert_equal 1, users(:one).notifications.count
16 | end
17 |
18 | test "read scope" do
19 | assert_equal 4, Noticed::Notification.read.count
20 | end
21 |
22 | test "unread scope" do
23 | assert_equal 0, Noticed::Notification.unread.count
24 | end
25 |
26 | test "seen scope" do
27 | assert_equal 4, Noticed::Notification.seen.count
28 | end
29 |
30 | test "unseen scope" do
31 | assert_equal 0, Noticed::Notification.unseen.count
32 | end
33 |
34 | test "mark_as_read" do
35 | Noticed::Notification.update_all(read_at: nil)
36 | assert_equal 0, Noticed::Notification.read.count
37 | Noticed::Notification.mark_as_read
38 | assert_equal 4, Noticed::Notification.read.count
39 | end
40 |
41 | test "mark_as_unread" do
42 | Noticed::Notification.update_all(read_at: Time.current)
43 | assert_equal 4, Noticed::Notification.read.count
44 | Noticed::Notification.mark_as_unread
45 | assert_equal 0, Noticed::Notification.read.count
46 | end
47 |
48 | test "mark_as_seen" do
49 | Noticed::Notification.update_all(seen_at: nil)
50 | assert_equal 0, Noticed::Notification.seen.count
51 | Noticed::Notification.mark_as_seen
52 | assert_equal 4, Noticed::Notification.seen.count
53 | end
54 |
55 | test "mark_as_unseen" do
56 | Noticed::Notification.update_all(seen_at: Time.current)
57 | assert_equal 4, Noticed::Notification.seen.count
58 | Noticed::Notification.mark_as_unseen
59 | assert_equal 0, Noticed::Notification.seen.count
60 | end
61 |
62 | test "mark_as_read_and_seen" do
63 | Noticed::Notification.update_all(read_at: nil, seen_at: nil)
64 | assert_equal 0, Noticed::Notification.read.count
65 | assert_equal 0, Noticed::Notification.seen.count
66 | Noticed::Notification.mark_as_read_and_seen
67 | assert_equal 4, Noticed::Notification.read.count
68 | assert_equal 4, Noticed::Notification.seen.count
69 | end
70 |
71 | test "mark_as_unread_and_unseen" do
72 | Noticed::Notification.update_all(read_at: Time.current, seen_at: Time.current)
73 | assert_equal 4, Noticed::Notification.read.count
74 | assert_equal 4, Noticed::Notification.seen.count
75 | Noticed::Notification.mark_as_unread_and_unseen
76 | assert_equal 0, Noticed::Notification.read.count
77 | assert_equal 0, Noticed::Notification.seen.count
78 | end
79 |
80 | test "read?" do
81 | assert noticed_notifications(:one).read?
82 | end
83 |
84 | test "unread?" do
85 | assert_not noticed_notifications(:one).unread?
86 | end
87 |
88 | test "seen?" do
89 | assert noticed_notifications(:one).seen?
90 | end
91 |
92 | test "unseen?" do
93 | assert_not noticed_notifications(:one).unseen?
94 | end
95 |
96 | test "notification url helpers" do
97 | assert_equal "http://localhost:3000/", CommentNotifier::Notification.new.root_url
98 | end
99 |
100 | test "ephemeral notification url helpers" do
101 | assert_equal "http://localhost:3000/", EphemeralNotifier::Notification.new.root_url
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/test/noticed_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class NoticedTest < ActiveSupport::TestCase
4 | test "it has a version number" do
5 | assert Noticed::VERSION
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/notifier_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class NotifierTest < ActiveSupport::TestCase
4 | include ActiveJob::TestHelper
5 |
6 | class RecipientsBlock < Noticed::Event
7 | recipients do
8 | params.fetch(:recipients)
9 | end
10 | deliver_by :test
11 | end
12 |
13 | class RecipientsLambda < Noticed::Event
14 | recipients -> { params.fetch(:recipients) }
15 | deliver_by :test
16 | end
17 |
18 | class RecipientsMethod < Noticed::Event
19 | deliver_by :test
20 | recipients :recipients
21 |
22 | def recipients
23 | params.fetch(:recipients)
24 | end
25 | end
26 |
27 | class RecipientsLambdaEphemeral < Noticed::Ephemeral
28 | recipients -> { params.fetch(:recipients) }
29 | deliver_by :test
30 | end
31 |
32 | test "includes Rails urls" do
33 | assert_equal "http://localhost:3000/", SimpleNotifier.new.url
34 | end
35 |
36 | test "notifiers inherit required params" do
37 | assert_equal [:message], InheritedNotifier.required_params
38 | end
39 |
40 | test "notification_methods adds methods to Noticed::Notifications" do
41 | user = users(:one)
42 | event = SimpleNotifier.with(message: "test").deliver(user)
43 | assert_equal "hello #{user.email}", event.notifications.last.message
44 | end
45 |
46 | test "notification_methods url helpers" do
47 | assert_equal "http://localhost:3000/", SimpleNotifier::Notification.new.url
48 | end
49 |
50 | test "serializes globalid objects with text column" do
51 | user = users(:one)
52 | notification = Noticed::Event.create!(type: "SimpleNotifier", params: {user: user})
53 | assert_equal({user: user}, notification.params)
54 | end
55 |
56 | test "assigns record association from params" do
57 | user = users(:one)
58 | notifier = RecordNotifier.with(record: user)
59 | assert_equal user, notifier.record
60 | assert_empty notifier.params
61 | end
62 |
63 | test "can add validations for record association" do
64 | notifier = RecordNotifier.with({})
65 | refute notifier.valid?
66 | assert_equal ["can't be blank"], notifier.errors[:record]
67 | end
68 |
69 | test "recipients block" do
70 | event = RecipientsBlock.with(recipients: [User.create!(email: "foo"), User.create!(email: "bar")]).deliver
71 | assert_equal 2, event.notifications.count
72 | assert_equal User.find_by(email: "foo"), event.notifications.first.recipient
73 | end
74 |
75 | test "recipients lambda" do
76 | event = RecipientsLambda.with(recipients: [User.create!(email: "foo"), User.create!(email: "bar")]).deliver
77 | assert_equal 2, event.notifications.count
78 | assert_equal User.find_by(email: "foo"), event.notifications.first.recipient
79 | end
80 |
81 | test "recipients" do
82 | event = RecipientsMethod.with(recipients: [User.create!(email: "foo"), User.create!(email: "bar")]).deliver
83 | assert_equal 2, event.notifications.count
84 | assert_equal User.find_by(email: "foo"), event.notifications.first.recipient
85 |
86 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last]) do
87 | perform_enqueued_jobs
88 | end
89 | end
90 |
91 | test "recipients ephemeral" do
92 | users = [User.create!(email: "foo"), User.create!(email: "bar")]
93 |
94 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, "NotifierTest::RecipientsLambdaEphemeral::Notification", {recipient: User.find_by(email: "foo"), params: {recipients: users}}]) do
95 | RecipientsLambdaEphemeral.with(recipients: users).deliver
96 | end
97 | end
98 |
99 | test "deliver without recipients" do
100 | assert_nothing_raised do
101 | ReceiptNotifier.deliver
102 | end
103 | end
104 |
105 | test "deliver creates an event" do
106 | assert_difference "Noticed::Event.count" do
107 | ReceiptNotifier.deliver(User.first)
108 | end
109 | end
110 |
111 | test "deliver creates notifications for each recipient" do
112 | assert_no_difference "Noticed::Notification.count" do
113 | event = ReceiptNotifier.deliver
114 | assert_equal 0, event.notifications_count
115 | end
116 |
117 | assert_difference "Noticed::Notification.count" do
118 | event = ReceiptNotifier.deliver(User.first)
119 | assert_equal 1, event.notifications_count
120 | end
121 |
122 | assert_difference "Noticed::Notification.count", User.count do
123 | event = ReceiptNotifier.deliver(User.all)
124 | assert_equal User.count, event.notifications_count
125 | end
126 |
127 | assert_difference "Noticed::Notification.count", -1 do
128 | event = noticed_events(:one)
129 | event.notifications.destroy_all
130 | assert_equal 0, event.notifications_count
131 | end
132 | end
133 |
134 | test "deliver to STI recipient writes base class" do
135 | admin = Admin.first
136 | assert_difference "Noticed::Notification.count" do
137 | ReceiptNotifier.deliver(admin)
138 | end
139 | notification = Noticed::Notification.last
140 | assert_equal "User", notification.recipient_type
141 | assert_equal admin, notification.recipient
142 | end
143 |
144 | test "creates jobs for deliveries" do
145 | # Delivering a notification creates records
146 | assert_enqueued_jobs 1, only: Noticed::EventJob do
147 | ReceiptNotifier.deliver(User.first)
148 | end
149 |
150 | # Run the Event Job
151 | assert_enqueued_jobs 1, only: Noticed::DeliveryMethods::Test do
152 | perform_enqueued_jobs
153 | end
154 |
155 | # Run the individual deliveries
156 | perform_enqueued_jobs
157 |
158 | assert_equal Noticed::Notification.last, Noticed::DeliveryMethods::Test.delivered.last
159 | end
160 |
161 | test "creates jobs for bulk deliveries" do
162 | assert_enqueued_jobs 1, only: Noticed::EventJob do
163 | BulkNotifier.deliver
164 | end
165 |
166 | assert_enqueued_jobs 1, only: Noticed::BulkDeliveryMethods::Webhook do
167 | perform_enqueued_jobs
168 | end
169 | end
170 |
171 | test "creates jobs for bulk ephemeral deliveries" do
172 | assert_enqueued_jobs 1, only: Noticed::BulkDeliveryMethods::Test do
173 | EphemeralNotifier.deliver
174 | end
175 |
176 | assert_difference("Noticed::BulkDeliveryMethods::Test.delivered.length" => 1) do
177 | perform_enqueued_jobs
178 | end
179 | end
180 |
181 | test "deliver wait" do
182 | freeze_time
183 | assert_enqueued_with job: Noticed::EventJob, at: 5.minutes.from_now do
184 | ReceiptNotifier.deliver(User.first, wait: 5.minutes)
185 | end
186 | end
187 |
188 | test "deliver queue" do
189 | freeze_time
190 | assert_enqueued_with job: Noticed::EventJob, queue: "low_priority" do
191 | ReceiptNotifier.deliver(User.first, queue: :low_priority)
192 | end
193 | end
194 |
195 | test "wait delivery method option" do
196 | freeze_time
197 | event = WaitNotifier.deliver(User.first)
198 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], at: 5.minutes.from_now) do
199 | perform_enqueued_jobs
200 | end
201 | end
202 |
203 | test "wait_until delivery method option" do
204 | freeze_time
205 | event = WaitUntilNotifier.deliver(User.first)
206 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], at: 1.hour.from_now) do
207 | perform_enqueued_jobs
208 | end
209 | end
210 |
211 | test "queue delivery method option" do
212 | event = QueueNotifier.deliver(User.first)
213 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], queue: "example_queue") do
214 | perform_enqueued_jobs
215 | end
216 | end
217 |
218 | # assert_enqeued_with doesn't support priority before Rails 7
219 | if Rails.gem_version >= Gem::Version.new("7.0.0.alpha1")
220 | test "priority delivery method option" do
221 | event = PriorityNotifier.deliver(User.first)
222 | assert_enqueued_with(job: Noticed::DeliveryMethods::Test, args: [:test, event.notifications.last], priority: 2) do
223 | perform_enqueued_jobs
224 | end
225 | end
226 | end
227 |
228 | test "deprecations don't cause problems" do
229 | assert_nothing_raised do
230 | Noticed.deprecator.silence do
231 | DeprecatedNotifier.with(message: "test").deliver_later
232 | end
233 | end
234 | end
235 |
236 | test "inherits notification_methods from application notifier" do
237 | assert SimpleNotifier::Notification.new.respond_to?(:inherited_method)
238 | end
239 | end
240 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../test/dummy/config/environment"
5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
6 | require "rails/test_help"
7 | require "minitest/mock"
8 | require "webmock/minitest"
9 |
10 | # Load fixtures from the engine
11 | if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
12 | ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)]
13 | ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
14 | ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files"
15 | ActiveSupport::TestCase.fixtures :all
16 | elsif ActiveSupport::TestCase.respond_to?(:fixture_path=)
17 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
18 | ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
19 | ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files"
20 | ActiveSupport::TestCase.fixtures :all
21 | end
22 |
--------------------------------------------------------------------------------
/test/translation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class TranslationTest < ActiveSupport::TestCase
4 | class I18nExample < Noticed::Event
5 | deliver_by :test do |config|
6 | config.message = -> { t("hello") }
7 | end
8 |
9 | def message
10 | t("hello")
11 | end
12 |
13 | def html_message
14 | t("message_html")
15 | end
16 | end
17 |
18 | class Noticed::I18nExample < Noticed::Event
19 | def message
20 | t(".message")
21 | end
22 | end
23 |
24 | class ::ScopedI18nExample < Noticed::Event
25 | def i18n_scope
26 | :noticed
27 | end
28 |
29 | def message
30 | t(".message")
31 | end
32 | end
33 |
34 | test "I18n support" do
35 | assert_equal "hello", I18nExample.new.send(:scope_translation_key, "hello")
36 | assert_equal "Hello world", I18nExample.new.message
37 | end
38 |
39 | test "I18n supports namespaces" do
40 | assert_equal "notifiers.noticed.i18n_example.message", Noticed::I18nExample.new.send(:scope_translation_key, ".message")
41 | assert_equal "This is a notification", Noticed::I18nExample.new.message
42 | end
43 |
44 | test "I18n supports custom scopes" do
45 | assert_equal "noticed.scoped_i18n_example.message", ScopedI18nExample.new.send(:scope_translation_key, ".message")
46 | assert_equal "This is a custom scoped translation", ScopedI18nExample.new.message
47 | end
48 |
49 | if defined?(ActiveSupport::HtmlSafeTranslation)
50 | test "I18n supports html safe translations" do
51 | message = I18nExample.new.html_message
52 | assert_equal "Hello world
", message
53 | assert message.html_safe?
54 | end
55 | end
56 |
57 | test "delivery method blocks can use translations" do
58 | block = I18nExample.delivery_methods[:test].config[:message]
59 | assert_equal "Hello world", noticed_notifications(:one).instance_exec(&block)
60 | end
61 |
62 | test "ephemeral translations" do
63 | assert_equal "Hello world", EphemeralNotifier.new.t("hello")
64 | end
65 |
66 | test "ephemeral notification translations" do
67 | assert_equal "Hello world", EphemeralNotifier::Notification.new.t("hello")
68 | end
69 | end
70 |
--------------------------------------------------------------------------------