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'; @@ -45,7 +46,9 @@ apnMaxNotificationPayloadByteSize, fcmMaxNotificationPayloadByteSize, 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 +297,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 +761,24 @@ return notification; } +type WNSNotifInputData = { + +notifTexts: ResolvedNotifTexts, + +threadID: string, + +unreadCount: number, +}; +async function prepareWNSNotification( + inputData: WNSNotifInputData, +): Promise { + const { notifTexts, threadID, unreadCount } = inputData; + const { merged, ...rest } = notifTexts; + const notification = { + ...rest, + unreadCount, + threadID, + }; + return notification; +} + type NotificationInfo = | { +source: 'new_message', @@ -803,8 +841,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, deviceType: 'android', @@ -893,6 +931,48 @@ return result; } +type WNSDelivery = { + +source: $PropertyType, + +deviceType: 'windows', + +wnsIDs: $ReadOnlyArray, + +deviceTokens: $ReadOnlyArray, + +codeVersion?: number, + +errors?: $ReadOnlyArray, +}; +type WNSResult = { + +info: NotificationInfo, + +delivery: WNSDelivery, + +invalidTokens?: $ReadOnlyArray, +}; +async function sendWNSNotification( + notification: WNSNotification, + deviceTokens: $ReadOnlyArray, + notificationInfo: NotificationInfo, +): Promise { + const { source, codeVersion } = notificationInfo; + + const response = await wnsPush({ + notification, + deviceTokens, + }); + + const wnsIDs = response.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, 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,14 @@ import type { ResponseFailure } from '@parse/node-apn'; import type { FirebaseApp, FirebaseError } from 'firebase-admin'; import invariant from 'invariant'; +import fetch 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 +21,7 @@ getAPNProvider, getFCMProvider, ensureWebPushInitialized, + getWNSToken, } from './providers.js'; import { dbQuery, SQL } from '../database/database.js'; @@ -30,6 +35,7 @@ const apnBadTokenErrorString = 'BadDeviceToken'; const apnMaxNotificationPayloadByteSize = 4096; const webInvalidTokenErrorCodes = [404, 410]; +const wnsInvalidTokenErrorCodes = [404, 410]; type APNPushResult = | { +success: true } @@ -259,10 +265,101 @@ return { ...result }; } +export type WNSPushError = { + +statusCode: number, + +headers: { +[string]: string }, + +body: string, +}; +type WNSPushResult = { + +success?: true, + +wnsIDs?: $ReadOnlyArray, + +errors?: $ReadOnlyArray, + +invalidTokens?: $ReadOnlyArray, +}; +async function wnsPush({ + notification, + deviceTokens, +}: { + +notification: WNSNotification, + +deviceTokens: $ReadOnlyArray, +}): Promise { + 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 = await Promise.all( + deviceTokens.map(async deviceTokenString => { + try { + return { + wnsID: await wnsSinglePush( + token, + notificationString, + deviceTokenString, + ), + }; + } catch (error) { + return { error }; + } + }), + ); + + const errors = []; + const ids = []; + const invalidTokens = []; + for (let i = 0; i < pushResults.length; i++) { + const pushResult = pushResults[i]; + if (pushResult.error) { + errors.push(pushResult.error); + if (wnsInvalidTokenErrorCodes.includes(Number(pushResult.error.code))) { + invalidTokens.push(deviceTokens[i]); + } + } else { + ids.push(pushResult.wnsID); + } + } + + const result = {}; + if (ids.length > 0) { + result.wnsIDs = ids; + } + 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, + deviceToken: string, +): Promise { + const result = await fetch(deviceToken, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + 'X-WNS-Type': 'wns/raw', + 'Authorization': `Bearer ${token}`, + }, + body: notification, + }); + return result.headers.get('X-WNS-MSG-ID'); +} + export { apnPush, fcmPush, webPush, + wnsPush, getUnreadCounts, apnMaxNotificationPayloadByteSize, fcmMaxNotificationPayloadByteSize, 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, +};