atmosx-nwws-parser 1.0.185 → 1.0.201
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 +3 -86
- package/dist/cjs/bootstrap.cjs +1000 -0
- package/dist/cjs/database.cjs +1105 -0
- package/dist/cjs/dictionaries/awips.cjs +370 -0
- package/dist/cjs/dictionaries/events.cjs +139 -0
- package/dist/cjs/dictionaries/icao.cjs +265 -0
- package/dist/cjs/dictionaries/offshore.cjs +40 -0
- package/dist/cjs/dictionaries/signatures.cjs +132 -0
- package/dist/cjs/eas.cjs +2851 -0
- package/dist/cjs/helper.cjs +3008 -0
- package/dist/cjs/parsers/events.cjs +2851 -0
- package/dist/cjs/parsers/stanza.cjs +1099 -0
- package/dist/cjs/parsers/text.cjs +1133 -0
- package/dist/cjs/parsers/types/api.cjs +2851 -0
- package/dist/cjs/parsers/types/cap.cjs +2851 -0
- package/dist/cjs/parsers/types/text.cjs +2851 -0
- package/dist/cjs/parsers/types/ugc.cjs +2851 -0
- package/dist/cjs/parsers/types/vtec.cjs +2851 -0
- package/dist/cjs/parsers/ugc.cjs +1130 -0
- package/dist/cjs/parsers/vtec.cjs +1051 -0
- package/dist/cjs/types.cjs +17 -0
- package/dist/cjs/utils.cjs +2851 -0
- package/dist/cjs/xmpp.cjs +2851 -0
- package/dist/esm/bootstrap.mjs +963 -0
- package/dist/esm/database.mjs +1070 -0
- package/dist/esm/dictionaries/awips.mjs +346 -0
- package/dist/esm/dictionaries/events.mjs +111 -0
- package/dist/esm/dictionaries/icao.mjs +241 -0
- package/dist/esm/dictionaries/offshore.mjs +16 -0
- package/dist/esm/dictionaries/signatures.mjs +106 -0
- package/dist/esm/eas.mjs +2818 -0
- package/dist/esm/helper.mjs +2968 -0
- package/dist/esm/parsers/events.mjs +2818 -0
- package/dist/esm/parsers/stanza.mjs +1063 -0
- package/dist/esm/parsers/text.mjs +1097 -0
- package/dist/esm/parsers/types/api.mjs +2818 -0
- package/dist/esm/parsers/types/cap.mjs +2818 -0
- package/dist/esm/parsers/types/text.mjs +2818 -0
- package/dist/esm/parsers/types/ugc.mjs +2818 -0
- package/dist/esm/parsers/types/vtec.mjs +2818 -0
- package/dist/esm/parsers/ugc.mjs +1095 -0
- package/dist/esm/parsers/vtec.mjs +1016 -0
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/utils.mjs +2818 -0
- package/dist/esm/xmpp.mjs +2818 -0
- package/package.json +47 -29
- package/src/bootstrap.ts +171 -0
- package/src/database.ts +99 -0
- package/src/dictionaries/awips.ts +342 -0
- package/src/dictionaries/events.ts +109 -0
- package/src/dictionaries/icao.ts +237 -0
- package/src/dictionaries/offshore.ts +12 -0
- package/src/dictionaries/signatures.ts +103 -0
- package/src/eas.ts +428 -0
- package/src/helper.ts +167 -0
- package/src/parsers/events.ts +289 -0
- package/src/parsers/stanza.ts +103 -0
- package/src/parsers/text.ts +167 -0
- package/src/parsers/types/api.ts +94 -0
- package/src/parsers/types/cap.ts +89 -0
- package/src/parsers/types/text.ts +54 -0
- package/src/parsers/types/ugc.ts +85 -0
- package/src/parsers/types/vtec.ts +60 -0
- package/src/parsers/ugc.ts +148 -0
- package/src/parsers/vtec.ts +66 -0
- package/src/types.ts +187 -0
- package/src/utils.ts +217 -0
- package/src/xmpp.ts +123 -0
- package/test.js +1 -36
- package/tsconfig.json +14 -0
- package/tsup.config.ts +11 -0
- package/bootstrap.js +0 -112
- package/index.js +0 -264
- package/src/events.js +0 -339
- package/src/stanza.js +0 -105
- package/src/text.js +0 -108
- package/src/ugc.js +0 -115
- package/src/vtec.js +0 -89
package/src/types.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| '_ ` _ \ / _ \/ __| '_ \| '_ \ / _ \ '__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Written by: k3yomi@GitHub
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface GlobalSettings {
|
|
15
|
+
useParentEvents: boolean,
|
|
16
|
+
betterEventParsing: boolean
|
|
17
|
+
easSettings: {
|
|
18
|
+
easAlerts: string[],
|
|
19
|
+
easDirectory: string,
|
|
20
|
+
easIntroWav: string | null
|
|
21
|
+
},
|
|
22
|
+
alertFiltering: {
|
|
23
|
+
filteredEvents: string[],
|
|
24
|
+
filteredICOAs: string[],
|
|
25
|
+
ignoredICOAs: string[],
|
|
26
|
+
ugcFilter: string[],
|
|
27
|
+
stateFilter: string[],
|
|
28
|
+
ignoredEvents: string[],
|
|
29
|
+
checkExpired: boolean
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DefaultParameters {
|
|
34
|
+
wmo: string,
|
|
35
|
+
source: string,
|
|
36
|
+
max_hail_size: string,
|
|
37
|
+
max_wind_gust: string,
|
|
38
|
+
damage_threat: string,
|
|
39
|
+
tornado_detection: string,
|
|
40
|
+
flood_detection: string,
|
|
41
|
+
discussion_tornado_intensity: string,
|
|
42
|
+
discussion_wind_intensity: string,
|
|
43
|
+
discussion_hail_intensity: string
|
|
44
|
+
WMOidentifier?: string[],
|
|
45
|
+
VTEC?: string,
|
|
46
|
+
maxHailSize?: string,
|
|
47
|
+
maxWindGust?: string,
|
|
48
|
+
thunderstormDamageThreat?: string[],
|
|
49
|
+
tornadoDetection?: string[],
|
|
50
|
+
waterspoutDetection?: string[],
|
|
51
|
+
floodDetection?: string[],
|
|
52
|
+
AWIPSidentifier?: string[],
|
|
53
|
+
NWSheadline?: string[],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface DefaultAttributes {
|
|
57
|
+
xmlns?: string,
|
|
58
|
+
id?: string,
|
|
59
|
+
issue?: string,
|
|
60
|
+
ttaaii?: string,
|
|
61
|
+
cccc?: string,
|
|
62
|
+
awipsid?: string,
|
|
63
|
+
getAwip?: Record<string, string>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ClientReconnectSettings {
|
|
67
|
+
canReconnect: boolean,
|
|
68
|
+
currentInterval: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ClientCredentialSettings {
|
|
72
|
+
username: string,
|
|
73
|
+
password: string,
|
|
74
|
+
nickname: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ClientCacheSettings {
|
|
78
|
+
read: boolean,
|
|
79
|
+
maxSizeMB: number,
|
|
80
|
+
directory: string,
|
|
81
|
+
maxHistory: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ClientAlertSettings {
|
|
85
|
+
isCapOnly: boolean,
|
|
86
|
+
isShapefileUGC: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface NoaaWeatherWireServiceSettings {
|
|
90
|
+
clientReconnections: ClientReconnectSettings,
|
|
91
|
+
clientCredentials: ClientCredentialSettings,
|
|
92
|
+
cache: ClientCacheSettings,
|
|
93
|
+
alertPreferences: ClientAlertSettings
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface NationalWeatherServiceSettings {
|
|
97
|
+
checkInterval: number,
|
|
98
|
+
endpoint: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface EnhancedEventCondition {
|
|
102
|
+
description?: string;
|
|
103
|
+
condition?: (value: string) => boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TypeAttributes {
|
|
107
|
+
awipsType?: Record<string, string>,
|
|
108
|
+
isCap: boolean,
|
|
109
|
+
awipsid?: string,
|
|
110
|
+
raw: boolean,
|
|
111
|
+
issue?: string,
|
|
112
|
+
type?: string,
|
|
113
|
+
prefix?: string,
|
|
114
|
+
getAwip?: { type: string, prefix: string }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface NationalWeatherServiceResponse {
|
|
118
|
+
error: boolean,
|
|
119
|
+
message?: { features: TypeAlert[] }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface VTECParsed {
|
|
123
|
+
raw: string,
|
|
124
|
+
tracking: string,
|
|
125
|
+
event: string,
|
|
126
|
+
status: string,
|
|
127
|
+
wmo: string,
|
|
128
|
+
expires: string
|
|
129
|
+
}
|
|
130
|
+
export interface UGCParsed {
|
|
131
|
+
zones: string[],
|
|
132
|
+
locations: string[],
|
|
133
|
+
expiry: Date | null,
|
|
134
|
+
polygon: [number, number][]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface BaseProperties {
|
|
138
|
+
event?: string,
|
|
139
|
+
locations: string,
|
|
140
|
+
issued: string,
|
|
141
|
+
expires: string,
|
|
142
|
+
geocode: { UGC: string[] },
|
|
143
|
+
description: string,
|
|
144
|
+
sender_name: string,
|
|
145
|
+
sender_icao: string,
|
|
146
|
+
attributes: DefaultAttributes,
|
|
147
|
+
parameters: DefaultParameters,
|
|
148
|
+
geometry: { type?: string, coordinates?: [number, number][] } | null
|
|
149
|
+
messageType?: string,
|
|
150
|
+
sent?: string,
|
|
151
|
+
areaDesc?: string,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface TypeAlert {
|
|
155
|
+
performance: number,
|
|
156
|
+
tracking: string,
|
|
157
|
+
header: string,
|
|
158
|
+
vtec: string,
|
|
159
|
+
history: { description: string, issued: string, type: string }[],
|
|
160
|
+
properties: BaseProperties
|
|
161
|
+
geometry: { type?: string, coordinates?: [number, number][] } | null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ClientSettings {
|
|
165
|
+
database?: string,
|
|
166
|
+
isNWWS: boolean,
|
|
167
|
+
NoaaWeatherWireService: NoaaWeatherWireServiceSettings,
|
|
168
|
+
NationalWeatherService: NationalWeatherServiceSettings,
|
|
169
|
+
global: GlobalSettings,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface TypeCompiled {
|
|
173
|
+
message: string | null,
|
|
174
|
+
attributes: DefaultAttributes,
|
|
175
|
+
isCap: boolean,
|
|
176
|
+
isApi: boolean,
|
|
177
|
+
isCapDescription: boolean,
|
|
178
|
+
isVtec: boolean,
|
|
179
|
+
isUGC: boolean,
|
|
180
|
+
getAwip?: { type: string, prefix: string },
|
|
181
|
+
awipsType: { type: string, prefix: string } | null,
|
|
182
|
+
awipsPrefix?: string,
|
|
183
|
+
ignore: boolean,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
export type HTTPSettings = { timeout?: number, headers?: Record<string, string> }
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Written by: KiyoWx (k3yomi)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import * as loader from './bootstrap';
|
|
16
|
+
import * as types from './types';
|
|
17
|
+
import StanzaParser from './parsers/stanza';
|
|
18
|
+
import EventParser from './parsers/events';
|
|
19
|
+
import Xmpp from './xmpp';
|
|
20
|
+
|
|
21
|
+
export class Utils {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Zzzzzzz... yeah not much to explain here. Simple sleep function that returns a promise after the specified milliseconds.
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
* @static
|
|
28
|
+
* @async
|
|
29
|
+
* @param {number} ms
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
public static async sleep(ms: number): Promise<void> {
|
|
33
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* loadCollectionCache reads cached alert files from the specified cache directory and processes them.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
* @static
|
|
41
|
+
* @async
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
public static async loadCollectionCache(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
const settings = loader.settings as types.ClientSettings;
|
|
47
|
+
if (settings.NoaaWeatherWireService.cache.read && settings.NoaaWeatherWireService.cache.directory) {
|
|
48
|
+
if (!loader.packages.fs.existsSync(settings.NoaaWeatherWireService.cache.directory)) return;
|
|
49
|
+
const cacheDir = settings.NoaaWeatherWireService.cache.directory;
|
|
50
|
+
const getAllFiles = loader.packages.fs.readdirSync(cacheDir).filter((file: string) => file.endsWith('.bin') && file.startsWith('cache-'));
|
|
51
|
+
for (const file of getAllFiles) {
|
|
52
|
+
const filepath = loader.packages.path.join(cacheDir, file);
|
|
53
|
+
const readFile = loader.packages.fs.readFileSync(filepath, { encoding: 'utf-8' });
|
|
54
|
+
const readSize = loader.packages.fs.statSync(filepath).size;
|
|
55
|
+
if (readSize == 0) { continue; }
|
|
56
|
+
const isCap = readFile.includes(`<?xml`);
|
|
57
|
+
if (isCap && !settings.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
58
|
+
if (!isCap && settings.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
59
|
+
const validate = StanzaParser.validate(readFile, { awipsid: file, isCap: isCap, raw: true, issue: undefined });
|
|
60
|
+
await EventParser.eventHandler(validate);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
loader.cache.events.emit('onError', { code: 'error-load-cache', message: `Failed to load cache: ${error.message}`});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* loadGeoJsonData fetches GeoJSON data from the National Weather Service endpoint and processes each alert.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
* @static
|
|
73
|
+
* @async
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
public static async loadGeoJsonData(): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
const settings = loader.settings as types.ClientSettings;
|
|
79
|
+
const response = await this.createHttpRequest(settings.NationalWeatherService.endpoint) as types.NationalWeatherServiceResponse
|
|
80
|
+
if (!response.error) {
|
|
81
|
+
EventParser.eventHandler({message: JSON.stringify(response.message), attributes: {}, isCap: true, isApi: true, isVtec: false, isUGC: false, isCapDescription: false, awipsType: { type: 'api', prefix: 'AP' }, ignore: false});
|
|
82
|
+
}
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
loader.cache.events.emit('onError', { code: 'error-fetching-nws-data', message: `Failed to fetch NWS data: ${error.message}`});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* detectUncaughtExceptions sets up a global handler for uncaught exceptions in the Node.js process,
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
* @static
|
|
93
|
+
*/
|
|
94
|
+
public static detectUncaughtExceptions(): void {
|
|
95
|
+
if (process.listeners('uncaughtException').some(l => l.name === 'uncaughtExceptionHandler')) return;
|
|
96
|
+
process.on(`uncaughtException`, (error: Error) => {
|
|
97
|
+
loader.cache.events.emit(`onError`, {message: `Uncaught Exception: ${error.message}`, code: `error-uncaught-exception`, stack: error.stack});
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* createHttpRequest performs an HTTP GET request to the specified URL with optional settings.
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
* @static
|
|
106
|
+
* @async
|
|
107
|
+
* @param {string} url
|
|
108
|
+
* @param {?types.HTTPSettings} [options]
|
|
109
|
+
* @returns {unknown}
|
|
110
|
+
*/
|
|
111
|
+
public static async createHttpRequest(url: string, options?: types.HTTPSettings) {
|
|
112
|
+
const defaultOptions = {
|
|
113
|
+
timeout: 10000,
|
|
114
|
+
headers: {
|
|
115
|
+
"User-Agent": "AtmosphericX",
|
|
116
|
+
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
117
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const requestOptions = {
|
|
121
|
+
...defaultOptions,
|
|
122
|
+
...options,
|
|
123
|
+
headers: { ...defaultOptions.headers, ...(options?.headers ?? {}) }
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
const resp = await loader.packages.axios.get(url, {
|
|
127
|
+
headers: requestOptions.headers,
|
|
128
|
+
timeout: requestOptions.timeout,
|
|
129
|
+
maxRedirects: 0,
|
|
130
|
+
validateStatus: (status) => status === 200 || status === 500
|
|
131
|
+
});
|
|
132
|
+
return { error: false, message: resp.data };
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
return { error: true, message: err?.message ?? String(err) };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* garbageCollectionCache removes files from the cache directory that exceed the specified maximum file size in megabytes.
|
|
140
|
+
*
|
|
141
|
+
* @public
|
|
142
|
+
* @static
|
|
143
|
+
* @param {number} maxFileMegabytes
|
|
144
|
+
*/
|
|
145
|
+
public static garbageCollectionCache(maxFileMegabytes: number): void {
|
|
146
|
+
try {
|
|
147
|
+
const settings = loader.settings as types.ClientSettings;
|
|
148
|
+
if (!settings.NoaaWeatherWireService.cache.directory) return;
|
|
149
|
+
if (!loader.packages.fs.existsSync(settings.NoaaWeatherWireService.cache.directory)) return;
|
|
150
|
+
const maxBytes = maxFileMegabytes * 1024 * 1024;
|
|
151
|
+
const cacheDirectory = settings.NoaaWeatherWireService.cache.directory;
|
|
152
|
+
const stackFiles: string[] = [cacheDirectory], files: {
|
|
153
|
+
file: string,
|
|
154
|
+
size: number,
|
|
155
|
+
}[] = [];
|
|
156
|
+
while (stackFiles.length) {
|
|
157
|
+
const currentDirectory = stackFiles.pop();
|
|
158
|
+
loader.packages.fs.readdirSync(currentDirectory).forEach((file: string) => {
|
|
159
|
+
const fullPath = loader.packages.path.join(currentDirectory, file);
|
|
160
|
+
const stat = loader.packages.fs.statSync(fullPath)
|
|
161
|
+
if (stat.isDirectory()) stackFiles.push(fullPath) ;
|
|
162
|
+
else files.push({ file: fullPath, size: stat.size });
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
if (!files.length) return;
|
|
166
|
+
files.forEach(f => {
|
|
167
|
+
if (f.size > maxBytes) loader.packages.fs.unlinkSync(f.file);
|
|
168
|
+
})
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
loader.cache.events.emit('onError', { code: 'error-garbage-collection', message: `Failed to perform garbage collection: ${error.message}`});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* handleCronJob performs periodic tasks based on whether the client is connected to NWWS or fetching data from NWS.
|
|
176
|
+
*
|
|
177
|
+
* @public
|
|
178
|
+
* @static
|
|
179
|
+
* @param {boolean} isNwws
|
|
180
|
+
*/
|
|
181
|
+
public static handleCronJob(isNwws: boolean): void {
|
|
182
|
+
try {
|
|
183
|
+
const settings = loader.settings as types.ClientSettings;
|
|
184
|
+
if (isNwws) {
|
|
185
|
+
if (settings.NoaaWeatherWireService.cache.read ) void this.garbageCollectionCache(settings.NoaaWeatherWireService.cache.maxSizeMB);
|
|
186
|
+
if (settings.NoaaWeatherWireService.clientReconnections.canReconnect ) void Xmpp.isSessionReconnectionEligible(settings.NoaaWeatherWireService.clientReconnections.currentInterval);
|
|
187
|
+
} else {
|
|
188
|
+
void this.loadGeoJsonData();
|
|
189
|
+
}
|
|
190
|
+
} catch (error: any) {
|
|
191
|
+
loader.cache.events.emit('onError', { code: 'error-cron-job', message: `Failed to perform scheduled tasks: ${error.message}`});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* mergeClientSettings merges user-provided settings into the existing client settings, allowing for nested objects to be merged correctly.
|
|
197
|
+
*
|
|
198
|
+
* @public
|
|
199
|
+
* @static
|
|
200
|
+
* @param {Record<string, any>} target
|
|
201
|
+
* @param {Record<string, any>} settings
|
|
202
|
+
*/
|
|
203
|
+
public static mergeClientSettings(target: Record<string, any>, settings: Record<string, any>): void {
|
|
204
|
+
for (const key in settings) {
|
|
205
|
+
if (settings.hasOwnProperty(key)) {
|
|
206
|
+
if (typeof settings[key] === 'object' && settings[key] !== null && !Array.isArray(settings[key])) {
|
|
207
|
+
if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; }
|
|
208
|
+
this.mergeClientSettings(target[key], settings[key]);
|
|
209
|
+
} else {
|
|
210
|
+
target[key] = settings[key];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default Utils;
|
package/src/xmpp.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Written by: KiyoWx (k3yomi)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import * as loader from './bootstrap';
|
|
16
|
+
import * as types from './types';
|
|
17
|
+
import Utils from './utils';
|
|
18
|
+
import StanzaParser from './parsers/stanza';
|
|
19
|
+
import Database from './database';
|
|
20
|
+
import EventParser from './parsers/events';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export class Xmpp {
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* isSessionReconnectionEligible checks if the XMPP session is eligible for reconnection based on the last
|
|
27
|
+
* received stanza time and current interval.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
* @static
|
|
31
|
+
* @async
|
|
32
|
+
* @param {number} currentInterval
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
public static async isSessionReconnectionEligible(currentInterval: number): Promise<void> {
|
|
36
|
+
const settings = loader.settings as types.ClientSettings;
|
|
37
|
+
if ((loader.cache.isConnected || loader.cache.sigHalt ) && loader.cache.session) {
|
|
38
|
+
const lastStanza = Date.now() - loader.cache.lastStanza;
|
|
39
|
+
if (lastStanza >= (currentInterval * 1000)) {
|
|
40
|
+
if (!loader.cache.attemptingReconnect) {
|
|
41
|
+
loader.cache.attemptingReconnect = true;
|
|
42
|
+
loader.cache.isConnected = false;
|
|
43
|
+
loader.cache.totalReconnects += 1;
|
|
44
|
+
loader.cache.events.emit(`onReconnect`, { reconnects: loader.cache.totalReconnects, lastStanza: lastStanza, lastName: settings.NoaaWeatherWireService.clientCredentials.nickname})
|
|
45
|
+
await loader.cache.session.stop().catch(() => {});
|
|
46
|
+
await loader.cache.session.start().catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* deploySession initializes and starts the XMPP client session, setting up event listeners for
|
|
54
|
+
* connection management and message handling. This function is specifically tailored for
|
|
55
|
+
* NoaaWeatherWireService and connects to their XMPP server.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
* @static
|
|
59
|
+
* @async
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
public static async deploySession(): Promise<void> {
|
|
63
|
+
const settings = loader.settings as types.ClientSettings
|
|
64
|
+
loader.cache.session = loader.packages.xmpp.client({
|
|
65
|
+
service: `xmpp://nwws-oi.weather.gov`,
|
|
66
|
+
domain: `nwws-oi.weather.gov`,
|
|
67
|
+
username: settings.NoaaWeatherWireService.clientCredentials.username,
|
|
68
|
+
password: settings.NoaaWeatherWireService.clientCredentials.password,
|
|
69
|
+
});
|
|
70
|
+
settings.NoaaWeatherWireService.clientCredentials.nickname ??= settings.NoaaWeatherWireService.clientCredentials.username;
|
|
71
|
+
loader.cache.session.on(`online`, async (address: string) => {
|
|
72
|
+
if (loader.cache.lastConnect && Date.now() - loader.cache.lastConnect < 10 * 1000) {
|
|
73
|
+
loader.cache.sigHalt = true;
|
|
74
|
+
Utils.sleep(2 * 1000).then(async () => {
|
|
75
|
+
await loader.cache.session.stop()
|
|
76
|
+
});
|
|
77
|
+
loader.cache.events.emit(`onError`, { code: `error-reconnecting-too-fast`, message: `The client is attempting to reconnect too fast. Please wait a few seconds before trying again.` });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
loader.cache.isConnected = true;
|
|
81
|
+
loader.cache.sigHalt = false;
|
|
82
|
+
loader.cache.lastConnect = Date.now();
|
|
83
|
+
loader.cache.session.send(loader.packages.xmpp.xml('presence', { to: `nwws@conference.nwws-oi.weather.gov/${settings.NoaaWeatherWireService.clientCredentials.nickname}`, xmlns: 'http://jabber.org/protocol/muc' }))
|
|
84
|
+
loader.cache.session.send(loader.packages.xmpp.xml('presence', { to: `nwws@conference.nwws-oi.weather.gov`, type: 'available' }))
|
|
85
|
+
loader.cache.events.emit(`onConnection`, settings.NoaaWeatherWireService.clientCredentials.nickname)
|
|
86
|
+
if (loader.cache.attemptingReconnect) {
|
|
87
|
+
Utils.sleep(15 * 1000).then(() => { loader.cache.attemptingReconnect = false; })
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
loader.cache.session.on(`offline`, async () => {
|
|
91
|
+
loader.cache.isConnected = false;
|
|
92
|
+
loader.cache.sigHalt = true;
|
|
93
|
+
loader.cache.events.emit(`onError`, { code: `connection-lost`, message: `XMPP connection went offline` });
|
|
94
|
+
});
|
|
95
|
+
loader.cache.session.on(`error`, async (error: Error) => {
|
|
96
|
+
loader.cache.isConnected = false;
|
|
97
|
+
loader.cache.sigHalt = true;
|
|
98
|
+
loader.cache.events.emit(`onError`, { code: `connection-error`, message: error.message });
|
|
99
|
+
});
|
|
100
|
+
loader.cache.session.on(`stanza`, async (stanza: any) => {
|
|
101
|
+
try {
|
|
102
|
+
loader.cache.lastStanza = Date.now()
|
|
103
|
+
if (stanza.is(`message`)) {
|
|
104
|
+
const validate = StanzaParser.validate(stanza);
|
|
105
|
+
if ( validate.ignore || (validate.isCap && !settings.NoaaWeatherWireService.alertPreferences.isCapOnly) || (!validate.isCap && settings.NoaaWeatherWireService.alertPreferences.isCapOnly) || (validate.isCap && !validate.isCapDescription) ) return;
|
|
106
|
+
EventParser.eventHandler(validate);
|
|
107
|
+
loader.cache.events.emit(`onMessage`, validate)
|
|
108
|
+
Database.stanzaCacheImport(JSON.stringify(validate))
|
|
109
|
+
}
|
|
110
|
+
if (stanza.is(`presence`) && stanza.attrs.from && stanza.attrs.from.startsWith('nwws@conference.nwws-oi.weather.gov/')) {
|
|
111
|
+
const occupant = stanza.attrs.from.split('/').slice(1).join('/');
|
|
112
|
+
loader.cache.events.emit('onOccupant', { occupant, type: stanza.attrs.type === 'unavailable' ? 'unavailable' : 'available' });
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
loader.cache.events.emit(`onError`, {code: `error-processing-stanza`, message: (e as Error).message})
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
await loader.cache.session.start()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default Xmpp;
|
package/test.js
CHANGED
|
@@ -1,36 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
let Client = new AtmosXWireParser({
|
|
4
|
-
alertSettings: {
|
|
5
|
-
onlyCap: false, // Set to true to only receive CAP messages only
|
|
6
|
-
betterEvents: true, // Set to true to receive better event handling
|
|
7
|
-
ugcPolygons: false, // Set to true to receive UGC Polygons instead of reading from raw products.
|
|
8
|
-
expiryCheck: true, // Set to true to filter out expired alerts
|
|
9
|
-
filteredAlerts: [] // Alerts you want to only log, leave empty to receive all alerts (Ex. ["Tornado Warning", "Radar Indicated Tornado Warning"])
|
|
10
|
-
},
|
|
11
|
-
xmpp: {
|
|
12
|
-
reconnect: true, // Set to true to enable automatic reconnection if you lose connection
|
|
13
|
-
reconnectInterval: 60, // Interval in seconds to attempt reconnection
|
|
14
|
-
},
|
|
15
|
-
cacheSettings: {
|
|
16
|
-
maxMegabytes: 2, // Maximum cache size in megabytes
|
|
17
|
-
cacheDir: `./cache`, // Directory for cache files
|
|
18
|
-
readCache: false, // Set to true if you wish to reupload the cache from earlier
|
|
19
|
-
},
|
|
20
|
-
authentication: {
|
|
21
|
-
username: `USERNAME_HERE`, // Your XMPP username
|
|
22
|
-
password: `PASSWORD_HERE`, // Your XMPP password
|
|
23
|
-
display: `DISPLAY_NAME` // Display name for your XMPP client
|
|
24
|
-
},
|
|
25
|
-
database: `./database.db`, // Path to the SQLite database file (It will be created if it doesn't exist and will be used to store UGC counties and zones.)
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
Client.onEvent(`onAlert`, (alert) => {console.log(alert)});
|
|
29
|
-
Client.onEvent(`onStormReport`, (report) => {});
|
|
30
|
-
Client.onEvent(`onMesoscaleDiscussion`, (discussions) => {});
|
|
31
|
-
Client.onEvent(`onMessage`, (message) => {});
|
|
32
|
-
Client.onEvent(`onOccupant`, (occupant) => {});
|
|
33
|
-
Client.onEvent(`onError`, (error) => {console.log(error)});
|
|
34
|
-
Client.onEvent(`onReconnect`, (service) => {
|
|
35
|
-
Client.setDisplayName(`${username} (x${service.reconnects})`)
|
|
36
|
-
})
|
|
1
|
+
// TODO:
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationDir": "dist/types",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/**/*.ts'],
|
|
5
|
+
format: ['esm', 'cjs'],
|
|
6
|
+
outDir: 'dist',
|
|
7
|
+
splitting: false,
|
|
8
|
+
clean: true,
|
|
9
|
+
outExtension({ format }) {return {js: format === 'esm' ? '.mjs' : '.cjs',};},
|
|
10
|
+
esbuildOptions(options, context) { options.outdir = `dist/${context.format}`; },
|
|
11
|
+
});
|