Page MenuHomePhabricator

D7284.diff
No OneTemporary

D7284.diff

diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -30,6 +30,7 @@
} from 'lib/types/message-types.js';
import type {
WebNotification,
+ WNSNotification,
ResolvedNotifTexts,
} from 'lib/types/notif-types.js';
import type { ServerThreadInfo } from 'lib/types/thread-types.js';
@@ -44,8 +45,11 @@
getUnreadCounts,
apnMaxNotificationPayloadByteSize,
fcmMaxNotificationPayloadByteSize,
+ wnsMaxNotificationPayloadByteSize,
webPush,
+ wnsPush,
type WebPushError,
+ type WNSPushError,
} from './utils.js';
import createIDs from '../creators/id-creator.js';
import { createUpdates } from '../creators/update-creator.js';
@@ -294,6 +298,23 @@
deliveryPromises.push(deliveryPromise);
}
}
+ const windowsVersionsToTokens = byPlatform.get('windows');
+ if (windowsVersionsToTokens) {
+ for (const [codeVersion, deviceTokens] of windowsVersionsToTokens) {
+ const deliveryPromise = (async () => {
+ const notification = await prepareWNSNotification({
+ notifTexts,
+ threadID: threadInfo.id,
+ unreadCount: unreadCounts[userID],
+ });
+ return await sendWNSNotification(notification, [...deviceTokens], {
+ ...notificationInfo,
+ codeVersion,
+ });
+ })();
+ deliveryPromises.push(deliveryPromise);
+ }
+ }
for (const newMessageInfo of remainingNewMessageInfos) {
const newDBID = dbIDs.shift();
@@ -741,6 +762,31 @@
return notification;
}
+type WNSNotifInputData = {
+ +notifTexts: ResolvedNotifTexts,
+ +threadID: string,
+ +unreadCount: number,
+};
+async function prepareWNSNotification(
+ inputData: WNSNotifInputData,
+): Promise<WNSNotification> {
+ const { notifTexts, threadID, unreadCount } = inputData;
+ const { merged, ...rest } = notifTexts;
+ const notification = {
+ ...rest,
+ unreadCount,
+ threadID,
+ };
+
+ if (
+ Buffer.byteLength(JSON.stringify(notification)) >
+ wnsMaxNotificationPayloadByteSize
+ ) {
+ console.warn('WNS notification exceeds size limit');
+ }
+ return notification;
+}
+
type NotificationInfo =
| {
+source: 'new_message',
@@ -803,8 +849,8 @@
return result;
}
-type PushResult = AndroidResult | APNsResult | WebResult;
-type PushDelivery = AndroidDelivery | APNsDelivery | WebDelivery;
+type PushResult = AndroidResult | APNsResult | WebResult | WNSResult;
+type PushDelivery = AndroidDelivery | APNsDelivery | WebDelivery | WNSDelivery;
type AndroidDelivery = {
source: $PropertyType<NotificationInfo, 'source'>,
deviceType: 'android',
@@ -893,6 +939,48 @@
return result;
}
+type WNSDelivery = {
+ +source: $PropertyType<NotificationInfo, 'source'>,
+ +deviceType: 'windows',
+ +wnsIDs: $ReadOnlyArray<string>,
+ +deviceTokens: $ReadOnlyArray<string>,
+ +codeVersion?: number,
+ +errors?: $ReadOnlyArray<WNSPushError>,
+};
+type WNSResult = {
+ +info: NotificationInfo,
+ +delivery: WNSDelivery,
+ +invalidTokens?: $ReadOnlyArray<string>,
+};
+async function sendWNSNotification(
+ notification: WNSNotification,
+ deviceTokens: $ReadOnlyArray<string>,
+ notificationInfo: NotificationInfo,
+): Promise<WNSResult> {
+ const { source, codeVersion } = notificationInfo;
+
+ const response = await wnsPush({
+ notification,
+ deviceTokens,
+ });
+
+ const wnsIDs = response.wnsIDs ?? [];
+ const delivery: WNSDelivery = {
+ source,
+ deviceType: 'windows',
+ wnsIDs,
+ deviceTokens,
+ codeVersion,
+ errors: response.errors,
+ };
+ const result: WNSResult = {
+ info: notificationInfo,
+ delivery,
+ invalidTokens: response.invalidTokens,
+ };
+ return result;
+}
+
type InvalidToken = {
+userID: string,
+tokens: $ReadOnlyArray<string>,
diff --git a/keyserver/src/push/utils.js b/keyserver/src/push/utils.js
--- a/keyserver/src/push/utils.js
+++ b/keyserver/src/push/utils.js
@@ -4,10 +4,15 @@
import type { ResponseFailure } from '@parse/node-apn';
import type { FirebaseApp, FirebaseError } from 'firebase-admin';
import invariant from 'invariant';
+import fetch from 'node-fetch';
+import type { Response } from 'node-fetch';
import webpush from 'web-push';
import type { PlatformDetails } from 'lib/types/device-types.js';
-import type { WebNotification } from 'lib/types/notif-types.js';
+import type {
+ WebNotification,
+ WNSNotification,
+} from 'lib/types/notif-types.js';
import { threadSubscriptions } from 'lib/types/subscription-types.js';
import { threadPermissions } from 'lib/types/thread-types.js';
@@ -17,6 +22,7 @@
getAPNProvider,
getFCMProvider,
ensureWebPushInitialized,
+ getWNSToken,
} from './providers.js';
import { dbQuery, SQL } from '../database/database.js';
@@ -30,6 +36,8 @@
const apnBadTokenErrorString = 'BadDeviceToken';
const apnMaxNotificationPayloadByteSize = 4096;
const webInvalidTokenErrorCodes = [404, 410];
+const wnsInvalidTokenErrorCodes = [404, 410];
+const wnsMaxNotificationPayloadByteSize = 5000;
type APNPushResult =
| { +success: true }
@@ -259,11 +267,109 @@
return { ...result };
}
+export type WNSPushError = any | string | Response;
+type WNSPushResult = {
+ +success?: true,
+ +wnsIDs?: $ReadOnlyArray<string>,
+ +errors?: $ReadOnlyArray<WNSPushError>,
+ +invalidTokens?: $ReadOnlyArray<string>,
+};
+async function wnsPush({
+ notification,
+ deviceTokens,
+}: {
+ +notification: WNSNotification,
+ +deviceTokens: $ReadOnlyArray<string>,
+}): Promise<WNSPushResult> {
+ const token = await getWNSToken();
+ if (!token && process.env.NODE_ENV === 'development') {
+ console.log(`no keyserver/secrets/wns_config.json so ignoring notifs`);
+ return { success: true };
+ }
+ invariant(token, `keyserver/secrets/wns_config.json should exist`);
+
+ const notificationString = JSON.stringify(notification);
+
+ const pushResults = deviceTokens.map(async devicePushURL => {
+ try {
+ return await wnsSinglePush(token, notificationString, devicePushURL);
+ } catch (error) {
+ return { error };
+ }
+ });
+
+ const errors = [];
+ const notifIDs = [];
+ const invalidTokens = [];
+ for (let i = 0; i < pushResults.length; i++) {
+ const pushResult = await pushResults[i];
+ if (pushResult.error) {
+ errors.push(pushResult.error);
+ if (
+ pushResult.error === 'invalidDomain' ||
+ wnsInvalidTokenErrorCodes.includes(pushResult.error?.status)
+ ) {
+ invalidTokens.push(deviceTokens[i]);
+ }
+ } else {
+ notifIDs.push(pushResult.wnsID);
+ }
+ }
+
+ const result = {};
+ if (notifIDs.length > 0) {
+ result.wnsIDs = notifIDs;
+ }
+ if (errors.length > 0) {
+ result.errors = errors;
+ } else {
+ result.success = true;
+ }
+ if (invalidTokens.length > 0) {
+ result.invalidTokens = invalidTokens;
+ }
+ return { ...result };
+}
+
+async function wnsSinglePush(token: string, notification: string, url: string) {
+ const parsedURL = new URL(url);
+ const domain = parsedURL.hostname.split('.').slice(-3);
+ if (
+ domain[0] !== 'notify' ||
+ domain[1] !== 'windows' ||
+ domain[2] !== 'com'
+ ) {
+ return { error: 'invalidDomain' };
+ }
+
+ try {
+ const result = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ 'X-WNS-Type': 'wns/raw',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: notification,
+ });
+
+ if (!result.ok) {
+ return { error: result };
+ }
+
+ return { wnsID: result.headers.get('X-WNS-MSG-ID') };
+ } catch (err) {
+ return { error: err };
+ }
+}
+
export {
apnPush,
fcmPush,
webPush,
+ wnsPush,
getUnreadCounts,
apnMaxNotificationPayloadByteSize,
fcmMaxNotificationPayloadByteSize,
+ wnsMaxNotificationPayloadByteSize,
};
diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js
--- a/lib/types/notif-types.js
+++ b/lib/types/notif-types.js
@@ -24,3 +24,11 @@
+id: string,
+threadID: string,
};
+
+export type WNSNotification = {
+ +body: string,
+ +prefix?: string,
+ +title: string,
+ +unreadCount: number,
+ +threadID: string,
+};

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 23, 2:28 PM (18 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2694880
Default Alt Text
D7284.diff (8 KB)

Event Timeline