ergochat / ergo

A modern IRC server (daemon/ircd) written in Go.
https://ergo.chat/
MIT License
2.25k stars 177 forks source link

Push notifications #1527

Open DanielOaks opened 3 years ago

DanielOaks commented 3 years ago

Let's do some weird interesting stuff and see if we can get this working in any reasonable way at all, whoo.

The only prior art I know of for this is Palaver's iOS push notif capability: https://github.com/cocodelabs/palaver-irc-capability/blob/master/Specification.md

What are our requirements for something like this, and do we have any clue what it'd look like?

Rough requirements:

In the v3 channel, sling mentioned something about push notif clearing. How can we do that nicely, and can we just clone how the palaver cap does it? If so, mad.

Another rough requirement:

So, what does this look like in our heads?

DanielOaks commented 3 years ago

tbh since palaver has a working implementation of this, cloning theirs as much as possible (at least for the ios side of things) is a very sensible way to go about this. maybe ask if they've had any issues with their system so far / any requests / anything that they would like to change?

(optimally, we'd just be able to provide a simple forward of the PALAVER command/cap to our system as well if our impl matches their one fairly closely)

DanielOaks commented 3 years ago

+1 rough requirement, we really want to encrypt message contents before sending 'em through the app's infrastructure, if the app does need to send it through to the platform. I remember the convo about where you could do that was messy and all over the place back when we discussed it in v3 a few years back, but if we can get some concrete docs saying yes/no on whether apps can process incoming push notif contents (and decrypt them) before displaying then mad.

encrypted with some key the app/client knows but hopefully the app infra that sends the push notification to <platform> doesn't know would be grand. https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension

DanielOaks commented 3 years ago

So, current differences between palaver spec and what we want to do:

  1. Add Android notifs. This'll be a case of probably seeing what kind of information android notifications require, and adjusting the 'contact blob' to suite. Though, it feels like that won't be much at all. Or, since the HTTP endpoint that we're going to be reaching out to (to send the notif through to $platform's services) is provided by the client, maybe they could just provide us with some opaque blob of 'credentials' that we feed back to the http endpoint as-is, so the server doesn't even know what the credentials are? (and so that this spec can keep working no matter what weird extra requirements or changes the platforms do to their push notification endpoints). honestly it feels sorta like the palaver spec already accounts for this, so... probably no change needed here?
  2. Don't send raw message through the provided http endpoint. Either:
    • We send a message ID, and expect the client to reconnect+CHATHISTORY to get that message once they receive the push notification, or:
    • We encrypt the message and send it encrypted, probably returning the encryption key/info/etc to the client so that it can decode the push notification content locally (and all the client's http endpoint sees is an encrypted blob).

On (2), kylef says this: "from a background device/battery life/network usage perspective, going down the route that the notification already has everything it needs (in some form, whether thats encrypted or not) is perhaps going to be the easiest". We seem to agree with that, and the restrictive OS decisions (how much apps can run in the background for, how restricted you are in terms of post-processing, etcetc) really seem to push the 'encrypt and decrypt' way over the 'ping and pull' way.

So this is likely going to become a case of going through and deciding exactly which encryption/decryption method to use, how to share the secret between client+server, how often to renegotiate the secret, etcetc.

Mikaela commented 3 years ago

Any other mobile OSes do push notifs?

Ubuntu Mobile appears to do them https://phone.docs.ubuntu.com/en/platform/guides/push-notifications-server-guide

SailfishOS seems to not have them https://forum.sailfishos.org/t/what-apis-are-missing-from-harbour/4017/22

slingamn commented 3 years ago

Android web push encryption:

  1. High level description: https://developers.google.com/web/updates/2016/03/web-push-encryption
  2. Spec: https://tools.ietf.org/html/draft-ietf-webpush-encryption-09

This is maybe a little overcomplicated but it has some nice properties, in particular, partial forward secrecy: the server has no long-term key material that can be compromised (since it generates a new ECDH keypair for each message). However, the client holds a long-term keypair that is sufficient to decrypt past messages.

I'm tempted to simplify this along the following lines: all messages are encrypted with XChaCha20-Poly1305, using a symmetric key that's generated by the client and transmitted via the secure channel, and then the client can rotate it when desired.

slingamn commented 3 years ago

The threat model here requires a coordinated compromise of both the server and some component of the push infrastructure (either the app's server or the mobile framework's), which doesn't seem too concerning.

slingamn commented 3 years ago

The web push spec includes a recommendation to allow padding, to prevent bicycle attacks.

slingamn commented 3 years ago

It's worth looking at what Signal-iOS is doing; AFAIK Signal makes a point of only sending pings, no message data.

kylef commented 3 years ago

In terms of the prior art in Palaver's push spec, here's the things I'd consider could do with re-thinking:

  1. Ignore nicks should become hostmasks (*!*@test.com). These should match and behaves the same as other matching in IRC (matching bans +b and exclusions).
  2. Ability to specify (and perhaps on a per-channel basis) the specific commands that notifications should occur for (there are competing views on NOTICE being irelevant or relevant for notifications I've heard competing use-cases from users). KICK, INVITE, MODE changes etc. You may desire notifications if you are kicked or invited to a channel.
  3. The matching rules with MENTION-KEYWORD, MENTION-CHANNEL, MENTION-NICK and exclusion (IGNORE-KEYWORD, IGNORE-CHANNEL, IGNORE-NICK) is a simplistic view which doesn't let you construct more powerful rules for notifications that may be desired. For example, I want a more complex set of rules to exclude (or receive) notifications using expressions (.and(.hostmask("zeta", nil, "example.com"), .keyword("IMGUR"))).
  4. Matching for account names, matching for bots (+B mode) etc. I've seen various use cases that have come up over the years.
  5. The regex uses for matching keywords is based on word boundaries, this makes sense for many use cases and how nick mentions work but this makes it not possible to write more powerful rules such as I want to get notified for messages that match ^LINK: in a staff channel.
  6. Notifications snoozing and disabled hours (i.e, how can I configure a way to not receive messages in early AM or weekends for channels which are more "work" orientated).
  7. Being able to from a notification say "snooze" on the thread or channel for a time period can be useful for spam or noisy occasions.
  8. This isn't a use case from me, but I have had feature requests to have different notification sounds for different channels or senders.
  9. I've also seen feature requests to have notifications when certain people come online (defining notifications for MONITOR targets)

One design note here is that people do want to configure notifications differently per device which is why these preferences are "device" specific not client specific. The protocol implements a preferences system and a versioning system so that we do not negotiate with the server all preferences for each connection.

The biggest use case that has come up is replying to messages or accepting invites from notifications directly. Connecting to IRC in the background to do this is complicated and may have side effects (such as bouncers like ZNC clearing history, or presence changing on traditional IRC, resetting AWAY state and idle information). This also gets complicated when it comes to having multiple active connections at once (you reply from notification on watch while using IRC on a phone). Launcing the entire application and bringing up IRC connection is slow (lots of handshakes/MOTDs/buffer's may be received automatically) and can have CPU and battery imact. I've been looking for a lighter way to send out of bound messages without bringing up an expensive IRC connection (capability negotiation is also a negotiation which gets slower and slower on high latency connections as more capabilities are needed like echo-message, batch, labeled-response, sasl etc).

Having a way to send IRC commands (with labled response) over HTTP was the solution I was hoping to bring. I've started defining this in the past at https://gist.github.com/kylef/ad79980a6ebe2e23a2862cfedbd62f8b. Another design goal was to be able to send messages from iOS share sheets too (such as from other third party applications). This HTTP based protocol would work by exposing a URL in isupport and some way to authenticate to the account or session to be able to send IRC messages and receive the response.

In doing so, I have also been rethinking notifications to be more generic. Instead thinking of them more like web-hooks in other services. For example in GitHub I can say that I want to receive a hook for opening a pull request or pushing to a branch. Twillio I can get a web hook for an SMS or a phone call. I was heading in the direction where we can solve push notifications in a generic way with web hooks which then opens up to more use-cases and makes the system less client specific. IRC bots along with many other tools can also make use of this style to be able to hook into receiving messages matching some expression (in a specific channel with a certain message prefix).

slingamn commented 3 years ago

Thanks, this is very helpful.

It sounds to me like most of these filtering rules belong in the client, rather than in the server --- i.e., the server should accept a relatively simple list of rules to generate an inclusive set of notifications, and then the client can decide which ones to display? This allows more flexibility around UI choices and more customization. (It also strengthens the case for including message content in the notification itself, rather than sending a ping; the client should be able to make the decision about whether to display the message without contacting the server.)

As discussed on IRC I think we should investigate #1420 as an optimization for the handshake (I think the objective should be for the typical client and server responses in the typical handshake to be smaller than the typical mobile network MTU).

DanielOaks commented 3 years ago

@slingamn I believe one reason to do the filtering rules before the client is because Apple does limit how many push notifs devices can receive, so if we're mixing both useful (actual pings) and non-useful (not actual pings) push notifs then we're likely to run into the issue of the client missing legitimate pings (see https://developer.apple.com/library/archive/technotes/tn2265/_index.html#//apple_ref/doc/uid/DTS40010376-CH1-TNTAG23 for more details). But that said I'm not super literate with Apple iOS documentation-speak so if actual iOS devs have input there then cool.

Thinking about that issue made me think "hey these should just be implemented in the client's http gateway instead, cool!!!" but with the encryption stuff we're doing yeah that's kinda impossible.

DanielOaks commented 3 years ago

Note to self re validating that we're not just DOSing some poor site out there, I imagine the network could just plain and simple keep a list of allowed gateways in it. Gives the network control over the clients it supports in this kind of way, etc. e.g. palaver.com, etc, etcetcetc.

Feels like something the networks may want to take ownership of.

slingamn commented 3 years ago

From discussion in #ircv3, it sounds like we're more or less reinventing RFC8030, which already provides a mechanism for a client to submit an arbitrary push endpoint to the server, and for the server to submit a standardized REST call to that endpoint. (The difference is that the endpoint will typically be controlled directly by the app's platform, rather than by the app itself, e.g., https://fcm.googleapis.com/fcm/send.)

It should be possible for Android and iOS apps to implement RFC8030, using FCM as the "push service". This is the compatibility layer for iOS: https://firebase.google.com/docs/cloud-messaging/ios/certs

DanielOaks commented 3 years ago

But very obviously we're going to be allowing apps to send their own alternate URL/platform to use as the push service for when FCM dies, right? Because there's no way that we're going to hardcode this piece of junk to a proprietary platform made by a company that's very, very well known for killing its proprietary platforms.

edit: I don't care whether networks need to manually whitelist every single alternate url/platform that clients try to use, that's fine, so long as we're not marrying this to a single proprietary platform that's likely to die, with no plan for fallback to alternatives when it actually does die.

slingamn commented 3 years ago

Correct, that's the purpose of RFC8030: the client designates an arbitrary push service and passes it to the server.

DanielOaks commented 3 years ago

Also useful if we're looking towards RFC 8030, RFC 8291 - Message Encryption for Web Push

kylef commented 3 years ago

To put together all the information for iOS/watchOS/macOS based on APNs from what has been discussed so far.

Sending "Pings"

The Apple terminology for these are "background notifications" or "silent notifications". I think this is out of the picture for "instant message" purposes given the limitations with this technology. This is designed for less frequent updates, and it launches the application in the background to be able to accomplish the task. From the docs:

The system treats background notifications as low priority: you can use them to refresh your app’s content, but the system doesn’t guarantee their delivery. In addition, the system may throttle the delivery of background notifications if the total number becomes excessive. The number of background notifications allowed by the system depends on current conditions, but don’t try to send more than two or three per hour.

We may not receive them all, thus we won't necessarily know about all different IRC servers and buffers which we may need to fetch messages for:

When a device receives a background notification, the system may hold and delay the delivery of the notification, which can have the following side effects:

  • When the system receives a new background notification, it discards the older notification and only holds the newest one.

Related Resources:

Notification Service Extensions

iOS, macOS and watchOS apps can create a service extension which can be used to alter a messages content before it is delivered to the end user. The intent of using the extension is so that the IRC server can encrypt the contents so that only the device is able to view the contents.

There are a few notes on this:

I think these are all feasible limitations to overcome. The 4096 byte limit is a bit tricky one because that is also the same as the minimum limit within the interface described by RFC8030 (which it seems we want to use). APNs provider will need to add JSON and APNs message framing which means the following MUST rule may be broken:

This may also be incompatible with IRCv3 multi-line messages, and message-tag limits. Message tags include a 8191 byte limit excluding the 512 byte limit for main message. How will a server determine what tags are important and what can be dropped? Some client-to-client tags like the reply and react can be more important. msg id will be needed so that a response from the notification will be linked correctly. Applications may also want the server-time and msgid, if they need to later correlate notification to the message received later in an IRC connection, so we are able to remove the notification when user has seen it via other means.

There is one unanswered question I have so far. How do notification service extensions interoperate with sharing notifications to other devices such as watchOS. At the moment Palaver iOS notification may be shared to a paired watch while I believe the phone is not around. Would the notification be delayed and relayed from iOS (if it is running?). Some of the wording from WWDC sessions put emphasis on fallback content which would lead me to speculate that the existing title/body may be used.

Resources:

slingamn commented 3 years ago

It occurs to me that as part of this, we really need to specify always-on as well! I believe existing implementations (like the ZNC plugins) just assume it implicitly.

"Always-on" is a bad name. Maybe we should call it STICKY?

DanielOaks commented 3 years ago

Just curious, 'always-on' in this context? I'm not quiiiite sure how that relates to this, here. For a Server -> ZNC -> Client setup, I imagine that ZNC would be the one sending out the push notifs instead of the server.

edit: For the purposes of a spec like this, TraditionalServer<->ZNC<->Client and OragonoAlwaysOn<->Client feels like the same thing, just with Oragono taking over the role of TraditionalServer<->ZNC, so I'm not exactly sure what extra needs to be negotiated/specified when dealing with our bouncer-like stuff versus clients interacting with actual bouncers.

DanielOaks commented 3 years ago

Okay, so. Client needs to know whether it can just set its push notification preferences and then disconnect from the server, and the server will keep sending it push notifications regardless (e.g. what happens on ZNC or on Oragono when always-on is enabled), or whether the client does need to try to stay materially connected to the server in some way for it to keep its connection open and keep receiving push notifications. Would we be introducing that second case, if e.g. push notifs were used in the traditional config?

slingamn commented 3 years ago

We will not be, but the feature makes a ton of sense even without push notifications, so it's time to specify it.

DanielOaks commented 3 years ago

We can't specify a separate thing in this spec just because we want to, it'll need to materially affect this specification in a way for us to dedicate a section to it. How do you imagine this touching the spec / what do you think the section of the spec that mentions this would look like?

DanielOaks commented 3 years ago

So, this idea is actually around the presence of multiple clients and is a pretty important part of the server (the entity dispatching push notifications to the gateway) knowing when and when not to send push notifs, and which client(s) to send them to.

The Palaver spec has the BACKGROUND and FOREGROUND commands, where background/foreground are macOS/iOS application states. The application is in foreground vs the application is in the background. The TCP connection is still active, and we may still send the client messages over the TCP connection. However if a client is backgrounded, it isn't active (the application isn't in focus), then the server may want to send notifications to other clients.

This page explains the rules for Palaver's notification dispatching - in a nutshell "Notifications will ONLY be sent if all your clients are disconnected, or marked as away". BACKGROUND/FOREGROUND commands on ZNC set this same "away" state (which is per client when you have the "clientaway" module).

From @kylef:

The thing I am trying to solve here, is that when you are active on one application you don't want notifications on the 3 other devices around you. I am talking here actively on iPad, I don't want my computer and phone/watch to be constantly notifying me so backgrounding application treats it the same way (its no longer active). maybe the terminology could be better, perhaps on a computer you may want to consider idle time too (as you may be active on another application).

it could essentially just be AWAY, though?

yeah, if what "away" did was desired and commonly understood for always on. by default on ZNC, away is global (not client specific). https://wiki.znc.in/Clientaway module brings what I'd consider to be the desired behaviour (each client having their own away state, and if all clients are away, or disconnected then away state is sent upstream to the server). I could imagine some people may not want their actual away state to change (which is why that module has setting to configure if it goes upstream to the server).

slingamn commented 3 years ago

I think @DanielOaks is basically right that STICKY can be disentangled from PUSH at the spec level; PUSH basically just needs an error code indicating, "you can't enable PUSH because you are not sticky".

DanielOaks commented 3 years ago

With reference to ^, ZNC has a very... interesting set of assumptions and custom commands added by the Palave spec and that Clientaway module that result in the server knowing which clients are active vs which are not, and lets the server decide where to send push notifs to.

However, we don't have anything like that built up. If we're building our system to let clients control that from scratch, it makes sense to define it and for it to be used by this in our implementation of a push notif system.

I dunno, this is a bit of a complex relationship to dig into. Would 'traditional irc servers' even make use of this, given you need some persistent presence in order to generate events which would dispatch push notifications? Or is this just an us-with-our-persistent-presence-and-bouncers spec?

slingamn commented 3 years ago

I think as a demo/MVP, we should get a version of this working that implements RFCs 8030 and 8291 and unconditionally sends a push message for every mention and DM. I think it would illuminate what needs to be in the spec --- and we'd be able to reuse almost all of the code.

slingamn commented 3 years ago

It appears that APNS doesn't implement RFC8030, but instead a proprietary API, necessitating this shim: https://github.com/DagAgren/toot-relay

poVoq commented 3 years ago

Worth considering is also this self-hosted and client independent option: https://gotify.net/ (There are mobile clients for it, at least Android & Ubuntu Touch and the general support for various notification services is quite big)

tacerus commented 3 years ago

Not sure if relevant or interesting to anyone - but the Pounce bouncer ships with a Palaver module, which is an easy way to get Palaver push notifications up and running to see how it can (and does) work: https://git.causal.agency/pounce/about/

emersion commented 2 years ago

Ref https://github.com/ircv3/ircv3-specifications/pull/471