diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 16251da..4e4e732 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -4,6 +4,7 @@ import fs from "fs"; import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import http2 from "http2"; +import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; export interface ScheduledNotificationData { deviceId: string; @@ -11,16 +12,10 @@ export interface ScheduledNotificationData { stopId: string; } -interface APNsUrl { - fullUrl: string; - path: string; - host: string; -} - export class ETANotificationScheduler { public readonly secondsThresholdForNotificationToFire = 180; - private apnsToken: string | undefined = undefined; + private appleNotificationSender = new AppleNotificationSender() private _lastRefreshedTimeMs: number | undefined = undefined; get lastRefreshedTimeMs() { @@ -29,8 +24,6 @@ export class ETANotificationScheduler { constructor(private repository: GetterRepository, private shouldActuallySendNotifications = true) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); - this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); - this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); @@ -45,48 +38,12 @@ export class ETANotificationScheduler { */ private deviceIdsToDeliverTo: { [key: string]: Set } = {} - public reloadAPNsTokenIfTimePassed() { - if (this.lastReloadedTimeForAPNsIsTooRecent()) { - return; - } - - const keyId = process.env.APNS_KEY_ID; - const teamId = process.env.APNS_TEAM_ID; - - const privateKeyBase64 = process.env.APNS_PRIVATE_KEY; - if (!privateKeyBase64) return; - const privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8'); - - const tokenHeader = { - alg: "ES256", - "kid": keyId, - }; - - const nowMs = Date.now(); - const claimsPayload = { - "iss": teamId, - "iat": Math.ceil(nowMs / 1000), // APNs requires number of seconds since Epoch - }; - - this.apnsToken = jwt.sign(claimsPayload, privateKey, { - algorithm: "ES256", - header: tokenHeader - }); - this._lastRefreshedTimeMs = nowMs; - } - - private lastReloadedTimeForAPNsIsTooRecent() { - const thirtyMinutesMs = 1800000; - return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; - } - private async sendEtaNotificationImmediately(notificationData: ScheduledNotificationData): Promise { if (!this.shouldActuallySendNotifications) { return true; // pretend that the notification sent } const { deviceId, shuttleId, stopId } = notificationData; - this.reloadAPNsTokenIfTimePassed(); const shuttle = await this.repository.getShuttleById(shuttleId); const stop = await this.repository.getStopById(stopId); @@ -107,72 +64,11 @@ export class ETANotificationScheduler { return false; } - // Send the fetch request - const bundleId = process.env.APNS_BUNDLE_ID; - if (typeof bundleId !== "string") { - throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); + const notificationAlertArguments: NotificationAlertArguments = { + title: "Shuttle is arriving", + body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.`, } - - const { path, host } = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); - - const headers = { - ':method': 'POST', - ':path': path, - 'authorization': `bearer ${this.apnsToken}`, - "apns-push-type": "alert", - "apns-expiration": "0", - "apns-priority": "10", - "apns-topic": bundleId, - }; - try { - const client = http2.connect(host); - const req = client.request(headers); - req.setEncoding('utf8'); - - await new Promise((resolve, reject) => { - req.on('response', (headers, flags) => { - if (headers[":status"] !== 200) { - reject(`APNs request failed with status ${headers[":status"]}`); - } - resolve(); - }); - - req.write(JSON.stringify({ - aps: { - alert: { - title: "Shuttle is arriving", - body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.` - } - } - })); - req.end(); - }); - return true; - } catch(e) { - console.error(e); - return false; - } - } - - public static getAPNsFullUrlToUse(deviceId: string): APNsUrl { - // Construct the fetch request - const devBaseUrl = "https://api.development.push.apple.com" - const prodBaseUrl = "https://api.push.apple.com" - - let hostToUse = devBaseUrl; - if (process.env.APNS_IS_PRODUCTION === "1") { - hostToUse = prodBaseUrl; - } - - const path = "/3/device/" + deviceId; - const fullUrl = hostToUse + path; - - const constructedObject = { - fullUrl, - host: hostToUse, - path, - } - return constructedObject; + return this.appleNotificationSender.sendNotificationImmediately(deviceId, notificationAlertArguments); } private async etaSubscriberCallback(eta: IEta) { diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index 002da8f..7bfa0d9 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -7,12 +7,12 @@ interface APNsUrl { host: string; } -interface NotificationAlertArguments { +export interface NotificationAlertArguments { title: string; body: string; } -class AppleNotificationSender { +export class AppleNotificationSender { private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; @@ -60,6 +60,8 @@ class AppleNotificationSender { * notification was sent successfully. */ public async sendNotificationImmediately(deviceId: string, notificationAlertArguments: NotificationAlertArguments) { + this.reloadAPNsTokenIfTimePassed(); + const bundleId = process.env.APNS_BUNDLE_ID; if (typeof bundleId !== "string") { throw new Error("APNS_BUNDLE_ID environment variable is not set correctly");