370 lines
10 KiB
PHP
370 lines
10 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;
|
|
|
|
/** @var bool Enable legacy mode for matrix API */
|
|
private $legacy_mode_enabled = false;
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Change legacy mode
|
|
* @param bool $mode Enabled if true
|
|
*/
|
|
public function set_legacy_mode_enabled($mode)
|
|
{
|
|
$this->legacy_mode_enabled = $mode;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
if ($this->legacy_mode_enabled) {
|
|
$path = "/_matrix/client/r0/rooms/{$room_id}/send/{$event_type}";
|
|
return $this->query($path, "POST", $body);
|
|
}
|
|
else {
|
|
$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" => $html,
|
|
"formatted_body" => $html,
|
|
"msgtype" => "m.text",
|
|
"format" => "org.matrix.custom.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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|