Skip to content

Commit f5f2374

Browse files
chore: accept additional push tokens intended for voip to iOS devices (RocketChat#39174)
1 parent 692a891 commit f5f2374

7 files changed

Lines changed: 92 additions & 35 deletions

File tree

apps/meteor/app/api/server/v1/push.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Push } from '@rocket.chat/core-services';
2-
import type { IPushToken } from '@rocket.chat/core-typings';
2+
import type { IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings';
33
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
44
import {
55
ajv,
@@ -23,9 +23,10 @@ import type { SuccessResult } from '../definition';
2323

2424
type PushTokenPOST = {
2525
id?: string;
26-
type: 'apn' | 'gcm';
26+
type: IPushTokenTypes;
2727
value: string;
2828
appName: string;
29+
voipToken?: string;
2930
};
3031

3132
const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
@@ -47,6 +48,10 @@ const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
4748
type: 'string',
4849
minLength: 1,
4950
},
51+
voipToken: {
52+
type: 'string',
53+
nullable: true,
54+
},
5055
},
5156
required: ['type', 'value', 'appName'],
5257
additionalProperties: false,
@@ -72,13 +77,13 @@ const PushTokenDELETESchema: JSONSchemaType<PushTokenDELETE> = {
7277

7378
export const isPushTokenDELETEProps = ajv.compile<PushTokenDELETE>(PushTokenDELETESchema);
7479

75-
type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt'>;
80+
type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt' | 'voipToken'>;
7681

7782
/**
7883
* Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in
7984
*/
8085
function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResult {
81-
const { _id, token, appName, userId, enabled, createdAt, _updatedAt } = result;
86+
const { _id, token, appName, userId, enabled, createdAt, _updatedAt, voipToken } = result;
8287

8388
return {
8489
_id,
@@ -88,6 +93,7 @@ function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResul
8893
enabled,
8994
createdAt,
9095
_updatedAt,
96+
voipToken,
9197
};
9298
}
9399

@@ -140,6 +146,9 @@ const pushTokenEndpoints = API.v1
140146
_updatedAt: {
141147
type: 'string',
142148
},
149+
voipToken: {
150+
type: 'string',
151+
},
143152
},
144153
additionalProperties: false,
145154
},
@@ -154,7 +163,11 @@ const pushTokenEndpoints = API.v1
154163
authRequired: true,
155164
},
156165
async function action() {
157-
const { id, type, value, appName } = this.bodyParams;
166+
const { id, type, value, appName, voipToken } = this.bodyParams;
167+
168+
if (voipToken && !id) {
169+
return API.v1.failure('voip-tokens-must-specify-device-id');
170+
}
158171

159172
const rawToken = this.request.headers.get('x-auth-token');
160173
if (!rawToken) {
@@ -168,6 +181,7 @@ const pushTokenEndpoints = API.v1
168181
authToken,
169182
appName,
170183
userId: this.userId,
184+
...(voipToken && { voipToken }),
171185
});
172186

173187
return API.v1.success({ result: cleanTokenResult(result) });

apps/meteor/server/services/push/service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ export class PushService extends ServiceClassInternal implements IPushService {
3636
): Promise<Omit<IPushToken, 'authToken'>> {
3737
const tokenId = await registerPushToken(data);
3838

39-
const removeResult = await PushToken.removeByTokenAndAppNameExceptId(data.token, data.appName, tokenId);
39+
const removeResult = await PushToken.removeDuplicateTokens({
40+
_id: tokenId,
41+
token: data.token,
42+
appName: data.appName,
43+
authToken: data.authToken,
44+
});
4045
if (removeResult.deletedCount) {
4146
logger.debug({ msg: 'Removed existing app items', removed: removeResult.deletedCount });
4247
}

apps/meteor/server/services/push/tokenManagement/findDocumentToUpdate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export async function findDocumentToUpdate(data: Partial<IPushToken>): Promise<I
99
}
1010
}
1111

12+
// VoIP tokens MUST match the id
13+
if (data.voipToken) {
14+
return null;
15+
}
16+
1217
if (data.token && data.appName) {
1318
return PushToken.findOneByTokenAndAppName(data.token, data.appName);
1419
}
Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
import type { IPushToken, Optional } from '@rocket.chat/core-typings';
22
import { PushToken } from '@rocket.chat/models';
33

4-
import { logger } from '../logger';
54
import { findDocumentToUpdate } from './findDocumentToUpdate';
5+
import { logger } from '../logger';
66

