mirror of
https://github.com/Skylar-Tech/node-red-contrib-matrix-chat.git
synced 2026-05-23 15:43:33 -06:00
Add Send Location node; expand Receive's m.location output
New matrix-send-location node publishes m.location events using matrix-js-sdk's makeLocationContent helper, so the wire format matches Element's "Share my location". Per-message inputs are typed inputs (msg / flow / global / num / str), defaulting to the obvious msg.* names. matrix-receive's m.location handler now also sets msg.latitude, msg.longitude, msg.altitude, msg.description, msg.assetType, and msg.timestamp on the output (additive - msg.geo_uri / msg.payload are unchanged), so a Receive node wired straight into Send Location resends a byte-equivalent event. Help docs in both nodes updated.
This commit is contained in:
+2
-1
@@ -56,7 +56,8 @@
|
||||
"matrix-get-event": "src/matrix-get-event.js",
|
||||
"matrix-event-relations": "src/matrix-event-relations.js",
|
||||
"matrix-verification": "src/matrix-verification.js",
|
||||
"matrix-verification-action": "src/matrix-verification-action.js"
|
||||
"matrix-verification-action": "src/matrix-verification-action.js",
|
||||
"matrix-send-location": "src/matrix-send-location.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+28
-1
@@ -357,9 +357,36 @@
|
||||
</li>
|
||||
|
||||
<li><code>msg.type</code> == '<strong>m.location</strong>'
|
||||
<p>
|
||||
The structured location fields are surfaced at the top level of
|
||||
<code>msg</code> so the message can be wired straight into a
|
||||
<code>matrix-send-location</code> node to resend the location
|
||||
without any field translation in between.
|
||||
</p>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.geo_uri <span class="property-type">string</span></dt>
|
||||
<dd>URI format of the geolocation</dd>
|
||||
<dd>The RFC 5870 geo URI from the event (e.g. <code>geo:48.85,2.35</code>).</dd>
|
||||
|
||||
<dt>msg.latitude <span class="property-type">number</span></dt>
|
||||
<dd>Latitude in decimal degrees, parsed from the geo URI.</dd>
|
||||
|
||||
<dt>msg.longitude <span class="property-type">number</span></dt>
|
||||
<dd>Longitude in decimal degrees, parsed from the geo URI.</dd>
|
||||
|
||||
<dt class="optional">msg.altitude <span class="property-type">number</span></dt>
|
||||
<dd>Metres above sea level, parsed from the geo URI. Only set when the geo URI includes an altitude component (<code>geo:lat,lng,alt</code>).</dd>
|
||||
|
||||
<dt class="optional">msg.description <span class="property-type">string</span></dt>
|
||||
<dd>The location's label (e.g. <em>Eiffel Tower</em>). Only set when the sender included one.</dd>
|
||||
|
||||
<dt>msg.assetType <span class="property-type">string</span></dt>
|
||||
<dd><code>"m.self"</code> when the sender was sharing their own location, <code>"m.pin"</code> for a generic dropped pin. Defaults to <code>"m.self"</code> when the event does not carry an explicit asset type (per the Matrix spec).</dd>
|
||||
|
||||
<dt class="optional">msg.timestamp <span class="property-type">number</span></dt>
|
||||
<dd>Milliseconds since the UNIX epoch when the location was correct (the event's <code>m.ts</code> field). Only set when the sender included a timestamp.</dd>
|
||||
|
||||
<dt>msg.payload <span class="property-type">string</span></dt>
|
||||
<dd>The event's <code>body</code> — a human-readable text fallback for clients that cannot render the map snippet.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
+34
-1
@@ -1,3 +1,16 @@
|
||||
// Parse an RFC 5870 geo URI into {latitude, longitude, altitude?}.
|
||||
// Returns null if the URI is missing or malformed.
|
||||
function parseGeoUri(uri) {
|
||||
if (typeof uri !== "string" || uri.indexOf("geo:") !== 0) return null;
|
||||
// strip any ";u=..." / ";crs=..." parameters
|
||||
const body = uri.slice(4).split(";")[0];
|
||||
const parts = body.split(",").map(function(s) { return parseFloat(s.trim()); });
|
||||
if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) return null;
|
||||
const result = { latitude: parts[0], longitude: parts[1] };
|
||||
if (parts.length >= 3 && Number.isFinite(parts[2])) result.altitude = parts[2];
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function(RED) {
|
||||
function MatrixReceiveMessage(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
@@ -146,11 +159,31 @@ module.exports = function(RED) {
|
||||
setThumbnailUrls('thumbnail_file');
|
||||
break;
|
||||
|
||||
case 'm.location':
|
||||
case 'm.location': {
|
||||
if (!node.acceptLocations) return;
|
||||
msg.geo_uri = msg.content.geo_uri;
|
||||
msg.payload = msg.content.body;
|
||||
// Surface the structured location fields at the top level
|
||||
// so a `matrix-send-location` node wired straight to this
|
||||
// output resends the same location. Both the stable
|
||||
// (m.location / m.asset / m.ts) and the MSC3488-prefixed
|
||||
// namespaces are checked, since Element currently emits
|
||||
// the prefixed form even though the spec is stable.
|
||||
const loc = msg.content["m.location"] || msg.content["org.matrix.msc3488.location"] || {};
|
||||
const asset = msg.content["m.asset"] || msg.content["org.matrix.msc3488.asset"] || {};
|
||||
let ts = msg.content["m.ts"];
|
||||
if (typeof ts !== "number") ts = msg.content["org.matrix.msc3488.ts"];
|
||||
const coords = parseGeoUri(loc.uri || msg.geo_uri);
|
||||
if (coords) {
|
||||
msg.latitude = coords.latitude;
|
||||
msg.longitude = coords.longitude;
|
||||
if (coords.altitude !== undefined) msg.altitude = coords.altitude;
|
||||
}
|
||||
if (loc.description) msg.description = loc.description;
|
||||
msg.assetType = asset.type || "m.self";
|
||||
if (typeof ts === "number") msg.timestamp = ts;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'm.reaction':
|
||||
if (!node.acceptReactions) return;
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType('matrix-send-location', {
|
||||
category: 'matrix',
|
||||
color: '#00b7ca',
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
server: { type: "matrix-server-config", required: true },
|
||||
roomId: { value: "" },
|
||||
latitudeType: { value: "msg" },
|
||||
latitudeValue: { value: "latitude" },
|
||||
longitudeType: { value: "msg" },
|
||||
longitudeValue: { value: "longitude" },
|
||||
altitudeType: { value: "msg" },
|
||||
altitudeValue: { value: "altitude" },
|
||||
geoUriType: { value: "msg" },
|
||||
geoUriValue: { value: "geo_uri" },
|
||||
descriptionType: { value: "msg" },
|
||||
descriptionValue: { value: "description" },
|
||||
assetTypeType: { value: "msg" },
|
||||
assetTypeValue: { value: "assetType" },
|
||||
timestampType: { value: "msg" },
|
||||
timestampValue: { value: "timestamp" },
|
||||
textType: { value: "msg" },
|
||||
textValue: { value: "payload" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 2,
|
||||
outputLabels: ["success", "error"],
|
||||
icon: "matrix.png",
|
||||
label: function() {
|
||||
return this.name || "Send Location";
|
||||
},
|
||||
paletteLabel: 'Send Location',
|
||||
oneditprepare: function() {
|
||||
var numTypes = ["msg", "flow", "global", "num"];
|
||||
var strTypes = ["msg", "flow", "global", "str"];
|
||||
|
||||
$("#node-input-latitudeValue").typedInput({
|
||||
typeField: "#node-input-latitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-longitudeValue").typedInput({
|
||||
typeField: "#node-input-longitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-altitudeValue").typedInput({
|
||||
typeField: "#node-input-altitudeType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-geoUriValue").typedInput({
|
||||
typeField: "#node-input-geoUriType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-descriptionValue").typedInput({
|
||||
typeField: "#node-input-descriptionType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-assetTypeValue").typedInput({
|
||||
typeField: "#node-input-assetTypeType",
|
||||
types: [
|
||||
{
|
||||
value: "str",
|
||||
label: "string",
|
||||
options: [
|
||||
{ value: "m.self", label: "My location (m.self)" },
|
||||
{ value: "m.pin", label: "Pin (m.pin)" }
|
||||
]
|
||||
},
|
||||
"msg",
|
||||
"flow",
|
||||
"global"
|
||||
],
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-timestampValue").typedInput({
|
||||
typeField: "#node-input-timestampType",
|
||||
types: numTypes,
|
||||
default: "msg"
|
||||
});
|
||||
$("#node-input-textValue").typedInput({
|
||||
typeField: "#node-input-textType",
|
||||
types: strTypes,
|
||||
default: "msg"
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-template-name="matrix-send-location">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-server"><i class="fa fa-server"></i> Server</label>
|
||||
<input type="text" id="node-input-server">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-roomId"><i class="fa fa-comments"></i> Room ID</label>
|
||||
<input type="text" id="node-input-roomId" placeholder="!room:matrix.org">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">Optional. If empty, <code>msg.topic</code> is used.</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-latitudeValue"><i class="fa fa-crosshairs"></i> Latitude</label>
|
||||
<input type="hidden" id="node-input-latitudeType">
|
||||
<input type="text" id="node-input-latitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-longitudeValue"><i class="fa fa-crosshairs"></i> Longitude</label>
|
||||
<input type="hidden" id="node-input-longitudeType">
|
||||
<input type="text" id="node-input-longitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-altitudeValue"><i class="fa fa-arrow-up"></i> Altitude</label>
|
||||
<input type="hidden" id="node-input-altitudeType">
|
||||
<input type="text" id="node-input-altitudeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Metres above sea level. When provided the geo URI becomes <code>geo:lat,lng,alt</code>.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-geoUriValue"><i class="fa fa-map"></i> geo_uri</label>
|
||||
<input type="hidden" id="node-input-geoUriType">
|
||||
<input type="text" id="node-input-geoUriValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. A pre-built RFC 5870 geo URI (e.g. <code>geo:48.85,2.35</code>). When set, Latitude / Longitude / Altitude are ignored.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-descriptionValue"><i class="fa fa-info-circle"></i> Description</label>
|
||||
<input type="hidden" id="node-input-descriptionType">
|
||||
<input type="text" id="node-input-descriptionValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Label for the location (e.g. <em>Eiffel Tower</em>).
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-assetTypeValue"><i class="fa fa-map-marker"></i> Type</label>
|
||||
<input type="hidden" id="node-input-assetTypeType">
|
||||
<input type="text" id="node-input-assetTypeValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
<b>My location</b> (<code>m.self</code>) says "this is where I am"; <b>Pin</b> (<code>m.pin</code>) marks a generic pinned point.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-timestampValue"><i class="fa fa-clock-o"></i> Timestamp</label>
|
||||
<input type="hidden" id="node-input-timestampType">
|
||||
<input type="text" id="node-input-timestampValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips" style="margin-bottom:12px;">
|
||||
Optional. Milliseconds since the UNIX epoch. Defaults to <em>now</em> when empty.
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-textValue"><i class="fa fa-file-text-o"></i> Body / text</label>
|
||||
<input type="hidden" id="node-input-textType">
|
||||
<input type="text" id="node-input-textValue" style="width:70%">
|
||||
</div>
|
||||
<div class="form-tips">
|
||||
Optional. Override the auto-generated text fallback used in <code>body</code> and <code>m.text</code> for clients that can't render the map.
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="matrix-send-location">
|
||||
<h3>Details</h3>
|
||||
<p>Sends an m.location event to a Matrix room.</p>
|
||||
|
||||
<p>
|
||||
Produces an <code>m.location</code> message just like Element's
|
||||
<em>"Share my location"</em> feature, so the location renders as a map
|
||||
snippet in clients that support it. The event content matches what
|
||||
Element sends — legacy <code>geo_uri</code> + <code>body</code>
|
||||
fields for older clients, plus the modern extensible-events fields
|
||||
(<code>m.location</code>, <code>m.asset</code>, <code>m.text</code>,
|
||||
<code>m.ts</code>) for newer ones. Built with
|
||||
<code>matrix-js-sdk</code>'s <code>makeLocationContent</code> helper so
|
||||
the wire format stays in lockstep with Element's.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Every input below is a <strong>typed input</strong>: pick the source
|
||||
(<code>msg</code>, <code>flow</code>, <code>global</code>,
|
||||
<code>num</code>, or <code>str</code>) and the value. The defaults
|
||||
read from <code>msg.*</code> using the documented names so the simple
|
||||
case <em>just works</em> — reach for the type selector when you
|
||||
want to pull from a flow / global variable or pin a static value on
|
||||
the node itself.
|
||||
</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<dl class="message-properties">
|
||||
<dt>Server</dt>
|
||||
<dd>The Matrix server config node.</dd>
|
||||
|
||||
<dt>Room ID</dt>
|
||||
<dd>Optional default room. If empty, <code>msg.topic</code> is used.</dd>
|
||||
|
||||
<dt>Latitude <span class="property-type">number</span></dt>
|
||||
<dd>Decimal degrees, between -90 and 90. Default: <code>msg.latitude</code>.</dd>
|
||||
|
||||
<dt>Longitude <span class="property-type">number</span></dt>
|
||||
<dd>Decimal degrees, between -180 and 180. Default: <code>msg.longitude</code>.</dd>
|
||||
|
||||
<dt>Altitude <span class="property-type">number, optional</span></dt>
|
||||
<dd>Metres above sea level. When provided, the geo URI becomes <code>geo:lat,lng,alt</code>. Default: <code>msg.altitude</code>.</dd>
|
||||
|
||||
<dt>geo_uri <span class="property-type">string, optional</span></dt>
|
||||
<dd>A pre-built RFC 5870 geo URI (e.g. <code>geo:48.85,2.35</code>). When set, the Latitude / Longitude / Altitude inputs are ignored. Default: <code>msg.geo_uri</code>.</dd>
|
||||
|
||||
<dt>Description <span class="property-type">string, optional</span></dt>
|
||||
<dd>Label for the location (e.g. <em>Eiffel Tower</em>). Default: <code>msg.description</code>.</dd>
|
||||
|
||||
<dt>Type</dt>
|
||||
<dd>
|
||||
<b>My location</b> (<code>m.self</code>) marks the location as
|
||||
"this is where I am right now" (the sender's location).
|
||||
<b>Pin</b> (<code>m.pin</code>) marks it as a generic pinned
|
||||
point. Defaults to <code>msg.assetType</code> (which the Receive
|
||||
node populates for incoming location events, so a Receive node
|
||||
wired straight to this one resends with the same asset type);
|
||||
falls back to <code>m.self</code> when not provided. Flip the
|
||||
type selector to <code>string</code> to pin a static value.
|
||||
</dd>
|
||||
|
||||
<dt>Timestamp <span class="property-type">number, optional</span></dt>
|
||||
<dd>Milliseconds since the UNIX epoch — when the location was correct. Defaults to <em>now</em>. Default source: <code>msg.timestamp</code>.</dd>
|
||||
|
||||
<dt>Body / text <span class="property-type">string, optional</span></dt>
|
||||
<dd>Override the auto-generated text fallback used in <code>body</code> and <code>m.text</code>. If left empty, a sensible default like <code>User Location "Eiffel Tower" geo:48.85,2.35 at 2026-05-23T04:44:41Z</code> is generated. Default source: <code>msg.payload</code> (matches what the Receive node sets for incoming events).</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Inputs</h4>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.topic <span class="property-type">string</span></dt>
|
||||
<dd>Room ID to send the location to. Used when Room ID is not set on the node.</dd>
|
||||
|
||||
<dt class="optional">msg.<configured></dt>
|
||||
<dd>The specific message keys depend on the node configuration. By default the node reads <code>msg.latitude</code>, <code>msg.longitude</code>, <code>msg.altitude</code>, <code>msg.geo_uri</code>, <code>msg.description</code>, <code>msg.assetType</code>, <code>msg.timestamp</code>, and <code>msg.payload</code> — the same field names the Receive node populates for incoming <code>m.location</code> events, so a Receive node wired straight to this one re-sends the location verbatim. Rename or pull from <code>flow</code>/<code>global</code> via the typed-input selectors.</dd>
|
||||
</dl>
|
||||
|
||||
<h4>Outputs</h4>
|
||||
<p>Two outputs — <b>success</b> (top) and <b>error</b> (bottom).</p>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.eventId <span class="property-type">string</span></dt>
|
||||
<dd>The event ID of the sent location message (on the success output).</dd>
|
||||
|
||||
<dt>msg.payload <span class="property-type">object</span></dt>
|
||||
<dd>The full content object that was sent to the room.</dd>
|
||||
|
||||
<dt>msg.error <span class="property-type">object</span></dt>
|
||||
<dd>The error (on the error output, when sending fails).</dd>
|
||||
</dl>
|
||||
</script>
|
||||
@@ -0,0 +1,202 @@
|
||||
// matrix-js-sdk's main entry does not re-export `makeLocationContent`, so we
|
||||
// deep-import the content-helpers module. The SDK has no `exports` field in
|
||||
// its package.json (verified against v41) so subpath imports are stable.
|
||||
const contentHelpersPromise = import("matrix-js-sdk/lib/content-helpers.js");
|
||||
|
||||
module.exports = function(RED) {
|
||||
const VALID_ASSET_TYPES = ["m.self", "m.pin"];
|
||||
|
||||
function MatrixSendLocation(n) {
|
||||
RED.nodes.createNode(this, n);
|
||||
let node = this;
|
||||
|
||||
this.name = n.name;
|
||||
this.server = RED.nodes.getNode(n.server);
|
||||
this.roomId = n.roomId;
|
||||
|
||||
// Dynamic inputs: each has a `*Type` (msg | flow | global | str | num)
|
||||
// and a `*Value` (the property path or literal). Defaults preserve the
|
||||
// documented per-message names so existing flows keep working.
|
||||
this.latitudeType = n.latitudeType || "msg";
|
||||
this.latitudeValue = n.latitudeValue || "latitude";
|
||||
this.longitudeType = n.longitudeType || "msg";
|
||||
this.longitudeValue = n.longitudeValue || "longitude";
|
||||
this.altitudeType = n.altitudeType || "msg";
|
||||
this.altitudeValue = n.altitudeValue || "altitude";
|
||||
this.geoUriType = n.geoUriType || "msg";
|
||||
this.geoUriValue = n.geoUriValue || "geo_uri";
|
||||
this.descriptionType = n.descriptionType || "msg";
|
||||
this.descriptionValue = n.descriptionValue || "description";
|
||||
this.assetTypeType = n.assetTypeType || "msg";
|
||||
this.assetTypeValue = n.assetTypeValue || "assetType";
|
||||
this.timestampType = n.timestampType || "msg";
|
||||
this.timestampValue = n.timestampValue || "timestamp";
|
||||
this.textType = n.textType || "msg";
|
||||
this.textValue = n.textValue || "payload";
|
||||
|
||||
if (!node.server) {
|
||||
node.warn("No configuration node");
|
||||
return;
|
||||
}
|
||||
node.server.register(node);
|
||||
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
node.server.on("disconnected", function() {
|
||||
node.status({ fill: "red", shape: "ring", text: "disconnected" });
|
||||
});
|
||||
node.server.on("connected", function() {
|
||||
node.status({ fill: "green", shape: "ring", text: "connected" });
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve a typed-input pair to its runtime value.
|
||||
* msg | flow | global - read the property path from that source.
|
||||
* num - parse the configured literal as a number;
|
||||
* empty string => undefined.
|
||||
* str - the configured literal; empty string => undefined.
|
||||
* bool - "true" => true, anything else => false.
|
||||
*
|
||||
* Returning `undefined` from this signals "not provided", which is
|
||||
* how optional fields opt out.
|
||||
*/
|
||||
function getToValue(msg, type, property) {
|
||||
if (type === "msg") {
|
||||
return RED.util.getMessageProperty(msg, property);
|
||||
}
|
||||
if (type === "flow" || type === "global") {
|
||||
try {
|
||||
return RED.util.evaluateNodeProperty(property, type, node, msg);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid " + type + " value evaluation for '" + property + "'");
|
||||
}
|
||||
}
|
||||
if (property === "" || property === undefined || property === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (type === "num") {
|
||||
const n = Number(property);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
if (type === "bool") {
|
||||
return property === "true";
|
||||
}
|
||||
// str / default
|
||||
return property;
|
||||
}
|
||||
|
||||
function isEmpty(v) {
|
||||
return v === undefined || v === null || v === "";
|
||||
}
|
||||
|
||||
node.on("input", async function(msg) {
|
||||
if (!node.server || !node.server.matrixClient) {
|
||||
node.warn("No matrix server selected");
|
||||
return;
|
||||
}
|
||||
if (!node.server.isConnected()) {
|
||||
node.error("Matrix server connection is currently closed", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.topic = node.roomId || msg.topic;
|
||||
if (!msg.topic) {
|
||||
node.error("Room must be specified in msg.topic or in configuration", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve every typed input up-front so a single bad config or
|
||||
// flow/global lookup surfaces as one clear error.
|
||||
let rawLat, rawLng, rawAlt, rawGeoUri, description, assetType, rawTimestamp, text;
|
||||
try {
|
||||
rawLat = getToValue(msg, node.latitudeType, node.latitudeValue);
|
||||
rawLng = getToValue(msg, node.longitudeType, node.longitudeValue);
|
||||
rawAlt = getToValue(msg, node.altitudeType, node.altitudeValue);
|
||||
rawGeoUri = getToValue(msg, node.geoUriType, node.geoUriValue);
|
||||
description = getToValue(msg, node.descriptionType, node.descriptionValue);
|
||||
assetType = getToValue(msg, node.assetTypeType, node.assetTypeValue);
|
||||
rawTimestamp = getToValue(msg, node.timestampType, node.timestampValue);
|
||||
text = getToValue(msg, node.textType, node.textValue);
|
||||
} catch (e) {
|
||||
node.error(e.message, msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the geo URI: prefer an explicit geo_uri when supplied;
|
||||
// otherwise build geo:<lat>,<lng>[,<alt>] from numeric inputs.
|
||||
let geoUri = isEmpty(rawGeoUri) ? null : String(rawGeoUri);
|
||||
if (!geoUri) {
|
||||
const lat = parseFloat(rawLat);
|
||||
const lng = parseFloat(rawLng);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
||||
node.error("Latitude and longitude (numbers) - or a geo_uri - are required", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
if (lat < -90 || lat > 90) {
|
||||
node.error("Latitude (" + lat + ") is out of range; must be between -90 and 90", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
if (lng < -180 || lng > 180) {
|
||||
node.error("Longitude (" + lng + ") is out of range; must be between -180 and 180", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
geoUri = "geo:" + lat + "," + lng;
|
||||
if (!isEmpty(rawAlt)) {
|
||||
const alt = parseFloat(rawAlt);
|
||||
if (!Number.isFinite(alt)) {
|
||||
node.error("Altitude must be a number when provided", msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
geoUri = "geo:" + lat + "," + lng + "," + alt;
|
||||
}
|
||||
}
|
||||
|
||||
// Asset type defaults to m.self if the resolved value is empty.
|
||||
if (isEmpty(assetType)) {
|
||||
assetType = "m.self";
|
||||
}
|
||||
if (VALID_ASSET_TYPES.indexOf(assetType) === -1) {
|
||||
node.error('Invalid asset type "' + assetType + '"; must be "m.self" or "m.pin"', msg);
|
||||
node.send([null, msg]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Timestamp the location was correct, in ms since the UNIX epoch.
|
||||
let timestamp = Date.now();
|
||||
if (!isEmpty(rawTimestamp)) {
|
||||
const ts = Number(rawTimestamp);
|
||||
if (Number.isFinite(ts)) {
|
||||
timestamp = ts;
|
||||
}
|
||||
}
|
||||
|
||||
// makeLocationContent uses `undefined` to mean "generate a default".
|
||||
const cleanDescription = isEmpty(description) ? undefined : String(description);
|
||||
const cleanText = isEmpty(text) ? undefined : String(text);
|
||||
|
||||
try {
|
||||
const { makeLocationContent } = await contentHelpersPromise;
|
||||
const content = makeLocationContent(cleanText, geoUri, timestamp, cleanDescription, assetType);
|
||||
const response = await node.server.matrixClient.sendMessage(msg.topic, content);
|
||||
msg.eventId = response.event_id;
|
||||
msg.payload = content;
|
||||
node.send([msg, null]);
|
||||
} catch (e) {
|
||||
node.error("Error sending location: " + e, msg);
|
||||
msg.error = e;
|
||||
node.send([null, msg]);
|
||||
}
|
||||
});
|
||||
|
||||
node.on("close", function() {
|
||||
node.server.deregister(node);
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("matrix-send-location", MatrixSendLocation);
|
||||
};
|
||||
Reference in New Issue
Block a user