Skip to content

Commit

Permalink
Add a triggerEvent helper method
Browse files Browse the repository at this point in the history
  • Loading branch information
swansontec committed Aug 5, 2022
1 parent 32b732a commit 615dddd
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 44 deletions.
11 changes: 8 additions & 3 deletions src/price-script/checkPriceChanges.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import io from '@pm2/io'
import { MetricType } from '@pm2/io/build/main/services/metrics'
import nano from 'nano'

import { syncedSettings } from '../db/couchSettings'
import { CurrencyThreshold } from '../models/CurrencyThreshold'
import { Device } from '../models/Device'
import { User } from '../models/User'
import { PushResult, PushSender } from '../util/pushSender'
import { serverConfig } from '../serverConfig'
import { makePushSender, PushResult } from '../util/pushSender'
import { fetchThresholdPrice } from './fetchThresholdPrices'

// Firebase Messaging API limits batch messages to 500
Expand All @@ -23,7 +25,10 @@ export interface NotificationPriceChange {
priceChange: number
}

export async function checkPriceChanges(sender: PushSender): Promise<void> {
export async function checkPriceChanges(apiKey: string): Promise<void> {
const { couchUri } = serverConfig
const sender = await makePushSender(nano(couchUri))

// Sends a notification to devices about a price change
async function sendNotification(
thresholdPrice: NotificationPriceChange,
Expand All @@ -40,7 +45,7 @@ export async function checkPriceChanges(sender: PushSender): Promise<void> {
const body = `${currencyCode} is ${direction} ${symbol}${priceChange}% to $${displayPrice} in the last ${time}.`
const data = {}

return await sender.send(title, body, deviceTokens, data)
return await sender.send(apiKey, deviceTokens, { title, body, data })
}

// Fetch list of threshold items and their prices
Expand Down
17 changes: 2 additions & 15 deletions src/price-script/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import io from '@pm2/io'
import { makePeriodicTask } from 'edge-server-tools'
import nano from 'nano'

import { getApiKeyByKey } from '../db/couchApiKeys'
import { syncedSettings } from '../db/couchSettings'
import { setupDatabases } from '../db/couchSetup'
import { serverConfig } from '../serverConfig'
import { makePushSender } from '../util/pushSender'
import { checkPriceChanges } from './checkPriceChanges'

const runCounter = io.counter({
Expand All @@ -25,24 +23,13 @@ async function main(): Promise<void> {
throw new Error('No partner apiKeys')
}

// Read the API keys from settings:
const senders = await Promise.all(
syncedSettings.doc.apiKeys.map(async partner => {
const apiKey = await getApiKeyByKey(connection, partner.apiKey)
if (apiKey == null) {
throw new Error(`Cannot find API key ${partner.apiKey}`)
}
return await makePushSender(apiKey)
})
)

// Check the prices every few minutes:
const task = makePeriodicTask(
async () => {
runCounter.inc()

for (const sender of senders) {
await checkPriceChanges(sender)
for (const apiKey of syncedSettings.doc.apiKeys) {
await checkPriceChanges(apiKey.apiKey)
}
},
60 * 1000 * syncedSettings.doc.priceCheckInMinutes,
Expand Down
7 changes: 4 additions & 3 deletions src/routes/notificationRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { makePushSender } from '../util/pushSender'
* Response body: unused
*/
export const sendNotificationV1Route: Serverlet<ApiRequest> = async request => {
const { apiKey, json, log } = request
const { apiKey, connection, json, log } = request
const { title, body, data, userId } = asSendNotificationBody(json)

if (!apiKey.admin) return errorResponse('Not an admin', { status: 401 })
const sender = await makePushSender(apiKey)
const sender = makePushSender(connection)

const user = await User.fetch(userId)
if (user == null) {
Expand All @@ -34,7 +34,8 @@ export const sendNotificationV1Route: Serverlet<ApiRequest> = async request => {
}
}

const response = await sender.send(title, body, tokens, data)
const message = { title, body, data }
const response = await sender.send(apiKey.apiKey, tokens, message)
const { successCount, failureCount } = response
log(
`Sent notifications to user ${userId} devices: ${successCount} success - ${failureCount} failure`
Expand Down
81 changes: 58 additions & 23 deletions src/util/pushSender.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import io from '@pm2/io'
import admin from 'firebase-admin'
import { ServerScope } from 'nano'

import { ApiKey } from '../types/pushTypes'
import { getApiKeyByKey } from '../db/couchApiKeys'

const successCounter = io.counter({
id: 'notifications:success:total',
Expand All @@ -19,43 +20,77 @@ export interface PushResult {

export interface PushSender {
send: (
title: string,
body: string,
tokens: string[],
data?: { [key: string]: string }
apiKey: string,
deviceTokens: string[],
message: {
title?: string
body?: string
data?: { [key: string]: string }
}
) => Promise<PushResult>
}

export async function makePushSender(apiKey: ApiKey): Promise<PushSender> {
const name = `app:${apiKey.appId}`
let app: admin.app.App
try {
admin.app(name)
} catch (err) {
app = admin.initializeApp(
{
// TODO: We have never passed the correct data type here,
// so either update our database or write a translation layer:
credential: admin.credential.cert(apiKey.adminsdk as any)
},
name
/**
* Creates a push notification sender object.
* This object uses a cache to map appId's to Firebase credentials,
* based on the Couch database.
*/
export function makePushSender(connection: ServerScope): PushSender {
// Map apiKey's to message senders, or `null` if missing:
const senders = new Map<string, admin.messaging.Messaging | null>()

async function getSender(
apiKey: string
): Promise<admin.messaging.Messaging | null> {
const cached = senders.get(apiKey)
// Null is a valid cache hit:
if (cached !== undefined) {
return cached
}

// Look up the API key for this appId:
const apiKeyRow = await getApiKeyByKey(connection, apiKey)
if (apiKeyRow == null || apiKeyRow.adminsdk == null) {
senders.set(apiKey, null)
return null
}

// TODO: We have never passed the correct data type here,
// so either update our database or write a translation layer:
const serviceAccount: any = apiKeyRow.adminsdk

// Create a sender if we have an API key for them:
const app = admin.initializeApp(
{ credential: admin.credential.cert(serviceAccount) },
serviceAccount.projectId ?? serviceAccount.project_id
)
const sender = app.messaging()
senders.set(apiKey, sender)
return sender
}

return {
async send(title, body, tokens, data = {}) {
const response = await app
.messaging()
async send(apiKey, tokens, message) {
const { title = '', body = '', data = {} } = message

const failure = {
successCount: 0,
failureCount: tokens.length
}

const sender = await getSender(apiKey)
if (sender == null) return failure

const response = await sender
.sendMulticast({
data,
notification: { title, body },
tokens
})
.catch(() => ({ successCount: 0, failureCount: tokens.length }))
.catch(() => failure)

successCounter.inc(response.successCount)
failureCounter.inc(response.failureCount)

return response
}
}
Expand Down
53 changes: 53 additions & 0 deletions src/util/triggerEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ServerScope } from 'nano'

import { getDeviceById, getDevicesByLoginId } from '../db/couchDevices'
import { PushEventRow } from '../db/couchPushEvents'
import { PushSender } from './pushSender'

/**
* Handles all the effects once a row has been triggered.
*/
export async function triggerEvent(
connection: ServerScope,
sender: PushSender,
eventRow: PushEventRow,
date: Date
): Promise<void> {
const { event } = eventRow
const { broadcastTxs = [], pushMessage } = event

if (pushMessage != null) {
const deviceRows =
event.deviceId != null
? [await getDeviceById(connection, event.deviceId, date)]
: event.loginId != null
? await getDevicesByLoginId(connection, event.loginId)
: []

// Sort the devices by app:
const apiKeys = new Map<string, string[]>()
for (const row of deviceRows) {
const { apiKey, deviceToken } = row.device
if (apiKey == null || deviceToken == null) continue
const tokens = apiKeys.get(apiKey) ?? []
tokens.push(deviceToken)
apiKeys.set(apiKey, tokens)
}

for (const [apiKey, tokens] of apiKeys) {
await sender.send(apiKey, tokens, pushMessage)
}

// TODO: Take note of any errors.
event.pushMessageError = undefined
}

for (const tx of broadcastTxs) {
console.log(tx) // TODO
event.broadcastTxErrors = []
}

event.state = event.recurring ? 'waiting' : 'triggered'
event.triggered = date
await eventRow.save()
}

0 comments on commit 615dddd

Please sign in to comment.