Difference between revisions of "Matrix"

From Organic Design wiki
m (Docker installation)
(Generate an Access Token: Add curl method.)
 
(105 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{stub}}
+
[https://matrix.org Matrix] is an open source project that publishes the [https://matrix.org/docs/spec Matrix open standard] for secure, decentralised, real-time communication, and its Apache licensed [https://github.com/matrix-org reference implementations]. Maintained by the non-profit [https://matrix.org/foundation Matrix.org Foundation] who aim to create an open platform which is as independent, vibrant and evolving as the Web itself... but for communication. As of June 2019, Matrix is [https://matrix.org/blog/2019/06/11/introducing-matrix-1-0-and-the-matrix-org-foundation out of beta], and the protocol is fully suitable for production usage.
  
 
== Docker installation ==
 
== Docker installation ==
First generate a default configuration file for your domain as follows, this will automatically pull the necessary containers as well.
+
First you'll need to configure your web-server as a reverse proxy from SSL ports 443 and 8448 to the internal non-SSL port 8008. This is the default Matrix port for unsecured HTTP traffic, so that a reverse proxy needs to be set up from your web-server to handle the HTTPS side of things on exposing the default Matrix HTTPS port of '''8448''' to the public that connects to the the internal HTTP port on 8008. Also there needs to be a connection from port '''443''', see the official [https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md reverse proxy notes] for details about the reverse proxy setup.
 +
 
 +
We'll be using ''PostgreSQL'' instead of the default ''SQLite'' database, which means that we'll need to use ''docker-compose''. So first create a directory for the configuration and data and then put a ''docker-compose.yml'' file in it with the following content which will create persistent volumes to put the ''synapse'' data in ''data/system'' and the ''PostgreSQL'' data in ''data/postgres''.
 +
<source lang="yaml">
 +
version: '3'
 +
services:
 +
 
 +
  postgres:
 +
    restart: unless-stopped
 +
    image: postgres:9.6-alpine
 +
    environment:
 +
      - POSTGRES_USER=synapse
 +
      - POSTGRES_DB=synapse
 +
    networks:
 +
    - internal_network
 +
    volumes:
 +
      - ./data/postgres:/var/lib/postgresql/data
 +
 
 +
  synapse:
 +
    image: matrixdotorg/synapse:latest
 +
    restart: unless-stopped
 +
    networks:
 +
      - external_network
 +
      - internal_network
 +
    ports:
 +
      - "127.0.0.1:8008:8008"
 +
    environment:
 +
      - SYNAPSE_SERVER_NAME=organicdesign.co.nz
 +
      - SYNAPSE_REPORT_STATS=no
 +
    depends_on:
 +
      - postgres
 +
    volumes:
 +
      - ./data/system:/data
 +
 
 +
networks:
 +
  external_network:
 +
  internal_network:
 +
    internal: true
 +
</source>
 +
 
 +
 
 +
Next, generate a default configuration file for your domain as follows. This will create a new volume with your persistent configuration file in it called ''homeserver.yaml'' as well as some keys for your domain.
 +
<source lang="bash">
 +
docker run -it --rm -v "{!/FULL/PATH/TO/DIR!}/data/system:/data" -e SYNAPSE_SERVER_NAME=organicdesign.co.nz -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate
 +
</source>
 +
 
 +
 
 +
Then start the container in the background.
 +
<source lang="bash">
 +
docker-compose up -d
 +
</source>
 +
 
 +
 
 +
Now we need to create a database with the correct encoding (we may need to drop an initially created one first). So first log in to the ''PostgreSQL'' database.
 +
<source lang="bash">
 +
docker exec -it synapse-docker_postgres_1 psql -U synapse
 +
</source>
 +
 
 +
 
 +
Connect to the ''postgres'' database so you can drop ''synapse'', and then create a new ''synapse'' database with the correct encoding.
 +
<source lang="pgsql">
 +
\connect postgres
 +
DROP DATABASE synapse;
 +
CREATE DATABASE synapse
 +
ENCODING 'UTF8'
 +
LC_COLLATE='C'
 +
LC_CTYPE='C'
 +
template=template0
 +
OWNER synapse;
 +
</source>
 +
 
 +
 
 +
Then edit the ''data/system/homeserver.yaml'' configuration and add the following to the database section. Note that the database host is ''postgres'' not ''localhost'', because it needs to be accessed via the hostname given to the database service defined in the ''docker-compose.yml'' file. The database name and database user must also match the environment given to the database service in the ''docker-compose.yml'' file.
 +
<source lang="yaml">
 +
database:
 +
  name: psycopg2
 +
  args:
 +
    user: synapse
 +
    database: synapse
 +
    host: {!postgres!}
 +
    cp_min: 5
 +
    cp_max: 10
 +
</source>
 +
 
 +
 
 +
Then exit out of ''PostgreSQL'', restart the container and set up a user (check the logs to ensure its running):
 +
<source lang="bash">
 +
docker-compose down
 +
docker-compose up -d
 +
docker exec -it synapse-docker_synapse_1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
 +
</source>
 +
 
 +
== Backup & restore ==
 +
Apart from backing up the data directory, it's a good idea to back up the database with a proper dump as well:
 +
<source lang="bash">
 +
docker exec synapse-docker_postgres_1 pg_dump -U synapse synapse > dump.pgsql
 +
</source>
 +
 
 +
 
 +
And to restore:
 +
<source lang="bash">
 +
docker exec -i synapse-docker_postgres_1 psql -U synapse synapse < dump.pgsql
 +
</source>
 +
 
 +
== Upgrade ==
 +
Simply upgrade the container and start a new instance:
 +
<source lang="bash">
 +
docker-compose pull
 +
docker-compose up -d
 +
</source>
 +
 
 +
You can check the version of the running server with:
 +
 
 +
https://YOUR_DOMAIN/_matrix/federation/v1/version
 +
 
 +
== Enabling email ==
 +
Synapse can use email for user password resetting and notification of missed messages. I was unable to figure out how to connect synapse's in-built SMTP sending facility to the local server, it might be a TLS version conflict judging from the messages in the Exim4 log. So for now a workaround is to add the following SMTP service into the ''docker-compose.yml'' configuration file:
 +
<source lang="yaml">
 +
  smtp:
 +
    image: juanluisbaptiste/postfix:latest
 +
    restart: unless-stopped
 +
    environment:
 +
      - SMTP_SERVER=organicdesign.co.nz
 +
      - SMTP_USERNAME=*****
 +
      - SMTP_PASSWORD=*****
 +
      - SERVER_HOSTNAME={!matrix.!}organicdesign.co.nz
 +
      - DEBUG=yes
 +
    networks:
 +
      - internal_network
 +
      - external_network
 +
    volumes:
 +
      - "/etc/localtime:/etc/localtime:ro"
 +
</source>
 +
'''Note:''' The ''SERVER_HOSTNAME'' must be different from the domain used by the email clients otherwise the relay server will try to perform local delivery on them instead of relaying them to Exim4.
 +
 
 +
 
 +
Use the following settings in the ''data/system/homeserver.yaml'' configuration's ''smtp'' section, and comment out any other authentication settings to use their defaults.
 +
<source lang="yaml">
 +
  smtp_host: smtp
 +
  notif_from: "Your Friendly %(app)s homeserver <noreply@organicdesign.co.nz>"
 +
  app_name: Organic Design Matrix
 +
  enable_notifs: true
 +
  notif_template_html: notif_mail.html
 +
  notif_template_text: notif_mail.txt
 +
</source>
 +
 
 +
 
 +
Finally, you need to add an email address in your settings in the client. In Riot, you need to enter the email address and click "add", then a confirmation email address gets sent. You click the link in the confirmation email, and then after that you click "continue" in the Riot client. If you click "continue" before you confirmed the email address, it will not be added. You can clearly see when it has been correctly added, because it will ask for your account password to confirm the addition and then is shown whenever you enter the settings window and has a big red "remove" button to the right of it. And don't forget to enable email notifications in the notifications tab of Riot's settings!
 +
 
 +
== Add a new user ==
 +
<source>
 +
docker exec -it synapse-docker_synapse_1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008
 +
</source>
 +
 
 +
== Change a user password ==
 +
First you need to create the password hash using the [https://github.com/matrix-org/synapse/blob/develop/scripts/hash_password hash_password] script. This needs to be downloaded and can run on the server or done locally. you may need to install some missing packages such as ''bcrypt'' which can be done with ''pip''.
 +
<source lang="bash">
 +
./hash_password -p "MyNewPassword"
 +
</source>
 +
<source>
 +
$2b$12$MEqHpJbwlIE206XZI/hInu8uKmGQMl0nQxRwFixU8yx7iqdXlZyHO
 +
</source>
 +
 
 +
 
 +
Next you need to log into the ''postgreSQL'' docker container.
 +
<source lang="bash">
 +
docker exec -it synapse-docker_postgres_1 psql -U synapse synapse
 +
</source>
 +
 
 +
Then list the users to get the exact user names, then set the password for the relevant user to the hash obtained above.
 +
<source lang="mysql">
 +
SELECT * FROM users;
 +
UPDATE users SET password_hash='{!$2b$12...XlZyHO!}' WHERE name='@foo:example.com';
 +
\q
 +
</source>
 +
 
 +
== Generate an Access Token ==
 +
The quickest way to generate an access token is to perform a curl request to login:
 +
 
 +
<source lang="bash">
 +
curl \
 +
  -X POST \
 +
  -H "'Content-Type': 'application/json'" \
 +
  -d '{
 +
        "user":"@example:example.com",
 +
        "password":"example-password",
 +
        "type":"m.login.password"
 +
      }' \
 +
  https://example-homeserver.com/_matrix/client/v3/login
 +
</source>
 +
 
 +
== Troubleshooting ==
 +
 
 +
=== Tools ===
 +
*[https://federationtester.matrix.org Federaton tester]
 +
 
 +
=== File structure ===
 +
The installation files such as ''res/templates'' are inside the container in the directory ''/usr/local/lib/python3.7/site-packages/synapse/'', e.g. list them as follows:
 +
<source lang="bash">
 +
docker exec -it synapse-docker_synapse_1 ls /usr/local/lib/python3.7/site-packages/synapse
 +
</source>
 +
 
 +
=== CORS issues ===
 +
Check <tt>https://YOURDOMAIN/_matrix/client/versions</tt> in a browser, it should respond with something like the following:
 +
<source lang="json">
 +
{
 +
  "versions": ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0", "r0.5.0"],
 +
  "unstable_features": {
 +
    "m.id_access_token": true,
 +
    "m.require_identity_server": false,
 +
    "m.separate_add_and_bind": true,
 +
    "org.matrix.label_based_filtering": true,
 +
    "org.matrix.e2e_cross_signing": true,
 +
    "org.matrix.msc2432": true
 +
  }
 +
}
 +
</source>
 +
 
 +
 
 +
And it should have the following CORS headers:
 +
<source>
 +
access-control-allow-headers  Origin, X-Requested-With, Content-Type, Accept, Authorization
 +
access-control-allow-methods  GET, POST, PUT, DELETE, OPTIONS
 +
access-control-allow-origin  *
 +
</source>
 +
 
 +
== Local Synapse Installation ==
 +
A local installation of synapse can be helpful to test configurations without worrying about doing a proper setup and federation.<br>
 +
Most of these instructions were obtained from the [https://github.com/matrix-org/synapse/tree/master/docker official installation guide].
 +
 
 +
=== Docker Installation ===
 +
Note: this will setup the server name as my.matrix.host - you can change this but for a local install it does not really matter.<br>
 +
The configuration needed for the client using this setup is as follows:
 +
<source lang="json">
 +
{
 +
"userId": "@USERNAME:my.matrix.host",
 +
"password": "PASSWORD",
 +
"baseUrl": "http://localhost:8008"
 +
}
 +
</source>
 +
 
 +
Prepare:
 +
<source lang="bash">
 +
mkdir /data # This is where synapse will mount to.
 +
</source>
 +
 
 +
Install synapse:
 +
<source lang="bash">
 +
docker run -it --rm \
 +
    --mount type=volume,src=synapse-data,dst=/data \
 +
    -e SYNAPSE_SERVER_NAME=my.matrix.host \
 +
    -e SYNAPSE_REPORT_STATS=no \
 +
    matrixdotorg/synapse:latest generate
 +
</source>
 +
 
 +
Run synapse:
 +
<source lang="bash">
 +
docker run -d --name synapse \
 +
    --mount type=volume,src=synapse-data,dst=/data \
 +
    -p 8008:8008 \
 +
    matrixdotorg/synapse:latest
 +
</source>
 +
 
 +
Start synapse (In the event of it stopping, e.g. restart)
 +
<source lang="bash">
 +
docker start /synapse
 +
</source>
 +
 
 +
Remove synapse
 +
<source lang="bash">
 +
docker rm /synapse
 +
</source>
 +
 
 +
Check that synapse is running:
 +
<source lang="bash">
 +
docker logs synapse # check logs
 +
http://localhost:8008/_matrix/static/ # check the page is running
 +
</source>
 +
 
 +
=== Configuration ===
 +
The homeserver.yaml can typically be found at:
 +
<source>
 +
/var/lib/docker/volumes/synapse-data/_data/homeserver.yaml
 +
</source>
 +
 
 +
=== Register User ===
 +
Since user registration is disabled by default, you will need to create one:
 +
<source lang="bash">
 +
docker exec -it synapse register_new_matrix_user http://localhost:8008 -u USERNAME -p PASSWORD -c /data/homeserver.yaml
 +
</source>
 +
 
 +
Obtain user's access token:
 +
<source lang="bash">
 +
curl -XPOST -d '{"type":"m.login.password", "user":"USERNAME", "password":"PASSWORD"}' "http://localhost:8008/_matrix/client/r0/login"
 +
</source>
 +
 
 +
=== Performing Queries ===
 +
See the [https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst user admin API] for more information.<br>
 +
<br>
 +
General format:
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" <the_rest_of_your_API_request>
 +
</source>
 +
 
 +
 
 +
Query user example:
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users/<username>"
 +
</source>
 +
 
 +
 
 +
Deactivate a user:
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" -XPOST -d '{"erase":true}' "localhost:8008/_synapse/admin/v1/deactivate/<user_id>"
 +
</source>
 +
 
 +
 
 +
Listing all the user with pretty output using ''jq'':
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users" | jq
 +
</source>
 +
Or to filter the output to only show the user names:
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users" | jq ".users[].name"
 +
</source>
 +
 
 +
 
 +
Create room example:
 +
<source lang="bash">
 +
curl -s --header "Authorization: Bearer <access_token>" -XPOST -d '{"name":"test"}' "localhost:8008/_matrix/client/r0/createRoom"
 +
</source>
 +
 
 +
 
 +
Create message example:
 +
<source lang="bash">
 +
curl -X PUT \
 +
-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!"}' \
 +
--header "Authorization: Bearer <access_token>" \
 +
'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/m.room.message/35'
 +
</source>
 +
 
 +
== The Element client ==
 +
Moved to [[Element_(Matrix)|Element (Matrix)]]
 +
 
 +
== P2P ==
 +
Matrix has been running tests to get Matrix working in P2P system by moving the homeserver onto the client's machine. The standard homeserver (Synapse) was too heavy for this to be feasible so a second generation homeserver of the name [https://github.com/matrix-org/dendrite Dendrite] was created. Dendrite has been compiled into WASM for running in the browser - you can try this for yourself: https://p2p.riot.im.
 +
 
 +
P2P Matrix is getting designed in a way that allows communication with the standard federated system and requires no client side changes - you can use your favorite matrix client without any modifications. Bridges and Bots will remain unchanged too. The one thing that I can see changing for the P2P users is application services - since they are made for running on the server they simply can't run if there is no server, however I believe they may have some creative uses client side.
 +
 
 +
* [https://github.com/matrix-org/dendrite/blob/master/docs/p2p.md Dendrite P2P docs].
 +
 
 +
== API ==
 +
Matrix has two sets of API, one for [https://matrix.org/docs/spec/client_server/latest Client-Server] communication and one for [https://matrix.org/docs/spec/server_server/r0.1.4 Server-Server] (federation) communication. Communication is done using JSON over REST making it quite simple and easy to start interacting with Matrix. Clients can implement whatever features they want but servers ''must'' implement the minimum required features (however it ''should'' implement much more than this), for this reason it is best to put it through some testing like [https://github.com/matrix-org/sytest systest] or [https://github.com/matrix-org/complement compliment].
 +
 
 +
=== OpenID ===
 +
Matrix supports the [https://openid.net/what-is-openid/ OpenID] standard for authentication of users.<br>
 +
Note: this token authentication method only implies existence of the user - the user may not exist if the server behind the domain is rouge so the server should check the existence of that user.
 +
<br>
 +
Request token from the client:
 +
<source lang="bash">
 +
curl -X POST \
 +
--header 'Content-Type: application/json' \
 +
--header 'Accept: application/json' \
 +
--header "Authorization: Bearer <YOUR_TOKEN>" \
 +
-d '{}' \
 +
'https://matrix.org/_matrix/client/r0/user/USERID/openid/request_token'
 +
</source>
 +
Sample output:
 +
<source lang="json">
 +
{"access_token":"<YOUR_OPENID_TOKEN>","token_type":"Bearer","matrix_server_name":"matrix.org","expires_in":3600.
 +
</source>
 +
Once the user has their token it should be passed to the server where the server will make a request like:
 +
<source lang="bash">
 +
curl -X GET \
 +
--header 'Accept: application/json' \
 +
'https://matrix.org/_matrix/federation/v1/openid/userinfo?access_token=<YOUR_OPENID_TOKEN>'
 +
</source>
 +
Which should in turn respond with:
 +
<source lang="json">
 +
{"sub":"USERID"}
 +
</source>
 +
Or if it failed:
 +
<source lang="json">
 +
{
 +
  "errcode": "M_UNKNOWN_TOKEN",
 +
  "error": "Access token unknown or expired"
 +
}
 +
</source>
 +
 
 +
==== Server Validation ====
 +
Using this method I believe it is still possible (but complicated) for a non-matix user in control of a domain to validate an account on that domain.<br>
 +
[https://code.organicdesign.nz/organicdesign/matrix-express-authorization matrix-express-authorization] is a basic application implementing this approach.<br>
 +
<br>
 +
# The user must pass their Matrix ID and OpenID access token to the server.
 +
# The server should check that the user exists using the profile api.
 +
# Then the server must retrieve the domain from that Matrix ID and send the token to that domain using the OpenID api.
 +
# The server must then check the response is the same as the Matrix ID provided.
 +
<br>
 +
If this last step successful the user is who they say they are.
 +
 
 +
=== Custom User Data ===
 +
Matrix allows users to have custom user data to be set for the user or the user and room combination.<br>
 +
By default the data is not encrypted on transport.<br>
 +
Other users cannot access or set your user data.<br>
 +
'''NOTE: custom user data is stored unencrypted in the home-server's database.''' [https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing#3-implementing-ssss See this for a solution.]
 +
 
 +
=== Custom Events ===
 +
Matrix allows clients to send custom events, by default the existing clients will ignore (since it is not setup to listen for it).<br>
 +
Custom events can be useful for custom client + application service configurations.<br>
 +
 
 +
A normal event of type message can be sent like so:
 +
<source lang="bash">
 +
curl -X PUT \
 +
-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!", "formatted_body": "Hello World!"}' \
 +
--header "Authorization: Bearer <access_token>" \
 +
'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/m.matrix.message/41'
 +
</source>
 +
 
 +
A custom event can be emitted like so:
 
<source lang="bash">
 
<source lang="bash">
docker run -it --rm --mount type=volume,src=synapse-data,dst=/data -e SYNAPSE_SERVER_NAME={!organicdesign.co.nz!} -e SYNAPSE_REPORT_STATS=yes matrixdotorg/synapse:latest generate
+
curl -X PUT \
 +
-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!", "formatted_body": "Hello World!"}' \
 +
--header "Authorization: Bearer <access_token>" \
 +
'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/custom.event/42'
 +
</source>
 +
 
 +
=== See Also ===
 +
* [https://matrix.org/docs/guides/ Matrix guides]
 +
* [https://matrix.org/docs/api/client-server Client-Server Swagger API]
 +
* [https://matrix.org/docs/spec/client_server/latest Client-Server Documentation]
 +
* [https://matrix.org/docs/spec/server_server/r0.1.4 Server-Server Documentation]
 +
* [https://github.com/matrix-org/matrix-doc/tree/master/content Full API Documentation]
 +
* [https://matrix.org/docs/guides/client-server-api How to use the client-server API]
 +
* [https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide E2EE features guide]
 +
* [https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing Advanced E2EE guide]
 +
* [https://github.com/matrix-org/sytest Homeserver black-box testing]
 +
* [https://spec.matrix.org/unstable/proposals/ Proposals]
 +
 
 +
== JS SDK ==
 +
Although not required, working with matrix from JS is easiest from the [https://github.com/matrix-org/matrix-js-sdk matrix-js-sdk] or using [https://github.com/matrix-org/matrix-appservice-node matrix-appservice-node].
 +
 
 +
=== Events ===
 +
Events can be listened to like so:
 +
 
 +
<source lang="javascript">
 +
client.on("event type", callback);
 +
</source>
 +
 
 +
==== Messages ====
 +
To check for messages you need to listen to these three events:
 +
* ''Room.timeline'' - the timeline of rooms for which our user is a member.
 +
* ''room.message'' - A message event (May be encrypted).
 +
* ''Event.decrypted'' - A event that has been decrypted.
 +
 
 +
There is also:
 +
* ''event'' - all events.
 +
 
 +
Both ''Room.timeline'' and ''room.message'' need to be checked if it is indeed a message and that it is not encrypted:
 +
 
 +
<source lang="javascript">
 +
if (event.isEncrypted() || event.getType() !== "m.room.message")
 +
return;
 +
 
 +
// Handle event
 +
</source>
 +
 
 +
The ''room.message'' event needs to check that it has successfully decrypted it and it is a message.
 +
<source lang="javascript">
 +
if (event.isDecryptionFailure() || event.getType() !== "m.room.message")
 +
return;
 +
 
 +
// Handle event
 +
</source>
 +
 
 +
==== Duplicate Events ====
 +
Sometimes a duplicate event may occur - I think it may be to do with [https://matrix.org/docs/spec/client_server/latest#syncing this].
 +
 
 +
==== Event History ====
 +
Event history is a little hard to figure out in the matrix-js-sdk when you don't know what you are looking for, so here is a quick example:
 +
 
 +
<source lang="javascript">
 +
matrix.client.scrollback(room, 30).then(() => {
 +
console.log(room.timeline);
 +
})
 +
</source>
 +
 
 +
=== Enabling Encryption ===
 +
The matrix client's method '''setRoomEncryption''' only sets the room encryption on the client side, an '''m.room.encryption''' event needs to also be pushed to enable it.
 +
 
 +
<source lang="javascript">
 +
const roomId = "!XXXXXX:example.com";
 +
const encryptionOptions = { algorithm: "m.megolm.v1.aes-sha2" };
 +
 
 +
await client.sendStateEvent(roomId, "m.room.encryption", encryptionOptions));
 +
await client.setRoomEncryption(roomId, encryptionOptions));
 +
</source>
 +
 
 +
=== Verifying Devices ===
 +
To verify a device it needs to both be marked as known and verified.
 +
 
 +
<source lang="javascript">
 +
await client.setDeviceKnown(userId, deviceId, true)
 +
await client.setDeviceVerified(userId, deviceId, true);
 +
</source>
 +
 
 +
You can verify everyone in a room by getting the members and all their devices.
 +
 
 +
<source lang="javascript">
 +
const roomId = "!XXXXXX:example.com";
 +
const room = client.getRoom(roomId);
 +
const members = await room.getEncryptionTargetMembers();
 +
 
 +
for (const member of members) {
 +
const devices = client.getStoredDevicesForUser(member.userId);
 +
 
 +
for (const device of devices) {
 +
if (device.isUnverified())
 +
// Verify the device
 +
}
 +
}
 +
</source>
 +
 
 +
=== Checking Login Status ===
 +
It's slightly more convoluted than it initially appears to see if the user is successfully logged in but here is the process I have arrived at.
 +
 
 +
<source lang="javascript">
 +
// userId will return anything if the user has attempted a login.
 +
const userId = client.getUserId();
 +
 
 +
if (!userId)
 +
return; // No login attempt made.
 +
 
 +
// user will only return something if the account has permission to access this user's details
 +
const user = client.getUser(userId);
 +
 
 +
if (!user)
 +
return; // Not your account.
 +
 
 +
// User must be logged in if they made a login attempt and can access that user's data.
 +
</source>
 +
 
 +
=== Reactivity with Vue ===
 +
Proper reactivity can be a little tricky to setup just right but once it is working everything should just work without issues.
 +
 
 +
<source lang="javascript">
 +
import matrix from "@/matrix";
 +
import { reactive } from "vue";
 +
 
 +
const matrixStore = new matrix.MemoryStore();
 +
 
 +
// Overwrite a property with a reactive version.
 +
const enableReactivity = prop => matrixStore[prop] = reactive(matrixStore[prop]);
 +
 
 +
// Enable reactivity on all properties you want to be reactive, e.g. "users"
 +
enableReactivity("rooms");
 +
 
 +
export default {
 +
state: {
 +
rooms: matrixStore.rooms,
 +
},
 +
 
 +
getters: {
 +
rooms (state) {
 +
return Object.values(state.rooms);
 +
}
 +
}
 +
};
 +
 
 +
matrix.createClient({
 +
// ...
 +
store: matrixStore
 +
});
 +
</source>
 +
 
 +
=== VOIP through Node ===
 +
Matrix VOIP is done through webRTC which is super simple if you are in a [https://github.com/matrix-org/matrix-js-sdk/tree/master/examples/voip browser environment] but if you are using node things are a bit more complicated.
 +
 
 +
* https://matrix.org/docs/spec/client_server/latest#voice-over-ip
 +
 
 +
Here is a working flow using [https://github.com/feross/simple-peer simple-peer]:
 +
<source>
 +
peer1 -----m.call.invite----> peer2
 +
peer1 ---m.call.candidates--> peer2
 +
peer1 <--m.call.candidates--- peer2
 +
peer1 <----m.call.answer----- peer2
 +
peer1 <----m.call.hangup----> peer2
 +
</source>
 +
 
 +
And here is a more complete flow from MSC2746. (Explained [https://github.com/matrix-org/matrix-doc/blob/dbkr/msc2746/proposals/2746-reliable-voip.md here])
 +
<source>
 +
peer1 -----m.call.invite----> peer2
 +
peer1 ---m.call.candidates--> peer2
 +
peer1 <--m.call.candidates--- peer2
 +
peer1 <----m.call.answer----- peer2
 +
peer1 -m.call.select_answer-> peer2
 +
peer1 <----m.call.hangup----> peer2
 +
</source>
 +
 
 +
Make sure you set a track on the node side, if element does not detect a track it seems to assume connection failure. I recommend checking out [https://github.com/feross/simple-peer#in-node simple-peer] alongside [https://github.com/node-webrtc/node-webrtc wrtc-node] for handling webRTC connections on the node side.
 +
 
 +
==== Simple-Peer Integration ====
 +
Your config should use turn data obtained from the server.
 +
<source lang="javascript">
 +
const turnData = await matrix.client.turnServer();
 +
 
 +
let peer = new Peer({
 +
wrtc,
 +
stream,
 +
trickle: true,
 +
initiator: false,
 +
 
 +
config: {
 +
iceServers: [
 +
{
 +
username: turnData.username,
 +
credential: turnData.password,
 +
urls: turnData.uris
 +
}
 +
],
 +
 
 +
iceTransportPolicy: undefined // "all" or "relay"
 +
}
 +
});
 +
 
 +
// Very important we add a track before sending otherwise element won't connect.
 +
peer.addTrack(track, stream);
 +
</source>
 +
 
 +
 
 +
When simple-peer sends candidates it sends them one by one, whereas matrix expects them to be group so you need to wait a reasonable amount of time for all candidates to be emitted then send them all:
 +
<source lang="javascript">
 +
callCandidates.push(data.candidate);
 +
 
 +
if (!timer) {
 +
// We need to wait to group the candidates.
 +
timer = setTimeout(async () => {
 +
await sendEvent(
 +
callRoomDetails.roomId,
 +
"m.call.candidates",
 +
{
 +
candidates: callCandidates,
 +
version: callRoomDetails.version,
 +
call_id: callRoomDetails.call_id,
 +
party_id: callRoomDetails.party_id
 +
}
 +
);
 +
 
 +
// Reset this so we can send candidates again.
 +
timer = undefined;
 +
callCandidates = [];
 +
 
 +
console.log("Sent candidates.");
 +
}, 500);
 +
}
 
</source>
 
</source>
This will create a new volume with your persistent configuration file in it in '''/var/lib/docker/volumes/synapse-data/_data/homeserver.yaml'''
 
  
Next, run the container on a local-only.
+
=== Bot Examples ===
 +
* [https://code.organicdesign.nz/Saul/matrix-echo-bot Barebones matrix echo bot]
 +
* [https://gitlab.com/aleixq/matrix-js-sdk-bot-template/-/blob/master/index.js matrix-js-sdk-bot-template]
 +
* [https://github.com/matrix-org/matrix-js-sdk/tree/master/examples/crypto-browser E2EE browser example]
 +
 
 +
=== See Also ===
 +
* [https://github.com/matrix-org/matrix-js-sdk matrix-js-sdk]
 +
* [https://matrix-org.github.io/matrix-js-sdk/9.5.1/index.html Matrix JS SDK Documentation]
 +
* [https://github.com/matrix-org/matrix-appservice-node matrix-appservice-node]
 +
* [https://matrix-org.github.io/matrix-appservice-node/index.html Matrix JS Appservice Documentation]
 +
 
 +
== Application Services ==
 +
Application services are a way to modify the matrix functionality by having an application running alongside the homeserver.
 +
 
 +
*[https://github.com/matrix-org/matrix-appservice-node matrix-appservice-node]
 +
*https://matrix.org/docs/guides/application-services
 +
*https://github.com/matrix-org/matrix-appservice-irc
 +
 
 +
=== Register Application Service ===
 +
To register the application service you need to edit the '''homeserver.yaml''' file and add the following config (it is commented out so search for ''app_service_config_files:'')
 +
<source>
 +
app_service_config_files:
 +
  - "/path/to/appservice/registration.yaml"
 +
</source>
 +
 
 +
If you are on a local client through docker you will want to copy your '''registration.yaml''' to '''/var/lib/docker/volumes/synapse-data/_data/''' and add the following config:
 +
<source>
 +
app_service_config_files:
 +
  - "/data/registration.yaml"
 +
</source>
 +
 
 +
Then restart the homeserver to apply changes and check the logs to ensure it hasn't thrown an error.
 +
 
 +
=== Troubleshooting ===
 +
Test that your application service gives a response when running:
 
<source lang="bash">
 
<source lang="bash">
docker run -d --name synapse --mount type=volume,src=synapse-data,dst=/data -p 127.0.0.1:8008:8008 matrixdotorg/synapse:latest
+
curl -X PUT http://localhost:8010/transactions/1?access_token=yourtoken
 +
</source>
 +
 
 +
==== localhost IPv6 Socket ====
 +
I had trouble getting an application service running initially the synapse logs were showing me:
 +
<source>
 +
INFO - as-recoverer-test-appservice-3 - Error sending request to PUT http://localhost:8010/transactions/1?access_token=<redacted>: ConnectError Cannot assign requested address
 +
</source>
 +
The issue was that I had set up the application service to use ''localhost'' and synapse was trying to create an IPv6 socket - which my application service was not setup for.<br>
 +
To resolve this I changed ''localhost'' to an IPv4 address.<br>
 +
<br>
 +
You may also want to check your ''/etc/hosts'' file to verify that ''localhost'' does not resolve to IPv6 (it wasn't in my case).
 +
 
 +
=== Matrix Appservice Node ===
 +
==== Register ====
 +
Note the ''setId'' method is not shown in the docs but '''is required'''. Also note url is the url of the app service not the homeserver.<br>
 +
Also note that if ''localhost'' is used synapse resolve this to an ipv6 socket which may not work.
 +
 
 +
registration.js (for creating registation.yaml file)
 +
<source lang="js">
 +
const { AppServiceRegistration } = require("matrix-appservice");
 +
 
 +
// creating registration files
 +
const reg = new AppServiceRegistration();
 +
reg.setId("test-appservice");
 +
reg.setAppServiceUrl("http://<YOUR IP ADDR>:8010");
 +
reg.setHomeserverToken(AppServiceRegistration.generateToken());
 +
reg.setAppServiceToken(AppServiceRegistration.generateToken());
 +
reg.setSenderLocalpart("example-appservice");
 +
reg.addRegexPattern("users", "@.*", true); // All users
 +
reg.addRegexPattern("aliases", "#_ap_bridge.*", true); // Only reserve _ap_bridge aliases.
 +
reg.setProtocols(["exampleservice"]); // For 3PID lookups
 +
reg.outputAsYaml("registration.yaml");
 +
 
 +
</source>
 +
 
 +
Example output:
 +
<source>
 +
id: test-appservice
 +
hs_token: 35d98ec4115123751d2dbab8e441e277d7d4e364130f474b59cdbfdb164f94a1
 +
as_token: e83a2ef0add9be4e77ab3b04aedb3df3d1dafdb9bb1b4c4374a169c5524d270d
 +
url: 'http://<YOUR IP ADDR>:8010'
 +
sender_localpart: example-appservice
 +
protocols:
 +
  - exampleservice
 +
namespaces:
 +
  users:
 +
    - exclusive: true
 +
      regex: '@.*'
 
</source>
 
</source>
This is the default Matrix port for unsecured HTTP traffic, so that a reverse proxy needs to be set up from your web-server to handle the HTTPS side of things on exposing the default Matrix HTTPS port of '''8448''' to the public that connects to the the internal HTTP port on 8008. Also there needs to be a connection from port '''443''', see the official [https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md reverse proxy notes] for details about the reverse proxy setup.
+
 
 +
== Bridges ==
 +
*[https://github.com/matrix-org/matrix-appservice-bridge/blob/master/HOWTO.md Bridges how to]
 +
*[https://matrix.org/docs/guides/application-services matrix-appservice-bridge]
 +
 
 +
== Widgets ==
 +
Widgets that are currently supported are "dumb" widgets - they are only webpages rendered in an i-frame, however more complicated widgets spec is being worked on to allow communication between the client and the web app.
 +
 
 +
* [https://github.com/matrix-org/matrix-widget-api, matrix-widget-api]
 +
* [https://github.com/matrix-org/matrix-doc/pull/2764 Widgets, the spec]
 +
* [https://github.com/matrix-org/matrix-doc/blob/travis/msc/widgets-http-transport/proposals/3009-widgets-ws-transport.md Websocket transport for client <--> widget communications]
  
 
== See also ==
 
== See also ==
 +
*[https://matrix.im Official Matrix site]
 +
*[https://matrix.org/docs/spec/ The Matrix open standard specification]
 +
*[https://matrix.org/foundation/ The Matrix.org Foundation]
 +
*[https://modular.im/ modular.im] ''- Matrix hosting''
 
*[https://github.com/matrix-org/synapse/tree/master/docker Official docker installation page]
 
*[https://github.com/matrix-org/synapse/tree/master/docker Official docker installation page]
*[[XMPP]]
+
*[https://jonnev.se/matrix-homeserver-synapse-v0-99-1-1-with-traefik/ Docker installation with PostgreSQL] & [https://pastebin.com/STh210JE Another docker-compose.yml sample]
 +
*[https://zerowidthjoiner.net/2020/03/20/setting-up-matrix-and-riot-with-docker Good installation procedure including email]
 +
*[https://github.com/spantaleev/matrix-docker-ansible-deploy Advanced Ansible playbook for deployment of many bridges]
 +
*[https://github.com/matrix-org/synapse/issues/1707 Github thread on getting tokens and using the admin API]
 +
*[https://matrix.org/blog/2020/07/10/this-week-in-matrix-2020-07-10#riot-ios-p2p-demo Riot P2P demo]
 +
*[https://matrix.org/blog/2020/06/02/introducing-p-2-p-matrix Introducing P2P Matrix]
 +
*[https://medium.com/@RiotChat/communities-aka-groups-are-here-announcing-riot-web-0-13-riot-ios-0-6-and-riot-android-0-7-4-933cb193a28e Communities in Matrix]
 +
*[https://github.com/matrix-org/matrix-doc/blob/matthew/msc1772/proposals/1772-groups-as-rooms.md Proposal for hierarchical rooms in Matrix]
 +
*[https://github.com/matrix-org/matrix-doc/blob/travis/msc/trees/proposals/3089-file-tree-structures.md Proposal for file trees in Matrix]
 +
*[https://github.com/matrix-org/pinecone Pinecone] ''- designed to provide E2EE connectivity between devices at a global scale over any compatible transport medium''
 +
*[https://matrix.org/blog/2019/03/12/breaking-the-100-bps-barrier-with-matrix-meshsim-coap-proxy Matrix working on 100bps mesh network]
 
[[Category:Libre software]]
 
[[Category:Libre software]]

Latest revision as of 22:22, 30 April 2024

Matrix is an open source project that publishes the Matrix open standard for secure, decentralised, real-time communication, and its Apache licensed reference implementations. Maintained by the non-profit Matrix.org Foundation who aim to create an open platform which is as independent, vibrant and evolving as the Web itself... but for communication. As of June 2019, Matrix is out of beta, and the protocol is fully suitable for production usage.

Docker installation

First you'll need to configure your web-server as a reverse proxy from SSL ports 443 and 8448 to the internal non-SSL port 8008. This is the default Matrix port for unsecured HTTP traffic, so that a reverse proxy needs to be set up from your web-server to handle the HTTPS side of things on exposing the default Matrix HTTPS port of 8448 to the public that connects to the the internal HTTP port on 8008. Also there needs to be a connection from port 443, see the official reverse proxy notes for details about the reverse proxy setup.

We'll be using PostgreSQL instead of the default SQLite database, which means that we'll need to use docker-compose. So first create a directory for the configuration and data and then put a docker-compose.yml file in it with the following content which will create persistent volumes to put the synapse data in data/system and the PostgreSQL data in data/postgres.

version: '3'
services:

  postgres:
    restart: unless-stopped
    image: postgres:9.6-alpine
    environment:
      - POSTGRES_USER=synapse
      - POSTGRES_DB=synapse
    networks:
     - internal_network
    volumes:
      - ./data/postgres:/var/lib/postgresql/data

  synapse:
    image: matrixdotorg/synapse:latest
    restart: unless-stopped
    networks:
      - external_network
      - internal_network
    ports:
      - "127.0.0.1:8008:8008"
    environment:
      - SYNAPSE_SERVER_NAME=organicdesign.co.nz
      - SYNAPSE_REPORT_STATS=no
    depends_on:
      - postgres
    volumes:
      - ./data/system:/data

networks:
  external_network:
  internal_network:
    internal: true


Next, generate a default configuration file for your domain as follows. This will create a new volume with your persistent configuration file in it called homeserver.yaml as well as some keys for your domain.

docker run -it --rm -v "/FULL/PATH/TO/DIR/data/system:/data" -e SYNAPSE_SERVER_NAME=organicdesign.co.nz -e SYNAPSE_REPORT_STATS=no matrixdotorg/synapse:latest generate


Then start the container in the background.

docker-compose up -d


Now we need to create a database with the correct encoding (we may need to drop an initially created one first). So first log in to the PostgreSQL database.

docker exec -it synapse-docker_postgres_1 psql -U synapse


Connect to the postgres database so you can drop synapse, and then create a new synapse database with the correct encoding.

\connect postgres
DROP DATABASE synapse;
CREATE DATABASE synapse
 ENCODING 'UTF8'
 LC_COLLATE='C'
 LC_CTYPE='C'
 template=template0
 OWNER synapse;


Then edit the data/system/homeserver.yaml configuration and add the following to the database section. Note that the database host is postgres not localhost, because it needs to be accessed via the hostname given to the database service defined in the docker-compose.yml file. The database name and database user must also match the environment given to the database service in the docker-compose.yml file.

database:
  name: psycopg2
  args:
    user: synapse
    database: synapse
    host: postgres
    cp_min: 5
    cp_max: 10


Then exit out of PostgreSQL, restart the container and set up a user (check the logs to ensure its running):

docker-compose down
docker-compose up -d
docker exec -it synapse-docker_synapse_1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008

Backup & restore

Apart from backing up the data directory, it's a good idea to back up the database with a proper dump as well:

docker exec synapse-docker_postgres_1 pg_dump -U synapse synapse > dump.pgsql


And to restore:

docker exec -i synapse-docker_postgres_1 psql -U synapse synapse < dump.pgsql

Upgrade

Simply upgrade the container and start a new instance:

docker-compose pull
docker-compose up -d

You can check the version of the running server with:

https://YOUR_DOMAIN/_matrix/federation/v1/version

Enabling email

Synapse can use email for user password resetting and notification of missed messages. I was unable to figure out how to connect synapse's in-built SMTP sending facility to the local server, it might be a TLS version conflict judging from the messages in the Exim4 log. So for now a workaround is to add the following SMTP service into the docker-compose.yml configuration file:

smtp:
    image: juanluisbaptiste/postfix:latest
    restart: unless-stopped
    environment:
      - SMTP_SERVER=organicdesign.co.nz
      - SMTP_USERNAME=*****
      - SMTP_PASSWORD=*****
      - SERVER_HOSTNAME=matrix.organicdesign.co.nz
      - DEBUG=yes
    networks:
      - internal_network
      - external_network
    volumes:
      - "/etc/localtime:/etc/localtime:ro"

Note: The SERVER_HOSTNAME must be different from the domain used by the email clients otherwise the relay server will try to perform local delivery on them instead of relaying them to Exim4.


Use the following settings in the data/system/homeserver.yaml configuration's smtp section, and comment out any other authentication settings to use their defaults.

smtp_host: smtp
  notif_from: "Your Friendly %(app)s homeserver <noreply@organicdesign.co.nz>"
  app_name: Organic Design Matrix
  enable_notifs: true
  notif_template_html: notif_mail.html
  notif_template_text: notif_mail.txt


Finally, you need to add an email address in your settings in the client. In Riot, you need to enter the email address and click "add", then a confirmation email address gets sent. You click the link in the confirmation email, and then after that you click "continue" in the Riot client. If you click "continue" before you confirmed the email address, it will not be added. You can clearly see when it has been correctly added, because it will ask for your account password to confirm the addition and then is shown whenever you enter the settings window and has a big red "remove" button to the right of it. And don't forget to enable email notifications in the notifications tab of Riot's settings!

Add a new user

docker exec -it synapse-docker_synapse_1 register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008

Change a user password

First you need to create the password hash using the hash_password script. This needs to be downloaded and can run on the server or done locally. you may need to install some missing packages such as bcrypt which can be done with pip.

./hash_password -p "MyNewPassword"
$2b$12$MEqHpJbwlIE206XZI/hInu8uKmGQMl0nQxRwFixU8yx7iqdXlZyHO


Next you need to log into the postgreSQL docker container.

docker exec -it synapse-docker_postgres_1 psql -U synapse synapse

Then list the users to get the exact user names, then set the password for the relevant user to the hash obtained above.

SELECT * FROM users;
UPDATE users SET password_hash='$2b$12...XlZyHO' WHERE name='@foo:example.com';
\q

Generate an Access Token

The quickest way to generate an access token is to perform a curl request to login:

curl \
  -X POST \
  -H "'Content-Type': 'application/json'" \
  -d '{
        "user":"@example:example.com",
        "password":"example-password",
        "type":"m.login.password"
      }' \
  https://example-homeserver.com/_matrix/client/v3/login

Troubleshooting

Tools

File structure

The installation files such as res/templates are inside the container in the directory /usr/local/lib/python3.7/site-packages/synapse/, e.g. list them as follows:

docker exec -it synapse-docker_synapse_1 ls /usr/local/lib/python3.7/site-packages/synapse

CORS issues

Check https://YOURDOMAIN/_matrix/client/versions in a browser, it should respond with something like the following:

{
  "versions": ["r0.0.1", "r0.1.0", "r0.2.0", "r0.3.0", "r0.4.0", "r0.5.0"],
  "unstable_features": {
    "m.id_access_token": true,
    "m.require_identity_server": false,
    "m.separate_add_and_bind": true,
    "org.matrix.label_based_filtering": true,
    "org.matrix.e2e_cross_signing": true,
    "org.matrix.msc2432": true
  }
}


And it should have the following CORS headers:

access-control-allow-headers  Origin, X-Requested-With, Content-Type, Accept, Authorization
access-control-allow-methods  GET, POST, PUT, DELETE, OPTIONS
access-control-allow-origin   *

Local Synapse Installation

A local installation of synapse can be helpful to test configurations without worrying about doing a proper setup and federation.
Most of these instructions were obtained from the official installation guide.

Docker Installation

Note: this will setup the server name as my.matrix.host - you can change this but for a local install it does not really matter.
The configuration needed for the client using this setup is as follows:

{
	"userId": "@USERNAME:my.matrix.host",
	"password": "PASSWORD",
	"baseUrl": "http://localhost:8008"
}

Prepare:

mkdir /data # This is where synapse will mount to.

Install synapse:

docker run -it --rm \
    --mount type=volume,src=synapse-data,dst=/data \
    -e SYNAPSE_SERVER_NAME=my.matrix.host \
    -e SYNAPSE_REPORT_STATS=no \
    matrixdotorg/synapse:latest generate

Run synapse:

docker run -d --name synapse \
    --mount type=volume,src=synapse-data,dst=/data \
    -p 8008:8008 \
    matrixdotorg/synapse:latest

Start synapse (In the event of it stopping, e.g. restart)

docker start /synapse

Remove synapse

docker rm /synapse

Check that synapse is running:

docker logs synapse # check logs
http://localhost:8008/_matrix/static/ # check the page is running

Configuration

The homeserver.yaml can typically be found at:

/var/lib/docker/volumes/synapse-data/_data/homeserver.yaml

Register User

Since user registration is disabled by default, you will need to create one:

docker exec -it synapse register_new_matrix_user http://localhost:8008 -u USERNAME -p PASSWORD -c /data/homeserver.yaml

Obtain user's access token:

curl -XPOST -d '{"type":"m.login.password", "user":"USERNAME", "password":"PASSWORD"}' "http://localhost:8008/_matrix/client/r0/login"

Performing Queries

See the user admin API for more information.

General format:

curl -s --header "Authorization: Bearer <access_token>" <the_rest_of_your_API_request>


Query user example:

curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users/<username>"


Deactivate a user:

curl -s --header "Authorization: Bearer <access_token>" -XPOST -d '{"erase":true}' "localhost:8008/_synapse/admin/v1/deactivate/<user_id>"


Listing all the user with pretty output using jq:

curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users" | jq

Or to filter the output to only show the user names:

curl -s --header "Authorization: Bearer <access_token>" "localhost:8008/_synapse/admin/v2/users" | jq ".users[].name"


Create room example:

curl -s --header "Authorization: Bearer <access_token>" -XPOST -d '{"name":"test"}' "localhost:8008/_matrix/client/r0/createRoom"


Create message example:

curl -X PUT \
	-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!"}' \
	--header "Authorization: Bearer <access_token>" \
	'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/m.room.message/35'

The Element client

Moved to Element (Matrix)

P2P

Matrix has been running tests to get Matrix working in P2P system by moving the homeserver onto the client's machine. The standard homeserver (Synapse) was too heavy for this to be feasible so a second generation homeserver of the name Dendrite was created. Dendrite has been compiled into WASM for running in the browser - you can try this for yourself: https://p2p.riot.im.

P2P Matrix is getting designed in a way that allows communication with the standard federated system and requires no client side changes - you can use your favorite matrix client without any modifications. Bridges and Bots will remain unchanged too. The one thing that I can see changing for the P2P users is application services - since they are made for running on the server they simply can't run if there is no server, however I believe they may have some creative uses client side.

API

Matrix has two sets of API, one for Client-Server communication and one for Server-Server (federation) communication. Communication is done using JSON over REST making it quite simple and easy to start interacting with Matrix. Clients can implement whatever features they want but servers must implement the minimum required features (however it should implement much more than this), for this reason it is best to put it through some testing like systest or compliment.

OpenID

Matrix supports the OpenID standard for authentication of users.
Note: this token authentication method only implies existence of the user - the user may not exist if the server behind the domain is rouge so the server should check the existence of that user.
Request token from the client:

curl -X POST \
	--header 'Content-Type: application/json' \
	--header 'Accept: application/json' \
	--header "Authorization: Bearer <YOUR_TOKEN>" \
	-d '{}' \
	'https://matrix.org/_matrix/client/r0/user/USERID/openid/request_token'

Sample output:

{"access_token":"<YOUR_OPENID_TOKEN>","token_type":"Bearer","matrix_server_name":"matrix.org","expires_in":3600.

Once the user has their token it should be passed to the server where the server will make a request like:

curl -X GET \
	--header 'Accept: application/json' \
	'https://matrix.org/_matrix/federation/v1/openid/userinfo?access_token=<YOUR_OPENID_TOKEN>'

Which should in turn respond with:

{"sub":"USERID"}

Or if it failed:

{
  "errcode": "M_UNKNOWN_TOKEN",
  "error": "Access token unknown or expired"
}

Server Validation

Using this method I believe it is still possible (but complicated) for a non-matix user in control of a domain to validate an account on that domain.
matrix-express-authorization is a basic application implementing this approach.

  1. The user must pass their Matrix ID and OpenID access token to the server.
  2. The server should check that the user exists using the profile api.
  3. Then the server must retrieve the domain from that Matrix ID and send the token to that domain using the OpenID api.
  4. The server must then check the response is the same as the Matrix ID provided.


If this last step successful the user is who they say they are.

Custom User Data

Matrix allows users to have custom user data to be set for the user or the user and room combination.
By default the data is not encrypted on transport.
Other users cannot access or set your user data.
NOTE: custom user data is stored unencrypted in the home-server's database. See this for a solution.

Custom Events

Matrix allows clients to send custom events, by default the existing clients will ignore (since it is not setup to listen for it).
Custom events can be useful for custom client + application service configurations.

A normal event of type message can be sent like so:

curl -X PUT \
	-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!", "formatted_body": "Hello World!"}' \
	--header "Authorization: Bearer <access_token>" \
	'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/m.matrix.message/41'

A custom event can be emitted like so:

curl -X PUT \
	-d '{"msgtype": "m.text", "format": "org.matrix.custom.html", "body": "Hello world!", "formatted_body": "Hello World!"}' \
	--header "Authorization: Bearer <access_token>" \
	'http://localhost:8008/_matrix/client/r0/rooms/!room:my.matrix.host/send/custom.event/42'

See Also

JS SDK

Although not required, working with matrix from JS is easiest from the matrix-js-sdk or using matrix-appservice-node.

Events

Events can be listened to like so:

client.on("event type", callback);

Messages

To check for messages you need to listen to these three events:

  • Room.timeline - the timeline of rooms for which our user is a member.
  • room.message - A message event (May be encrypted).
  • Event.decrypted - A event that has been decrypted.

There is also:

  • event - all events.

Both Room.timeline and room.message need to be checked if it is indeed a message and that it is not encrypted:

if (event.isEncrypted() || event.getType() !== "m.room.message")
	return;

// Handle event

The room.message event needs to check that it has successfully decrypted it and it is a message.

if (event.isDecryptionFailure() || event.getType() !== "m.room.message")
	return;

// Handle event

Duplicate Events

Sometimes a duplicate event may occur - I think it may be to do with this.

Event History

Event history is a little hard to figure out in the matrix-js-sdk when you don't know what you are looking for, so here is a quick example:

matrix.client.scrollback(room, 30).then(() => {
	console.log(room.timeline);
})

Enabling Encryption

The matrix client's method setRoomEncryption only sets the room encryption on the client side, an m.room.encryption event needs to also be pushed to enable it.

const roomId = "!XXXXXX:example.com";
const encryptionOptions = { algorithm: "m.megolm.v1.aes-sha2" };

await client.sendStateEvent(roomId, "m.room.encryption", encryptionOptions));
await client.setRoomEncryption(roomId, encryptionOptions));

Verifying Devices

To verify a device it needs to both be marked as known and verified.

await client.setDeviceKnown(userId, deviceId, true)
await client.setDeviceVerified(userId, deviceId, true);

You can verify everyone in a room by getting the members and all their devices.

const roomId = "!XXXXXX:example.com";
const room = client.getRoom(roomId);
const members = await room.getEncryptionTargetMembers();

for (const member of members) {
	const devices = client.getStoredDevicesForUser(member.userId);

	for (const device of devices) {
		if (device.isUnverified())
			// Verify the device
	}
}

Checking Login Status

It's slightly more convoluted than it initially appears to see if the user is successfully logged in but here is the process I have arrived at.

// userId will return anything if the user has attempted a login.
const userId = client.getUserId();

if (!userId)
	return; // No login attempt made.

// user will only return something if the account has permission to access this user's details
const user = client.getUser(userId);

if (!user)
	return; // Not your account.

// User must be logged in if they made a login attempt and can access that user's data.

Reactivity with Vue

Proper reactivity can be a little tricky to setup just right but once it is working everything should just work without issues.

import matrix from "@/matrix";
import { reactive } from "vue";

const matrixStore = new matrix.MemoryStore();

// Overwrite a property with a reactive version.
const enableReactivity = prop => matrixStore[prop] = reactive(matrixStore[prop]);

// Enable reactivity on all properties you want to be reactive, e.g. "users"
enableReactivity("rooms");

export default {
	state: {
		rooms: matrixStore.rooms,
	},

	getters: {
		rooms (state) {
			return Object.values(state.rooms);
		}
	}
};

matrix.createClient({
	// ...
	store: matrixStore
});

VOIP through Node

Matrix VOIP is done through webRTC which is super simple if you are in a browser environment but if you are using node things are a bit more complicated.

Here is a working flow using simple-peer:

peer1 -----m.call.invite----> peer2
peer1 ---m.call.candidates--> peer2
peer1 <--m.call.candidates--- peer2
peer1 <----m.call.answer----- peer2
peer1 <----m.call.hangup----> peer2

And here is a more complete flow from MSC2746. (Explained here)

peer1 -----m.call.invite----> peer2
peer1 ---m.call.candidates--> peer2
peer1 <--m.call.candidates--- peer2
peer1 <----m.call.answer----- peer2
peer1 -m.call.select_answer-> peer2
peer1 <----m.call.hangup----> peer2

Make sure you set a track on the node side, if element does not detect a track it seems to assume connection failure. I recommend checking out simple-peer alongside wrtc-node for handling webRTC connections on the node side.

Simple-Peer Integration

Your config should use turn data obtained from the server.

const turnData = await matrix.client.turnServer();

let peer = new Peer({
	wrtc,
	stream,
	trickle: true,
	initiator: false,

	config: {
		iceServers: [
			{
				username: turnData.username,
				credential: turnData.password,
				urls: turnData.uris
			}
		],

		iceTransportPolicy: undefined // "all" or "relay"
	}
});

// Very important we add a track before sending otherwise element won't connect.
peer.addTrack(track, stream);


When simple-peer sends candidates it sends them one by one, whereas matrix expects them to be group so you need to wait a reasonable amount of time for all candidates to be emitted then send them all:

callCandidates.push(data.candidate);

if (!timer) {
	// We need to wait to group the candidates.
	timer = setTimeout(async () => {
		await sendEvent(
			callRoomDetails.roomId,
			"m.call.candidates",
			{
				candidates: callCandidates,
				version: callRoomDetails.version,
				call_id: callRoomDetails.call_id,
				party_id: callRoomDetails.party_id
			}
		);

		// Reset this so we can send candidates again.
		timer = undefined;
		callCandidates = [];

		console.log("Sent candidates.");
	}, 500);
}

Bot Examples

See Also

Application Services

Application services are a way to modify the matrix functionality by having an application running alongside the homeserver.

Register Application Service

To register the application service you need to edit the homeserver.yaml file and add the following config (it is commented out so search for app_service_config_files:)

app_service_config_files:
  - "/path/to/appservice/registration.yaml"

If you are on a local client through docker you will want to copy your registration.yaml to /var/lib/docker/volumes/synapse-data/_data/ and add the following config:

app_service_config_files:
  - "/data/registration.yaml"

Then restart the homeserver to apply changes and check the logs to ensure it hasn't thrown an error.

Troubleshooting

Test that your application service gives a response when running:

curl -X PUT http://localhost:8010/transactions/1?access_token=yourtoken

localhost IPv6 Socket

I had trouble getting an application service running initially the synapse logs were showing me:

INFO - as-recoverer-test-appservice-3 - Error sending request to PUT http://localhost:8010/transactions/1?access_token=<redacted>: ConnectError Cannot assign requested address

The issue was that I had set up the application service to use localhost and synapse was trying to create an IPv6 socket - which my application service was not setup for.
To resolve this I changed localhost to an IPv4 address.

You may also want to check your /etc/hosts file to verify that localhost does not resolve to IPv6 (it wasn't in my case).

Matrix Appservice Node

Register

Note the setId method is not shown in the docs but is required. Also note url is the url of the app service not the homeserver.
Also note that if localhost is used synapse resolve this to an ipv6 socket which may not work.

registration.js (for creating registation.yaml file)

const { AppServiceRegistration } = require("matrix-appservice");

// creating registration files
const reg = new AppServiceRegistration();
reg.setId("test-appservice");
reg.setAppServiceUrl("http://<YOUR IP ADDR>:8010");
reg.setHomeserverToken(AppServiceRegistration.generateToken());
reg.setAppServiceToken(AppServiceRegistration.generateToken());
reg.setSenderLocalpart("example-appservice");
reg.addRegexPattern("users", "@.*", true); // All users
reg.addRegexPattern("aliases", "#_ap_bridge.*", true); // Only reserve _ap_bridge aliases.
reg.setProtocols(["exampleservice"]); // For 3PID lookups
reg.outputAsYaml("registration.yaml");

Example output:

id: test-appservice
hs_token: 35d98ec4115123751d2dbab8e441e277d7d4e364130f474b59cdbfdb164f94a1
as_token: e83a2ef0add9be4e77ab3b04aedb3df3d1dafdb9bb1b4c4374a169c5524d270d
url: 'http://<YOUR IP ADDR>:8010'
sender_localpart: example-appservice
protocols:
  - exampleservice
namespaces:
  users:
    - exclusive: true
      regex: '@.*'

Bridges

Widgets

Widgets that are currently supported are "dumb" widgets - they are only webpages rendered in an i-frame, however more complicated widgets spec is being worked on to allow communication between the client and the web app.

See also