node-red-contrib-ipgeolocation 1.0.0
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 +21 -0
- package/README.md +423 -0
- package/examples/01 - Single IP Lookup.json +102 -0
- package/examples/02 - Bulk IP Lookup.json +98 -0
- package/examples/03 - IP Security Lookup.json +98 -0
- package/examples/04 - Bulk IP Security.json +96 -0
- package/examples/05 - Abuse Contact Lookup.json +98 -0
- package/examples/06 - ASN Lookup.json +102 -0
- package/examples/07 - Timezone Lookup.json +101 -0
- package/examples/08 - Astronomy (Sun & Moon).json +109 -0
- package/examples/09 - User-Agent Parser.json +96 -0
- package/index.js +22 -0
- package/nodes/ipgeo-abuse.html +203 -0
- package/nodes/ipgeo-abuse.js +148 -0
- package/nodes/ipgeo-asn.html +297 -0
- package/nodes/ipgeo-asn.js +197 -0
- package/nodes/ipgeo-astronomy.html +242 -0
- package/nodes/ipgeo-astronomy.js +195 -0
- package/nodes/ipgeo-bulk-security.html +166 -0
- package/nodes/ipgeo-bulk-security.js +177 -0
- package/nodes/ipgeo-bulk.html +158 -0
- package/nodes/ipgeo-bulk.js +146 -0
- package/nodes/ipgeo-config.html +72 -0
- package/nodes/ipgeo-config.js +30 -0
- package/nodes/ipgeo-lookup.html +300 -0
- package/nodes/ipgeo-lookup.js +165 -0
- package/nodes/ipgeo-security.html +214 -0
- package/nodes/ipgeo-security.js +150 -0
- package/nodes/ipgeo-timezone.html +201 -0
- package/nodes/ipgeo-timezone.js +154 -0
- package/nodes/ipgeo-useragent.html +133 -0
- package/nodes/ipgeo-useragent.js +110 -0
- package/nodes/lib/api-client.js +210 -0
- package/nodes/lib/node-helpers.js +214 -0
- package/package.json +72 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ipgeo-asn.js
|
|
3
|
+
*
|
|
4
|
+
* Node-RED node: "ipgeo asn"
|
|
5
|
+
*
|
|
6
|
+
* Performs an ASN (Autonomous System Number) lookup via the
|
|
7
|
+
* IPGeolocation.io /v3/asn endpoint (GET).
|
|
8
|
+
*
|
|
9
|
+
* Unlike the other ipgeo-* nodes, this endpoint accepts EITHER an `ip`
|
|
10
|
+
* OR an `asn` query parameter (mutually exclusive) — or neither, in
|
|
11
|
+
* which case the API auto-detects the caller's IP and returns ASN data
|
|
12
|
+
* for it.
|
|
13
|
+
*
|
|
14
|
+
* Features
|
|
15
|
+
* ─────────
|
|
16
|
+
* • Query mode: auto-detect caller IP | lookup by IP | lookup by ASN
|
|
17
|
+
* • Configurable `include` modules: peers, downstreams, upstreams,
|
|
18
|
+
* routes, whois_response (comma-separated)
|
|
19
|
+
* • `fields` and `excludes` for bandwidth optimisation
|
|
20
|
+
* • IP/ASN value can be sourced from node config OR msg/flow/global property
|
|
21
|
+
* • Two outputs: [success, error] — error output is optional
|
|
22
|
+
* • Exposes credits consumed on msg.ipgeo_credits (flat 1 credit/lookup)
|
|
23
|
+
* • Full retry + exponential backoff via shared api-client
|
|
24
|
+
*
|
|
25
|
+
* Shares the same `ipgeo-config` config node (API key) as the other
|
|
26
|
+
* ipgeo-* nodes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
"use strict";
|
|
30
|
+
|
|
31
|
+
const { request, IPGeoError } = require("./lib/api-client");
|
|
32
|
+
const {
|
|
33
|
+
setStatus,
|
|
34
|
+
resolveValue,
|
|
35
|
+
setOutputProperty,
|
|
36
|
+
handleNodeError,
|
|
37
|
+
buildCommonParams,
|
|
38
|
+
} = require("./lib/node-helpers");
|
|
39
|
+
|
|
40
|
+
module.exports = function (RED) {
|
|
41
|
+
function IPGeoASNNode(config) {
|
|
42
|
+
RED.nodes.createNode(this, config);
|
|
43
|
+
|
|
44
|
+
// ── Config node (API key) ─────────────────────────────────────────────
|
|
45
|
+
const configNode = RED.nodes.getNode(config.server);
|
|
46
|
+
|
|
47
|
+
// ── Node configuration ────────────────────────────────────────────────
|
|
48
|
+
this.name = config.name;
|
|
49
|
+
this.queryType = config.queryType || "auto"; // 'auto' | 'ip' | 'asn'
|
|
50
|
+
this.ipType = config.ipType || "str"; // 'str' | 'msg' | 'flow' | 'global'
|
|
51
|
+
this.ipValue = config.ipValue || "";
|
|
52
|
+
this.asnType = config.asnType || "str"; // 'str' | 'msg' | 'flow' | 'global'
|
|
53
|
+
this.asnValue = config.asnValue || "";
|
|
54
|
+
this.include = config.include || ""; // comma-sep list of modules
|
|
55
|
+
this.fields = config.fields || "";
|
|
56
|
+
this.excludes = config.excludes || "";
|
|
57
|
+
this.outputProp = config.outputProp || "payload"; // output property
|
|
58
|
+
this.outputType = config.outputType || "msg";
|
|
59
|
+
this.twoOutputs = config.twoOutputs !== false; // default true
|
|
60
|
+
|
|
61
|
+
const node = this;
|
|
62
|
+
|
|
63
|
+
// ── Pre-flight: warn if no config node ───────────────────────────────
|
|
64
|
+
if (!configNode) {
|
|
65
|
+
setStatus(node, "NO_KEY");
|
|
66
|
+
node.warn(
|
|
67
|
+
"No IPGeolocation.io config node selected — configure an API key first."
|
|
68
|
+
);
|
|
69
|
+
} else {
|
|
70
|
+
setStatus(node, "IDLE");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Input handler ─────────────────────────────────────────────────────
|
|
74
|
+
node.on("input", async function (msg, send, done) {
|
|
75
|
+
// Node-RED 0.x compat
|
|
76
|
+
send =
|
|
77
|
+
send ||
|
|
78
|
+
function () {
|
|
79
|
+
node.send.apply(node, arguments);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ── Validate API key availability ─────────────────────────────────
|
|
83
|
+
const apiKey =
|
|
84
|
+
configNode && configNode.credentials
|
|
85
|
+
? configNode.credentials.apiKey
|
|
86
|
+
: null;
|
|
87
|
+
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
setStatus(node, "NO_KEY");
|
|
90
|
+
const err = new IPGeoError("No API key configured", "NO_API_KEY");
|
|
91
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Resolve ip / asn depending on the selected query mode ─────────
|
|
96
|
+
// Only one of ip/asn is ever sent. If queryType is 'auto', neither
|
|
97
|
+
// is sent and the API auto-detects the caller's IP.
|
|
98
|
+
let ip = "";
|
|
99
|
+
let asn = "";
|
|
100
|
+
|
|
101
|
+
if (node.queryType === "ip") {
|
|
102
|
+
if (
|
|
103
|
+
node.ipType === "msg" ||
|
|
104
|
+
node.ipType === "flow" ||
|
|
105
|
+
node.ipType === "global"
|
|
106
|
+
) {
|
|
107
|
+
ip =
|
|
108
|
+
resolveValue(RED, node, msg, {
|
|
109
|
+
value: node.ipValue,
|
|
110
|
+
type: node.ipType,
|
|
111
|
+
}) || "";
|
|
112
|
+
} else {
|
|
113
|
+
ip = (node.ipValue || "").trim();
|
|
114
|
+
}
|
|
115
|
+
ip = String(ip).trim();
|
|
116
|
+
} else if (node.queryType === "asn") {
|
|
117
|
+
if (
|
|
118
|
+
node.asnType === "msg" ||
|
|
119
|
+
node.asnType === "flow" ||
|
|
120
|
+
node.asnType === "global"
|
|
121
|
+
) {
|
|
122
|
+
asn =
|
|
123
|
+
resolveValue(RED, node, msg, {
|
|
124
|
+
value: node.asnValue,
|
|
125
|
+
type: node.asnType,
|
|
126
|
+
}) || "";
|
|
127
|
+
} else {
|
|
128
|
+
asn = (node.asnValue || "").trim();
|
|
129
|
+
}
|
|
130
|
+
// Accept "AS24940" or "24940" — API expects the bare number.
|
|
131
|
+
asn = String(asn).trim().replace(/^as/i, "").trim();
|
|
132
|
+
}
|
|
133
|
+
// 'auto' — leave both ip and asn unset; caller IP is used.
|
|
134
|
+
|
|
135
|
+
// ── Build query parameters ────────────────────────────────────────
|
|
136
|
+
// ASN endpoint has no `lang` param — apiKey / ip|asn / include /
|
|
137
|
+
// fields / excludes.
|
|
138
|
+
const params = buildCommonParams(apiKey, {
|
|
139
|
+
fields: node.fields,
|
|
140
|
+
excludes: node.excludes,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (ip) params.ip = ip;
|
|
144
|
+
if (asn) params.asn = asn;
|
|
145
|
+
|
|
146
|
+
// Build include list (may come from node config or msg override)
|
|
147
|
+
const includeList = (msg.ipgeo_include || node.include || "").trim();
|
|
148
|
+
if (includeList) params.include = includeList;
|
|
149
|
+
|
|
150
|
+
// ── Set working status ────────────────────────────────────────────
|
|
151
|
+
setStatus(node, "WORKING");
|
|
152
|
+
|
|
153
|
+
// ── API call ──────────────────────────────────────────────────────
|
|
154
|
+
try {
|
|
155
|
+
const { data, credits } = await request({
|
|
156
|
+
method: "GET",
|
|
157
|
+
path: "/asn",
|
|
158
|
+
params,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Success ────────────────────────────────────────────────────
|
|
162
|
+
setStatus(
|
|
163
|
+
node,
|
|
164
|
+
"OK",
|
|
165
|
+
data.asn?.as_number || data.asn?.organization || data.ip || "ok"
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Write result to configured output property
|
|
169
|
+
setOutputProperty(
|
|
170
|
+
RED,
|
|
171
|
+
node,
|
|
172
|
+
msg,
|
|
173
|
+
{ value: node.outputProp, type: node.outputType },
|
|
174
|
+
data
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Expose credits consumed (informational) — flat 1 credit/lookup
|
|
178
|
+
if (credits !== null) msg.ipgeo_credits = credits;
|
|
179
|
+
|
|
180
|
+
// Clear error property if previously set
|
|
181
|
+
delete msg.error;
|
|
182
|
+
|
|
183
|
+
send(node.twoOutputs ? [msg, null] : msg);
|
|
184
|
+
if (done) done();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── Clean up on node removal/redeploy ─────────────────────────────────
|
|
191
|
+
node.on("close", function () {
|
|
192
|
+
setStatus(node, "IDLE");
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
RED.nodes.registerType("ipgeo-asn", IPGeoASNNode);
|
|
197
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<!-- ============================================================
|
|
2
|
+
ipgeo-astronomy.html
|
|
3
|
+
Astronomy (sun & moon) node
|
|
4
|
+
============================================================ -->
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
RED.nodes.registerType('ipgeo-astronomy', {
|
|
11
|
+
category: 'IPGeolocation',
|
|
12
|
+
color: '#5da0d0',
|
|
13
|
+
icon: 'font-awesome/fa-sun-o',
|
|
14
|
+
paletteLabel:'ipgeo astronomy',
|
|
15
|
+
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: '' },
|
|
18
|
+
server: { value: '', type: 'ipgeo-config', required: true },
|
|
19
|
+
mode: { value: 'coords' },
|
|
20
|
+
inputType: { value: 'str' },
|
|
21
|
+
inputValue: { value: '' },
|
|
22
|
+
latType: { value: 'str' },
|
|
23
|
+
latValue: { value: '' },
|
|
24
|
+
lngType: { value: 'str' },
|
|
25
|
+
lngValue: { value: '' },
|
|
26
|
+
dateType: { value: 'str' },
|
|
27
|
+
dateValue: { value: '' },
|
|
28
|
+
tzType: { value: 'str' },
|
|
29
|
+
tzValue: { value: '' },
|
|
30
|
+
elevationType: { value: 'str' },
|
|
31
|
+
elevationValue: { value: '' },
|
|
32
|
+
langType: { value: 'str' },
|
|
33
|
+
langValue: { value: '' },
|
|
34
|
+
outputProp: { value: 'payload' },
|
|
35
|
+
outputType: { value: 'msg' },
|
|
36
|
+
twoOutputs: { value: true },
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
inputs: 1,
|
|
40
|
+
outputs: 2,
|
|
41
|
+
outputLabels: function (idx) { return idx === 0 ? 'success' : 'error'; },
|
|
42
|
+
|
|
43
|
+
label: function () { return this.name || 'ipgeo astronomy'; },
|
|
44
|
+
|
|
45
|
+
oneditprepare: function () {
|
|
46
|
+
const node = this;
|
|
47
|
+
|
|
48
|
+
const typedOpts = { types: ['msg', 'flow', 'global', 'str'] };
|
|
49
|
+
|
|
50
|
+
$('#node-input-inputValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-inputType' });
|
|
51
|
+
$('#node-input-latValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-latType' });
|
|
52
|
+
$('#node-input-lngValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-lngType' });
|
|
53
|
+
$('#node-input-dateValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-dateType' });
|
|
54
|
+
$('#node-input-tzValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-tzType' });
|
|
55
|
+
$('#node-input-elevationValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-elevationType' });
|
|
56
|
+
$('#node-input-langValue').typedInput({ ...typedOpts, default: 'str', typeField: '#node-input-langType' });
|
|
57
|
+
|
|
58
|
+
$('#node-input-outputProp').typedInput({
|
|
59
|
+
default: 'msg',
|
|
60
|
+
typeField: '#node-input-outputType',
|
|
61
|
+
types: ['msg', 'flow', 'global'],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
$('#node-input-twoOutputs').prop('checked', node.twoOutputs !== false);
|
|
65
|
+
|
|
66
|
+
// Show only the inputs relevant to the selected mode.
|
|
67
|
+
const placeholders = {
|
|
68
|
+
location: 'e.g. New York, USA',
|
|
69
|
+
ip: 'e.g. 8.8.8.8',
|
|
70
|
+
};
|
|
71
|
+
function refreshMode() {
|
|
72
|
+
const mode = $('#node-input-mode').val();
|
|
73
|
+
$('.ipgeo-coords-row').toggle(mode === 'coords');
|
|
74
|
+
$('.ipgeo-single-row').toggle(mode === 'location' || mode === 'ip');
|
|
75
|
+
if (placeholders[mode]) {
|
|
76
|
+
$('#node-input-inputValue').attr('placeholder', placeholders[mode]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
$('#node-input-mode').on('change', refreshMode);
|
|
80
|
+
refreshMode();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
oneditsave: function () {
|
|
84
|
+
this.twoOutputs = $('#node-input-twoOutputs').prop('checked');
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}());
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<!-- ── Edit template ── -->
|
|
91
|
+
<script type="text/html" data-template-name="ipgeo-astronomy">
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
94
|
+
<input type="text" id="node-input-name" placeholder="ipgeo astronomy" style="width:70%"/>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-server"><i class="fa fa-key"></i> API Config</label>
|
|
98
|
+
<input type="text" id="node-input-server" style="width:70%"/>
|
|
99
|
+
</div>
|
|
100
|
+
<hr/>
|
|
101
|
+
|
|
102
|
+
<!-- Lookup mode -->
|
|
103
|
+
<div class="form-row">
|
|
104
|
+
<label for="node-input-mode"><i class="fa fa-crosshairs"></i> Look up by</label>
|
|
105
|
+
<select id="node-input-mode" style="width:70%">
|
|
106
|
+
<option value="coords">Coordinates (lat & long)</option>
|
|
107
|
+
<option value="location">Location / city address</option>
|
|
108
|
+
<option value="ip">IP address</option>
|
|
109
|
+
<option value="auto">Caller IP (no input)</option>
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Coordinates -->
|
|
114
|
+
<div class="form-row ipgeo-coords-row">
|
|
115
|
+
<label for="node-input-latValue"><i class="fa fa-arrows-v"></i> Latitude</label>
|
|
116
|
+
<input type="text" id="node-input-latValue" placeholder="e.g. 40.7128" style="width:70%"/>
|
|
117
|
+
<input type="hidden" id="node-input-latType"/>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="form-row ipgeo-coords-row">
|
|
120
|
+
<label for="node-input-lngValue"><i class="fa fa-arrows-h"></i> Longitude</label>
|
|
121
|
+
<input type="text" id="node-input-lngValue" placeholder="e.g. -74.0060" style="width:70%"/>
|
|
122
|
+
<input type="hidden" id="node-input-lngType"/>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Single value (location / ip) -->
|
|
126
|
+
<div class="form-row ipgeo-single-row">
|
|
127
|
+
<label for="node-input-inputValue"><i class="fa fa-map-marker"></i> Value</label>
|
|
128
|
+
<input type="text" id="node-input-inputValue" style="width:70%"/>
|
|
129
|
+
<input type="hidden" id="node-input-inputType"/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<hr/>
|
|
133
|
+
|
|
134
|
+
<!-- Optional date -->
|
|
135
|
+
<div class="form-row">
|
|
136
|
+
<label for="node-input-dateValue"><i class="fa fa-calendar"></i> Date</label>
|
|
137
|
+
<input type="text" id="node-input-dateValue" placeholder="YYYY-MM-DD (leave empty for today)" style="width:70%"/>
|
|
138
|
+
<input type="hidden" id="node-input-dateType"/>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Optional timezone -->
|
|
142
|
+
<div class="form-row">
|
|
143
|
+
<label for="node-input-tzValue"><i class="fa fa-clock-o"></i> Time zone</label>
|
|
144
|
+
<input type="text" id="node-input-tzValue" placeholder="e.g. America/New_York (optional)" style="width:70%"/>
|
|
145
|
+
<input type="hidden" id="node-input-tzType"/>
|
|
146
|
+
<div style="margin-top:4px;font-size:0.82em;color:#888;">
|
|
147
|
+
Converts all event times into this IANA time zone. Leave empty to use
|
|
148
|
+
the location's local time.
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Optional elevation -->
|
|
153
|
+
<div class="form-row">
|
|
154
|
+
<label for="node-input-elevationValue"><i class="fa fa-mountain"></i> Elevation</label>
|
|
155
|
+
<input type="text" id="node-input-elevationValue" placeholder="metres, 0 to 10000 (optional)" style="width:70%"/>
|
|
156
|
+
<input type="hidden" id="node-input-elevationType"/>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Optional language -->
|
|
160
|
+
<div class="form-row">
|
|
161
|
+
<label for="node-input-langValue"><i class="fa fa-language"></i> Language</label>
|
|
162
|
+
<input type="text" id="node-input-langValue" placeholder="e.g. en, de, fr (optional, paid plans)" style="width:70%"/>
|
|
163
|
+
<input type="hidden" id="node-input-langType"/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<hr/>
|
|
167
|
+
<div class="form-row">
|
|
168
|
+
<label for="node-input-outputProp"><i class="fa fa-sign-out"></i> Output to</label>
|
|
169
|
+
<input type="text" id="node-input-outputProp" style="width:70%"/>
|
|
170
|
+
<input type="hidden" id="node-input-outputType"/>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="form-row">
|
|
173
|
+
<label><i class="fa fa-random"></i> Outputs</label>
|
|
174
|
+
<input type="checkbox" id="node-input-twoOutputs" style="width:auto;margin-right:6px"/>
|
|
175
|
+
<label for="node-input-twoOutputs" style="display:inline;font-weight:normal">
|
|
176
|
+
Use second output for errors
|
|
177
|
+
</label>
|
|
178
|
+
</div>
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<!-- ── Help panel ── -->
|
|
182
|
+
<script type="text/html" data-help-name="ipgeo-astronomy">
|
|
183
|
+
<p>
|
|
184
|
+
Retrieves sun and moon rise/set times, twilight phases, and positional
|
|
185
|
+
data via the <strong>IPGeolocation.io Astronomy API</strong>.
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
<h3>Look up by</h3>
|
|
189
|
+
<p>The location can be derived in one of four ways:</p>
|
|
190
|
+
<ul>
|
|
191
|
+
<li><b>Coordinates</b>: latitude + longitude.</li>
|
|
192
|
+
<li><b>Location / city address</b>: e.g. <code>New York, USA</code>.</li>
|
|
193
|
+
<li><b>IP address</b>: any IPv4 or IPv6 address.</li>
|
|
194
|
+
<li><b>Caller IP</b>: send no location and let the API use the caller's IP.</li>
|
|
195
|
+
</ul>
|
|
196
|
+
<p>
|
|
197
|
+
If more than one is supplied, the API prefers coordinates, then location,
|
|
198
|
+
then IP.
|
|
199
|
+
</p>
|
|
200
|
+
|
|
201
|
+
<h3>Inputs</h3>
|
|
202
|
+
<dl class="message-properties">
|
|
203
|
+
<dt class="optional">Latitude / Longitude <span class="property-type">number | string</span></dt>
|
|
204
|
+
<dd>Decimal degrees. Required in <i>Coordinates</i> mode.</dd>
|
|
205
|
+
<dt class="optional">Value <span class="property-type">string</span></dt>
|
|
206
|
+
<dd>The address or IP. Required in <i>Location</i> or <i>IP</i> mode.</dd>
|
|
207
|
+
<dt class="optional">Date <span class="property-type">string</span></dt>
|
|
208
|
+
<dd><code>YYYY-MM-DD</code>. Defaults to today at the location.</dd>
|
|
209
|
+
<dt class="optional">Time zone <span class="property-type">string</span></dt>
|
|
210
|
+
<dd>IANA name; converts event times into this zone.</dd>
|
|
211
|
+
<dt class="optional">Elevation <span class="property-type">number | string</span></dt>
|
|
212
|
+
<dd>Metres above sea level, 0 to 10000. Defaults to 0.</dd>
|
|
213
|
+
<dt class="optional">Language <span class="property-type">string</span></dt>
|
|
214
|
+
<dd>Language for the <code>location</code> object (paid plans).</dd>
|
|
215
|
+
</dl>
|
|
216
|
+
|
|
217
|
+
<h3>Output</h3>
|
|
218
|
+
<dl class="message-properties">
|
|
219
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
220
|
+
<dd>
|
|
221
|
+
Object with a <code>location</code> block and an <code>astronomy</code>
|
|
222
|
+
block. The latter holds <code>sunrise</code>, <code>sunset</code>,
|
|
223
|
+
<code>solar_noon</code>, <code>day_length</code>, <code>sun_altitude</code>,
|
|
224
|
+
<code>sun_azimuth</code>, <code>sun_distance</code>, <code>moonrise</code>,
|
|
225
|
+
<code>moonset</code>, <code>moon_phase</code>,
|
|
226
|
+
<code>moon_illumination_percentage</code>, plus <code>morning</code> and
|
|
227
|
+
<code>evening</code> twilight phases. When looked up by IP, an
|
|
228
|
+
<code>ip</code> field is also returned.
|
|
229
|
+
</dd>
|
|
230
|
+
<dt>ipgeo_credits <span class="property-type">number</span></dt>
|
|
231
|
+
<dd>Credits charged for the request (1 per astronomy lookup).</dd>
|
|
232
|
+
</dl>
|
|
233
|
+
|
|
234
|
+
<h3>References</h3>
|
|
235
|
+
<ul>
|
|
236
|
+
<li>
|
|
237
|
+
<a href="https://ipgeolocation.io/documentation/astronomy-api.html" target="_blank">
|
|
238
|
+
Astronomy API documentation
|
|
239
|
+
</a>
|
|
240
|
+
</li>
|
|
241
|
+
</ul>
|
|
242
|
+
</script>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ipgeo-astronomy.js
|
|
3
|
+
*
|
|
4
|
+
* Node-RED node: "ipgeo astronomy"
|
|
5
|
+
*
|
|
6
|
+
* Fetches sun & moon rise/set times and positional data via the
|
|
7
|
+
* IPGeolocation.io /v3/astronomy endpoint.
|
|
8
|
+
*
|
|
9
|
+
* Input modes (the API derives the location from one of these)
|
|
10
|
+
* ────────────────────────────────────────────────────────────
|
|
11
|
+
* auto — send no location parameter; the API uses the caller's IP
|
|
12
|
+
* coords — latitude + longitude (lat / long)
|
|
13
|
+
* location — a free-text address, preferably a city (location)
|
|
14
|
+
* ip — an IPv4 or IPv6 address (ip)
|
|
15
|
+
*
|
|
16
|
+
* Per the API: if several location parameters are present, lat/long win,
|
|
17
|
+
* then location, then ip; with none, the caller's IP is used.
|
|
18
|
+
*
|
|
19
|
+
* Optional inputs (any mode)
|
|
20
|
+
* ──────────────────────────
|
|
21
|
+
* date — "YYYY-MM-DD"; defaults to today at the location
|
|
22
|
+
* time_zone — IANA timezone name; event times are converted to it
|
|
23
|
+
* elevation — metres above sea level (0 to 10000); default 0
|
|
24
|
+
* lang — response language for the location object (paid plans)
|
|
25
|
+
*
|
|
26
|
+
* Every value can be static (node config) or sourced dynamically from
|
|
27
|
+
* msg / flow / global context.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const { request, IPGeoError } = require('./lib/api-client');
|
|
33
|
+
const {
|
|
34
|
+
setStatus,
|
|
35
|
+
resolveValue,
|
|
36
|
+
setOutputProperty,
|
|
37
|
+
handleNodeError,
|
|
38
|
+
} = require('./lib/node-helpers');
|
|
39
|
+
|
|
40
|
+
// Editor mode -> single-value query parameter name. 'coords' is handled
|
|
41
|
+
// separately because it maps to two parameters (lat + long); 'auto' sends
|
|
42
|
+
// no location parameter at all.
|
|
43
|
+
const MODE_PARAM = {
|
|
44
|
+
auto: null,
|
|
45
|
+
coords: null,
|
|
46
|
+
location: 'location',
|
|
47
|
+
ip: 'ip',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = function (RED) {
|
|
51
|
+
|
|
52
|
+
function IPGeoAstronomyNode(config) {
|
|
53
|
+
RED.nodes.createNode(this, config);
|
|
54
|
+
|
|
55
|
+
const configNode = RED.nodes.getNode(config.server);
|
|
56
|
+
|
|
57
|
+
this.name = config.name;
|
|
58
|
+
this.mode = config.mode || 'coords'; // auto | coords | location | ip
|
|
59
|
+
this.inputType = config.inputType || 'str'; // for location / ip modes
|
|
60
|
+
this.inputValue = config.inputValue || '';
|
|
61
|
+
this.latType = config.latType || 'str';
|
|
62
|
+
this.latValue = config.latValue || '';
|
|
63
|
+
this.lngType = config.lngType || 'str';
|
|
64
|
+
this.lngValue = config.lngValue || '';
|
|
65
|
+
this.dateType = config.dateType || 'str';
|
|
66
|
+
this.dateValue = config.dateValue || ''; // empty = API default (today)
|
|
67
|
+
this.tzType = config.tzType || 'str';
|
|
68
|
+
this.tzValue = config.tzValue || ''; // empty = API determines tz
|
|
69
|
+
this.elevationType = config.elevationType || 'str';
|
|
70
|
+
this.elevationValue = config.elevationValue || ''; // empty = API default (0)
|
|
71
|
+
this.langType = config.langType || 'str';
|
|
72
|
+
this.langValue = config.langValue || ''; // empty = API default (en)
|
|
73
|
+
this.outputProp = config.outputProp || 'payload';
|
|
74
|
+
this.outputType = config.outputType || 'msg';
|
|
75
|
+
this.twoOutputs = config.twoOutputs !== false;
|
|
76
|
+
|
|
77
|
+
const node = this;
|
|
78
|
+
|
|
79
|
+
if (!configNode) {
|
|
80
|
+
setStatus(node, 'NO_KEY');
|
|
81
|
+
} else {
|
|
82
|
+
setStatus(node, 'IDLE');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
node.on('input', async function (msg, send, done) {
|
|
86
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
87
|
+
|
|
88
|
+
const apiKey = configNode && configNode.credentials
|
|
89
|
+
? configNode.credentials.apiKey : null;
|
|
90
|
+
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
setStatus(node, 'NO_KEY');
|
|
93
|
+
const err = new IPGeoError('No API key configured', 'NO_API_KEY');
|
|
94
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const params = { apiKey };
|
|
99
|
+
const mode = node.mode;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (mode === 'coords') {
|
|
103
|
+
const lat = resolveValue(RED, node, msg,
|
|
104
|
+
{ value: node.latValue, type: node.latType });
|
|
105
|
+
const lng = resolveValue(RED, node, msg,
|
|
106
|
+
{ value: node.lngValue, type: node.lngType });
|
|
107
|
+
|
|
108
|
+
if (lat === undefined || lat === null || String(lat).trim() === '') {
|
|
109
|
+
throw new IPGeoError('Latitude is required for coords mode', 'INVALID_INPUT');
|
|
110
|
+
}
|
|
111
|
+
if (lng === undefined || lng === null || String(lng).trim() === '') {
|
|
112
|
+
throw new IPGeoError('Longitude is required for coords mode', 'INVALID_INPUT');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
params.lat = String(lat).trim();
|
|
116
|
+
params.long = String(lng).trim();
|
|
117
|
+
|
|
118
|
+
} else if (mode === 'location' || mode === 'ip') {
|
|
119
|
+
const paramKey = MODE_PARAM[mode];
|
|
120
|
+
const inputVal = resolveValue(RED, node, msg,
|
|
121
|
+
{ value: node.inputValue, type: node.inputType });
|
|
122
|
+
|
|
123
|
+
if (!inputVal && inputVal !== 0) {
|
|
124
|
+
throw new IPGeoError(`Input value is empty for mode "${mode}"`, 'INVALID_INPUT');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
params[paramKey] = String(inputVal).trim();
|
|
128
|
+
|
|
129
|
+
} else if (mode !== 'auto') {
|
|
130
|
+
throw new IPGeoError(`Unknown lookup mode: ${mode}`, 'INVALID_MODE');
|
|
131
|
+
}
|
|
132
|
+
// 'auto' sends no location parameter; the API falls back to the caller IP.
|
|
133
|
+
} catch (err) {
|
|
134
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Optional parameters (all modes) ───────────────────────────────
|
|
139
|
+
const date = resolveValue(RED, node, msg,
|
|
140
|
+
{ value: node.dateValue, type: node.dateType });
|
|
141
|
+
if (date && String(date).trim()) {
|
|
142
|
+
params.date = String(date).trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const tz = resolveValue(RED, node, msg,
|
|
146
|
+
{ value: node.tzValue, type: node.tzType });
|
|
147
|
+
if (tz && String(tz).trim()) {
|
|
148
|
+
params.time_zone = String(tz).trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const elevation = resolveValue(RED, node, msg,
|
|
152
|
+
{ value: node.elevationValue, type: node.elevationType });
|
|
153
|
+
if (elevation !== undefined && elevation !== null && String(elevation).trim() !== '') {
|
|
154
|
+
params.elevation = String(elevation).trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const lang = resolveValue(RED, node, msg,
|
|
158
|
+
{ value: node.langValue, type: node.langType });
|
|
159
|
+
if (lang && String(lang).trim()) {
|
|
160
|
+
params.lang = String(lang).trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setStatus(node, 'WORKING');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const { data, credits } = await request({
|
|
167
|
+
method: 'GET',
|
|
168
|
+
path: '/astronomy',
|
|
169
|
+
params,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The response nests the data under an `astronomy` object.
|
|
173
|
+
const sunrise = data && data.astronomy ? data.astronomy.sunrise : undefined;
|
|
174
|
+
setStatus(node, 'OK', sunrise ? `sunrise ${sunrise}` : 'ok');
|
|
175
|
+
|
|
176
|
+
setOutputProperty(RED, node, msg,
|
|
177
|
+
{ value: node.outputProp, type: node.outputType }, data);
|
|
178
|
+
|
|
179
|
+
if (credits !== null) msg.ipgeo_credits = credits;
|
|
180
|
+
delete msg.error;
|
|
181
|
+
|
|
182
|
+
send(node.twoOutputs ? [msg, null] : msg);
|
|
183
|
+
if (done) done();
|
|
184
|
+
|
|
185
|
+
} catch (err) {
|
|
186
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
node.on('close', () => setStatus(node, 'IDLE'));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
RED.nodes.registerType('ipgeo-astronomy', IPGeoAstronomyNode);
|
|
194
|
+
|
|
195
|
+
};
|