phpmatrix/phpmatrix.php

350 lines
9.9 KiB
PHP

<?php
class MatrixClient
{
/** @var string Matrix server (https://domain.tld) */
private $matrix_server;
/** @var string MXID (@login:domain.tld) */
private $mxid;
/** @var MatrixRoom[] List of subscribed rooms */
private $rooms;
/** @var int API transaction ID */
private $transaction_id;
/** @var string API Access token */
private $access_token;
/** @var int Update period in microseconds */
private $update_period = 1000000;
/**
* @param string $matrix_server Matrix server (https://domain.tld)
*/
function __construct($matrix_server)
{
$this->matrix_server = $matrix_server;
$this->ch = curl_init();
// Supposed to be unique for each transaction of an access token
// A bit rough, but use microtime...
$this->transaction_id = microtime();
}
/**
* @return int
*/
private function get_new_transaction_id()
{
$this->transaction_id = microtime();
return $this->transaction_id;
}
/**
* @return string
*/
public function get_access_token()
{
return $this->access_token;
}
/**
* @return string
*/
public function get_mxid()
{
return $this->mxid;
}
/**
* Change update period (for events update)
* @param float $seconds Period in seconds
*/
public function set_update_period($seconds)
{
$this->update_period = intval($seconds * 1000000);
}
/**
* Try to login to Matrix API and get an access token.
* Will throw a MatrixRequestException if it fails
* @param string $mxid client's login (@login:domain.tld)
* @param string $password client's password
*/
public function login_with_password($mxid, $password)
{
$this->mxid = $mxid;
$body = [
"identifier" => [
"type" => "m.id.user",
"user" => $mxid,
],
"initial_device_display_name" => "MatrixPhpClient",
"password" => $password,
"type" => "m.login.password",
];
$res = $this->query("/_matrix/client/v3/login",
"POST", $body, null, false);
$this->access_token = $res["access_token"];
}
/**
* Query to the Matrix API
* Will throw a MatrixRequestException if it fails
* TODO: change default timeouts?
* @param string $url_path: path after the Matrix server
* @param string $method: GET, PUT, POST
* @param string[] $body: array of key/values for POST/PUT
* @param string[] $params: URL params
* @param bool $require_token: if true, access_token needed
* @return string[] json decoded response
*/
public function query($url_path, $method="GET", $body=null, $params=null, $require_token=true)
{
if (!$this->access_token && $require_token) {
throw new MatrixRequestException("Missing access token", 0);
}
if (!$params) {
$params = [];
}
$params["access_token"] = $this->access_token;
curl_reset($this->ch);
$url = $this->matrix_server . $url_path . "?" . http_build_query($params);
curl_setopt($this->ch, CURLOPT_URL, $url);
switch ($method) {
case "POST":
curl_setopt($this->ch, CURLOPT_POST, 1);
if ($body) {
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($body));
}
break;
case "PUT":
curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, "PUT");
if ($body) {
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($body));
}
break;
default:
break;
}
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($this->ch);
if ($res === false) {
throw new MatrixRequestException("Matrix request failed: " . curl_error($this->ch),
curl_errno($this->ch));
}
$res = json_decode($res, true);
if (isset($res['errcode'])) {
$error = "[{$res['errcode']}] {$res['error']}";
throw new MatrixRequestException("Matrix request failed: $error", 0);
}
return $res;
}
/**
* Send a room event
* @param string $room_id Room ID (!xxxxxx:domain.tld. #alias:domain.tld seems to fail)
* @param string $event_type Event type (e.g. m.room.message)
* @param string[] $body key-value array
* @return string[] json decoded response
*/
public function send_room_event($room_id, $event_type, $body)
{
$txn_id = $this->get_new_transaction_id();
$room_id = curl_escape($this->ch, $room_id);
$path = "/_matrix/client/v3/rooms/{$room_id}/send/{$event_type}/{$txn_id}";
return $this->query($path, "PUT", $body);
}
/**
* Will just store the mxid/access token
* TODO: check if token valid?
* @param string $mxid: mxid (@xxxx:yyyy.zz) to identify the user
* @param string $access_token: the access token returned by a login
*/
public function login_with_token($mxid, $access_token)
{
$this->mxid = $mxid;
$this->access_token = $access_token;
}
/**
* Subscribe to a room: will allow to receive/send events from/to this room
* NB: the user has to have already join the room
* @param string $room_id room id or alias (#xxxx:yyy.zz)
* @return MatrixRoom
*/
public function join_room($room_id)
{
$room = new MatrixRoom($this, $room_id);
$this->rooms[$room_id] = $room;
return $room;
}
/**
* Blocking loop to get rooms' events
*/
public function run()
{
while (true) {
foreach ($this->rooms as $room) {
$room->get_events();
}
usleep($this->update_period);
}
}
}
class MatrixRequestException extends Exception
{
function __construct($message, $code = 0, $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
class MatrixEvent
{
public $room;
public $event_type;
public $sender;
public $content;
public $event_id;
public $origin_server_ts;
public $user_id;
public $age;
/**
* @param MatrixRoom $room the room that received the event
* @param string[] $contents the event's contents
*/
function __construct($room, $contents)
{
$this->room = $room;
$this->event_type = $contents["type"];
$this->sender = $contents["sender"];
$this->content = $contents["content"];
$this->event_id = $contents["event_id"];
// TODO: convert to datetime?
$this->origin_server_ts = $contents["origin_server_ts"];
$this->user_id = $contents["user_id"];
$this->age = isset($contents["age"]) ? $contents["age"] : null;
}
}
class MatrixRoom
{
private $client;
private $room_id;
private $listeners = [];
private $last_end_token = null;
/**
* @param MatrixClient $client
* @param string $room_id
*/
function __construct($client, $room_id)
{
$this->client = $client;
$this->room_id = $room_id;
}
/**
* Add an event listener: will be called each time we received a room's event
* NB: logged-in user's events are ignored
* TODO: param to allow them?
* @param $callback function(room, event)
*/
public function add_listener($callback)
{
$this->listeners[] = $callback;
}
/**
* Helper to send a simple text message (m.room.message with a text body)
* Will throw a MatrixRequest Exception if it fails
* @param string $text the raw text
* @return string[] json decoded response
*/
public function send_text($text)
{
$body = [
"body" => $text,
"msgtype" => "m.text"
];
return $this->client->send_room_event($this->room_id, "m.room.message", $body);
}
/**
* Helper to send a simple html message (m.room.message with html body)
* TODO: text body too? (RTFM)
* Will throw a MatrixRequest Exception if it fails
* @param string $html the HTML
* @return string[] json decoded response
*/
public function send_html($html)
{
$body = [
"body" => $text,
"msgtype" => "m.html"
];
return $this->client->send_room_event($this->room_id, "m.room.message", $body);
}
/**
* Read the room's last X events
* TODO: param to change the limit
*/
public function get_events()
{
$params = [
"limit" => 10,
"dir" => $this->last_end_token ? "f" : "b",
];
// (a bit dirty) trick:
// First time: get events in reverse order (most recent first) to get the last token (start)
// After that: get events chronologically (oldest first) with the end token
if ($this->last_end_token) {
$params["from"] = $this->last_end_token;
}
$room_id = curl_escape($this->client->ch, $this->room_id);
$res = $this->client->query("/_matrix/client/v3/rooms/{$room_id}/messages",
"GET", null, $params);
$dispatch_events = $this->last_end_token !== null;
if (!$this->last_end_token) {
$this->last_end_token = $res["start"];
} elseif (isset($res["end"])) {
$this->last_end_token = $res["end"];
}
// First time we get events : do nothing (it's history)
if ($dispatch_events) {
foreach ($res["chunk"] as $event) {
foreach ($this->listeners as $listener) {
// Ignore our events
if ($event["sender"] === $this->client->get_mxid()) {
continue;
}
$listener(new MatrixEvent($this, $event));
}
}
}
}
}