diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..5c2bf3f
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,84 @@
+# Visual bot creation using Node-RED
+
+It's very easy to create simple interactions with a [Matrix](https://matrix.org) chatroom without programming. Discover [Node-RED](http://nodered.org/), a visual tool to wire together APIs. We have extended Node-RED with nodes to listen and talk in Matrix chatrooms.
+
+![Sample Node-RED application](nodered-twitter.png)
+
+## How to install Node-RED
+
+Node-RED is an open-source web application that is easy to deploy as a Docker container. You can deploy it locally on your computer using Docker. Or you can use any of the available online Docker hosting offers, such as [sloppy.io](https://sloppy.io/) or [hyper_](https://hyper.sh/).
+
+The custom Docker image which includes Node-RED plus additional nodes is available on our [DockerHub repository](https://hub.docker.com/r/maski/node-red-docker/)
+
+### Install Node-RED on your local machine
+
+First, [install Docker](http://www.docker.com/products/docker) if you haven't done it yet.
+
+Then you just need to run:
+
+ docker run -it -p 1880:1880 --name mynodered maski/node-red-docker
+
+Let's dissect that command...
+
+ docker run - run this container... and build locally if necessary first.
+ -it - attach a terminal session so we can see what is going on
+ -p 1880:1880 - connect local port 1880 to the exposed internal port 1880
+ --name mynodered - give this machine a friendly local name
+ maski/node-red-docker - the image to base it on
+
+
+Running that command should give a terminal window with a running instance of Node-RED
+
+ Welcome to Node-RED
+ ===================
+ 8 Apr 12:13:44 - [info] Node-RED version: v0.14.5
+ 8 Apr 12:13:44 - [info] Node.js version: v4.4.7
+ .... etc
+
+You can then browse to `http://{host-ip}:1880` to get the familiar Node-RED desktop.
+
+
+### Install Node-RED on an online Docker hosting
+
+A quick and easy way to host it online is to use Sloppy.io's 1-month free trial.
+Once you have created an account, go to the Dashboard and create a new project, a new service inside it, and an app within it using `maski/node-red-docker` as image path and pick a domain URI, such as `my-node-red.sloppy.zone`. Deploy the project, and you will have your own Node-RED server up and running at the selected URI.
+
+## How to use Node-RED
+
+You can learn how to use Node-RED by following the [Node-RED getting started guide](http://nodered.org/docs/getting-started/first-flow). You can find additional tutorials [here](http://noderedguide.com/).
+
+## How to use the Matrix nodes in Node-RED
+
+The package `node-red-contrib-matrixbot`, included in the abovementioned Docker image, adds 3 node types to the Node-RED palette (Matrix section, at the bottom):
+* *Matrix sender*: sends messages from your Node-RED flow to the chatroom
+* *Matrix receiver*: listens to messages in a chatroom and sends them to your Node-RED flow
+* *Matrix command*: listens only to messages starting with a specific command and sends them to your Node-RED flow
+
+All of these nodes require a Matrix Configuration with the following settings:
+
+* *User ID*: the user ID in the matrix server, for instance @mybot:matrix.org
+* *Access token*: the access token of the user in the matrix server
+* *Server URL*: URL of the Matrix homeserver, e.g. https://matrix.org
+* *Room ID*: ID of the chatroom to join when starting. If no room is specified, it will automatically join any room where it is invited
+
+## A simple application
+
+We will create a simple application that will send messages to a chatroom whenever a RSS feed gets updated:
+
+* First, invite the bot to the chatroom where it will be speaking and note the room ID.
+* In Node-RED, pick the *Feedparse* node from the palette (you can filter by typing the first letters at the top) and drop it on the canvas
+* Double-click on it to configure it and then enter a RSS feed, for instance `http://rss.nytimes.com/services/xml/rss/nyt/World.xml`. Give the node a name if you want and then click *Done*.
+* Pick the *Matrix sender* node and drop it on the canvas. Link the output of the previous node to its input.
+* Now double click on it and click on the pencil next to *Connection* to configure the Matrix settings. Follow the instructions described in the previous section.
+* Once you're done, click the *Deploy* button at the top right corner and you're done!
+* If you want to show also the link in the resulting message, try adding a *function* node in the middle with the following code:
+```
+var title = msg.article.title;
+var link = msg.topic;
+msg.payload = title + " \n" + link;
+return msg;
+```
+
+![Sample Node-RED application](nodered-rss.png)
+
+![ ](https://ga-beacon.appspot.com/UA-63227151-9/docs/README.md?pixel)
\ No newline at end of file
diff --git a/docs/nodered-rss.png b/docs/nodered-rss.png
new file mode 100644
index 0000000..1f4b186
Binary files /dev/null and b/docs/nodered-rss.png differ
diff --git a/docs/nodered-twitter.png b/docs/nodered-twitter.png
new file mode 100644
index 0000000..e61ba98
Binary files /dev/null and b/docs/nodered-twitter.png differ
diff --git a/matrixbot/99-matrixbot.html b/matrixbot/99-matrixbot.html
new file mode 100644
index 0000000..32c00cb
--- /dev/null
+++ b/matrixbot/99-matrixbot.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/matrixbot/99-matrixbot.js b/matrixbot/99-matrixbot.js
new file mode 100644
index 0000000..1885258
--- /dev/null
+++ b/matrixbot/99-matrixbot.js
@@ -0,0 +1,303 @@
+module.exports = function(RED) {
+
+ "use strict";
+
+ var sdk = require("matrix-js-sdk");
+ var md = require("markdown-it")();
+
+// --------------------------------------------------------------------------------------------
+ // 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);
+
+
+// --------------------------------------------------------------------------------------------
+ // 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);
+
+}
diff --git a/matrixbot/icons/matrix.png b/matrixbot/icons/matrix.png
new file mode 100644
index 0000000..ecec1cc
Binary files /dev/null and b/matrixbot/icons/matrix.png differ
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f8d2cb0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "node-red-contrib-matrixbot",
+ "version": "0.0.3",
+ "description": "Matrix bot nodes for Node-RED",
+ "dependencies": {
+ "matrix-js-sdk": "^2.0.0",
+ "markdown-it": "^8.4.1"
+ },
+ "node-red": {
+ "nodes": {
+ "matrixbot": "matrixbot/99-matrixbot.js"
+ }
+ },
+ "keywords": [
+ "node-red", "matrix", "bot"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mlopezr/node-red-contrib-matrixbot"
+ },
+ "author": "nobody@nowhere"
+}