Sending a Message

Preparation

Before you can send an instant message programmatically with the IM Service, you must first be logged in. At this point you will have stored in variables the Matrix username, password, and server URL.

Data Model

There are a few key elements in Matrix you should be aware of:

  • User: A user account which matches one-to-one with a “subscriber” in the Administration Service.
  • Device: A machine or browser where the BUZZ client or app is running. A single user can run multiple devices.
  • Room: A virtual location where events happen.
  • Event: Every time something of interest happens in Matrix, an event is produced. There are many kinds of events:
    • name (room name changed)
    • topic (room topic changed)
    • avatar (room avatar changed)
    • message (message posted). Again there are many types of messages:
      • text (html formatted)
      • image/file/video/audio/location (document url or upload)

Instant Messaging data model 

There are other types of objects but these are the most important ones. For this tutorial, we will concentrate on rooms and events (text messages).

Receiving Events (and Rooms) by Polling

In Matrix, there are a couple ways to retrieve a list of rooms. This is going to be important because you have to know the identifiers of the rooms before you can send messages to them. One way to do it is to get a list of events. This in turn contains a list of rooms that the user has joined so far. This is the technique we’ll use here, since it kills two birds with one stone.

Once you have the rooms you can also subscribe to asynchronously receive events. This is covered in the Push Notification tutorial.

Furthermore, to learn other ways to retrieve rooms, refer to the Retrieving Rooms tutorial.

The first step is to write a function that will sync up all events from all rooms by polling. First do a GET on the Matrix endpoint “/_matrix/client/r0/sync” including the Bearer Authorization header:

function sync(callback) {
    var url = matrix_server + "_matrix/client/r0/sync";

    $.ajax({
        url: url,
        method: "GET",
        contentType: 'application/json',
        headers: {
            "Authorization": "Bearer " + im_token
        },
        success: function(result) {
            var join = result["rooms"]["join"];
            retrieveRoomIDsAndNames(join);
            retrieveRoomEvents(join);
            callback();
        },
        error: function(xhr, status, errorThrown) {
            console.log("Error syncing. " + xhr.status + " " + xhr.text + " " + errorThrown);
        }
    });
}
func sync(callback: @escaping () -> Void) {
    var address = matrixServer! + "_matrix/client/r0/sync"
    if lastSync != nil {
        address += "?since=" + lastSync!
    }
    
    let url = URL(string: address)!
    let session = URLSession.shared
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.setValue("Bearer " + imToken!, forHTTPHeaderField: "Authorization")
    request.addValue("application/json", forHTTPHeaderField: "Accept")

    let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
        guard error == nil else {
            print(error)
            return
        }
        guard let data = data else {
            return
        }
        do {
            if let syncResponse = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
                let allRooms = syncResponse["rooms"] as! [String: Any]
                let join = allRooms["join"] as! [String: Any]
                self.lastSync = syncResponse["next_batch"] as? String //Represents to the last event in this batch
                self.retrieveRoomIDsAndNames(join: join) {
                    self.retrieveRoomEvents(join: join) {
                        callback()
                    }
                }
            }
        } catch let error {
            print(error.localizedDescription)
        }
    })
    task.resume()
}

⚠️WARNING: You might be tempted to call this function periodically to keep your application up to date, but that would be a bad idea because it is inefficient. It will retrieve all messages for all rooms even if you already have them in memory. Polling is a particularly bad idea on mobile because it can use up the battery and bandwidth. Fortunately, there are ways to make this code more efficient (using the “since” parameter as well as push notifications), and we will introduce that in section “Optimization”.

At this point, the result contains a Join object, which in turn contains a map of room-ID/Room-object. While the room ID is the only thing needed to use the IM APIs, often you also have to display the room name for the end user. The human-readable room name is not included in the Room object. So a second call is required to retrieve each of the room names:

var rooms = new Map();

function retrieveRoomIDsAndNames(join) {
    roomIDs = Object.keys(join);
    for (var i = 0; i < roomIDs.length; i++) {
        var thisRoom = roomIDs[i];
        var roomName = getRoomName(thisRoom, function(roomName) {
            var r = {
                "id": thisRoom,
                "name": roomName
            };
            rooms.set(thisRoom, r);
        });
    }
}
private HashMap<String,Room> rooms = new HashMap<>();

private void retrieveRoomIDsAndNames(JsonNode join) {

    for (Iterator<String> i = join.fieldNames(); i.hasNext();) {

        String thisRoom = i.next();

        Room r = new Room();
        r.setId(thisRoom);
        String roomName = getRoomName(thisRoom);
        if(roomName == null) {
            continue; //This room no longer exists.
        }
        r.setName(roomName);
        rooms.put(thisRoom, r);
    }
}

