Skip to content

Commit aa4166b

Browse files
committed
Adding meteoalarm post to processMessage along with a couple of edits for the v2 message
1 parent c66277d commit aa4166b

11 files changed

Lines changed: 683 additions & 12 deletions

File tree

docker/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ CPX_DB_CONNECTION_STRING=postgres://${CPX_DB_USERNAME}:${CPX_DB_PASSWORD}@capxml
5555
CPX_REDIS_HOST=cap-xml-redis
5656
CPX_REDIS_PORT=6379
5757
CPX_REDIS_TLS=false
58+
CPX_METEOALARM_API_URL=http://mock-api:8080 # wiremock url
59+
CPX_METEOALARM_API_USERNAME=username
60+
CPX_METEOALARM_API_PASSWORD=password
5861
PGADMIN_DEFAULT_PASSWORD=pgadmin
5962
POSTGRES_PASSWORD=postgres
6063
LIQUIBASE_COMMAND_CHANGELOG_FILE=./changelog/db.changelog-master.xml

docker/docker/wiremock/mappings/third-party-api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"urlPath": "/warnings"
77
},
88
"response": {
9-
"status": 200,
9+
"status": 201,
1010
"body": "{\"warning\": {\"uuid\": \"{{randomValue type='UUID'}}\" } }"
1111
}
1212
},

docker/scripts/register-lambda-functions.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ cpx_agw_url=$(echo CPX_AGW_URL=$deployed_cpx_agw_url)
1616
cpx_redis_host=$(echo CPX_REDIS_HOST=$CPX_REDIS_HOST)
1717
cpx_redis_port=$(echo CPX_REDIS_PORT=$CPX_REDIS_PORT)
1818
cpx_redis_tls=$(echo CPX_REDIS_TLS=$CPX_REDIS_TLS)
19-
set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls
19+
cpx_meteoalarm_api_url=$(echo CPX_METEOALARM_API_URL=$CPX_METEOALARM_API_URL)
20+
cpx_meteoalarm_api_username=$(echo CPX_METEOALARM_API_USERNAME=$CPX_METEOALARM_API_USERNAME)
21+
cpx_meteoalarm_api_password=$(echo CPX_METEOALARM_API_PASSWORD=$CPX_METEOALARM_API_PASSWORD)
22+
set -- $cpx_db_username $cpx_db_password $cpx_db_name $cpx_db_host $cpx_agw_url $cpx_redis_host $cpx_redis_port $cpx_redis_tls $cpx_meteoalarm_api_url $cpx_meteoalarm_api_username $cpx_meteoalarm_api_password
2023
custom_environment_variables=$(printf '%s,' "$@" | sed 's/,*$//g')
2124

2225
# Iterate over each file in lambda_functions_dir

lib/functions/processMessage.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ const path = require('node:path')
1111
const xsdSchema = fs.readFileSync(path.join(__dirname, '..', 'schemas', 'CAP-v1.2.xsd'), 'utf8')
1212
const additionalCapMessageSchema = require('../schemas/additionalCapMessageSchema')
1313
const Message = require('../models/message')
14-
const EA_WHO = '2.49.0.0.826.1'
14+
const EA_WHO = '2.49.0.1.826.1'
1515
const CODE = 'MCP:v2.0'
1616
const severityV2Mapping = require('../models/v2MessageMapping')
1717
const redis = require('../helpers/redis')
18+
const meteoalarm = require('../helpers/meteoalarm')
1819

