diff --git a/99-matrixbot.html b/99-matrixbot.html new file mode 100644 index 0000000..de45b25 --- /dev/null +++ b/99-matrixbot.html @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/99-matrixbot.js b/99-matrixbot.js new file mode 100644 index 0000000..f266278 --- /dev/null +++ b/99-matrixbot.js @@ -0,0 +1,393 @@ +module.exports = function (RED) { + + "use strict"; + + var sdk = require("matrix-js-sdk"); + var md = require("markdown-it")(); + const fs = require('fs'); + + // -------------------------------------------------------------------------------------------- + // The configuration node holds the configuration and credentials for all nodes. + + function MatrixBotNode(config) { + RED.nodes.createNode(this, config); + + // copy "this" object in case we need it in context of callbacks of other functions. + var node = this; + + node.log("Initializing Matrix Bot node"); + + // Configuration options passed by Node Red + node.userId = config.userId; + node.room = config.room; + + // TODO: Switch from configuration to credentials and check with if (this.credentials) + node.accessToken = config.accessToken; + node.matrixServerURL = config.matrixServerURL; + + node.matrixClient = sdk.createClient({ + baseUrl: node.matrixServerURL, + accessToken: node.accessToken, + userId: node.userId + }); + + // If no room is specified, join any room where we are invited + if (!node.room || node.room === "") { + node.matrixClient.on("RoomMember.membership", function (event, member) { + if (member.membership === "invite" && member.userId === node.userId) { + node.log("Trying to join room " + member.roomId); + node.matrixClient.joinRoom(member.roomId).then(function () { + node.log("Automatically accepted invitation to join room " + member.roomId); + }).catch(function (e) { + node.warn("Cannot join room (probably because I was kicked) " + member.roomId + ": " + e); + }); + } + }); + } + + node.matrixClient.on("sync", function (state, prevState, data) { + switch (state) { + case "ERROR": + // update UI to say "Connection Lost" + node.warn("Connection to Matrix server lost"); + node.updateConnectionState(false); + break; + case "SYNCING": + // update UI to remove any "Connection Lost" message + node.updateConnectionState(true); + break; + case "PREPARED": + // the client instance is ready to be queried. + node.log("Synchronized to Matrix server."); + + if (node.room) { + node.log("Trying to join room " + node.room); + + node.matrixClient.joinRoom(node.room, { syncRoom: false }) + .then(function (joinedRoom) { + node.log("Joined " + node.room); + node.room = joinedRoom.roomId; + node.updateConnectionState(true); + }).catch(function (e) { + node.warn("Error joining " + node.room + ": " + e); + }); + } else { + node.log("No room configured. Will only join rooms where I'm invited"); + } + break; + } + }); + + node.log("Connecting to Matrix server..."); + + node.matrixClient.startClient(); + + // Called when the connection state may have changed + this.updateConnectionState = function (connected) { + if (node.connected !== connected) { + node.connected = connected; + if (connected) { + node.emit("connected"); + } else { + node.emit("disconnected"); + } + } + }; + + // When Node-RED updates nodes, disconnect from server to ensure a clean start + node.on("close", function (done) { + node.log("Matrix configuration node closing..."); + if (node.matrixClient) { + node.log("Disconnecting from Matrix server..."); + node.matrixClient.stopClient(); + node.updateConnectionState(false); + } + done(); + }); + + } + + RED.nodes.registerType("matrix bot", MatrixBotNode); + + // -------------------------------------------------------------------------------------------- + // The output node sends a message to the chat. + + function MatrixOutNode(config) { + RED.nodes.createNode(this, config); + + // copy "this" object in case we need it in context of callbacks of other functions. + var node = this; + + // Configuration options passed by Node Red + node.configNode = RED.nodes.getNode(config.bot); + + node.configNode.on("connected", function () { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.configNode.on("disconnected", function () { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + this.on("input", function (msg) { + if (!node.configNode || !node.configNode.matrixClient) { + node.warn("No configuration"); + return; + } + + if (msg.payload) { + node.log("Sending message " + msg.payload); + + var destRoom = ""; + if (msg.roomId) { + destRoom = msg.roomId; + } else if (node.configNode.room) { + destRoom = node.configNode.room; + } else { + node.warn("Room must be specified in msg.roomId or in configuration"); + return; + } + + node.configNode.matrixClient.sendHtmlMessage(destRoom, msg.payload.toString(), md.render(msg.payload.toString())) + .then(function () { + node.log("Message sent: " + msg.payload); + }).catch(function (e) { + node.warn("Error sending message " + e); + }); + } else { + node.warn("msg.payload is empty"); + } + }); + + this.on("close", function (done) { + node.log("Matrix out node closing..."); + done(); + }); + } + + RED.nodes.registerType("matrix sender", MatrixOutNode); + + // -------------------------------------------------------------------------------------------- + // Tim: File Send node + + function MatrixFileOutNode(config) { + RED.nodes.createNode(this, config); + + const node = this; + + node.configNode = RED.nodes.getNode(config.bot); + + node.configNode.on('connected', () => { + node.status({ fill: 'green', shape: 'ring', text: 'connected' }); + }); + + node.configNode.on('disconnected', () => { + node.status({ fill: 'red', shape: 'ring', text: 'disconnected' }); + }); + + this.on('input', msg => { + if (!node.configNode || !node.configNode.matrixClient) { + node.warn('No configuration'); + return; + } + + if (msg.payload) { + node.log('Sending message ' + msg.payload); + + let destRoom = ''; + + if (msg.roomId) { + destRoom = msg.roomId; + } + else if (node.configNode.room) { + destRoom = node.configNode.room; + } + else { + node.warn('Room must be specified in msg.roomId or in config'); + return; + } + + const filePath = msg.payload; + fs.readFile(filePath, (err, data) => { + if (err) { + node.warn('Error reading file', err); + return; + } + + const tks = filePath.split('/'); + const fname = tks[tks.length - 1]; + + const opts = { + rawResponse: false, + name: fname + } + + node.configNode.matrixClient.uploadContent(data, opts).then(res => { + node.log('success uploading file', res); + console.log('Uploaded file', res); + + const imageUri = res.content_uri; + console.log('imageUri', imageUri); + + node.configNode.matrixClient.sendImageMessage(destRoom, imageUri, {}, 'image.png', () => { + console.log('image send func'); + }).then(() => { + node.log('image sent success'); + console.log('image sent success') + }).catch(e => { + node.warn('error sending img: ', e); + console.log('error sending image', e) + }) + }).catch(err => { + node.warn('Error uploading file', err); + }) + }); + } + else { + node.warn('msg.payload empty') + } + }); + + this.on('close', done => { + node.log('matrix file out node closing...'); + done(); + }); + } + + RED.nodes.registerType("matrix file sender", MatrixFileOutNode); + + + // -------------------------------------------------------------------------------------------- + // The input node receives messages from the chat. + + function MatrixInNode(config) { + RED.nodes.createNode(this, config); + + // copy "this" object in case we need it in context of callbacks of other functions. + var node = this; + node.configNode = RED.nodes.getNode(config.bot); + + node.log("MatrixInNode initializing..."); + + if (!node.configNode) { + node.warn("No configuration node"); + return; + } + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.configNode.on("disconnected", function () { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.configNode.on("connected", function () { + node.status({ fill: "green", shape: "ring", text: "connected" }); + node.configNode.matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline, data) { + if (toStartOfTimeline) { + return; // don't print paginated results + } + if (event.getType() !== "m.room.message") { + return; // only keep messages + } + if (!event.getSender() || event.getSender() === node.configNode.userId) { + return; // ignore our own messages + } + if (!event.getUnsigned() || event.getUnsigned().age > 1000) { + return; // ignore old messages + } + // TODO process messages other than text + node.log( + // the room name will update with m.room.name events automatically + "Received chat message: (" + room.name + ") " + event.getSender() + " :: " + event.getContent().body + ); + var msg = { + payload: event.getContent().body, + sender: event.getSender(), + roomId: room.roomId + }; + node.send(msg); + }); + }); + + this.on("close", function (done) { + node.log("Matrix in node closing..."); + done(); + }); + + } + + RED.nodes.registerType("matrix receiver", MatrixInNode); + + // -------------------------------------------------------------------------------------------- + // The command node receives messages from the chat. + + function MatrixCommandNode(config) { + RED.nodes.createNode(this, config); + + // copy "this" object in case we need it in context of callbacks of other functions. + var node = this; + node.command = config.command; + node.configNode = RED.nodes.getNode(config.bot); + + node.log("MatrixCommandNode initializing..."); + + if (!node.configNode) { + node.warn("No configuration node"); + return; + } + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + node.configNode.on("disconnected", function () { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + + node.configNode.on("connected", function () { + node.status({ fill: "green", shape: "ring", text: "connected" }); + node.configNode.matrixClient.on("Room.timeline", function (event, room, toStartOfTimeline, data) { + if (toStartOfTimeline) { + return; // don't print paginated results + } + if (event.getType() !== "m.room.message") { + return; // only keep messages + } + if (!event.getSender() || event.getSender() === node.configNode.userId) { + return; // ignore our own messages + } + if (!event.getUnsigned() || event.getUnsigned().age > 1000) { + return; // ignore old messages + } + // TODO process messages other than text + node.log( + // the room name will update with m.room.name events automatically + "Received chat message: (" + room.name + ") " + event.getSender() + " :: " + event.getContent().body + ); + + var message = event.getContent().body; + + var tokens = message.split(" "); + + if (tokens[0] == node.command) { + node.log("Recognized command " + node.command + " Processing..."); + var remainingText = message.replace(node.command, ""); + var msg = { + payload: remainingText, + sender: event.getSender(), + roomId: room.roomId, + originalMessage: message + }; + node.send([msg, null]); + } + + }); + }); + + this.on("close", function (done) { + node.log("Matrix command node closing..."); + done(); + }); + } + + RED.nodes.registerType("matrix command", MatrixCommandNode); + +}