commit ce86994af08dd6ab38167ac91b400dda5cfc49f8 Author: groug Date: Sun Oct 22 14:06:29 2023 +0200 first draft diff --git a/phpmatrix.php b/phpmatrix.php new file mode 100644 index 0000000..26302bd --- /dev/null +++ b/phpmatrix.php @@ -0,0 +1,279 @@ +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(); + } + + private function get_new_transaction_id() + { + $this->transaction_id = microtime(); + return $this->transaction_id; + } + + public function get_access_token() + { + return $this->access_token; + } + + public function get_mxid() + { + return $this->mxid; + } + + 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"]; + } + + /** + * @var string $url_path: path after the TLD + * @var string $method: GET, PUT, POST + * @var string[] $body: array of key/values for POST/PUT + * @var string[] $params: URL params + * @var bool $require_token: if true, access_token needed + */ + 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; + } + + 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); + } + + /** + * @var string $mxid: mxid (@xxxx:yyyy.zz) to identify the user + * @var string $access_token: the access token returned by a login + */ + function login_with_token($mxid, $access_token) + { + $this->mxid = $mxid; + $this->access_token = $access_token; + } + + /** + * @var string $room_id room id or alias (#xxxx:yyy.zz) + * @return MatrixRoom + */ + function join_room($room_id) + { + $room = new MatrixRoom($this, $room_id); + $this->rooms[$room_id] = $room; + + return $room; + } + + function run() + { + while (true) { + foreach ($this->rooms as $room) { + $room->get_events(); + } + usleep(500000); + } + } +} + +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; + + 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"]; + $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; + + function __construct($client, $room_id) + { + $this->client = $client; + $this->room_id = $room_id; + } + + /** + * @var $callback function(room, event) + */ + function add_listener($callback) + { + $this->listeners[] = $callback; + } + + function send_text($text) + { + $body = [ + "body" => $text, + "msgtype" => "m.text" + ]; + return $this->client->send_room_event($this->room_id, "m.room.message", $body); + } + + function send_html($html) + { + $body = [ + "body" => $text, + "msgtype" => "m.html" + ]; + return $this->client->send_room_event($this->room_id, "m.room.message", $body); + } + + function get_events() + { + $params = [ + "limit" => 10, + "dir" => "b", + ]; + + if ($this->last_end_token) { + $params["to"] = $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; + $this->last_end_token = $res["start"]; + + // First time we get events: do nothing + 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)); + } + } + } + } +} + +$cb = function($event) +{ + if ($event->event_type == "m.room.message") { + var_dump($event); + if ($event->content["body"] && $event->content["body"][0] === "!") { + $command_parts = explode(" ", $event->content["body"], 2); + $command = $command_parts[0]; + $arg = isset($command_parts[1]) ? $command_parts[1] : ""; + + switch($command) { + case "!echo": + $event->room->send_text($arg); + break; + case "!help": + $event->room->send_text("Usage: !COMMAND ARG1 ARG2...\n!echo TEXT => repeat TEXT"); + break; + } + } + } +};