1920
module.exports.processMessage = async (event) => {
2021
try {
@@ -58,8 +59,15 @@ module.exports.processMessage = async (event) => {
5859
}
5960

6061
const { message: redisMessage, query: dbQuery } = message.putQuery(message, messageV2)
61-
// store the message in database and redis/elasticache
62-
await Promise.all([service.putMessage(dbQuery), redis.set(redisMessage.identifier, redisMessage)])
62+
// store the message in database, redis/elasticache, and post to Meteoalarm
63+
await Promise.all([
64+
service.putMessage(dbQuery),
65+
redis.set(redisMessage.identifier, redisMessage),
66+
meteoalarm.postWarning(messageV2.toString(), message.identifier).catch(err => {
67+
// Log error but don't fail the entire process if Meteoalarm post fails
68+
console.error(`Failed to post to Meteoalarm: ${err.message}`)
69+
})
70+
])
6371
console.log(`Finished processing CAP message: ${message.identifier} for ${message.fwisCode}`)
6472

6573
return {
@@ -167,6 +175,7 @@ const processMessageV2 = (message, lastMessage) => {
167175
messageV2.references = referencesV2
168176
}
169177
messageV2.event = `${severityV2Mapping[message.severity]?.description}: ${messageV2.areaDesc}`
178+
messageV2.responseType = 'Monitor'
170179
messageV2.severity = severityV2Mapping[message.severity]?.severity || ''
171180
messageV2.onset = message.sent
172181
messageV2.headline = `${severityV2Mapping[message.severity]?.headline}: ${messageV2.areaDesc}`
@@ -188,5 +197,7 @@ const processMessageV2 = (message, lastMessage) => {
188197
messageV2.addParameter('use_polygon_over_geocode', 'true')
189198
messageV2.addParameter('uk_ea_ta_code', message.fwisCode)
190199

200+
messageV2.removeNode('geocode')
201+
191202
return messageV2
192203
}

lib/helpers/meteoalarm.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
'use strict'
2+
3+
const axios = require('axios')
4+
const https = require('https')
5+
let cachedToken = null
6+
let tokenExpiry = null
7+
const CPX_METEOALARM_API_URL = process.env.CPX_METEOALARM_API_URL
8+
const CPX_METEOALARM_API_USERNAME = process.env.CPX_METEOALARM_API_USERNAME
9+
const CPX_METEOALARM_API_PASSWORD = process.env.CPX_METEOALARM_API_PASSWORD
10+
const MAX_RETRIES = 3
11+
const config = {
12+
retryDelayMultiplier: 1000 // Default 1000ms, can be overridden for testing
13+
}
14+
15+
const getValidToken = async () => {
16+
// Check if we have a cached token that hasn't expired
17+
if (cachedToken && tokenExpiry && new Date() < tokenExpiry) {
18+
return cachedToken
19+
}
20+
21+
try {
22+
const response = await axios.post(`${CPX_METEOALARM_API_URL}/tokens`, {
23+
username: CPX_METEOALARM_API_USERNAME,
24+
password: CPX_METEOALARM_API_PASSWORD
25+
}, {
26+
headers: {
27+
'Content-Type': 'application/json'
28+
},
29+
httpsAgent: new https.Agent({
30+
rejectUnauthorized: false
31+
})
32+
})
33+
34+
if (response.status !== 200) {
35+
throw new Error(`Failed to authenticate: ${response.status} ${response.statusText}`)
36+
}
37+
38+
cachedToken = response.data.token
39+
// Set token expiry to 1 hour from now
40+
tokenExpiry = new Date(Date.now() + 3600000)
41+
return cachedToken
42+
} catch (err) {
43+
console.error('Error fetching bearer token:', err.message)
44+
throw new Error(`Failed to authenticate with Meteoalarm: ${err.message}`)
45+
}
46+
}
47+
48+
const postWarning = async (xmlMessage, identifier) => {
49+
let lastError = null
50+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
51+
try {
52+
const token = await getValidToken()
53+
const response = await axios.post(`${CPX_METEOALARM_API_URL}/warnings`, xmlMessage, {
54+
headers: {
55+
Authorization: `Bearer ${token}`,
56+
'Content-Type': 'application/xml'
57+
},
58+
timeout: 10000,
59+
httpsAgent: new https.Agent({
60+
rejectUnauthorized: false
61+
})
62+
})
63+
64+
if (response.status === 201) {
65+
console.log(`Successfully posted warning to Meteoalarm: ${identifier}`)
66+
console.log(response.data)
67+
return response.data
68+
}
69+
throw new Error(`Received non-201 response: ${response.status}`)
70+
} catch (err) {
71+
lastError = err
72+
console.error(`Meteoalarm post attempt ${attempt} failed: ${err.message}`)
73+
if (err.response?.data) {
74+
console.error(JSON.stringify(err.response.data))
75+
}
76+
77+
// If it's a 401 error, clear the cached token and retry
78+
if (err.response?.status === 401) {
79+
console.log('Received 401, clearing cached token')
80+
cachedToken = null
81+
tokenExpiry = null
82+
}
83+
84+
// If this isn't the last attempt, wait before retrying
85+
if (attempt < MAX_RETRIES) {
86+
const delayMs = attempt * config.retryDelayMultiplier
87+
console.log(`Waiting ${delayMs}ms before retry...`)
88+
await new Promise(resolve => setTimeout(resolve, delayMs))
89+
}
90+
}
91+
}
92+
throw new Error(`Failed to post warning to Meteoalarm after ${MAX_RETRIES} attempts: ${lastError.message}`)
93+
}
94+
95+
const clearTokenCache = () => {
96+
cachedToken = null
97+
tokenExpiry = null
98+
}
99+
100+
const setRetryDelayMultiplier = (multiplier) => {
101+
config.retryDelayMultiplier = multiplier
102+
}
103+
104+
module.exports = {
105+
postWarning,
106+
clearTokenCache,
107+
// Export for testing
108+
getValidToken,
109+
setRetryDelayMultiplier
110+
}

lib/models/message.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ class Message {
8989
this.getFirstElement('event').textContent = value
9090
}
9191

92+
get responseType () {
93+
return this.getFirstElement('responseType')?.textContent || ''
94+
}
95+
96+
set responseType (value) {
97+
const responseTypeEl = this.getFirstElement('responseType')
98+
if (responseTypeEl) {
99+
responseTypeEl.textContent = value
100+
} else {
101+
this.addElement('event', 'responseType', value)
102+
}
103+
}
104+
92105
get severity () {
93106
return this.getFirstElement('severity')?.textContent || ''
94107
}
@@ -176,6 +189,14 @@ class Message {
176189
}
177190
}
178191

192+
removeNode (name) {
193+
const nodes = this.doc.getElementsByTagName(name)
194+
for (let i = nodes.length - 1; i >= 0; i--) {
195+
const node = nodes[i]
196+
node.parentNode.removeChild(node)
197+
}
198+
}
199+
179200
toString () {
180201
return xmlFormat(new xmldom.XMLSerializer().serializeToString(this.doc), { indentation: ' ', collapseContent: true })
181202
}

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ This project provides CAP XML services through the use of AWS Lambda.
2020
| CPX_REDIS_HOST | Redis/Elasticache host| yes | | |
2121
| CPX_REDIS_PORT | Redis/Elasticache port| yes | | |
2222
| CPX_REDIS_TLS | Redis/Elasticache tls | yes | | |
23+
| CPX_METEOALARM_API_URL | Meteoalarm url | yes | | |
24+
| CPX_METEOALARM_API_USERNAME | Meteoalarm username | yes | | |
25+
| CPX_METEOALARM_API_PASSWORD | Meteoalarm password | yes | | |
2326

2427
## Prerequisites
2528

0 commit comments

Comments
 (0)