-
-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Web Push Notifications #3243
Web Push Notifications #3243
Conversation
I was just about to ask how you managed to get Chrome to do push notifications without a GCM/FCM ID, but it looks like they've removed that requirement, which is going to make our lives a lot easier. Hooray! 🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some questions and found something suspicious, so please review them. I'm looking forward this feature.
current_account.web_push_subscriptions << web_subscription | ||
current_account.save! | ||
|
||
render json: web_subscription.as_payload, status: 200 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it need explicit status?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think so.
app/javascript/mastodon/web_push.js
Outdated
|
||
try { | ||
getRegistration() | ||
.then(getPushSubscription()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you mean getPushSubscription
instead of getPushSubscription()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
db/schema.rb
Outdated
@@ -358,6 +358,16 @@ | |||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree | |||
end | |||
|
|||
create_table "web_push_subscriptions", force: :cascade do |t| | |||
t.integer "account_id" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not index?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was generated by the following command, so I cannot answer that 😄 How should it look like?
rails generate model WebPushSubscription account_id:integer endpoint:string:unique key_p256dh:string key_auth:string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please look at other tables. Here is an example:
t.index ["account_id"], name: "index_reports_on_account_id", using: :btree
@@ -0,0 +1,20 @@ | |||
# frozen_string_literal: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec?
@@ -1,20 +1,49 @@ | |||
# frozen_string_literal: true | |||
|
|||
class Api::Web::SettingsController < ApiController | |||
include ActionView::Helpers::TranslationHelper | |||
|
|||
respond_to :json | |||
|
|||
before_action :require_user! | |||
|
|||
def update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec this, too.
private | ||
|
||
def web_notification | ||
@_web_push_notification ||= WebPushNotifications.where(user: current_user).first_or_initialize(user: current_user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where WebPushNotifications
comes from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby magic 😄 ? Guess it is the database model?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ruby can sometimes do magic, but it should be in schema.rb
anyway if it is a database model.
@@ -1,20 +1,49 @@ | |||
# frozen_string_literal: true | |||
|
|||
class Api::Web::SettingsController < ApiController | |||
include ActionView::Helpers::TranslationHelper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is it for?
Will try to see tomorrow how Rails tests work and maybe add some specs if I can figure things out. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found an error with CI.
@@ -1,20 +1,47 @@ | |||
# frozen_string_literal: true | |||
|
|||
class Api::Web::SettingsController < ApiController |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes the spec fail.
If this spec gets traction we might even be able to reply from the notification. |
33 files change - I'm already scared :D Is this ready for review? |
I still haven't figured out how to attach the subscription to the session, so that when the user logs out the subscription gets deleted. Also need to pass the VAPID keys via config files / environment (right now they are generated when the server starts). |
@akihikodaki I explored a bit our options for clearing the subscriptions. We have the following in # Reuse access token for the same resource owner within an application (disabled by default)
# Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
reuse_access_token What I understand from this is that we do not keep track of each user session (we use one token for all sessions), so there is no way to associate a push subscription with the session that created it. What is the reason for this? What would be the implications of issuing a new token for each login and deleting the token when the user logs out (attaching the push subscription to this token and deleting them together). Another approach would be to manually delete the push subscription from the front-end when the user logs out, but that would create a huge security / privacy issue. Don't really know how to proceed with this. |
The web UI uses the API like any other app would use the API, so it needs an access token. Reusing of the access token was implemented in response to #1681 |
Could you tell me about your security concern? |
Result: the user is logged out, but the browser keeps getting push notifications for that account. Also someone can mess with the code and skip the request to delete the subscription. Signing out and deleting the subscription should be an atomic operation, handled by the back-end. Ideally, there would be another model ( |
Even now we have |
before do | ||
sign_in(user) | ||
|
||
ActiveSession.deactivate warden.raw_session['auth_id'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests fail because these two lines need to be executed after each call to sign_in
(for all tests). Tried putting the following into spec_helper.rb
but didn't help. Any pointers?
Devise.setup do |config|
Warden::Manager.after_set_user except: :fetch do |user, auth, opts|
ActiveSession.deactivate warden.raw_session['auth_id']
warden.raw_session['auth_id'] = user.activate_session
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is because Devise::Test::ControllerHelpers
does not support configuration by Devise.setup
.
https://github.com/plataformatec/devise/blob/9fe7040db90be1acb499d2534b077266dbf3b209/lib/devise/test/controller_helpers.rb#L63
Devise::Test::ControllerHelpers
is included in rails_helper
. You may hook sign_in
there. (it's is a bit hacky, but it cannot be helped, I think.)
https://github.com/tootsuite/mastodon/blob/fd66f7cdc0931a67a2e24d887fb79f44fc42e2bc/spec/rails_helper.rb#L26
I suggest extracting Devise modifications into a separate PR to make reviewing easier |
before do | ||
sign_in(user) | ||
|
||
ActiveSession.deactivate warden.raw_session['auth_id'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is because Devise::Test::ControllerHelpers
does not support configuration by Devise.setup
.
https://github.com/plataformatec/devise/blob/9fe7040db90be1acb499d2534b077266dbf3b209/lib/devise/test/controller_helpers.rb#L63
Devise::Test::ControllerHelpers
is included in rails_helper
. You may hook sign_in
there. (it's is a bit hacky, but it cannot be helped, I think.)
https://github.com/tootsuite/mastodon/blob/fd66f7cdc0931a67a2e24d887fb79f44fc42e2bc/spec/rails_helper.rb#L26
Will continue work once #3616 is merged. |
config/routes.rb
Outdated
@@ -206,6 +206,12 @@ | |||
|
|||
namespace :web do | |||
resource :settings, only: [:update] | |||
resource :push_subscriptions, only: [:create] do | |||
# Why doesn't PUT /api/web/push_subscriptions/:id work? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Gargron Do you have some insights into this? If I call axios.put('/api/web/push_subscriptions/2'
I get this in the logs:
Started PUT "/api/web/push_subscriptions/2" for 127.0.0.1 at 2017-06-24 16:34:57 +0200
Processing by ApplicationController#raise_not_found as HTML
axios.put('/api/web/push_subscriptions')
works fine though.
Cannot seem to figure it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"resource" means there's no index, show with no ID (e.g. there is always one item)
"resources" is what gives you both index and show with ID
I think this is ready for review. The service worker is configured not to cache anything. The For testing, after you see the "Subscription registered" notification, go to the settings notifications and:
This will make it less confusing which type of notifications you are receiving. Would be nice if you could take a look at Chrome and Firefox on Android. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I, Beatrix Bitrot, being of sound mind and of my own volition APPROVE THESE CHANGES
@sorin-davidoi it's hard to see how user-side logging is really going to help in this case (it's difficult enough to get users to tell us when something goes wrong, much less get them to have the dev console open at the right time) but if you think it's important to keep it, I defer to your judgement |
ah, about the console log statements? i'm indifferent for most users i don't think they be any use but on bleeding edge instances they may come in handy maybe leave them in master for a bit and remove before the next tagged release? idk another option is to leave them in here, then make another PR that removes them so that if anyone needs to enable them for testing all they need to do is revert the commit that removes them idk |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added many comments, but they are trivial and the logic itself looks fine. I hope they will be addressed soon and the change will be merged.
end | ||
|
||
web_subscription = ::Web::PushSubscription.new( | ||
endpoint: params[:data][:endpoint], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not params.require
?
active_session.save! | ||
end | ||
|
||
web_subscription = ::Web::PushSubscription.new( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use create!
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, does not seem to work, as this is called from an endpoint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the problem with calling create from an endpoint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, think I misunderstood the comment. I took it as to change the name of the method from create
to create!
. Will update it after I fix the conflicts 🙄
|
||
web_subscription.save! | ||
|
||
active_session.web_push_subscription = web_subscription |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use update!
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as create!
.
if (action.todo === 'expand') { | ||
return expandNotification(event.notification); | ||
} else if (action.todo === 'request') { | ||
return makeRequest(event.notification, action) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following statement would be enough. A promise as resolved value will be evaluated and expanded.
resolve(makeRequest(event.notification, action)
.then(() => removeActionFromNotification(event.notification, action)));
.catch(reject); | ||
} | ||
|
||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returned values does not really matter for promises.
} | ||
}); | ||
} else { | ||
// eslint-disable-next-line no-console |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are aware of, but here I note you should be consistent, whether you use eslint-disable-next-line
or not. If you use, apply for all other console
. If you do not, remove this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will remove the other logs.
@@ -0,0 +1,12 @@ | |||
class CreateWebPushSubscriptions < ActiveRecord::Migration[5.1] | |||
def change | |||
create_table :web_push_subscriptions do |t| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the following columns cannot not be null, can they?
app/services/notify_service.rb
Outdated
rescue Webpush::PayloadTooLarge, Webpush::TooManyRequests => e | ||
Rails.logger.error(e) | ||
rescue Webpush::Error => e | ||
# Failing to send push notifications should not result in Internal Server Error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Failing to send push notifications should not result in Internal Server Error
Yes, but that is controllers' business. If it is just to eliminate Internal Server Error, rescue in controllers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really sure where to move the rescues then (this is called from a ton of places, all of them services - should each of them duplicate the rescues?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sending notifications synchronously? that seems really bad.
If it's from sidekiq, then we can just let the error propagate so we retry the job.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I don't really know - don't have a strong grasp on the whole sidekiq thing.
describe Api::Web::PushSubscriptionsController do | ||
render_views | ||
|
||
let!(:user) { Fabricate(:user) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think let
is fine for these cases.
@@ -0,0 +1,6 @@ | |||
Fabricator(:web_push_subscription) do | |||
account_id 1 | |||
endpoint "MyString" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Faker gem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All problems I have requested are addressed, but I found a few other suspicious things.
config/webpack/production.js
Outdated
cacheName: 'mastodon', | ||
output: '../sw.js', | ||
publicPath: '/sw.js', | ||
minify: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you minify?
@@ -0,0 +1,6 @@ | |||
Fabricator(:web_push_subscription) do | |||
account_id 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Web::PushSubscription
does not have account_id
column.
Seems to be addressed. There are some code style issues (from a human PoV, not code climate) that I intend to refactor in the future, but I think overall this is fine
For things that were left unaddressed here see #4200. |
This PR introduces the ability to send Web Push Notifications. The basic idea is to save a push subscription to the backend, which can then be used to wake up the Service Worker and display a notification when something happened, even if Mastodon is not opened. Together with #3052 this can make Mastodon pretty much indistinguishable from a native application (at least on Android).
Features to implement:
applicationServerKey
SettingsController
- I could not figure out how to create a new endpoint for this, so I've hijacked this controller for adding and updating subscriptions.Browser support: Chrome and Firefox. Cannot be polyfilled.
Closes #1122, #1039.