Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Implement Push Notifications #1842

Merged

Conversation

danpe
Copy link
Contributor

@danpe danpe commented Apr 23, 2021

Added support for Push Notifications.according to Client-Server Push Notifications API, resolves #611.

This PR adds a new "Pusher Database" based on the Pusher Model.

Design thoughts 🤔

  1. Should Push Notification API be inside clientapi or in a separate component?
  2. Should Push Rules API be inside clientapi or in a separate component?
  3. What background service we should use to send /notify requests?

What's left to do ❓

  • Implement GET /_matrix/client/r0/pushers
  • Implement POST /_matrix/client/r0/pushers/set
  • Implement GET /_matrix/client/r0/notifications
  • Implement push rules APIs - tracked in Implement Push Rules API #481

Tests 🔧

tests/14account/01change-password.pl:

  • Test 123 Pushers created with a different access token are deleted on password change...
  • Test 124 Pushers created with a the same access token are not deleted on password change...

tests/30rooms/60version_upgrade.pl:

  • Test 290 local user has push rules copied to upgraded room...
  • Test 291 remote user has push rules copied to upgraded room...

tests/61push/01message-pushed.pl:

  • Test 744 Test that a message is pushed...
  • Test 745 Invites are pushed...
  • Test 746 Rooms with names are correctly named in pushes...
  • Test 747 Rooms with canonical alias are correctly named in pushed...
  • Test 748 Rooms with many users are correctly pushed...
  • Test 749 Don't get pushed for rooms you've muted...
  • Test 750 Rejected events are not pushed...

tests/61push/02add_rules.pl:

  • Test 751 Can add global push rule for room...
  • Test 752 Can add global push rule for sender...
  • Test 753 Can add global push rule for content...
  • Test 754 Can add global push rule for override...
  • Test 755 Can add global push rule for underride...
  • Test 756 Can add global push rule for content...
  • Test 757 New rules appear before old rules by default...
  • Test 758 Can add global push rule before an existing rule...
  • Test 759 Can add global push rule after an existing rule...
  • Test 760 Can delete a push rule...
  • Test 761 Can disable a push rule...
  • Test 762 Adding the same push rule twice is idempotent...

tests/61push/03_unread_count.pl:

  • Test 763 Messages that notify from another user increment notification_count...
  • Test 764 Messages that org.matrix.msc2625.mark_unread from another user increment org.matrix.msc2625.unread_count...
  • Test 765 Messages that highlight from another user increment unread highlight count...

tests/61push/05_set_actions.pl:

  • Test 766 Can change the actions of default rules...
  • Test 767 Changing the actions of an unknown default rule fails with 404..
  • Test 768 Can change the actions of a user specified rule...
  • Test 769 Changing the actions of an unknown rule fails with 404...

tests/61push/06_get_pusher.pl:

  • Test 770 Can fetch a user's pushers...

tests/61push/06_push_rules_in_sync.pl:

  • Test 771 Push rules come down in an initial /sync...
  • Test 772 Adding a push rule wakes up an incremental /sync...
  • Test 773 Disabling a push rule wakes up an incremental /sync...
  • Test 774 Enabling a push rule wakes up an incremental /sync...
  • Test 775 Setting actions for a push rule wakes up an incremental /sync...

tests/61push/07_set_enabled.pl:

  • Test 776 Can enable/disable default rules...
  • Test 777 Enabling an unknown default rule fails with 404...

tests/61push/08_rejected_pushers.pl:

  • Test 778 Test that rejected pushers are removed....

tests/61push/09_notifications_api.pl:

  • Test 779 Notifications can be viewed with GET /notifications...

tests/61push/80torture.pl:

  • Test 780 Trying to add push rule with no scope fails with 400...
  • Test 781 Trying to add push rule with invalid scope fails with 400...
  • Test 782 Trying to add push rule with missing template fails with 400...
  • Test 783 Trying to add push rule with missing rule_id fails with 400...
  • Test 784 Trying to add push rule with empty rule_id fails with 400...
  • Test 785 Trying to add push rule with invalid template fails with 400...
  • Test 786 Trying to add push rule with rule_id with slashes fails with 400...
  • Test 787 Trying to add push rule with override rule without conditions fails with 400...
  • Test 788 Trying to add push rule with underride rule without conditions fails with 400...
  • Test 789 Trying to add push rule with condition without kind fails with 400...
  • Test 790 Trying to add push rule with content rule without pattern fails with 400...
  • Test 791 Trying to add push rule with no actions fails with 400...
  • Test 792 Trying to add push rule with invalid action fails with 400...
  • Test 793 Trying to add push rule with invalid attr fails with 400...
  • Test 794 Trying to add push rule with invalid value for enabled fails with 400...
  • Test 795 Trying to get push rules with no trailing slash fails with 400...
  • Test 796 Trying to get push rules with scope without trailing slash fails with 400...
  • Test 797 Trying to get push rules with template without tailing slash fails with 400...
  • Test 798 Trying to get push rules with unknown scope fails with 400...
  • Test 799 Trying to get push rules with unknown template fails with 400...
  • Test 800 Trying to get push rules with unknown attribute fails with 400...
  • Test 801 Trying to get push rules with unknown rule_id fails with 404...

