@atmosx/event-product-parser 3.0.44 → 3.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -31
- package/SUPPORTED_EVENTS.md +369 -0
- package/dist/cjs/index.cjs +681 -403
- package/dist/esm/index.mjs +659 -382
- package/dist/index.d.mts +54 -25
- package/dist/index.d.ts +54 -25
- package/package.json +1 -1
- package/src/@building/building.attachments.ts +83 -0
- package/src/@building/building.enhance.ts +7 -3
- package/src/@building/building.office.ts +2 -2
- package/src/@building/building.properties.ts +12 -11
- package/src/@building/building.signature.ts +11 -11
- package/src/@building/building.tags.ts +2 -2
- package/src/@building/building.tracking.ts +3 -3
- package/src/@building/building.validate.ts +2 -2
- package/src/@core/core.createEvent.ts +83 -0
- package/src/@dictionaries/{dictionaries.eventActions.ts → dictionaries.actions.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventAwipAbreviations.ts → dictionaries.awips.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventCancelMessages.ts → dictionaries.cancellation.ts} +2 -1
- package/src/@dictionaries/{dictionaries.eventCauses.ts → dictionaries.causes.ts} +1 -1
- package/src/@dictionaries/{dictionaries.statusCorrelationText.ts → dictionaries.correlations.ts} +1 -1
- package/src/@dictionaries/{dictionaries.betterEventNames.ts → dictionaries.enhanced.ts} +13 -2
- package/src/@dictionaries/{dictionaries.eventTypes.ts → dictionaries.events.ts} +1 -1
- package/src/@dictionaries/{dictionaries.regExp.ts → dictionaries.expressions.ts} +2 -1
- package/src/@dictionaries/{dictionaries.hailStrings.ts → dictionaries.hail.ts} +4 -1
- package/src/@dictionaries/{dictionaries.officeICAOs.ts → dictionaries.icao.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventsMatchText.ts → dictionaries.matches.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventProducts.ts → dictionaries.products.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventRecords.ts → dictionaries.records.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventSeverity.ts → dictionaries.severity.ts} +1 -1
- package/src/@dictionaries/{dictionaries.shapefileLinks.ts → dictionaries.shapefiles.ts} +1 -1
- package/src/@dictionaries/dictionaries.states.ts +72 -0
- package/src/@dictionaries/{dictionaries.eventStatus.ts → dictionaries.status.ts} +1 -1
- package/src/@dictionaries/{dictionaries.eventTags.ts → dictionaries.tags.ts} +1 -1
- package/src/@dictionaries/{dictionaries.testSignatures.ts → dictionaries.testing.ts} +3 -2
- package/src/@events/events.api.ts +18 -15
- package/src/@events/events.text.ts +2 -2
- package/src/@events/events.ugc.ts +2 -2
- package/src/@manager/manager.mkEvent.ts +3 -3
- package/src/@manager/manager.rmEvent.ts +3 -2
- package/src/@manager/manager.updateListener.ts +163 -0
- package/src/@modules/@database/database.broadcastify.ts +67 -0
- package/src/@modules/@database/database.init.ts +14 -1
- package/src/@modules/@database/database.shapefiles.ts +3 -6
- package/src/@modules/@eas/eas.getCleanDescription.ts +2 -0
- package/src/@modules/@eas/eas.getTTS.ts +19 -8
- package/src/@modules/@eas/eas.setEasTone.ts +15 -10
- package/src/@modules/@stanza/stanza.getAwipsType.ts +2 -2
- package/src/@modules/@stanza/stanza.validate.ts +3 -3
- package/src/@modules/@utilities/utilities.createHttp.ts +56 -47
- package/src/@modules/@utilities/utilities.setCronSchedule.ts +5 -2
- package/src/@modules/@xmpp/xmpp.xReconnect.ts +1 -1
- package/src/@parsers/@hvtec/hvtec.extract.ts +8 -8
- package/src/@parsers/@pvtec/pvtec.extract.ts +12 -12
- package/src/@parsers/@text/text.getDescriptionFromProduct.ts +2 -2
- package/src/@parsers/@text/text.getEmbed.ts +58 -0
- package/src/@parsers/@ugc/ugc.header.ts +3 -3
- package/src/@types/type.event.ts +3 -3
- package/src/@types/{type.webhook.ts → type.listener.ts} +11 -7
- package/src/@types/type.properties.ts +10 -9
- package/src/@types/type.settings.ts +13 -6
- package/src/bootstrap.ts +13 -6
- package/src/index.ts +2 -1
- package/test.js +30 -22
- package/src/@building/building.outlooks.ts +0 -36
- package/src/@manager/manager.createWebhook.ts +0 -129
- package/src/@manager/manager.updateWebhooks.ts +0 -49
- package/src/@modules/@utilities/utilities.isImageReady.ts +0 -27
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| '_ ` _ \ / _ \/ __| '_ \| '_ \ / _ \ '__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Created with ♥ by the AtmosphericX Team (KiyoWx, StarflightWx, & CJ Ziegler)
|
|
12
|
+
Discord: https://atmosphericx-discord.scriptkitty.cafe
|
|
13
|
+
Ko-Fi: https://ko-fi.com/k3yomi
|
|
14
|
+
Documentation: http://localhost/documentation | https://atmosphericx.scriptkitty.cafe/documentation
|
|
15
|
+
|
|
16
|
+
Internal Package: @atmosx/event-product-parser
|
|
17
|
+
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import { TypeEvent } from "../@types/type.event";
|
|
22
|
+
import { TypeListener } from "../@types/type.listener";
|
|
23
|
+
import { bootstrap } from "../bootstrap"
|
|
24
|
+
import { setEasTone } from "../@modules/@eas/eas.setEasTone"
|
|
25
|
+
import { setTimeoutAction } from "../@modules/@utilities/utilities.setTimeoutAction";
|
|
26
|
+
import { getEmebed } from "../@parsers/@text/text.getEmbed";
|
|
27
|
+
import { getCleanedEvent } from "../@building/building.clean"
|
|
28
|
+
import { createHttp } from "../@modules/@utilities/utilities.createHttp";
|
|
29
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from "fs";
|
|
30
|
+
import FormData from "form-data";
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export const updateListener = async (event: TypeEvent): Promise<void> => {
|
|
35
|
+
const settings = bootstrap.settings;
|
|
36
|
+
const listeners = settings.ListenerSettings as TypeListener[];
|
|
37
|
+
const properties = event.properties;
|
|
38
|
+
const metadata = {
|
|
39
|
+
file: null,
|
|
40
|
+
eas: null,
|
|
41
|
+
name: properties.event,
|
|
42
|
+
status: properties.status,
|
|
43
|
+
description: properties.description,
|
|
44
|
+
tracking: properties.metadata.tracking,
|
|
45
|
+
header: properties.metadata.header,
|
|
46
|
+
raw: properties.metadata.raw,
|
|
47
|
+
expired: properties.status_metadata.is_expired,
|
|
48
|
+
attachments: properties.metadata.attachments
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const listener of listeners) {
|
|
52
|
+
const events = listener?.events;
|
|
53
|
+
const webhook = listener?.webhook;
|
|
54
|
+
const uploads = listener?.uploads
|
|
55
|
+
const isMatched = events.some(pattern => {
|
|
56
|
+
if (!pattern) return false;
|
|
57
|
+
if (pattern === "*" || pattern === metadata.name) return true;
|
|
58
|
+
if (pattern.includes("*")) {
|
|
59
|
+
const regex = "^" +
|
|
60
|
+
pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$";
|
|
61
|
+
return new RegExp(regex).test(metadata.name);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (events?.length == 0 || isMatched) {
|
|
67
|
+
if (uploads?.eas) {
|
|
68
|
+
metadata.eas = await setEasTone({
|
|
69
|
+
title: `${metadata.name}_${metadata.status}_${metadata.tracking}`,
|
|
70
|
+
message: metadata.description,
|
|
71
|
+
header: metadata.header
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (uploads?.file) {
|
|
76
|
+
const fDestination = settings.GlobalSettings.ArchiveSettings.TextDirectory;
|
|
77
|
+
if (fDestination) {
|
|
78
|
+
if (!existsSync(fDestination)) { mkdirSync(fDestination, { recursive: true }); }
|
|
79
|
+
if (existsSync(`${fDestination}/${metadata.name}_${metadata.status}_${metadata.tracking}.txt`)) {
|
|
80
|
+
appendFileSync(`${fDestination}/${metadata.name}_${metadata.status}_${metadata.tracking}.txt`, metadata.raw);
|
|
81
|
+
} else {
|
|
82
|
+
writeFileSync(`${fDestination}/${metadata.name}_${metadata.status}_${metadata.tracking}.txt`, metadata.raw);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
metadata.file = metadata.raw;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (uploads?.event) {
|
|
89
|
+
const eDestination = settings.GlobalSettings.ArchiveSettings.EventDirectory;
|
|
90
|
+
if (eDestination) {
|
|
91
|
+
if (!existsSync(eDestination)) { mkdirSync(eDestination, { recursive: true }); }
|
|
92
|
+
writeFileSync(`${eDestination}/${metadata.name}_${metadata.status}_${metadata.tracking}.json`, JSON.stringify(getCleanedEvent(event), null, 2));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (webhook.enabled && webhook.destination) {
|
|
97
|
+
const ratelimit = setTimeoutAction({ identifier: webhook.destination, interval: webhook.ratelimit, max: webhook.ratelimit, addTime: true })
|
|
98
|
+
const form = new FormData();
|
|
99
|
+
const embed = {
|
|
100
|
+
title: `${metadata.name} (${metadata.status})`,
|
|
101
|
+
description: getEmebed(event),
|
|
102
|
+
fields: [],
|
|
103
|
+
color: 16711680,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
footer: { text: webhook.title ?? `AtmosphericX` }
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (ratelimit.limited) { return }
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if (metadata.description && !metadata.expired) {
|
|
112
|
+
if (metadata.description.length > 900) {
|
|
113
|
+
metadata.description = metadata.description.substring(0, 900) + "\n\n[Message truncated due to length]";
|
|
114
|
+
}
|
|
115
|
+
embed.fields.push({
|
|
116
|
+
name: "Description",
|
|
117
|
+
value: metadata.description ? '```' + '\n' + metadata.description.split('\n').map(l => l.trim()).filter(Boolean).join('\n') + '\n' + '```' : ""
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if (metadata.attachments?.length > 0) {
|
|
123
|
+
metadata.attachments = metadata.attachments.slice(0, 5);
|
|
124
|
+
embed.fields.push({
|
|
125
|
+
name: "Attachments",
|
|
126
|
+
value: metadata.attachments.map(attachment => `- [${attachment.name.length > 45 ? attachment.name.substring(0, 45) + '...' : attachment.name}](${attachment.link})`).join('\n')
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (uploads?.file) {
|
|
131
|
+
form.append("fUpload", Buffer.from(metadata.file), {
|
|
132
|
+
filename: `${properties.event}_${properties.status}_${properties.metadata.tracking}.txt`,
|
|
133
|
+
contentType: "application/text"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (uploads?.eas) {
|
|
138
|
+
if (metadata.eas) {
|
|
139
|
+
const file = readFileSync(metadata.eas)
|
|
140
|
+
form.append("fEas", Buffer.from(file), {
|
|
141
|
+
filename: `${properties.event}_${properties.status}_${properties.metadata.tracking}_eas.mp3`,
|
|
142
|
+
contentType: "audio/mpeg"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
form.append("payload_json", JSON.stringify({
|
|
148
|
+
username: webhook.title ?? "AtmosphericX",
|
|
149
|
+
content: webhook.message ?? "",
|
|
150
|
+
embeds: [embed]
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
await createHttp({
|
|
154
|
+
url: webhook.destination,
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
method: `POST`,
|
|
157
|
+
headers: form.getHeaders(),
|
|
158
|
+
body: form
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| '_ ` _ \ / _ \/ __| '_ \| '_ \ / _ \ '__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Created with ♥ by the AtmosphericX Team (KiyoWx, StarflightWx, & CJ Ziegler)
|
|
12
|
+
Discord: https://atmosphericx-discord.scriptkitty.cafe
|
|
13
|
+
Ko-Fi: https://ko-fi.com/k3yomi
|
|
14
|
+
Documentation: http://localhost/documentation | https://atmosphericx.scriptkitty.cafe/documentation
|
|
15
|
+
|
|
16
|
+
Internal Package: @atmosx/event-product-parser
|
|
17
|
+
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { bootstrap } from "../../bootstrap"
|
|
21
|
+
import { createHttp } from '../@utilities/utilities.createHttp'
|
|
22
|
+
import { setWarning } from "../@utilities/utilities.setWarning";
|
|
23
|
+
|
|
24
|
+
export const importBroadcastify = async (): Promise<void> => {
|
|
25
|
+
const settings = bootstrap.settings;
|
|
26
|
+
|
|
27
|
+
const isBroadcastifyImported = bootstrap.database
|
|
28
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='broadcastify';`)
|
|
29
|
+
.get();
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if (!settings.BroadcastifySettings.BroadcastifyAttachments) { return }
|
|
33
|
+
if (isBroadcastifyImported) { return }
|
|
34
|
+
|
|
35
|
+
const broadcastify = await createHttp({
|
|
36
|
+
url: settings.BroadcastifySettings.BroadcastifyDatabase,
|
|
37
|
+
timeout: 5000,
|
|
38
|
+
});
|
|
39
|
+
if (!broadcastify.error) {
|
|
40
|
+
bootstrap.database.prepare(`CREATE TABLE broadcastify ( state TEXT, county TEXT, feed TEXT, type TEXT, link TEXT);`)
|
|
41
|
+
.run();
|
|
42
|
+
const states = JSON.parse(broadcastify.message);
|
|
43
|
+
const insert = bootstrap.database.prepare(`INSERT INTO broadcastify (state, county, feed, type, link) VALUES (?, ?, ?, ?, ?)`);
|
|
44
|
+
const transaction = bootstrap.database.transaction((rows: any[]) => {
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
insert.run(row);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const batch: any[] = [];
|
|
50
|
+
for (const state of states) {
|
|
51
|
+
setWarning({ message: `Importing ${state.counties.length} counties from ${state.state} for Broadcastify` });
|
|
52
|
+
for (const county of state.counties) {
|
|
53
|
+
const countyName = county.county;
|
|
54
|
+
for (const feed of county.feeds) {
|
|
55
|
+
batch.push([
|
|
56
|
+
state.state,
|
|
57
|
+
countyName,
|
|
58
|
+
feed.name,
|
|
59
|
+
feed.type,
|
|
60
|
+
feed.link
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
transaction(batch);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -23,6 +23,7 @@ import { TypeSettings } from "../../@types/type.settings"
|
|
|
23
23
|
import { bootstrap } from "../../bootstrap"
|
|
24
24
|
import { setWarning } from '../@utilities/utilities.setWarning'
|
|
25
25
|
import { importShapefiles } from './database.shapefiles'
|
|
26
|
+
import { importBroadcastify } from "./database.broadcastify"
|
|
26
27
|
|
|
27
28
|
export const initializeDatabase = async (): Promise<void> => {
|
|
28
29
|
const settings = bootstrap.settings as TypeSettings;
|
|
@@ -38,7 +39,19 @@ export const initializeDatabase = async (): Promise<void> => {
|
|
|
38
39
|
bootstrap.database
|
|
39
40
|
.prepare(`CREATE TABLE IF NOT EXISTS shapefiles (id TEXT PRIMARY KEY, location TEXT, geometry TEXT)`)
|
|
40
41
|
.run();
|
|
41
|
-
|
|
42
|
+
const isNeedingShapefiles = bootstrap.database
|
|
43
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='shapefiles';`)
|
|
44
|
+
.get();
|
|
45
|
+
const isNeedingBroadcastify = bootstrap.database
|
|
46
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='broadcastify';`)
|
|
47
|
+
.get();
|
|
48
|
+
|
|
49
|
+
if (!isNeedingShapefiles || !isNeedingBroadcastify) {
|
|
50
|
+
setWarning({message: `Required database tables are currently building, please ${bootstrap.ansi_colors.RED}DO NOT${bootstrap.ansi_colors.RESET} close your terminal. The building will not finish and will remain incomplete. If you do mess up, you will need to delete ${settings.Database} and restart the application.` })
|
|
51
|
+
await importBroadcastify();
|
|
52
|
+
await importShapefiles();
|
|
53
|
+
setWarning({ message: `Building has completed, you can now continue or close the terminal`})
|
|
54
|
+
}
|
|
42
55
|
} catch (error) {
|
|
43
56
|
setWarning({ message: `An error occurred while initializing the database: ${error.message}` })
|
|
44
57
|
}
|
|
@@ -25,7 +25,7 @@ import { TypeSettings } from "../../@types/type.settings"
|
|
|
25
25
|
import { bootstrap } from "../../bootstrap"
|
|
26
26
|
import { setSleep } from '../@utilities/utilities.setSleep'
|
|
27
27
|
import { setWarning } from '../@utilities/utilities.setWarning'
|
|
28
|
-
import {
|
|
28
|
+
import { dict_shapefiles } from '../../@dictionaries/dictionaries.shapefiles'
|
|
29
29
|
|
|
30
30
|
export const importShapefiles = async (): Promise<void> => {
|
|
31
31
|
const settings = bootstrap.settings as TypeSettings;
|
|
@@ -35,8 +35,7 @@ export const importShapefiles = async (): Promise<void> => {
|
|
|
35
35
|
.get().count;
|
|
36
36
|
if (tShapefiles === 0) {
|
|
37
37
|
await setSleep({timeout: 1e3});
|
|
38
|
-
|
|
39
|
-
for (const shapefile of shapefileLinks) {
|
|
38
|
+
for (const shapefile of dict_shapefiles) {
|
|
40
39
|
const response = await fetch(shapefile.link);
|
|
41
40
|
const arrayBuff = await response.arrayBuffer();
|
|
42
41
|
const content = await loadAsync(arrayBuff);
|
|
@@ -56,7 +55,7 @@ export const importShapefiles = async (): Promise<void> => {
|
|
|
56
55
|
filepath,
|
|
57
56
|
filepath,
|
|
58
57
|
);
|
|
59
|
-
setWarning({ message: `Importing ${features.length} features from ${shapefile.name}_${shapefile.id}` })
|
|
58
|
+
setWarning({ message: `Importing ${features.length} features from ${shapefile.name}_${shapefile.id} for Shapefiles` })
|
|
60
59
|
const insert = bootstrap.database
|
|
61
60
|
.prepare(`INSERT OR REPLACE INTO shapefiles (id, location, geometry) VALUES (?, ?, ?)`)
|
|
62
61
|
const transaction = bootstrap.database.transaction((entries: any[]) => {
|
|
@@ -84,10 +83,8 @@ export const importShapefiles = async (): Promise<void> => {
|
|
|
84
83
|
})
|
|
85
84
|
fs.unlinkSync(`${filepath}.shp`)
|
|
86
85
|
fs.unlinkSync(`${filepath}.dbf`)
|
|
87
|
-
setWarning({ message: `Cleaned up temporary files for ${shapefile.name}_${shapefile.id}` })
|
|
88
86
|
transaction(features)
|
|
89
87
|
}
|
|
90
|
-
setWarning({ message: `Shapefiles have finished compiling, you can now continue and close this terminal` })
|
|
91
88
|
fs.rm(resolve(__dirname, '../../shapefiles'), { recursive: true, force: true }, () => {});
|
|
92
89
|
}
|
|
93
90
|
} catch (error) {
|
|
@@ -77,9 +77,11 @@ export const getCleanDescription = (message: string): string => {
|
|
|
77
77
|
message = message.replace(regex, replacement)
|
|
78
78
|
}
|
|
79
79
|
return message
|
|
80
|
+
.replace(/'/g, "''")
|
|
80
81
|
.replace(/\b(\d{1,2}):(\d{2})\s*(AM|PM)\b/gi, (_, h, m, ap) => fmt(+h, +m, ap.toUpperCase()))
|
|
81
82
|
.replace(/\b(\d{1,2})\s+(\d{2})\s+(AM|PM)\b/gi, (_, h, m, ap) => fmt(+h, +m, ap.toUpperCase()))
|
|
82
83
|
.replace(/\b(\d{2})(\d{2})\s+(AM|PM)\b/gi, (_, hh, mm, ap) => fmt(+hh, +mm, ap.toUpperCase()))
|
|
84
|
+
.replace(/\b(\d{1,2})\s+(AM|PM)\b/gi, (_, h, ap) => fmt(+h, 0, ap.toUpperCase()))
|
|
83
85
|
.replace(/\*+/g, " ")
|
|
84
86
|
.replace(/-/g, " ")
|
|
85
87
|
.replace(/\n/g, " ")
|
|
@@ -19,23 +19,34 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { platform } from 'os'
|
|
22
|
+
import { writeFileSync } from "fs"
|
|
22
23
|
import { execSync } from 'child_process'
|
|
23
24
|
|
|
24
25
|
export const getTTS = async (text: string, outputPath: string): Promise<void> => {
|
|
25
26
|
const vPlatform = platform();
|
|
26
27
|
switch (vPlatform) {
|
|
27
28
|
case 'win32': {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
try {
|
|
30
|
+
const txtPath = outputPath + ".txt";
|
|
31
|
+
writeFileSync(txtPath, text, "utf8");
|
|
32
|
+
const command = [
|
|
33
|
+
"Add-Type -AssemblyName System.Speech;",
|
|
34
|
+
`$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer;`,
|
|
35
|
+
`$text = Get-Content -Raw -LiteralPath '${txtPath}';`,
|
|
36
|
+
`$speak.SetOutputToWaveFile('${outputPath}');`,
|
|
37
|
+
`$speak.Speak($text);`,
|
|
38
|
+
`$speak.Dispose();`
|
|
39
|
+
].join(" ");
|
|
40
|
+
execSync(`powershell -Command "${command}"`, { stdio: 'inherit' })
|
|
41
|
+
} catch(error) {
|
|
42
|
+
console.error('Error occurred while generating TTS:', error);
|
|
43
|
+
}
|
|
35
44
|
break;
|
|
36
45
|
}
|
|
37
46
|
case 'linux':
|
|
38
|
-
|
|
47
|
+
try {
|
|
48
|
+
execSync(`espeak -w "${outputPath}" "${text.replace(/"/g, '\\"')}"`);
|
|
49
|
+
} catch {}
|
|
39
50
|
break;
|
|
40
51
|
default: break;
|
|
41
52
|
}
|
|
@@ -38,14 +38,16 @@ import { platform } from 'os'
|
|
|
38
38
|
interface GenerateEASOptions {
|
|
39
39
|
message: string
|
|
40
40
|
header: string
|
|
41
|
+
title: string
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const setEasTone = async (options: GenerateEASOptions): Promise<string> => {
|
|
44
45
|
const settings = bootstrap.settings as TypeSettings;
|
|
45
|
-
const directory = settings.GlobalSettings.
|
|
46
|
-
const prefix = settings.GlobalSettings.
|
|
46
|
+
const directory = settings.GlobalSettings.ArchiveSettings.EasDirectory;
|
|
47
|
+
const prefix = settings.GlobalSettings.ArchiveSettings.EasToneout;
|
|
47
48
|
let message = options.message;
|
|
48
49
|
let header = options.header;
|
|
50
|
+
let title = options.title ?? `${Math.random().toString(36).substring(2, 15)}-${header.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
49
51
|
if (!message || !header) {
|
|
50
52
|
setWarning({
|
|
51
53
|
title: `EAS`,
|
|
@@ -60,15 +62,15 @@ export const setEasTone = async (options: GenerateEASOptions): Promise<string> =
|
|
|
60
62
|
if (!existsSync(directory)) {
|
|
61
63
|
mkdirSync(directory, { recursive: true });
|
|
62
64
|
}
|
|
63
|
-
if (!existsSync(join(directory, `/
|
|
64
|
-
mkdirSync(join(directory, `/
|
|
65
|
+
if (!existsSync(join(directory, `/tts`))) {
|
|
66
|
+
mkdirSync(join(directory, `/tts`), { recursive: true });
|
|
65
67
|
}
|
|
66
|
-
if (!existsSync(join(directory, `/
|
|
67
|
-
mkdirSync(join(directory, `/
|
|
68
|
+
if (!existsSync(join(directory, `/encoded`))) {
|
|
69
|
+
mkdirSync(join(directory, `/encoded`), { recursive: true });
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
const tmpTTS = join(directory, `/
|
|
71
|
-
const outTTS = join(directory, `/
|
|
72
|
+
const tmpTTS = join(directory, `/tts/${title}.wav`)
|
|
73
|
+
const outTTS = join(directory, `/encoded/${title}.wav`)
|
|
72
74
|
const vPlatform = platform();
|
|
73
75
|
|
|
74
76
|
if (vPlatform === 'darwin') {
|
|
@@ -83,10 +85,13 @@ export const setEasTone = async (options: GenerateEASOptions): Promise<string> =
|
|
|
83
85
|
await new Promise<void>((resolve, reject) => {
|
|
84
86
|
const tMsg = getCleanDescription(message)
|
|
85
87
|
getTTS(tMsg, tmpTTS).then(() => {
|
|
86
|
-
buffTTS = readFileSync(tmpTTS);
|
|
87
88
|
resolve();
|
|
88
89
|
});
|
|
89
90
|
});
|
|
91
|
+
if (!existsSync(tmpTTS)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
buffTTS = readFileSync(tmpTTS);
|
|
90
95
|
const vWav = getWavPCM16(buffTTS);
|
|
91
96
|
const vSamples = getSampledPCM16(vWav.samples, vWav.sampleRate, 8000)
|
|
92
97
|
const vRadio = setRadioEffect(vSamples, 8000)
|
|
@@ -97,7 +102,7 @@ export const setEasTone = async (options: GenerateEASOptions): Promise<string> =
|
|
|
97
102
|
|
|
98
103
|
if (tWav == null) {
|
|
99
104
|
try {
|
|
100
|
-
const converted = join(directory, `/
|
|
105
|
+
const converted = join(directory, `/tts/${title}.converted.wav`)
|
|
101
106
|
execSync(`ffmpeg -y -i "${prefix}" -ar 8000 -ac 1 -sample_fmt s16 "${converted}"`, { stdio: 'ignore' })
|
|
102
107
|
if (existsSync(converted)) {
|
|
103
108
|
tBuffer = readFileSync(converted)
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { TypeAttributes } from "../../@types/type.attributes"
|
|
21
|
-
import {
|
|
21
|
+
import { dict_awips } from "../../@dictionaries/dictionaries.awips"
|
|
22
22
|
|
|
23
23
|
interface ValidateOptions {
|
|
24
24
|
attributes: TypeAttributes
|
|
@@ -37,7 +37,7 @@ export const getAwipsType = (options: ValidateOptions): ValidiateResponse => {
|
|
|
37
37
|
prefix: null
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
for (const [prefix, type] of Object.entries(
|
|
40
|
+
for (const [prefix, type] of Object.entries(dict_awips)) {
|
|
41
41
|
if (attributes.awipsid.startsWith(prefix)) {
|
|
42
42
|
return { type, prefix }
|
|
43
43
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { TypeStanza } from "../../@types/type.stanza"
|
|
21
21
|
import { TypeStanzaCompiled } from "../../@types/type.compiled"
|
|
22
|
-
import {
|
|
22
|
+
import { dict_expressions } from "../../@dictionaries/dictionaries.expressions"
|
|
23
23
|
import { getAwipsType } from "./stanza.getAwipsType"
|
|
24
24
|
|
|
25
25
|
interface ValidateOptions {
|
|
@@ -35,8 +35,8 @@ export const validate = (options: ValidateOptions): TypeStanzaCompiled => {
|
|
|
35
35
|
if (attributes.awipsid && attributes.awipsid.length > 1) {
|
|
36
36
|
const isCapEvent = message.includes(`<?xml`);
|
|
37
37
|
const isCapAreaDescription = message.includes(`<areaDesc>`)
|
|
38
|
-
const isVTEC = message.match(
|
|
39
|
-
const isUGC = message.match(
|
|
38
|
+
const isVTEC = message.match(dict_expressions.pvtec) != null;
|
|
39
|
+
const isUGC = message.match(dict_expressions.ugc1) != null;
|
|
40
40
|
const getType = getAwipsType({ attributes: attributes})
|
|
41
41
|
if (getType.type != null) {
|
|
42
42
|
return {
|
|
@@ -31,55 +31,64 @@ interface CreateHttpOptions {
|
|
|
31
31
|
|
|
32
32
|
export const createHttp = async (options: CreateHttpOptions): Promise<any> => {
|
|
33
33
|
return new Promise((resolve, reject) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
request(requestOptions, (error, response, body) => {
|
|
53
|
-
if (error) {
|
|
54
|
-
return resolve({
|
|
55
|
-
error: true,
|
|
56
|
-
options: requestOptions,
|
|
57
|
-
status: -1,
|
|
58
|
-
message: error.message ?? `Unknown Error`
|
|
59
|
-
})
|
|
34
|
+
try {
|
|
35
|
+
const requestOptions = {
|
|
36
|
+
url: options.url ?? `https://api.weather.gov/alerts/active`,
|
|
37
|
+
headers: options.headers ?? {
|
|
38
|
+
"User-Agent": "AtmosphericX",
|
|
39
|
+
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
40
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
41
|
+
},
|
|
42
|
+
method: options.method ?? `GET`,
|
|
43
|
+
timeout: options.timeout ?? 10e3,
|
|
44
|
+
proxy: options.proxy ?? null,
|
|
45
|
+
maxRedirects: 1,
|
|
46
|
+
};
|
|
47
|
+
if (options.formData) {
|
|
48
|
+
requestOptions['formData'] = options.formData;
|
|
49
|
+
} else if (options.body) {
|
|
50
|
+
requestOptions['body'] = options.body;
|
|
60
51
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
|
|
53
|
+
request(requestOptions, (error, response, body) => {
|
|
54
|
+
if (error) {
|
|
55
|
+
return resolve({
|
|
56
|
+
error: true,
|
|
57
|
+
options: requestOptions,
|
|
58
|
+
status: -1,
|
|
59
|
+
message: error.message ?? `Unknown Error`
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
63
|
+
return resolve({
|
|
64
|
+
error: true,
|
|
65
|
+
options: requestOptions,
|
|
66
|
+
status: response.statusCode ?? -1,
|
|
67
|
+
message: `HTTP Status Code ${response.statusCode ?? `Unknown Status Code`} (${body})`
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
if (body == undefined || body == null) {
|
|
71
|
+
return resolve({
|
|
72
|
+
error: true,
|
|
73
|
+
options: requestOptions,
|
|
74
|
+
status: response.statusCode ?? -1,
|
|
75
|
+
message: `Empty Response Body`
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
resolve({
|
|
79
|
+
error: false,
|
|
64
80
|
options: requestOptions,
|
|
65
81
|
status: response.statusCode ?? -1,
|
|
66
|
-
message:
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
resolve({
|
|
78
|
-
error: false,
|
|
79
|
-
options: requestOptions,
|
|
80
|
-
status: response.statusCode ?? -1,
|
|
81
|
-
message: body
|
|
82
|
-
})
|
|
83
|
-
})
|
|
82
|
+
message: body
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return resolve({
|
|
87
|
+
error: true,
|
|
88
|
+
options: {},
|
|
89
|
+
status: -1,
|
|
90
|
+
message: error.message ?? `Unknown Error`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
84
93
|
})
|
|
85
94
|
}
|
|
@@ -30,8 +30,9 @@ import { join } from "path";
|
|
|
30
30
|
export const setCronSchedule = async (): Promise<void> => {
|
|
31
31
|
const settings = bootstrap.settings as TypeSettings;
|
|
32
32
|
|
|
33
|
-
const TTL = settings.GlobalSettings.
|
|
33
|
+
const TTL = settings.GlobalSettings.ArchiveSettings.TTL
|
|
34
34
|
const TTLCUT = Date.now() - TTL * 1000;
|
|
35
|
+
|
|
35
36
|
const walk = (dir: string): void => {
|
|
36
37
|
if (existsSync(dir)) {
|
|
37
38
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -49,7 +50,9 @@ export const setCronSchedule = async (): Promise<void> => {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
};
|
|
52
|
-
walk(settings.GlobalSettings.
|
|
53
|
+
walk(settings.GlobalSettings.ArchiveSettings.TextDirectory)
|
|
54
|
+
walk(settings.GlobalSettings.ArchiveSettings.EasDirectory)
|
|
55
|
+
walk(settings.GlobalSettings.ArchiveSettings.EventDirectory)
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
|
|
@@ -50,7 +50,7 @@ export const xReconnect = async (interval: number): Promise<void> => {
|
|
|
50
50
|
},
|
|
51
51
|
type: `reconnect`,
|
|
52
52
|
error: true
|
|
53
|
-
},
|
|
53
|
+
}, message: `Attempting to reconnect to XMPP Service (Reconnect Attempt ${bootstrap.cache.tReconnects})`,
|
|
54
54
|
})
|
|
55
55
|
await bootstrap.session_xmpp.stop().catch(() => {});
|
|
56
56
|
await bootstrap.session_xmpp.start().catch(() => {});
|
|
@@ -18,22 +18,22 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { TypeHVTEC } from "../../@types/type.hvtec";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
21
|
+
import { dict_causes } from "../../@dictionaries/dictionaries.causes";
|
|
22
|
+
import { dict_records } from "../../@dictionaries/dictionaries.records";
|
|
23
|
+
import { dict_expressions } from "../../@dictionaries/dictionaries.expressions";
|
|
24
|
+
import { dict_severity } from "../../@dictionaries/dictionaries.severity";
|
|
25
25
|
|
|
26
26
|
export const hvExtract = (message: string): TypeHVTEC[] | null => {
|
|
27
|
-
const getHVTECs = message.match(
|
|
27
|
+
const getHVTECs = message.match(dict_expressions.hvtec) ?? [];
|
|
28
28
|
const vtecs: TypeHVTEC[] = [];
|
|
29
29
|
for (const vtec of getHVTECs) {
|
|
30
30
|
const sub = vtec.split(`.`);
|
|
31
31
|
if (sub.length < 7) continue;
|
|
32
32
|
vtecs.push({
|
|
33
33
|
hvtec: vtec,
|
|
34
|
-
severity:
|
|
35
|
-
cause:
|
|
36
|
-
record:
|
|
34
|
+
severity: dict_severity[sub[1]],
|
|
35
|
+
cause: dict_causes[sub[2]],
|
|
36
|
+
record: dict_records[sub[6]]
|
|
37
37
|
})
|
|
38
38
|
}
|
|
39
39
|
return vtecs.length > 0 ? vtecs : null;
|