as-services/services/tasks/src/queues/alert-notify.js
devthejo 74e1b5f10b
Some checks failed
/ build (map[dockerfile:./services/api/Dockerfile name:api]) (push) Successful in 1m28s
/ build (map[dockerfile:./services/app/Dockerfile name:app]) (push) Successful in 1m45s
/ build (map[dockerfile:./services/files/Dockerfile name:files]) (push) Successful in 1m55s
/ build (map[dockerfile:./services/watchers/Dockerfile name:watchers]) (push) Successful in 2m10s
/ build (map[dockerfile:./services/tasks/Dockerfile name:tasks]) (push) Successful in 2m18s
/ build (map[dockerfile:./services/hasura/Dockerfile name:hasura]) (push) Successful in 2m20s
/ build (map[dockerfile:./services/web/Dockerfile name:web]) (push) Failing after 12m45s
/ deploy (push) Has been cancelled
fix: multi device lost notifications
2025-08-24 13:01:30 +02:00

250 lines
6.8 KiB
JavaScript

const async = require("async")
const { ctx } = require("@modjo/core")
const { taskCtx } = require("@modjo/microservice-worker/ctx")
const humanizeDistance = require("utils/geo/humanizeDistance")
const addNotification = require("~/services/add-notification")
const {
getAlertNotificationExpirationInSeconds,
ALERT_NOTIFICATION_EXPIRATION_INTERVAL,
} = require("~/constants/time")
const {
LEVEL_COLORS,
LEVEL_DESCRIPTIONS,
} = require("~/constants/notifications")
const MAX_PARALLEL_PUSHES = 10
function createAlertNotification({
code,
reason,
level,
alertId,
alertingId,
initialDistance,
}) {
const baseText =
reason === "relative"
? "Un de vos proches a besoin d'aide"
: "Besoin d'aide"
const levelText = LEVEL_DESCRIPTIONS[level] || LEVEL_DESCRIPTIONS.green
const distanceText = initialDistance
? ` à ${humanizeDistance(initialDistance)}`
: ""
const notificationText = `${baseText} - ${levelText}${distanceText}`
const color = LEVEL_COLORS[level] || LEVEL_COLORS.green
const expires = getAlertNotificationExpirationInSeconds()
return {
data: {
action: "alert",
alertingId,
alertId,
level,
expires,
code,
initialDistance,
reason,
},
notification: {
title: `Nouvelle Alerte - #${code}`,
body: notificationText,
channel: "alert",
icon: `notif-${level}`,
priority: "high",
ttl: ALERT_NOTIFICATION_EXPIRATION_INTERVAL,
color,
actionId: "open-alert",
},
}
}
const numericLevel = {
green: 1,
yellow: 2,
red: 3,
}
module.exports = async function () {
async function setNotificationSent(alertingId, state) {
const sql = ctx.require("postgres")
const logger = taskCtx.require("logger")
logger.debug({ alertingId, state }, "Setting notification sent state")
if (state === false) {
logger.debug(
{ alertingId },
"Updating alerting record to mark notification_sent as FALSE"
)
await sql`
UPDATE
"alerting"
SET
"notification_sent" = FALSE
WHERE
"id" = ${alertingId}
`
} else {
logger.debug(
{ alertingId },
"Updating alerting record to mark notification_sent as TRUE with current timestamp"
)
await sql`
UPDATE
"alerting"
SET
"notification_sent" = TRUE,
"notification_sent_at" = NOW()
WHERE
"id" = ${alertingId}
`
}
logger.debug({ alertingId }, "Successfully updated notification sent state")
}
return Object.assign(
async function alertNotify(params) {
const logger = taskCtx.require("logger")
logger.info({ params }, "Starting alertNotify process")
const sql = ctx.require("postgres")
const { alertingId } = params
logger.debug({ alertingId }, "Querying alerting record")
const [alertingRow] = await sql`
SELECT
"alerting"."device_id" as "deviceId",
"alerting"."alert_id" as "alertId",
"alerting"."user_id" as "userId",
"alerting"."reason" as "reason",
"alerting"."initial_distance" as "initialDistance"
FROM
"alerting"
WHERE
"alerting"."id" = ${alertingId}
AND "alerting"."notification_sent" IS NULL
`
if (!alertingRow) {
logger.info({ alertingId }, "No alerting record found, exiting process")
return
}
const { reason, alertId, userId: alertingUserId } = alertingRow
logger.debug({ reason, alertId, alertingUserId }, "Found alerting record")
const devicesList = await sql`
SELECT
"id",
"fcm_token" as "fcmToken",
"notification_alert_level" as "notificationAlertLevel"
FROM
"device"
WHERE
"user_id" = ${alertingUserId}
AND "fcm_token" IS NOT NULL
`
const devices = devicesList.map((device) => ({ ...device }))
logger.debug({ alertId }, "Querying alert record")
const [{ userId: alertUserId, level, code }] = await sql`
SELECT
"alert"."level" as "level",
"alert"."user_id" as "userId",
"alert"."code" as "code"
FROM
"alert"
WHERE
"alert"."id" = ${alertId}
`
let sentOnce = false
await async.allLimit(devices, MAX_PARALLEL_PUSHES, async (device) => {
const { id: deviceId, fcmToken } = device
const notificationAlertLevel = device.notificationAlertLevel || "green"
logger.debug(
{ deviceId, notificationAlertLevel },
"Found device record"
)
if (alertUserId === alertingUserId) {
logger.info(
{ alertUserId, alertingUserId },
"Alert creator matches alerting user, skipping notification"
)
await setNotificationSent(alertingId, false)
// user doesn't receive it's own alerts notifications
// disable/comment return for dev/debug
return
}
const acceptLevel =
numericLevel[level] >= numericLevel[notificationAlertLevel]
logger.debug(
{ level, notificationAlertLevel, acceptLevel },
"Checking alert level threshold"
)
if (!acceptLevel) {
logger.info(
{ level, notificationAlertLevel },
"Alert level below device threshold, skipping notification"
)
await setNotificationSent(alertingId, false)
return
}
try {
logger.debug({ deviceId }, "Attempting to send push notification")
const notificationConfig = createAlertNotification({
code,
reason,
level,
alertId,
alertingId,
initialDistance: alertingRow.initialDistance,
})
const { messageId, success } = await addNotification({
fcmToken,
deviceId,
...notificationConfig,
userId: alertingUserId,
type: "alert",
})
if (!success) {
logger.error(
{ deviceId, alertingId },
"Failed to send push notification"
)
return
}
logger.info(
{ messageId, alertingId },
"Successfully sent push notification"
)
sentOnce = true
} catch (e) {
logger.error(
{ error: e, deviceId, alertingId },
"Failed to send push notification"
)
console.error(e)
}
})
logger.info(
{ alertingId, sentOnce },
"Setting final notification sent state"
)
await setNotificationSent(alertingId, sentOnce)
},
{
dedupOptions: { enabled: true },
}
)
}