private String getRoomName(String roomId) {
    HttpEntity<String> request = createIMEntity(null);
    try {
        ResponseEntity<String> response = rest.exchange(matrixServer + "_matrix/client/r0/rooms/" + roomId + "/state/m.room.name", HttpMethod.GET, request, String.class);
        if (response.getStatusCode().is2xxSuccessful()) {
            JsonNode roomResponse = mapper.readValue(response.getBody(), JsonNode.class);
            return roomResponse.get("name").asText(); //Success
        }
        return "Error getting room name: " + response.toString();
    } catch (HttpClientErrorException e) {
        if(e.getStatusCode() == HttpStatus.NOT_FOUND) { //Not found, skip this room
            return null;
        }
        return "Error getting room name: " + e.getStatusCode().toString();
    } catch (Exception e2) {
        return "Problem getting room name: " + e2.toString();
    }
}

Parallel to that, you can also process the events that were returned by the sync call and store them in a list. These are located in the Room object under the Timeline object, which in turn contains a list of Event objects. At this point let’s concentrate on a single type of event, instant messages (type = m.room.message), and ignore the other ones. We’ll put the messages along with the sender in a structure to retrieve them by room ID.

var rooms = new Map();

function retrieveRoomIDsAndNames(join) {
    roomIDs = Object.keys(join);
    for (var i = 0; i < roomIDs.length; i++) {
        var thisRoom = roomIDs[i];
        var roomName = getRoomName(thisRoom, function(roomName) {
            var r = {
                "id": thisRoom,
                "name": roomName
            };
            rooms.set(thisRoom, r);
        });
    }
}

function getRoomName(roomId, callback) {
    $.ajax({
        url: matrix_server + "_matrix/client/r0/rooms/" + roomId + "/state/m.room.name",
        method: "GET",
        contentType: 'application/json',
        headers: {
            "Authorization": "Bearer " + im_token
        },
        async: false,
        success: function(result) {
            callback(result.name);
        },
        error: function(xhr, status, errorThrown) {
            console.log("Error getting room name. " + xhr.status + " " + xhr.text + " " + errorThrown);
        }
    });
}

var messages = [];
var currentRoomId = "";

function retrieveRoomEvents(join) {
    if (typeof join[currentRoomId] === 'undefined') {
        return;
    }

    var room = join[currentRoomId];

    var timeline = room.timeline;
    if (typeof timeline === 'undefined') {
        return;
    }
    var events = timeline.events;
    if (typeof events === 'undefined') {
        return;
    }

    events.forEach(function(event, index) {
        var type = event.type;
        if (type === 'm.room.message') {
            var sender = event.sender;
            var content = event.content;
            var body = content.body;
            messages.push("[from " + sender + "] " + body);
        }
    });
}

function setChosenRoom(roomId) {
    if (roomId === currentRoomId) {
        //No change since last.
        return;
    }

    currentRoomId = roomId;
    messages = [];
}
var rooms = new Map();

function retrieveRoomIDsAndNames (join) {
    roomIDs = Object.keys(join);
    for (var i = 0; i < roomIDs.length; i++) {
     var thisRoom = roomIDs[i];
     var roomName = getRoomName(thisRoom, function(roomName) {
 var r = {"id": thisRoom, "name" : roomName};
 rooms.set(thisRoom, r);
     });
 }
}

function getRoomName (roomId, callback) {
 $.ajax({
 url: matrix_server+"_matrix/client/r0/rooms/"+ roomId +"/state/m.room.name",
 method: "GET",
 contentType: 'application/json',
 headers: {
 "Authorization": "Bearer " + im_token
 },
 async: false,
 success: function(result) {
 callback(result.name);
 },
 error: function (xhr, status, errorThrown) {
 console.log("Error getting room name. " + xhr.status + " " + xhr.text + " " + errorThrown);
 }
 });
}

var messages = [];
var currentRoomId = "";

function retrieveRoomEvents (join) {
 if(typeof join[currentRoomId] === 'undefined') {
 return;
 }
 
 var room = join[currentRoomId];
 
 var timeline = room.timeline;
 if(typeof timeline === 'undefined') {
 return;
 }
 var events = timeline.events;
 if(typeof events === 'undefined') {
 return;
 }

 events.forEach(function (event, index) {
 var type = event.type;
 if(type === 'm.room.message') {
 var sender = event.sender;
 var content = event.content;
 var body = content.body;
 messages.push("[from " + sender + "] " + body);
 }
 });
}

function setChosenRoom(roomId) {
 if (roomId === currentRoomId) {
 //No change since last.
 return;
 }
 
 currentRoomId = roomId;
 messages = [];
}

Now you have both a list of rooms and for each room the instant messages it contains. From this moment forward, you have everything you need to send messages.

Posting a Message to a Room

Let’s assume that you displayed the list of rooms to the user and he selected a specific room. Let’s further assume that he wants to send a message to this selected room. The next step is to actually post a message. Write a function that accepts a room ID and a message, and posts this message to this room.

var transactionNumber = 1;

