Skip to content

notification: Use user_id to find account if multiple on same realm #5108

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

Merged
merged 10 commits into from
Nov 24, 2021

Conversation

chrisbobbe
Copy link
Contributor

@chrisbobbe chrisbobbe commented Nov 5, 2021

Along with making the property required on APNs and FCM payloads.

I noticed the opportunity to do this after reviewing #5100 and #5105.

Fixes: #4631

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! It'll be great to fix this bug.

Specific comments below. Then more broadly, we just talked through in the office the question of requiring this 2.1+ feature, and concluded we aren't ready to drop notifications entirely from pre-2.1 servers. But this can be revised to keep it as optional, just pass it through, and make use of it.

Comment on lines 146 to 147
fun `optional fields missing cause no error`() {
expect.that(parse(Example.pm.minus("user_id")).identity.userId).isNull()
Copy link
Member

Choose a reason for hiding this comment

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

This should turn into a case where we test that required fields are required. See 9620629 or other recent changes in git log --stat -p android/app/src/test/.

Comment on lines 87 to 89
TODO(server-1.8): Simplify this comment a lot.
TODO(server-1.9): Simplify further.
TODO(server-2.1): Simplify further.
Copy link
Member

Choose a reason for hiding this comment

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

In addition to the paragraphs just above, the other simplification I had in mind here is to delete the distinctions between "antiquity" and the earliest supported version on the various properties, and to simplify the paragraphs at the top.

Comment on lines 166 to 167
if (user_id === undefined) {
throw err('archaic (pre-2.1.x)');
Copy link
Member

Choose a reason for hiding this comment

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

This should get a test case. See the existing mentions of archaic in the tests.

... Which raises the question why this "baseline" case didn't break:

      // baseline
      expect(make({ realm_uri, recipient_type: 'private', sender_email })).toBeTruthy();
      // missing recipient_type
      expect(make({ realm_uri, sender_email })).toThrow(/archaic/);

It looks like the answer is that it's passing because the make return value is just a closure, and so always truthy -- it needs to be called to actually test anything. Oops. So a prep commit should fix that.


type NotificationBase = {|
realm_uri: string,
user_id: UserId,
Copy link
Member

Choose a reason for hiding this comment

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

notif: Expose payload's realm_uri, present since server 2.1, to inner code

s/realm_uri/user_id/?

*/
export const getAccountFromNotificationData = (
data: Notification,
identities: $ReadOnlyArray<Identity>,
accounts: $ReadOnlyArray<Account>,
Copy link
Member

Choose a reason for hiding this comment

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

The Identity type doesn't have the user ID, and it's not super
convenient to add it there. That's because the Identity type is
based on the Auth type (and we make Identity objects out of Auth
objects at runtime), and the Auth type doesn't have the user ID.
It's not super convenient to give it to the Auth type because there
are places where we make Auth objects but we don't have a user ID,
like `authFromCallbackUrl` in src/start/webAuth.js.

Oof. The Auth does of course have the email. So anywhere we have an Auth and can't readily make an Identity, because we lack the user ID, is an obstacle to converting from emails to IDs, #3764.

In authFromCallbackUrl, we're decoding one of those zulip:// URLs the server used to complete a web login and get us the API key. So we should get the server to start giving us user IDs there, even if we won't be able to count on their presence for a while. That should be a thread in #api design, then.

@@ -79,10 +79,8 @@ export const getAccountFromNotificationData = (
realm_uri,
parsed_url: realmUrl,
match_count: urlMatches.length,
unique_identities_count: new Set(urlMatches.map(matchIndex => identities[matchIndex].email))
.size,
unique_accounts_count: new Set(urlMatches.map(matchIndex => accounts[matchIndex].email)).size,
Copy link
Member

Choose a reason for hiding this comment

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

Best to keep the name for this one commit. 🙂

  • The commit is marked NFC, which it isn't if a name sent in logging changes.
  • "Unique identities" is still an accurate description, and clearer what it means, because we're only counting up distinct emails (for this realm), not looking for other properties of an Account.

});
// TODO get user_id into accounts data, and use that
Copy link
Member

Choose a reason for hiding this comment

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

nit: this TODO comment should go away in the later commit where we start using the user_id

Comment on lines 73 to 84
if (urlMatches.length > 1) {
// The user has several accounts in the notification's realm. We should
// be able to tell the right one using the notification's `user_id`...
// except we don't store user IDs in `accounts`, only emails. Until we
// fix that, just ignore the information.
logging.warn('notification realm_uri ambiguous; multiple matches found', {
realm_uri,
parsed_url: realmUrl,
match_count: urlMatches.length,
unique_identities_count: new Set(urlMatches.map(matchIndex => identities[matchIndex].email))
.size,
});
// TODO get user_id into accounts data, and use that
return null;
// The user has several accounts in the notification's realm. Pick one
// based on the notification's `user_id`.
const userMatch = urlMatches.find(urlMatch => accounts[urlMatch].userId === user_id);
if (userMatch == null) {
logging.warn('notification account not found with realm_uri and user_id');
return null;
}
return userMatch;
}

return urlMatches[0];
Copy link
Member

Choose a reason for hiding this comment

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

Let's do the search for a user_id match regardless of whether there are one or several realm-URL matches. That way we notice if the one account in the realm isn't actually for the right user.

// commit 447a517e6, PR #12172.)
// TODO(server-2.1): Require this.
userId = data["user_id"]?.parseInt("user_id")
userId = data.require("user_id").parseInt("user_id")
Copy link
Member

Choose a reason for hiding this comment

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

Let's leave on this a one-line comment saying it was new in 2.1. That'll be helpful to refer to when seeing Sentry warnings about dropping FCM messages where it's absent.

(I see I didn't do this in 9620629, but I realize now I should have.)

@@ -112,6 +112,7 @@ data class MessageFcmMessage(
fun dataForOpen(): Bundle = Bundle().apply {
// NOTE: Keep the JS-side type definition in sync with this code.
identity.realmUri.let { putString("realm_uri", it.toString()) }
identity.userId.let { putInt("user_id", it) }
Copy link
Member

Choose a reason for hiding this comment

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

The let here is redundant; can simplify it out.

(It's redundant on the previous line too. Looks like it got left behind in 9620629 when I made realmUri non-nullable; before that it was ?.let and was doing something useful.)

@chrisbobbe chrisbobbe force-pushed the pr-notification-user-id branch from eb706b4 to 68db03f Compare November 15, 2021 23:57
@chrisbobbe
Copy link
Contributor Author

Thanks for the review! Revision pushed.

Then more broadly, we just talked through in the office the question of requiring this 2.1+ feature, and concluded we aren't ready to drop notifications entirely from pre-2.1 servers. But this can be revised to keep it as optional, just pass it through, and make use of it.

Indeed. Because of this, there are several of your comments (from before we decided that) that I haven't acted on—please let me know if I've dropped some that I shouldn't have. 🙂

@chrisbobbe chrisbobbe force-pushed the pr-notification-user-id branch from 68db03f to 145833b Compare November 16, 2021 22:41
@gnprice
Copy link
Member

gnprice commented Nov 16, 2021

Thanks for the revision! This looks good, modulo a couple of things in the last commit. Comments below, and it looks like #5108 (comment) still applies.

}

return urlMatches[0];
// The user has several accounts in the notification's realm. Pick one
Copy link
Member

Choose a reason for hiding this comment

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

This isn't quite true yet -- this code will apply when there's just one element in urlMatches.

Comment on lines 95 to 108
const userMatch = urlMatches.find(urlMatch => accounts[urlMatch].userId === user_id);
if (userMatch == null) {
logging.warn('notification account not found with realm_uri and user_id');
Copy link
Member

Choose a reason for hiding this comment

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

Although at this point we have a user_id from the notification, it may be the case that the account it's meant for doesn't have userId non-null in the accounts state. That's because:

  /**
   * The user's numeric ID.
   *
   * We learn the user ID each time we complete an initial fetch.
   *
   * This is `null` briefly when we've logged in but not yet completed our
   * first initial fetch on the account.  It's also `null` when representing
   * an account which was last used on a version of the app which didn't
   * record this information.  It's never `null` for an account for which we
   * have server data.
   */

Probably if there's just one URL match, and its userId is null, then we should go for it.

Copy link
Contributor Author

@chrisbobbe chrisbobbe Nov 17, 2021

Choose a reason for hiding this comment

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

Probably if there's just one URL match, and its userId is null, then we should go for it.

My current revision uses a slightly different algorithm. If there are two URL matches, and one of them has a userId that's wrong, but the other one has null, we still want to go for the null one. I think.

@chrisbobbe
Copy link
Contributor Author

chrisbobbe commented Nov 17, 2021

Thanks for the review! Revision pushed.

it looks like #5108 (comment) still applies.

Oh, actually that was one that I thought I'd fixed.

@chrisbobbe chrisbobbe force-pushed the pr-notification-user-id branch from 145833b to de294bd Compare November 17, 2021 01:33
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! A couple of comments below.

// based on the notification's `user_id`.
const userMatch = urlMatches.find(urlMatch => {
const { userId } = accounts[urlMatch];
return userId !== null && userId === user_id;
Copy link
Member

Choose a reason for hiding this comment

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

This userId !== null is always true if userId === user_id is, right? So it can be dropped, and this function simplifies back down to just returning an expression.

Comment on lines 108 to 109
logging.warn('notification account not found with realm_uri and user_id');
return null;
Copy link
Member

Choose a reason for hiding this comment

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

This log message won't always be accurate -- there could have been two accounts that had the right realm_uri and null user_id.

(Well, it could be read as accurate depending on how one interprets "found with realm_uri and user_id". But a reading that makes it accurate would make the same message equally accurate in the case where there's a match we actually use, just above here.)

We've been pretty precise in the different error cases in this function so far, because it's been a fairly common set of errors and it's good to know exactly what conditions are driving that. So let's make the distinction here too between zero matches and multiple matches.

@gnprice
Copy link
Member

gnprice commented Nov 23, 2021

Oh also: let's mark this as fixing #4631! This is an issue I run into regularly when using test accounts -- looking forward to getting it fixed. 🙂

…sages

Oops -- `make` just returns a function, and functions are truthy.
Instead, we want to test that that returned function returns true
when called.
As we're already doing on Android; see extractIdentity in
FcmMessage.kt.

Next, we'll expose this data from iOS and Android notification
payloads, when present, to inner app code, by adding
`user_id?: UserId` to our Notification type.

This brings the implementation in line with the comment at the top
of the `extract` module that says that servers have started
including a user_id.
…er_id

We make a distinction like this in the counterpart Android code,
with `data class Identity` in FcmMessage.kt.
By adding it to our `Notification` type.
…ties

When we added this function in 056930c, we may have specifically
chosen Identity objects over Account objects, to avoid adding a
place where we pass around auth credentials. Hmm.

Still, we've recently started storing the user ID in Account objects
(4fdefb0), and we have a TODO in this function for using it.

The Identity type doesn't have the user ID, and it's not currently
convenient to add it there. That's because the Identity type is
based on the Auth type (and we make Identity objects from Auth
objects), and the Auth type doesn't have the user ID. It's not super
convenient to add the user ID to the Auth type because there are
places where we make Auth objects but we don't have a user ID, like
`authFromCallbackUrl` in src/start/webAuth.js. As Greg points
out [1], those places are obstacles to zulip#3764, "Convert from emails
to user IDs to identify users".
This comment was slightly out of date -- we have been storing user
IDs in `state.accounts`, since 4fdefb0.

Fixes: zulip#4631
@chrisbobbe chrisbobbe force-pushed the pr-notification-user-id branch from de294bd to b3e24ed Compare November 23, 2021 23:38
@chrisbobbe
Copy link
Contributor Author

Thanks! Revision pushed.

@gnprice
Copy link
Member

gnprice commented Nov 24, 2021

Thanks! Looks good -- merging.

@gnprice gnprice merged commit b3e24ed into zulip:main Nov 24, 2021
@chrisbobbe chrisbobbe deleted the pr-notification-user-id branch November 24, 2021 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Opening notif in one of several accounts on same realm doesn't switch accounts
2 participants