7-
export async function registerPushToken(
8-
data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>,
9-
): Promise<IPushToken['_id']> {
10-
const doc = await findDocumentToUpdate(data);
11-
12-
if (!doc) {
13-
const insertResult = await PushToken.insertToken({
14-
...(data._id && { _id: data._id }),
15-
token: data.token,
16-
authToken: data.authToken,
17-
appName: data.appName,
18-
userId: data.userId,
19-
...(data.metadata && { metadata: data.metadata }),
20-
});
7+
export type PushTokenData = Optional<
8+
Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata' | 'voipToken'>,
9+
'_id' | 'metadata'
10+
>;
2111

22-
const { authToken: _, ...dataWithNoAuthToken } = data;
23-
logger.debug({ msg: 'Push token added', dataWithNoAuthToken, insertResult });
12+
function canModifyTokenDocument(doc: IPushToken, data: Partial<IPushToken>): boolean {
13+
// If there's no voip on either side of the operation, any doc can be updated
14+
if (!doc.voipToken && !data.voipToken) {
15+
return true;
16+
}
2417

25-
return insertResult.insertedId;
18+
// VoIP tokens MUST be referenced by id, so if there's no id on the data, do not let this doc be changed
19+
if (!data._id || data._id !== doc._id) {
20+
return false;
2621
}
2722

23+
return true;
24+
}
25+
26+
async function insertToken(data: PushTokenData): Promise<IPushToken['_id']> {
27+
const insertResult = await PushToken.insertToken({
28+
...(data._id && { _id: data._id }),
29+
token: data.token,
30+
authToken: data.authToken,
31+
appName: data.appName,
32+
userId: data.userId,
33+
...(data.metadata && { metadata: data.metadata }),
34+
...(data.voipToken && data._id && { voipToken: data.voipToken }),
35+
});
36+
37+
const { authToken: _, ...dataWithNoAuthToken } = data;
38+
logger.debug({ msg: 'Push token added', dataWithNoAuthToken, insertResult });
39+
40+
return insertResult.insertedId;
41+
}
42+
43+
async function updateToken(doc: IPushToken, data: PushTokenData): Promise<IPushToken['_id']> {
2844
const updateResult = await PushToken.refreshTokenById(doc._id, {
2945
token: data.token,
3046
authToken: data.authToken,
3147
appName: data.appName,
3248
userId: data.userId,
49+
...(data.voipToken && { voipToken: data.voipToken }),
3350
});
3451

3552
if (updateResult.modifiedCount) {
@@ -39,3 +56,13 @@ export async function registerPushToken(
3956

4057
return doc._id;
4158
}
59+
60+
export async function registerPushToken(data: PushTokenData): Promise<IPushToken['_id']> {
61+
const doc = await findDocumentToUpdate(data);
62+
63+
if (!doc || !canModifyTokenDocument(doc, data)) {
64+
return insertToken(data);
65+
}
66+
67+
return updateToken(doc, data);
68+
}

packages/core-typings/src/IPushToken.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export interface IPushToken extends IRocketChatRecord {
1111
authToken: ILoginToken['hashedToken'];
1212
metadata?: Record<string, unknown>;
1313
createdAt: Date;
14+
voipToken?: string;
1415
}

packages/model-typings/src/models/IPushTokenModel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export interface IPushTokenModel extends IBaseModel<IPushToken> {
1313
insertToken(data: AtLeast<IPushToken, 'token' | 'authToken' | 'appName' | 'userId'>): Promise<InsertOneResult<IPushToken>>;
1414
refreshTokenById(
1515
id: IPushToken['_id'],
16-
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>,
16+
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId' | 'voipToken'>,
1717
): Promise<UpdateResult<IPushToken>>;
1818

1919
removeByUserIdExceptTokens(userId: string, tokens: IPushToken['authToken'][]): Promise<DeleteResult>;
20-
removeByTokenAndAppNameExceptId(token: IPushToken['token'], appName: IPushToken['appName'], id: IPushToken['_id']): Promise<DeleteResult>;
20+
removeDuplicateTokens(tokenData: Pick<IPushToken, '_id' | 'token' | 'appName' | 'authToken'>): Promise<DeleteResult>;
2121

2222
removeAllByUserId(userId: string): Promise<DeleteResult>;
2323
removeAllByTokenStringAndUserId(token: string, userId: string): Promise<DeleteResult>;

packages/models/src/models/PushToken.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel
5656

5757
async refreshTokenById(
5858
id: IPushToken['_id'],
59-
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>,
59+
data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId' | 'voipToken'>,
6060
): Promise<UpdateResult<IPushToken>> {
6161
return this.updateOne(
6262
{ _id: id },
@@ -66,7 +66,9 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel
6666
authToken: data.authToken,
6767
appName: data.appName,
6868
userId: data.userId,
69+
...(data.voipToken && { voipToken: data.voipToken }),
6970
},
71+
...(!data.voipToken && { $unset: { voipToken: 1 } }),
7072
},
7173
);
7274
}
@@ -85,15 +87,18 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel
8587
});
8688
}
8789

88-
removeByTokenAndAppNameExceptId(
89-
token: IPushToken['token'],
90-
appName: IPushToken['appName'],
91-
id: IPushToken['_id'],
92-
): Promise<DeleteResult> {
90+
removeDuplicateTokens(tokenData: Pick<IPushToken, '_id' | 'token' | 'appName' | 'authToken'>): Promise<DeleteResult> {
9391
return this.deleteMany({
94-
token,
95-
appName,
96-
_id: { $ne: id },
92+
_id: { $ne: tokenData._id },
93+
$or: [
94+
{
95+
token: tokenData.token,
96+
appName: tokenData.appName,
97+
},
98+
{
99+
authToken: tokenData.authToken,
100+
},
101+
],
97102
});
98103
}
99104

0 commit comments

Comments
 (0)