Pull Request Checklist ✅

  • I have added any new tests that need to pass to sytest-whitelist as specified in docs/sytest.md
  • Pull request includes a sign off

Signed-off-by: Dan Peleg <[email protected]>

Copy link
Contributor

@S7evinK S7evinK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things that caught my eye.
Besides that, good job :)

// Edit: Removed the comment about null, we need those, yea. :)

);

-- Pusher IDs must be unique for a given user.
CREATE UNIQUE INDEX IF NOT EXISTS pusher_localpart_id_idx ON pusher_pushers(localpart, pusher_id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pusher_id isn't defined in the schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @S7evinK it was a work in progress, I just pushed the final piece :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I should have waited a bit. :) (was a bit bored, and push was also on my list)
You can mark a PR as "WIP" if you're not done yet, btw. :)

`

const selectPushersByLocalpartSQL = "" +
"SELECT pusher_id, display_name, last_seen_ts, ip, user_agent FROM pusher_pushers WHERE localpart = $1 AND pusher_id != $2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, pusher_id doesn't seem to exist.

Copy link
Member

@kegsay kegsay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good start overall. It looks like some state refactor work has leaked through into this PR @neilalexander - any idea?

It would be nice to try to get this into a mergable state now to avoid having to land a massively scary change all at once.

return jsonerror.InternalServerError()
}
} else if body.Kind == "" {
if targetPusher == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible for this to be hit.

ctx context.Context, session_id int64,
pushkey, kind, appid, appdisplayname, devicedisplayname, profiletag, lang, data, localpart string,
) error {
return d.pushers.insertPusher(ctx, nil, session_id, pushkey, kind, appid, appdisplayname, devicedisplayname, profiletag, lang, data, localpart)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All writes need to use d.writer else it can race with other writes causing database is locked errors on sqlite.

) error {
stmt := sqlutil.TxStmt(txn, s.insertPusherStmt)
_, err := stmt.ExecContext(ctx, localpart, session_id, pushkey, kind, appid, appdisplayname, devicedisplayname, profiletag, lang, data)
logrus.Debugf("🥳 Created pusher %d", session_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whilst the emoji are cute, we don't use them anywhere else. For consistency, please keep to the boring log lines :)

lang TEXT,
data TEXT,

UNIQUE (app_id, pushkey, localpart)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the unique key then I'm surprised to see selecting pushers and updating pushers use WHERE localpart = $1 AND pushkey = $2.

) error {
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
if err := d.pushers.deletePusher(ctx, txn, appid, pushkey, localpart); err != sql.ErrNoRows {
return err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fire if err == nil e.g on success, which is presumably not what you want. It's a bit of a weird way of doing this. I would just have an extra if statement for clarity e.g:

err := d.pushers.deletePusher(ctx, txn, appid, pushkey, localpart)
if err == nil || err == sql.ErrNoRows {
    return nil
}
return err

@kegsay kegsay assigned danpe and unassigned kegsay Jul 12, 2021
@kegsay kegsay added the stale This issue or PR is at risk of being closed without further feedback label Jul 19, 2021
@kegsay
Copy link
Member

kegsay commented Jul 26, 2021

Please re-open when you have brought this back up-to-date.

@kegsay kegsay closed this Jul 26, 2021
@danpe
Copy link
Contributor Author

danpe commented Sep 12, 2021

Hi @kegsay, we've updated the branch but don't have the permission to reopen this pull request.

@kegsay
Copy link
Member

kegsay commented Sep 13, 2021

Please make a new pull request - I can't seem to reopen this as this branch was either force pushed or recreated.

@danpe
Copy link
Contributor Author

danpe commented Sep 13, 2021

According to the solution found in How to reopen a pull-request after a force-push?

We've restored original branch by $ git push -f origin bf92734:implement-push-notifications (SHA of last commit from https://github.com/matrix-org/dendrite/pull/1842/commits).

@kegsay I think you should be able to reopen PR now (SHA from step 1 and 2 are different and we can use just instructions from gist instead of comment).

@PiotrKozimor
Copy link
Contributor

Dendrite in sytest is failing with Corrupt database returned no messages between 174 and 0 because .db file from push server is opened by second instance of homeserver. I will make PR in sytest with necessary configuration.

@kegsay kegsay removed the stale This issue or PR is at risk of being closed without further feedback label Sep 16, 2021
@PiotrKozimor
Copy link
Contributor

I am almost there :D I have two failing test in sytest - one is this flakey Local device key changes appear in /keys/changes and the other is Inviting an AS-hosted user asks the AS server which I have already encountered: #1970 (comment). I also forgot to clean Pusher database. I will commit it soon.

@tommie
Copy link
Contributor

tommie commented Sep 24, 2021

Thanks for working on this! I'm switching from XMPP to Matrix and thought I'd go for Dendrite.

Once this is in, I'm guessing this would work with Sygnal. Is the longterm plan to also port Sygnal/Push gateway to Dendrite/Go?

@tommie
Copy link
Contributor

tommie commented Oct 2, 2021

I was playing around with this and managed to get push notifications working between Element Webs. Element Android background processing still doesn't work, but it does show notifications when the app is active, so that might be on the phone side.

Steps I had to take

  1. Apply tommie@abce199 to set some sane push rules, taken from an Element Web account against matrix.org.
  2. Add the push_server config entry.
  3. Create the dendrite_pushserver database.
  4. Copy the template from userapi/storage/accounts/common/pushrules.go and set user ID, or the /pushrules endpoint.
  5. Update the accounts database to have the push rules end up in /sync:
    • \set c `cat pushrules.json`
    • UPDATE account_data SET content = :'c' WHERE localpart = '<user>' AND type = 'm.push_rules';
  6. Reload Element Web. Element Android caches quite a lot. I think it's enough to tell it to clear the cache, but you might have to sign out/in.

I'd be interested if anyone might know why Element Android isn't receiving the background push events. The phone is a stock Pixel 5.

Edit I just realized that the actual pushing isn't implemented in this PR yet (the /notify task). Element Web probably works because it has a persistent connection, and all it required was non-empty pushrules.

@PiotrKozimor
Copy link
Contributor

@tommie you are right, here we are getting events from roomserver and we need to parse them according to push rules and sent to Push Gateway.

Sygnal is implementation of Push Gateway API and it will work just fine with Dendrite. I haven't looked for Go implementations though.

@tommie
Copy link
Contributor

tommie commented Oct 4, 2021

@PiotrKozimor thanks for the confirmation.

I'd love to see this, as I think phone notifications would be the only thing stopping me from replacing XMPP.

I'm currently waiting for review on #2014 (the SSO support). Is there anything I can help with on this PR while that's in queue?

@PiotrKozimor
Copy link
Contributor

@tommie your help would be more than appreciated. I think we should focus on implementing notifications with default rules -
that would mean skipping Push Rules API for now. Most of the work would be around code that I linked - fetching (and possibly caching) room membership, and sending requests to Push Server according to default rules. I have some uncommited work around these rules. I will commit it soon.

@tommie
Copy link
Contributor

tommie commented Oct 6, 2021

I think we should focus on implementing notifications with default rules -
that would mean skipping Push Rules API for now.
We still need some Push Rules API, otherwise Element clients refuse to do any notifications at all. You can't access the notification settings, even to change per-device settings.

See tommie@abce199, it implements the GET /pushrules-side. Though the rules I'm using there are not minimal. Just copy-pasted.

Element Android even requires a specific rule ID: .m.rule.master

However, they seem happy even without the PUT endpoints. Notably, we need the rules both in /sync and /pushrules. So I agree we can use a slim hard-coded default, but it needs to be available to clients. Not sure what to do with the empty push rules that are already written to Accounts DB. Perhaps it's best to simply ignore them, but change the current default for new accounts. Another approach is to do a string comparison and also UPDATE old accounts, e.g. on startup or login.

fetching (and possibly caching) room membership, and sending requests to Push Server according to default rules. I have some uncommited work around these rules. I will commit it soon.

To summarize the current holes:

  • Push Rules GET support for /pushrules and /sync.
  • Fetching room memberships to filter which accounts to involve.
  • Fetching account push rules to figure out which devices to involve.
  • Sytest compliance.

Let me know if I missed some area. What are you planning on working on (if anything) in the near term? I'm new to Matrix, but am comfortable with Go and distributed systems in general. I don't see any problem with me working on any of these, but there's obviously some dependencies, and I don't know how big the room membership and push rules evaluation tasks are.

@PiotrKozimor
Copy link
Contributor

I will get back to notifications in 3-4 weeks - that's the current plan. It would be awesome if you could work on it in the meantime.

  • I think pushrules should be stored in pushserver component. Meaning that that GET /pushrules could make use of new method from PushserverInternalAPI (which would return default rules for now).
  • The same method could be used in initial /sync request (I am not familiar though with syncapi component).
  • Push rules are not stored yet in Accounts DB, so I don't see a problem here :)
  • Here is the documentation for predefined rules - I (a.k.a. default rules).

We also have another big hole - do we want to push notification for user that are already syncing? If yes, then we need presence module - #1879. Documentation is not very informative on this topic, maybe we need to check synapse?

@tommie
Copy link
Contributor

tommie commented Oct 7, 2021

Push rules are not stored yet in Accounts DB, so I don't see a problem here :)

They are, though. As "account data", which is read by Element, see link in my previous comment. That JSON ends up in /sync. I don't know if push rules in account data is legacy, or if they're supposed to be synchronized, but I guess I'll have to figure that out.

I will get back to notifications in 3-4 weeks.

Ok, then it sounds like I should just continue on anything you mentioned for now. :)

@tommie
Copy link
Contributor

tommie commented Oct 10, 2021

I got a first notification on Elements Android with tommie@4ff4502.

  • Email pushers are ignored.
  • I added a basic unit test for the room server consumer.
  • Push rules are simply "notify for everything". (And because of this, there are no "tweaks" sent.)
  • There's excessive info logging.

The next step is push rules evaluation.

@tommie
Copy link
Contributor

tommie commented Oct 10, 2021

Initial pushrules evaluator in tommie@e741b84.

  • It hardcodes the default rules from the spec.
  • No tests, but it runs on my private server for now.
  • I have no idea what to do about action coalesce, but support for it is optional.
  • Found some ambiguities in the spec, marked as TODOs.

The next step should be unit tests for the pushrules package. After that would be user-provided pushrules. Will pick that up later.

@neilalexander
Copy link
Contributor

OK, I've made quite a few significant changes here, the biggest being that there's no longer a push server — the new code is now folded into the User API. This fits in with our new plans for component scaling. I've also fixed various bugs, like with the JetStream consumers, the SQL table creation and detecting when the user doesn't have existing push rules.

I'm running this on dendrite.neilalexander.dev and it's working rather nicely. There are still a couple failing tests to figure out but then I think we're on the home stretch of getting this reviewed & merged.

Copy link
Contributor

@genofire genofire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small and unimportend bugs - should be solved before merge

build/docker/config/dendrite.yaml Show resolved Hide resolved
build/docker/postgres/create_db.sh Outdated Show resolved Hide resolved
@neilalexander neilalexander merged commit f05ce47 into matrix-org:main Mar 3, 2022
@tommie
Copy link
Contributor

tommie commented Mar 3, 2022

Awesome. Thanks!

@genofire
Copy link
Contributor

genofire commented Mar 3, 2022

Could not wait for the next release ;)

@genofire
Copy link
Contributor

genofire commented Mar 3, 2022

Hmm in main the sytest failed, now:

tests/61push/09_notifications_api.pl:
    Test 757 Notifications can be viewed with GET /notifications... FAIL

@cmuench
Copy link

cmuench commented Mar 3, 2022

Wow. That's the most awaited PR.
👍

@PiotrKozimor
Copy link
Contributor

Hi everyone, https://github.com/globekeeper/dendrite/commits/main (v0.8.1 with 2 modifications of mine) runs on dendrite.stg.globekeeper.com in polylith mode with clustered nats and CockroachDB as a database. Feel free to try it out!

@mholt
Copy link
Contributor

mholt commented Jul 19, 2022

Does this mean we should be getting push notifications on mobile now (Element Android)? If we're not, should I file a bug report?

@genofire
Copy link
Contributor

Yes you should be able. Beside Element, there is hydrogen from your web browser with WebPush Notification (both works well on my dendrite)

Element Android I use together with my own https://ntfy.sh instance to receive the notification over UnifiedPush instatt of FCM (over google's server).

Yes you should create issues / bug reports, if it does not work

@S7evinK
Copy link
Contributor

S7evinK commented Jul 20, 2022

Does this mean we should be getting push notifications on mobile now (Element Android)? If we're not, should I file a bug report?

If you used the create-account utility, this is a known issue, where the account data isn't correctly propagated to the SyncAPI. There is #2484 to fix this.

@mholt
Copy link
Contributor

mholt commented Jul 20, 2022

@S7evinK Ah, I did use that! I will track that PR.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement push notifications
9 participants