function sendMessage(message, callback){
    var jsonData = JSON.stringify({
        "msgtype": "m.text",
        "body": message,
        "formattedBody": message,
        "format": "org.matrix.custom.html"        
    });
    
    var url = matrix_server+"_matrix/client/r0/rooms/"+ currentRoomId +"/send/m.room.message/"+transactionNumber;
    transactionNumber++;

    $.ajax({
        url: url,
        method: "PUT",
        contentType: 'application/json',
        data: jsonData,
        headers: {
            "Authorization": "Bearer " + im_token
        },
        async: false,
        success: function(result) {
            callback(result);
        },
        error: function (xhr, status, errorThrown) {
            console.log("Error sending message. " + xhr.status + " " + xhr.text + " " + errorThrown);
        }
    });
}
def send_message(message, room, txn_id, matrix_token):
auth = f"Bearer {matrix_token}"
  url = matrix_server + "_matrix/client/r0/rooms/" + room + "/send/m.room.message/" + str(txn_id)
  message = message.replace("\"", "")
  data = '''{
          "msgtype": "m.text",
          "body": "%s"
      }'''
  return requests.put(url, (data % message), headers={'Authorization': self.auth})


response = send_message("Hello, World!", "!12345abcde:buzz", 1, matrix_token)
public void sendMessage(String message) throws JsonProcessingException {
  StringBuffer sb = new StringBuffer();
  sb.append(matrixServer);
  sb.append("_matrix/client/r0/rooms/");
  sb.append(currentRoomId);
  sb.append("/send/m.room.message/");
  sb.append(transactionNumber++);

  ObjectNode content = mapper.createObjectNode();
  content.set("msgtype", new TextNode("m.text"));
  content.set("body", new TextNode(message));
  content.set("formattedBody", new TextNode("<p>" + message + "</p>"));
  content.set("format", new TextNode("org.matrix.custom.html"));

  HttpEntity<String> request = createIMEntity(mapper.writeValueAsString(content));

  ResponseEntity<String> response = rest.exchange(sb.toString(), HttpMethod.PUT, request, String.class);
  try {
      if (response.getStatusCode().is2xxSuccessful()) {

          return; //Success
      }
      System.err.println("Error syncing with IM server: " + response.toString());
  } catch (HttpClientErrorException e) {
      System.err.println("Error syncing with IM server: " + e.getStatusCode().toString());
  } catch (Exception e2) {
      System.err.println("Problem syncing with IM server: " + e2.toString());
  }
}

The body is an HTML-formatted string. It is possible to post documents and images as well, but this will be covered in another tutorial.

Optimisation

As stated before, repeatedly polling for all events is problematic. One way to optimize this is to retrieve fewer events using the “since” parameter. This will let you retrieve only the new events that were not retrieved in the past. We will cover this technique in a separate tutorial.

function setChosenRoom(roomId) {
    if (roomId === currentRoomId) {
        //No change since last.
        return;
    }

    currentRoomId = roomId;
    messages = [];
    lastSync = null;
}

function sync(callback) {
    var url = matrix_server + "_matrix/client/r0/sync";
    if (lastSync != null) {
        url += "?since=";
        url += lastSync;
    }

    $.ajax({
        url: url,
        method: "GET",
        contentType: 'application/json',
        headers: {
            "Authorization": "Bearer " + im_token
        },
        success: function(result) {
            var join = result["rooms"]["join"];
            lastSync = result.next_batch;
            retrieveRoomIDsAndNames(join);
            retrieveRoomEvents(join);
            callback();
        },
        error: function(xhr, status, errorThrown) {
            console.log("Error syncing. " + xhr.status + " " + xhr.text + " " + errorThrown);
        }
    });
}
public String setChosenRoom(String roomId) {
    if(roomId != null && roomId.equals(currentRoomId)) {
        //No change since last.
        return null;
    }
    this.currentRoomId = roomId;
    messages.clear(); //First time in this room, clear cached messages
    lastSync = null;
    return null;
}

private String lastSync = null;

public void sync() {
    HttpEntity request = createIMEntity(null);
    StringBuffer sb = new StringBuffer();
    sb.append(matrixServer);
    sb.append("_matrix/client/r0/sync");
    if (lastSync != null) {
        sb.append("?since=");
        sb.append(lastSync);
    }

    ResponseEntity response = rest.exchange(sb.toString(), HttpMethod.GET, request, String.class);
    try {
        if (response.getStatusCode().is2xxSuccessful()) {
            JsonNode syncResponse = mapper.readValue(response.getBody(), JsonNode.class);

            JsonNode allRooms = syncResponse.get("rooms");
            if (allRooms == null) {
                return;
            }
            JsonNode join = allRooms.get("join");
            if (join == null) {
                return;
            }
            lastSync = syncResponse.get("next_batch").asText(); //Represents to the last event in this batch
            retrieveRoomIDsAndNames(join);
            retrieveRoomEvents(join);


            return; //Success
        }
        System.err.println("Error syncing with IM server: " + response.toString());
    } catch (HttpClientErrorException e) {
        System.err.println("Error syncing with IM server: " + e.getStatusCode().toString());
    } catch (Exception e2) {
        System.err.println("Problem syncing with IM server: " + e2.toString());
    }
}

On mobile platforms (iOS and Android), another way is to use push notifications instead of polling. That way your code won’t have to connect to the server at all. The server will contact your application whenever there’s a new event. Again this technique will be explained in its own tutorial.