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)); } } } } }