Replace Pusher Beams with Rpush PN delivery ✂️ ✂️ (#13588)

* Remove feature flag

* Remove Pusher Beams implementation

* Remove Pusher Beams require from initializer

* Adds consumer apps to sidebar + consider :admin_restructure

* Fix routes + PN target URL

* Remove old file that sneaked in

* Adds docs

* docs tweaks

* Apply suggestions from code review

Co-authored-by: rhymes <rhymes@hey.com>

Co-authored-by: rhymes <rhymes@hey.com>
This commit is contained in:
Fernando Valverde 2021-05-05 08:37:20 -06:00 committed by GitHub
parent 355d510f88
commit 121603a44b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 46 additions and 214 deletions

View file

@ -120,8 +120,6 @@ PUSHER_APP_ID=
PUSHER_CLUSTER=
PUSHER_KEY=
PUSHER_SECRET=
PUSHER_BEAMS_ID=
PUSHER_BEAMS_KEY=
# Google recaptcha (Optional)
# (https://developers.google.com/recaptcha/intro)

View file

@ -70,7 +70,6 @@ gem "pg_search", "~> 2.3.5" # PgSearch builds Active Record named scopes that ta
gem "puma", "~> 5.2.2" # Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server
gem "pundit", "~> 2.1" # Object oriented authorization for Rails applications
gem "pusher", "~> 2.0" # Ruby library for Pusher Channels HTTP API
gem "pusher-push-notifications", "~> 2.0" # Pusher Push Notifications Ruby server SDK
gem "rack-attack", "~> 6.5.0" # Used to throttle requests to prevent brute force attacks
gem "rack-cors", "~> 1.1" # Middleware that will make Rack-based apps CORS compatible
gem "rack-timeout", "~> 0.6" # Rack middleware which aborts requests that have been running for longer than a specified timeout

View file

@ -526,9 +526,6 @@ GEM
httpclient (~> 2.8)
multi_json (~> 1.15)
pusher-signature (~> 0.1.8)
pusher-push-notifications (2.0.1)
jwt (~> 2.1, >= 2.1.0)
rest-client (~> 2.0, >= 2.0.2)
pusher-signature (0.1.8)
raabro (1.4.0)
racc (1.5.2)
@ -933,7 +930,6 @@ DEPENDENCIES
pundit (~> 2.1)
pundit-matchers (~> 1.6)
pusher (~> 2.0)
pusher-push-notifications (~> 2.0)
rack-attack (~> 6.5.0)
rack-cors (~> 1.1)
rack-host-redirect (~> 1.3)

View file

@ -54,6 +54,7 @@ class AdminMenu
scope :apps, "palette-line", [
item(name: "chat channels"),
item(name: "consumer apps", controller: "consumer_apps"),
item(name: "events"),
item(name: "listings"),
item(name: "welcome"),

View file

@ -34,27 +34,17 @@ module Notifications
)
end
# Send PNs using Rpush - respecting users' notificaton delivery settings
targets = User.where(id: user_ids, mobile_comment_notifications: true).ids
# Pusher Beams uses named Pub/Sub channels instead of raw user_ids
target_channels = targets.map { |id| "user-notifications-#{id}" }
# Sends the push notification to Pusher Beams channels.
# Batch is in place to respect Pusher 100 channel limit.
target_channels.each_slice(100) { |batch| send_push_notifications(batch) }
if FeatureFlag.enabled?(:mobile_notifications)
# Send PNs using Rpush
url_path = Rails.application.routes.url_helpers.notifications_path(:comments)
PushNotifications::Send.call(
user_ids: targets,
title: "@#{comment.user.username}",
body: "Re: #{comment.parent_or_root_article.title.strip}",
payload: {
url: URL.url(url_path),
type: "new comment"
},
)
end
PushNotifications::Send.call(
user_ids: targets,
title: "@#{comment.user.username}",
body: "Re: #{comment.parent_or_root_article.title.strip}",
payload: {
url: URL.url(comment.path),
type: "new comment"
},
)
return unless comment.commentable.organization_id
@ -97,40 +87,6 @@ module Notifications
user_ids_for("only_author_comments")
end
def send_push_notifications(channels)
return unless ApplicationConfig["PUSHER_BEAMS_KEY"] && ApplicationConfig["PUSHER_BEAMS_KEY"].size == 64
Pusher::PushNotifications.publish_to_interests(
interests: channels,
payload: push_notification_payload,
)
end
def push_notification_payload
title = "@#{comment.user.username}"
subtitle = "re: #{comment.parent_or_root_article.title.strip}"
data_payload = { url: URL.url("/notifications/comments") }
{
apns: {
aps: {
alert: {
title: title,
subtitle: subtitle,
body: CGI.unescapeHTML(comment.title.strip)
}
},
data: data_payload
},
fcm: {
notification: {
title: title,
body: subtitle
},
data: data_payload
}
}
end
end
end
end

View file

@ -12,8 +12,6 @@ module PushNotifications
end
def call
return unless FeatureFlag.enabled?(:mobile_notifications)
Device.where(user_id: @user_ids).find_each do |device|
device.create_notification(@title, @body, @payload)
end

View file

@ -4,7 +4,10 @@
<h1 class="crayons-title mb-6">Consumer Apps</h1>
<p>
The following are apps that will be able to receive Push Notifications from <%= SiteConfig.community_name %>.
Consumer apps are standalone mobile apps that users can use to browse <%= SiteConfig.community_name %> as an alternative to web browsers.
</p>
<p class="mt-3">
If the app is listed as <strong>operational</strong>, it means <%= SiteConfig.community_name %> will be able to deliver push notifications directly to users' devices.
</p>
</div>

View file

@ -1,5 +1,3 @@
require "pusher/push_notifications"
if ApplicationConfig["PUSHER_APP_ID"].present?
Pusher.app_id = ApplicationConfig["PUSHER_APP_ID"]
Pusher.key = ApplicationConfig["PUSHER_KEY"]
@ -7,9 +5,4 @@ if ApplicationConfig["PUSHER_APP_ID"].present?
Pusher.cluster = ApplicationConfig["PUSHER_CLUSTER"]
Pusher.logger = Rails.logger
Pusher.encrypted = true
Pusher::PushNotifications.configure do |config|
config.instance_id = ApplicationConfig["PUSHER_BEAMS_ID"]
config.secret_key = ApplicationConfig["PUSHER_BEAMS_KEY"]
end
end

View file

@ -134,10 +134,8 @@ Rails.application.routes.draw do
end
resources :comment_mutes, only: %i[update]
resources :users, only: %i[index], defaults: { format: :json } do # internal API
constraints(-> { FeatureFlag.enabled?(:mobile_notifications) }) do
collection do
resources :devices, only: %i[create destroy]
end
collection do
resources :devices, only: %i[create destroy]
end
end
resources :users, only: %i[update]

View file

@ -14,7 +14,6 @@ namespace :admin do
resources :organization_memberships, only: %i[update destroy create]
resources :permissions, only: %i[index]
resources :reactions, only: [:update]
resources :consumer_apps, only: %i[index new create edit update destroy]
namespace :settings do
resources :authentications, only: [:create]
resources :campaigns, only: [:create]
@ -123,6 +122,7 @@ namespace :admin do
delete :remove_user
end
end
resources :consumer_apps, only: %i[index new create edit update destroy]
resources :events, only: %i[index create update new edit]
resources :listings, only: %i[index edit update destroy]
resources :listing_categories, only: %i[index edit update new create

View file

@ -0,0 +1,27 @@
---
title: Push Notifications
---
# Push Notifications Delivery
Forem instances rely on [Rpush](https://github.com/rpush/rpush) to deliver push
notifications. This decision was heavily influenced by the desire to
provide as much flexibility as possible to Creators. In order to do this, Forem
instances can register and configure a `ConsumerApp`.
These consumer apps represent mobile applications that users use to browse and consume content
on a Forem. Authenticated users of a specific Forem instance can register a `Device`
associated to a `ConsumerApp`. With these pieces we are able to deliver
push notifications to users devices.
The `ConsumerApp` configuration page can be found at
`/admin/apps/consumer_apps`. The official Forem apps are supported by default
and require their secret credential to be provided via ENV variable.
## Rpush Implementation
We use Rpush's `rpush-redis` implementation (read
[this thread](https://github.com/forem/forem/pull/12419/files#r564660917) for
the reasons why), hence all `Rpush` models are persisted in Redis. More
details about how this works
[here](https://github.com/rpush/rpush/wiki/Using-Redis).

View file

@ -40,68 +40,3 @@ you'll need to provide those keys to the Rails application.
![pusher-3](https://user-images.githubusercontent.com/22895284/51086058-0626d780-1742-11e9-9c2a-26b9b10fa77f.png)
5. Done.
## Mobile Push Notifications
These steps are required only when working with the native Apps. In order to
setup Push Notifications to mobile devices you need to create a Pusher Beams
instance and retrieve its credentials
1. [Sign up](https://dashboard.pusher.com/accounts/sign_up) or
[sign in](https://dashboard.pusher.com/) to your Pusher account.
2. Once signed in, go in the "BEAMS" section on the left sidebar and fill in the
prompt to create a new Pusher Beams instance.
![pusher-beams-1](https://user-images.githubusercontent.com/6045239/78602158-2e9d6480-7813-11ea-9da1-4f1310776570.png)
3. In your new Pusher Beams instance, click the "Credentials" tab.
![pusher-beams-2](https://user-images.githubusercontent.com/6045239/78602934-82f51400-7814-11ea-8cc8-6933c43079bc.png)
4. Change your keys accordingly (name of Pusher key -> name of our application
key):
```text
Instance ID -> PUSHER_BEAMS_ID
Instance Key -> PUSHER_BEAMS_KEY
```
5. Done. You now have your server configured to use Pusher Beams.
However, in order to send Push Notifications to devices you'll need to do some
platform specific configuration as well.
### Firebase for Android Push Notifications
1. Sign up or Sign in to [Firebase](https://firebase.google.com/)
2. Add a new project. Google Analytics doesn't need to be configured for this
Push Notifications to work.
![fcm-1](https://user-images.githubusercontent.com/6045239/78604287-cfd9ea00-7816-11ea-9b47-b374b4e60faf.png)
![fcm-2](https://user-images.githubusercontent.com/6045239/78609833-d2d9d800-7820-11ea-9d78-53dfe823bd3f.png)
3. Add Android support for your iOS app
![fcm-3](https://user-images.githubusercontent.com/6045239/78609955-1b919100-7821-11ea-8a2f-9f96cf26185c.png)
![fcm-4](https://user-images.githubusercontent.com/6045239/78612418-885b5a00-7826-11ea-91b8-125981367068.png)
4. Download the `google-services.json` file (to be used in the Android app)
![fcm-5](https://user-images.githubusercontent.com/6045239/78612412-85f90000-7826-11ea-8996-dcb568e7b037.png)
5. Finish off the next steps (not required at this moment). Now head over to
your Firebase project's settings page and copy the Server key from the "Cloud
Messaging" tab
![fcm-6](https://user-images.githubusercontent.com/6045239/78615948-11c35a00-7830-11ea-8470-675488a0ecc1.png)
6. Now back in your Pusher Beams page account add the Server key from the
previous step under the "Settings" tab
![fcm-7](https://user-images.githubusercontent.com/6045239/78616350-353ad480-7831-11ea-8d63-2f183c2f233b.png)
7. Done.

View file

@ -19,6 +19,7 @@ items:
- notification.md
- scheduled-jobs.md
- metrics.md
- push-notifications.md
- tracking.md
- service-objects.md
---

View file

@ -5,7 +5,6 @@ RSpec.describe "Devices", type: :request do
let(:consumer_app) { create(:consumer_app) }
before do
allow(FeatureFlag).to receive(:enabled?).with(:mobile_notifications).and_return(true)
sign_in user
end

View file

@ -1,42 +0,0 @@
require "rails_helper"
RSpec.describe "User devices routes", type: :routing do
it "renders the user devices routes if the mobile_notifications feature flag is enabled", :aggregate_failures do
allow(FeatureFlag).to receive(:enabled?).twice.with(:mobile_notifications).and_return(true)
expect(post: devices_path).to route_to(
controller: "devices",
action: "create",
format: :json,
locale: nil,
)
expect(delete: "#{devices_path}/1").to route_to(
controller: "devices",
action: "destroy",
id: "1",
format: :json,
locale: nil,
)
end
it "does not render the user devices routes if the mobile_notifications feature flag is disabled",
:aggregate_failures do
allow(FeatureFlag).to receive(:enabled?).at_least(:twice).with(:mobile_notifications).and_return(false)
expect(post: devices_path).not_to route_to(
controller: "devices",
action: "create",
format: :json,
locale: nil,
)
expect(delete: "#{devices_path}/1").not_to route_to(
controller: "devices",
action: "destroy",
id: "1",
format: :json,
locale: nil,
)
end
end

View file

@ -97,22 +97,4 @@ RSpec.describe Notifications::NewComment::Send, type: :service do
expect(Notification.where(notifiable_type: "Comment", notifiable_id: child_comment.id,
organization_id: organization.id)).to be_any
end
it "sends Push Notifications using Pusher Beams when configured" do
allow(ApplicationConfig).to receive(:[]).with("PUSHER_BEAMS_KEY").and_return("x" * 64)
allow(ApplicationConfig).to receive(:[]).with("APP_PROTOCOL").and_return("http://")
allow(ApplicationConfig).to receive(:[]).with("APP_DOMAIN").and_return("localhost:3000")
allow(Pusher::PushNotifications).to receive(:publish_to_interests)
comment_sent = child_comment
described_class.call(comment_sent)
channels = ["user-notifications-#{user2.id}", "user-notifications-#{user.id}"]
payload = described_class.new(comment_sent).__send__(:push_notification_payload)
expect(Pusher::PushNotifications).to have_received(:publish_to_interests) do |block|
expect(block[:interests]).to match_array(channels)
expect(block[:payload]).to eq(payload)
end
end
end

View file

@ -20,18 +20,8 @@ RSpec.describe PushNotifications::Send, type: :service do
}
end
context "with feature flag set to false/off" do
before { allow(FeatureFlag).to receive(:enabled?).with(:mobile_notifications).and_return(false) }
it "does nothing if the feature flag is disabled" do
expect { described_class.call(**params) }
.not_to change { Rpush::Client::Redis::Notification.all.count }
end
end
context "with no devices for user" do
before do
allow(FeatureFlag).to receive(:enabled?).with(:mobile_notifications).and_return(true)
user.devices.delete
end
@ -44,7 +34,6 @@ RSpec.describe PushNotifications::Send, type: :service do
context "with devices for one user" do
before do
allow(FeatureFlag).to receive(:enabled?).with(:mobile_notifications).and_return(true)
allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return("dGVzdGluZw==")
allow(ApplicationConfig).to receive(:[]).with("COMMUNITY_NAME").and_return("Forem")
create(:device, user: user)
@ -67,7 +56,6 @@ RSpec.describe PushNotifications::Send, type: :service do
context "with devices for multiple users" do
before do
allow(FeatureFlag).to receive(:enabled?).with(:mobile_notifications).and_return(true)
allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return("dGVzdGluZw==")
allow(ApplicationConfig).to receive(:[]).with("COMMUNITY_NAME").and_return("Forem")
create(:device, user: user)

Binary file not shown.