atmosx-nwws-parser 1.0.2
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/LICENSE +17 -0
- package/README.md +6 -0
- package/dist/cjs/bootstrap.cjs +1009 -0
- package/dist/cjs/database.cjs +1114 -0
- package/dist/cjs/dictionaries/awips.cjs +379 -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 +2857 -0
- package/dist/cjs/helper.cjs +3014 -0
- package/dist/cjs/parsers/events.cjs +2857 -0
- package/dist/cjs/parsers/stanza.cjs +1108 -0
- package/dist/cjs/parsers/text.cjs +1142 -0
- package/dist/cjs/parsers/types/api.cjs +2857 -0
- package/dist/cjs/parsers/types/cap.cjs +2857 -0
- package/dist/cjs/parsers/types/text.cjs +2857 -0
- package/dist/cjs/parsers/types/ugc.cjs +2857 -0
- package/dist/cjs/parsers/types/vtec.cjs +2857 -0
- package/dist/cjs/parsers/ugc.cjs +1139 -0
- package/dist/cjs/parsers/vtec.cjs +1060 -0
- package/dist/cjs/types.cjs +17 -0
- package/dist/cjs/utils.cjs +2857 -0
- package/dist/cjs/xmpp.cjs +2857 -0
- package/dist/esm/bootstrap.mjs +972 -0
- package/dist/esm/database.mjs +1079 -0
- package/dist/esm/dictionaries/awips.mjs +355 -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 +2824 -0
- package/dist/esm/helper.mjs +2974 -0
- package/dist/esm/parsers/events.mjs +2824 -0
- package/dist/esm/parsers/stanza.mjs +1072 -0
- package/dist/esm/parsers/text.mjs +1106 -0
- package/dist/esm/parsers/types/api.mjs +2824 -0
- package/dist/esm/parsers/types/cap.mjs +2824 -0
- package/dist/esm/parsers/types/text.mjs +2824 -0
- package/dist/esm/parsers/types/ugc.mjs +2824 -0
- package/dist/esm/parsers/types/vtec.mjs +2824 -0
- package/dist/esm/parsers/ugc.mjs +1104 -0
- package/dist/esm/parsers/vtec.mjs +1025 -0
- package/dist/esm/types.mjs +0 -0
- package/dist/esm/utils.mjs +2824 -0
- package/dist/esm/xmpp.mjs +2824 -0
- package/package.json +47 -0
- package/shapefiles/FireCounties.dbf +0 -0
- package/shapefiles/FireCounties.shp +0 -0
- package/shapefiles/FireZones.dbf +0 -0
- package/shapefiles/FireZones.shp +0 -0
- package/shapefiles/ForecastZones.dbf +0 -0
- package/shapefiles/ForecastZones.shp +0 -0
- package/shapefiles/Marine.dbf +0 -0
- package/shapefiles/Marine.shp +0 -0
- package/shapefiles/OffShoreZones.dbf +0 -0
- package/shapefiles/OffShoreZones.shp +0 -0
- package/shapefiles/USCounties.dbf +0 -0
- package/shapefiles/USCounties.shp +0 -0
- package/src/bootstrap.ts +171 -0
- package/src/database.ts +99 -0
- package/src/dictionaries/awips.ts +351 -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 +216 -0
- package/src/xmpp.ts +123 -0
- package/test.js +1 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +11 -0
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,216 @@
|
|
|
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 start = Date.now();
|
|
53
|
+
const filepath = loader.packages.path.join(cacheDir, file);
|
|
54
|
+
const readFile = loader.packages.fs.readFileSync(filepath, { encoding: 'utf-8' });
|
|
55
|
+
const isCap = readFile.includes(`<?xml`);
|
|
56
|
+
if (isCap && !settings.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
57
|
+
if (!isCap && settings.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
58
|
+
const validate = StanzaParser.validate(readFile, { awipsid: file, isCap: isCap, raw: true, issue: undefined });
|
|
59
|
+
await EventParser.eventHandler(validate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
loader.cache.events.emit('onError', { code: 'error-load-cache', message: `Failed to load cache: ${error.message}`});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* loadGeoJsonData fetches GeoJSON data from the National Weather Service endpoint and processes each alert.
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
* @static
|
|
72
|
+
* @async
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
public static async loadGeoJsonData(): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const settings = loader.settings as types.ClientSettings;
|
|
78
|
+
const response = await this.createHttpRequest(settings.NationalWeatherService.endpoint) as types.NationalWeatherServiceResponse
|
|
79
|
+
if (!response.error) {
|
|
80
|
+
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});
|
|
81
|
+
}
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
loader.cache.events.emit('onError', { code: 'error-fetching-nws-data', message: `Failed to fetch NWS data: ${error.message}`});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* detectUncaughtExceptions sets up a global handler for uncaught exceptions in the Node.js process,
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
* @static
|
|
92
|
+
*/
|
|
93
|
+
public static detectUncaughtExceptions(): void {
|
|
94
|
+
if (process.listeners('uncaughtException').some(l => l.name === 'uncaughtExceptionHandler')) return;
|
|
95
|
+
process.on(`uncaughtException`, (error: Error) => {
|
|
96
|
+
loader.cache.events.emit(`onError`, {message: `Uncaught Exception: ${error.message}`, code: `error-uncaught-exception`, stack: error.stack});
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* createHttpRequest performs an HTTP GET request to the specified URL with optional settings.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
* @static
|
|
105
|
+
* @async
|
|
106
|
+
* @param {string} url
|
|
107
|
+
* @param {?types.HTTPSettings} [options]
|
|
108
|
+
* @returns {unknown}
|
|
109
|
+
*/
|
|
110
|
+
public static async createHttpRequest(url: string, options?: types.HTTPSettings) {
|
|
111
|
+
const defaultOptions = {
|
|
112
|
+
timeout: 10000,
|
|
113
|
+
headers: {
|
|
114
|
+
"User-Agent": "AtmosphericX",
|
|
115
|
+
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
116
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const requestOptions = {
|
|
120
|
+
...defaultOptions,
|
|
121
|
+
...options,
|
|
122
|
+
headers: { ...defaultOptions.headers, ...(options?.headers ?? {}) }
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
const resp = await loader.packages.axios.get(url, {
|
|
126
|
+
headers: requestOptions.headers,
|
|
127
|
+
timeout: requestOptions.timeout,
|
|
128
|
+
maxRedirects: 0,
|
|
129
|
+
validateStatus: (status) => status === 200 || status === 500
|
|
130
|
+
});
|
|
131
|
+
return { error: false, message: resp.data };
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
return { error: true, message: err?.message ?? String(err) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* garbageCollectionCache removes files from the cache directory that exceed the specified maximum file size in megabytes.
|
|
139
|
+
*
|
|
140
|
+
* @public
|
|
141
|
+
* @static
|
|
142
|
+
* @param {number} maxFileMegabytes
|
|
143
|
+
*/
|
|
144
|
+
public static garbageCollectionCache(maxFileMegabytes: number): void {
|
|
145
|
+
try {
|
|
146
|
+
const settings = loader.settings as types.ClientSettings;
|
|
147
|
+
if (!settings.NoaaWeatherWireService.cache.directory) return;
|
|
148
|
+
if (!loader.packages.fs.existsSync(settings.NoaaWeatherWireService.cache.directory)) return;
|
|
149
|
+
const maxBytes = maxFileMegabytes * 1024 * 1024;
|
|
150
|
+
const cacheDirectory = settings.NoaaWeatherWireService.cache.directory;
|
|
151
|
+
const stackFiles: string[] = [cacheDirectory], files: {
|
|
152
|
+
file: string,
|
|
153
|
+
size: number,
|
|
154
|
+
}[] = [];
|
|
155
|
+
while (stackFiles.length) {
|
|
156
|
+
const currentDirectory = stackFiles.pop();
|
|
157
|
+
loader.packages.fs.readdirSync(currentDirectory).forEach((file: string) => {
|
|
158
|
+
const fullPath = loader.packages.path.join(currentDirectory, file);
|
|
159
|
+
const stat = loader.packages.fs.statSync(fullPath)
|
|
160
|
+
if (stat.isDirectory()) stackFiles.push(fullPath) ;
|
|
161
|
+
else files.push({ file: fullPath, size: stat.size });
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
if (!files.length) return;
|
|
165
|
+
files.forEach(f => {
|
|
166
|
+
if (f.size > maxBytes) loader.packages.fs.unlinkSync(f.file);
|
|
167
|
+
})
|
|
168
|
+
} catch (error: any) {
|
|
169
|
+
loader.cache.events.emit('onError', { code: 'error-garbage-collection', message: `Failed to perform garbage collection: ${error.message}`});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* handleCronJob performs periodic tasks based on whether the client is connected to NWWS or fetching data from NWS.
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
* @static
|
|
178
|
+
* @param {boolean} isNwws
|
|
179
|
+
*/
|
|
180
|
+
public static handleCronJob(isNwws: boolean): void {
|
|
181
|
+
try {
|
|
182
|
+
const settings = loader.settings as types.ClientSettings;
|
|
183
|
+
if (isNwws) {
|
|
184
|
+
if (settings.NoaaWeatherWireService.cache.read ) void this.garbageCollectionCache(settings.NoaaWeatherWireService.cache.maxSizeMB);
|
|
185
|
+
if (settings.NoaaWeatherWireService.clientReconnections.canReconnect ) void Xmpp.isSessionReconnectionEligible(settings.NoaaWeatherWireService.clientReconnections.currentInterval);
|
|
186
|
+
} else {
|
|
187
|
+
void this.loadGeoJsonData();
|
|
188
|
+
}
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
loader.cache.events.emit('onError', { code: 'error-cron-job', message: `Failed to perform scheduled tasks: ${error.message}`});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* mergeClientSettings merges user-provided settings into the existing client settings, allowing for nested objects to be merged correctly.
|
|
196
|
+
*
|
|
197
|
+
* @public
|
|
198
|
+
* @static
|
|
199
|
+
* @param {Record<string, any>} target
|
|
200
|
+
* @param {Record<string, any>} settings
|
|
201
|
+
*/
|
|
202
|
+
public static mergeClientSettings(target: Record<string, any>, settings: Record<string, any>): void {
|
|
203
|
+
for (const key in settings) {
|
|
204
|
+
if (settings.hasOwnProperty(key)) {
|
|
205
|
+
if (typeof settings[key] === 'object' && settings[key] !== null && !Array.isArray(settings[key])) {
|
|
206
|
+
if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; }
|
|
207
|
+
this.mergeClientSettings(target[key], settings[key]);
|
|
208
|
+
} else {
|
|
209
|
+
target[key] = settings[key];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
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
|
+
});
|