1<?php 2 3namespace MatrixPhp; 4 5use MatrixPhp\Exceptions\MatrixException; 6use MatrixPhp\Exceptions\MatrixHttpLibException; 7use MatrixPhp\Exceptions\MatrixRequestException; 8use MatrixPhp\Exceptions\MatrixUnexpectedResponse; 9use MatrixPhp\Exceptions\ValidationException; 10use GuzzleHttp\Client; 11use GuzzleHttp\Exception\GuzzleException; 12use GuzzleHttp\RequestOptions; 13 14/** 15 * Contains all raw Matrix HTTP Client-Server API calls. 16 * For room and sync handling, consider using MatrixClient. 17 * 18 * Examples: 19 * Create a client and send a message:: 20 * 21 * $matrix = new MatrixHttpApi("https://matrix.org", $token="foobar"); 22 * $response = $matrix.sync(); 23 * $response = $matrix->sendMessage("!roomid:matrix.org", "Hello!"); 24 * 25 * @see https://matrix.org/docs/spec/client_server/latest 26 * 27 * @package MatrixPhp 28 */ 29class MatrixHttpApi { 30 31 const MATRIX_V2_API_PATH = '/_matrix/client/r0'; 32 const MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'; 33 const VERSION = '0.0.1-dev'; 34 35 /** 36 * @var string 37 */ 38 private $baseUrl; 39 40 /** 41 * @var string|null 42 */ 43 private $token; 44 45 /** 46 * @var string|null 47 */ 48 private $identity; 49 50 /** 51 * @var int 52 */ 53 private $default429WaitMs; 54 55 /** 56 * @var bool 57 */ 58 private $useAuthorizationHeader; 59 60 /** 61 * @var int 62 */ 63 private $txnId; 64 65 /** 66 * @var bool 67 */ 68 private $validateCert; 69 70 /** 71 * @var Client 72 */ 73 private $client; 74 75 /** 76 * MatrixHttpApi constructor. 77 * 78 * @param string $baseUrl The home server URL e.g. 'http://localhost:8008' 79 * @param string|null $token Optional. The client's access token. 80 * @param string|null $identity Optional. The mxid to act as (For application services only). 81 * @param int $default429WaitMs Optional. Time in milliseconds to wait before retrying a request 82 * when server returns a HTTP 429 response without a 'retry_after_ms' key. 83 * @param bool $useAuthorizationHeader Optional. Use Authorization header instead of access_token query parameter. 84 * @throws MatrixException 85 */ 86 public function __construct(string $baseUrl, ?string $token = null, ?string $identity = null, 87 int $default429WaitMs = 5000, bool $useAuthorizationHeader = true) { 88 if (!filter_var($baseUrl, FILTER_VALIDATE_URL)) { 89 throw new MatrixException("Invalid homeserver url $baseUrl"); 90 } 91 92 if (!array_get(parse_url($baseUrl), 'scheme')) { 93 throw new MatrixException("No scheme in homeserver url $baseUrl"); 94 } 95 $this->baseUrl = $baseUrl; 96 $this->token = $token; 97 $this->identity = $identity; 98 $this->txnId = 0; 99 $this->validateCert = true; 100 $this->client = new Client(); 101 $this->default429WaitMs = $default429WaitMs; 102 $this->useAuthorizationHeader = $useAuthorizationHeader; 103 } 104 105 public function setClient(Client $client) { 106 $this->client = $client; 107 } 108 109 /** 110 * @param string|null $since Optional. A token which specifies where to continue a sync from. 111 * @param int $timeoutMs 112 * @param null $filter 113 * @param bool $fullState 114 * @param string|null $setPresence 115 * @return array|string 116 * @throws MatrixException 117 */ 118 public function sync(?string $since = null, int $timeoutMs = 30000, $filter = null, 119 bool $fullState = false, ?string $setPresence = null) { 120 $request = [ 121 'timeout' => (int)$timeoutMs, 122 ]; 123 124 if ($since) { 125 $request['since'] = $since; 126 } 127 128 if ($filter) { 129 $request['filter'] = $filter; 130 } 131 132 if ($fullState) { 133 $request['full_state'] = json_encode($fullState); 134 } 135 136 if ($setPresence) { 137 $request['set_presence'] = $setPresence; 138 } 139 140 return $this->send('GET', "/sync", null, $request); 141 } 142 143 public function validateCertificate(bool $validity) { 144 $this->validateCert = $validity; 145 } 146 147 /** 148 * Performs /register. 149 * 150 * @param array $authBody Authentication Params. 151 * @param string $kind Specify kind of account to register. Can be 'guest' or 'user'. 152 * @param bool $bindEmail Whether to use email in registration and authentication. 153 * @param string|null $username The localpart of a Matrix ID. 154 * @param string|null $password The desired password of the account. 155 * @param string|null $deviceId ID of the client device. 156 * @param string|null $initialDeviceDisplayName Display name to be assigned. 157 * @param bool $inhibitLogin Whether to login after registration. Defaults to false. 158 * @return array|string 159 * @throws MatrixException 160 */ 161 public function register(array $authBody = [], string $kind = "user", bool $bindEmail = false, 162 ?string $username = null, ?string $password = null, ?string $deviceId = null, 163 ?string $initialDeviceDisplayName = null, bool $inhibitLogin = false) { 164 $content = [ 165 'kind' => $kind 166 ]; 167 if ($authBody) { 168 $content['auth'] = $authBody; 169 } 170 if ($username) { 171 $content['username'] = $username; 172 } 173 if ($password) { 174 $content['password'] = $password; 175 } 176 if ($deviceId) { 177 $content['device_id'] = $deviceId; 178 } 179 if ($initialDeviceDisplayName) { 180 $content['initial_device_display_name'] = $initialDeviceDisplayName; 181 } 182 if ($bindEmail) { 183 $content['bind_email'] = $bindEmail; 184 } 185 if ($inhibitLogin) { 186 $content['inhibit_login'] = $inhibitLogin; 187 } 188 189 return $this->send('POST', '/register', $content, ['kind' => $kind]); 190 } 191 192 /** 193 * Perform /login. 194 * 195 * @param string $loginType The value for the 'type' key. 196 * @param array $args Additional key/values to add to the JSON submitted. 197 * @return array|string 198 * @throws MatrixException 199 */ 200 public function login(string $loginType, array $args) { 201 $args["type"] = $loginType; 202 203 return $this->send('POST', '/login', $args); 204 } 205 206 /** 207 * Perform /logout. 208 * 209 * @return array|string 210 * @throws MatrixException 211 */ 212 public function logout() { 213 return $this->send('POST', '/logout'); 214 } 215 216 /** 217 * Perform /logout/all. 218 * 219 * @return array|string 220 * @throws MatrixException 221 */ 222 public function logoutAll() { 223 return $this->send('POST', '/logout/all'); 224 } 225 226 /** 227 * Perform /createRoom. 228 * 229 * @param string|null $alias Optional. The room alias name to set for this room. 230 * @param string|null $name Optional. Name for new room. 231 * @param bool $isPublic Optional. The public/private visibility. 232 * @param array|null $invitees Optional. The list of user IDs to invite. 233 * @param bool|null $federate Optional. Сan a room be federated. Default to True. 234 * @return array|string 235 * @throws MatrixException 236 */ 237 public function createRoom(string $alias = null, string $name = null, bool $isPublic = false, 238 array $invitees = null, bool $federate = null) { 239 $content = [ 240 "visibility" => $isPublic ? "public" : "private" 241 ]; 242 if ($alias) { 243 $content["room_alias_name"] = $alias; 244 } 245 if ($invitees) { 246 $content["invite"] = $invitees; 247 } 248 if ($name) { 249 $content["name"] = $name; 250 } 251 if ($federate != null) { 252 $content["creation_content"] = ['m.federate' => $federate]; 253 } 254 return $this->send("POST", "/createRoom", $content); 255 } 256 257 /** 258 * Performs /join/$room_id 259 * 260 * @param string $roomIdOrAlias The room ID or room alias to join. 261 * @return array|string 262 * @throws MatrixException 263 */ 264 public function joinRoom(string $roomIdOrAlias) { 265 $path = sprintf("/join/%s", urlencode($roomIdOrAlias)); 266 267 return $this->send('POST', $path); 268 } 269 270 /** 271 * Perform PUT /rooms/$room_id/state/$event_type 272 * 273 * @param string $roomId The room ID to send the state event in. 274 * @param string $eventType The state event type to send. 275 * @param array $content The JSON content to send. 276 * @param string $stateKey Optional. The state key for the event. 277 * @param int|null $timestamp Set origin_server_ts (For application services only) 278 * @return array|string 279 * @throws MatrixException 280 */ 281 public function sendStateEvent(string $roomId, string $eventType, array $content, 282 string $stateKey = "", int $timestamp = null) { 283 $path = sprintf("/rooms/%s/state/%s", urlencode($roomId), urlencode($eventType)); 284 if ($stateKey) { 285 $path .= sprintf("/%s", urlencode($stateKey)); 286 } 287 $params = []; 288 if ($timestamp) { 289 $params["ts"] = $timestamp; 290 } 291 292 return $this->send('PUT', $path, $content, $params); 293 } 294 295 /** 296 * Perform GET /rooms/$room_id/state/$event_type 297 * 298 * @param string $roomId The room ID. 299 * @param string $eventType The type of the event. 300 * @return array|string 301 * @throws MatrixRequestException (code=404) if the state event is not found. 302 * @throws MatrixException 303 */ 304 public function getStateEvent(string $roomId, string $eventType) { 305 $path = sprintf('/rooms/%s/state/%s', urlencode($roomId), urlencode($eventType)); 306 307 return $this->send('GET', $path); 308 } 309 310 /** 311 * @param string $roomId The room ID to send the message event in. 312 * @param string $eventType The event type to send. 313 * @param array $content The JSON content to send. 314 * @param int $txnId Optional. The transaction ID to use. 315 * @param int $timestamp Set origin_server_ts (For application services only) 316 * @return array|string 317 * @throws MatrixException 318 * @throws MatrixHttpLibException 319 * @throws MatrixRequestException 320 */ 321 public function sendMessageEvent(string $roomId, string $eventType, array $content, 322 int $txnId = null, int $timestamp = null) { 323 if (!$txnId) { 324 $txnId = $this->makeTxnId(); 325 } 326 $path = sprintf('/rooms/%s/send/%s/%s', urlencode($roomId), urlencode($eventType), urlencode($txnId)); 327 $params = []; 328 if ($timestamp) { 329 $params['ts'] = $timestamp; 330 } 331 332 return $this->send('PUT', $path, $content, $params); 333 } 334 335 /** 336 * Perform PUT /rooms/$room_id/redact/$event_id/$txn_id/ 337 * 338 * @param string $roomId The room ID to redact the message event in. 339 * @param string $eventId The event id to redact. 340 * @param string $reason Optional. The reason the message was redacted. 341 * @param int|null $txnId Optional. The transaction ID to use. 342 * @param int|null $timestamp Optional. Set origin_server_ts (For application services only) 343 * @return array|string 344 * @throws MatrixException 345 * @throws MatrixHttpLibException 346 * @throws MatrixRequestException 347 */ 348 public function redactEvent(string $roomId, string $eventId, ?string $reason = null, 349 int $txnId = null, int $timestamp = null) { 350 if (!$txnId) { 351 $txnId = $this->makeTxnId(); 352 } 353 $path = sprintf('/rooms/%s/redact/%s/%s', urlencode($roomId), urlencode($eventId), urlencode($txnId)); 354 $params = []; 355 $content = []; 356 if ($reason) { 357 $content['reason'] = $reason; 358 } 359 if ($timestamp) { 360 $params['ts'] = $timestamp; 361 } 362 363 return $this->send('PUT', $path, $content, $params); 364 } 365 366 /** 367 * $content_type can be a image,audio or video 368 * extra information should be supplied, see 369 * https://matrix.org/docs/spec/r0.0.1/client_server.html 370 * 371 * @param string $roomId 372 * @param string $itemUrl 373 * @param string $itemName 374 * @param string $msgType 375 * @param array $extraInformation 376 * @param int|null $timestamp 377 * @return array|string 378 * @throws MatrixException 379 * @throws MatrixHttpLibException 380 * @throws MatrixRequestException 381 */ 382 public function sendContent(string $roomId, string $itemUrl, string $itemName, string $msgType, 383 array $extraInformation = [], int $timestamp = null) { 384 $contentPack = [ 385 "url" => $itemUrl, 386 "msgtype" => $msgType, 387 "body" => $itemName, 388 "info" => $extraInformation, 389 ]; 390 391 return $this->sendMessageEvent($roomId, 'm.room.message', $contentPack, null, $timestamp); 392 } 393 394 395 /** 396 * Send m.location message event 397 * http://matrix.org/docs/spec/client_server/r0.2.0.html#m-location 398 * 399 * @param string $roomId The room ID to send the event in. 400 * @param string $geoUri The geo uri representing the location. 401 * @param string $name Description for the location. 402 * @param string|null $thumbUrl URL to the thumbnail of the location. 403 * @param array|null $thumbInfo Metadata about the thumbnail, type ImageInfo. 404 * @param int|null $timestamp Set origin_server_ts (For application services only) 405 * @return array|string 406 * @throws MatrixException 407 * @throws MatrixHttpLibException 408 * @throws MatrixRequestException 409 */ 410 public function sendLocation(string $roomId, string $geoUri, string $name, string $thumbUrl = null, 411 array $thumbInfo = null, int $timestamp = null) { 412 $contentPack = [ 413 "geo_uri" => $geoUri, 414 "msgtype" => "m.location", 415 "body" => $name, 416 ]; 417 if ($thumbUrl) { 418 $contentPack['thumbnail_url'] = $thumbUrl; 419 } 420 if ($thumbInfo) { 421 $contentPack['thumbnail_info'] = $thumbInfo; 422 } 423 424 return $this->sendMessageEvent($roomId, 'm.room.message', $contentPack, null, $timestamp); 425 } 426 427 /** 428 * Perform PUT /rooms/$room_id/send/m.room.message 429 * 430 * @param string $roomId The room ID to send the event in. 431 * @param string $textContent The m.text body to send. 432 * @param string $msgType 433 * @param int|null $timestamp Set origin_server_ts (For application services only) 434 * @return array|string 435 * @throws MatrixException 436 * @throws MatrixHttpLibException 437 * @throws MatrixRequestException 438 */ 439 public function sendMessage(string $roomId, string $textContent, string $msgType = 'm.text', int $timestamp = null) { 440 $textBody = $this->getTextBody($textContent, $msgType); 441 442 return $this->sendMessageEvent($roomId, 'm.room.message', $textBody, null, $timestamp); 443 } 444 445 /** 446 * Perform PUT /rooms/$room_id/send/m.room.message with m.emote msgtype 447 * 448 * @param string $roomId The room ID to send the event in. 449 * @param string $textContent The m.emote body to send. 450 * @param int|null $timestamp Set origin_server_ts (For application services only) 451 * @return array|string 452 * @throws MatrixException 453 * @throws MatrixHttpLibException 454 * @throws MatrixRequestException 455 */ 456 public function sendEmote(string $roomId, string $textContent, int $timestamp = null) { 457 $body = $this->getEmoteBody($textContent); 458 459 return $this->sendMessageEvent($roomId, 'm.room.message', $body, null, $timestamp); 460 } 461 462 /** 463 * Perform PUT /rooms/$room_id/send/m.room.message with m.notice msgtype 464 * 465 * @param string $roomId The room ID to send the event in. 466 * @param string $textContent The m.emote body to send. 467 * @param int|null $timestamp Set origin_server_ts (For application services only) 468 * @return array|string 469 * @throws MatrixException 470 * @throws MatrixHttpLibException 471 * @throws MatrixRequestException 472 */ 473 public function sendNotice(string $roomId, string $textContent, int $timestamp = null) { 474 $body = [ 475 'msgtype' => 'm.notice', 476 'body' => $textContent, 477 ]; 478 479 return $this->sendMessageEvent($roomId, 'm.room.message', $body, null, $timestamp); 480 } 481 482 /** 483 * @param string $roomId The room's id. 484 * @param string $token The token to start returning events from. 485 * @param string $direction The direction to return events from. One of: ["b", "f"]. 486 * @param int $limit The maximum number of events to return. 487 * @param string|null $to The token to stop returning events at. 488 * @return array|string 489 * @throws MatrixException 490 * @throws MatrixHttpLibException 491 * @throws MatrixRequestException 492 */ 493 public function getRoomMessages(string $roomId, string $token, string $direction, int $limit = 10, string $to = null) { 494 $query = [ 495 "roomId" => $roomId, 496 "from" => $token, 497 'dir' => $direction, 498 'limit' => $limit, 499 ]; 500 501 if ($to) { 502 $query['to'] = $to; 503 } 504 $path = sprintf('/rooms/%s/messages', urlencode($roomId)); 505 506 return $this->send('GET', $path, null, $query); 507 } 508 509 /** 510 * Perform GET /rooms/$room_id/state/m.room.name 511 * 512 * @param string $roomId The room ID 513 * @return array|string 514 * @throws MatrixException 515 * @throws MatrixRequestException 516 */ 517 public function getRoomName(string $roomId) { 518 return $this->getStateEvent($roomId, 'm.room.name'); 519 } 520 521 /** 522 * Perform PUT /rooms/$room_id/state/m.room.name 523 * 524 * @param string $roomId The room ID 525 * @param string $name The new room name 526 * @param int|null $timestamp Set origin_server_ts (For application services only) 527 * @return array|string 528 * @throws MatrixException 529 */ 530 public function setRoomName(string $roomId, string $name, int $timestamp = null) { 531 $body = ['name' => $name]; 532 533 return $this->sendStateEvent($roomId, 'm.room.name', $body, '', $timestamp); 534 } 535 536 /** 537 * Perform GET /rooms/$room_id/state/m.room.topic 538 * 539 * @param string $roomId The room ID 540 * @return array|string 541 * @throws MatrixException 542 * @throws MatrixRequestException 543 */ 544 public function getRoomTopic(string $roomId) { 545 return $this->getStateEvent($roomId, 'm.room.topic'); 546 } 547 548 /** 549 * Perform PUT /rooms/$room_id/state/m.room.topic 550 * 551 * @param string $roomId The room ID 552 * @param string $topic The new room topic 553 * @param int|null $timestamp Set origin_server_ts (For application services only) 554 * @return array|string 555 * @throws MatrixException 556 */ 557 public function setRoomTopic(string $roomId, string $topic, int $timestamp = null) { 558 $body = ['topic' => $topic]; 559 560 return $this->sendStateEvent($roomId, 'm.room.topic', $body, '', $timestamp); 561 } 562 563 564 /** 565 * Perform GET /rooms/$room_id/state/m.room.power_levels 566 * 567 * 568 * @param string $roomId The room ID 569 * @return array|string 570 * @throws MatrixException 571 * @throws MatrixRequestException 572 */ 573 public function getPowerLevels(string $roomId) { 574 return $this->getStateEvent($roomId, 'm.room.power_levels'); 575 } 576 577 /** 578 * Perform PUT /rooms/$room_id/state/m.room.power_levels 579 * 580 * Note that any power levels which are not explicitly specified 581 * in the content arg are reset to default values. 582 * 583 * 584 * Example: 585 * $api = new MatrixHttpApi("http://example.com", $token="foobar") 586 * $api->setPowerLevels("!exampleroom:example.com", 587 * [ 588 * "ban" => 50, # defaults to 50 if unspecified 589 * "events": [ 590 * "m.room.name" => 100, # must have PL 100 to change room name 591 * "m.room.power_levels" => 100 # must have PL 100 to change PLs 592 * ], 593 * "events_default" => 0, # defaults to 0 594 * "invite" => 50, # defaults to 50 595 * "kick" => 50, # defaults to 50 596 * "redact" => 50, # defaults to 50 597 * "state_default" => 50, # defaults to 50 if m.room.power_levels exists 598 * "users" => [ 599 * "@someguy:example.com" => 100 # defaults to 0 600 * ], 601 * "users_default" => 0 # defaults to 0 602 * ] 603 * ); 604 * 605 * @param string $roomId 606 * @param array $content 607 * @return array|string 608 * @throws MatrixException 609 */ 610 public function setPowerLevels(string $roomId, array $content) { 611 // Synapse returns M_UNKNOWN if body['events'] is omitted, 612 // as of 2016-10-31 613 if (!array_key_exists('events', $content)) { 614 $content['events'] = []; 615 } 616 617 return $this->sendStateEvent($roomId, 'm.room.power_levels', $content); 618 } 619 620 /** 621 * Perform POST /rooms/$room_id/leave 622 * 623 * @param string $roomId The room ID 624 * @return array|string 625 * @throws MatrixException 626 * @throws MatrixHttpLibException 627 * @throws MatrixRequestException 628 */ 629 public function leaveRoom(string $roomId) { 630 return $this->send('POST', sprintf('/rooms/%s/leave', urlencode($roomId))); 631 } 632 633 /** 634 * Perform POST /rooms/$room_id/forget 635 * 636 * @param string $roomId The room ID 637 * @return array|string 638 * @throws MatrixException 639 * @throws MatrixHttpLibException 640 * @throws MatrixRequestException 641 */ 642 public function forgetRoom(string $roomId) { 643 return $this->send('POST', sprintf('/rooms/%s/forget', urlencode($roomId)), []); 644 } 645 646 /** 647 * Perform POST /rooms/$room_id/invite 648 * 649 * @param string $roomId The room ID 650 * @param string $userId The user ID of the invitee 651 * @return array|string 652 * @throws MatrixException 653 * @throws MatrixHttpLibException 654 * @throws MatrixRequestException 655 */ 656 public function inviteUser(string $roomId, string $userId) { 657 $body = ['user_id' => $userId]; 658 659 return $this->send('POST', sprintf('/rooms/%s/invite', urlencode($roomId)), $body); 660 } 661 662 /** 663 * Calls set_membership with membership="leave" for the user_id provided 664 * 665 * @param string $roomId The room ID 666 * @param string $userId The user ID 667 * @param string $reason Optional. The reason for kicking them out 668 * @return mixed 669 * @throws MatrixException 670 */ 671 public function kickUser(string $roomId, string $userId, string $reason = '') { 672 return $this->setMembership($roomId, $userId, 'leave', $reason); 673 } 674 675 /** 676 * Perform GET /rooms/$room_id/state/m.room.member/$user_id 677 * 678 * @param string $roomId The room ID 679 * @param string $userId The user ID 680 * @return array|string 681 * @throws MatrixException 682 * @throws MatrixHttpLibException 683 * @throws MatrixRequestException 684 */ 685 public function getMembership(string $roomId, string $userId) { 686 $path = sprintf('/rooms/%s/state/m.room.member/%s', urlencode($roomId), urlencode($userId)); 687 688 return $this->send('GET', $path); 689 } 690 691 /** 692 * Perform PUT /rooms/$room_id/state/m.room.member/$user_id 693 * 694 * @param string $roomId The room ID 695 * @param string $userId The user ID 696 * @param string $membership New membership value 697 * @param string $reason The reason 698 * @param array $profile 699 * @param int|null $timestamp Set origin_server_ts (For application services only) 700 * @return array|string 701 * @throws MatrixException 702 */ 703 public function setMembership(string $roomId, string $userId, string $membership, string $reason = '', array $profile = [], int $timestamp = null) { 704 $body = [ 705 'membership' => $membership, 706 'reason' => $reason, 707 ]; 708 if (array_key_exists('displayname', $profile)) { 709 $body['displayname'] = $profile['displayname']; 710 } 711 if (array_key_exists('avatar_url', $profile)) { 712 $body['avatar_url'] = $profile['avatar_url']; 713 } 714 715 return $this->sendStateEvent($roomId, 'm.room.member', $body, $userId, $timestamp); 716 } 717 718 /** 719 * Perform POST /rooms/$room_id/ban 720 * 721 * @param string $roomId The room ID 722 * @param string $userId The user ID of the banee(sic) 723 * @param string $reason The reason for this ban 724 * @return array|string 725 * @throws MatrixException 726 * @throws MatrixHttpLibException 727 * @throws MatrixRequestException 728 */ 729 public function banUser(string $roomId, string $userId, string $reason = '') { 730 $body = [ 731 'user_id' => $userId, 732 'reason' => $reason, 733 ]; 734 735 return $this->send('POST', sprintf('/rooms/%s/ban', urlencode($roomId)), $body); 736 } 737 738 /** 739 * Perform POST /rooms/$room_id/unban 740 * 741 * @param string $roomId The room ID 742 * @param string $userId The user ID of the banee(sic) 743 * @return array|string 744 * @throws MatrixException 745 * @throws MatrixHttpLibException 746 * @throws MatrixRequestException 747 */ 748 public function unbanUser(string $roomId, string $userId) { 749 $body = [ 750 'user_id' => $userId, 751 ]; 752 753 return $this->send('POST', sprintf('/rooms/%s/unban', urlencode($roomId)), $body); 754 } 755 756 /** 757 * @param string $userId 758 * @param string $roomId 759 * @return array|string 760 * @throws MatrixException 761 * @throws MatrixHttpLibException 762 * @throws MatrixRequestException 763 */ 764 public function getUserTags(string $userId, string $roomId) { 765 $path = sprintf('/user/%s/rooms/%s/tags', urlencode($userId), urlencode($roomId)); 766 767 return $this->send('GET', $path); 768 } 769 770 /** 771 * @param string $userId 772 * @param string $roomId 773 * @param string $tag 774 * @return array|string 775 * @throws MatrixException 776 * @throws MatrixHttpLibException 777 * @throws MatrixRequestException 778 */ 779 public function removeUserTag(string $userId, string $roomId, string $tag) { 780 $path = sprintf('/user/%s/rooms/%s/tags/%s', urlencode($userId), urlencode($roomId), urlencode($tag)); 781 782 return $this->send('DELETE', $path); 783 } 784 785 /** 786 * @param string $userId 787 * @param string $roomId 788 * @param string $tag 789 * @param float|null $order 790 * @param array $body 791 * @return array|string 792 * @throws MatrixException 793 * @throws MatrixHttpLibException 794 * @throws MatrixRequestException 795 */ 796 public function addUserTag(string $userId, string $roomId, string $tag, ?float $order = null, array $body = []) { 797 if ($order) { 798 $body['order'] = $order; 799 } 800 $path = sprintf('/user/%s/rooms/%s/tags/%s', urlencode($userId), urlencode($roomId), urlencode($tag)); 801 802 return $this->send('PUT', $path, $body); 803 } 804 805 /** 806 * @param string $userId 807 * @param string $type 808 * @param array $accountData 809 * @return array|string 810 * @throws MatrixException 811 * @throws MatrixHttpLibException 812 * @throws MatrixRequestException 813 */ 814 public function setAccountData(string $userId, string $type, array $accountData) { 815 $path = sprintf("/user/%s/account_data/%s", urlencode($userId), urlencode($type)); 816 817 return $this->send('PUT', $path, $accountData); 818 } 819 820 /** 821 * @param string $userId 822 * @param string $roomId 823 * @param string $type 824 * @param array $accountData 825 * @return array|string 826 * @throws MatrixException 827 * @throws MatrixHttpLibException 828 * @throws MatrixRequestException 829 */ 830 public function setRoomAccountData(string $userId, string $roomId, string $type, array $accountData) { 831 $path = sprintf( 832 '/user/%s/rooms/%s/account_data/%s', 833 urlencode($userId), urlencode($roomId), urlencode($type) 834 ); 835 836 return $this->send('PUT', $path, $accountData); 837 } 838 839 /** 840 * Perform GET /rooms/$room_id/state 841 * 842 * @param string $roomId The room ID 843 * @return array|string 844 * @throws MatrixException 845 * @throws MatrixHttpLibException 846 * @throws MatrixRequestException 847 */ 848 public function getRoomState(string $roomId) { 849 return $this->send('GET', sprintf('/rooms/%s/state', urlencode($roomId))); 850 } 851 852 public function getTextBody(string $textContent, string $msgType = 'm.text'): array { 853 return [ 854 'msgtype' => $msgType, 855 'body' => $textContent, 856 ]; 857 } 858 859 private function getEmoteBody(string $textContent): array { 860 return $this->getTextBody($textContent, 'm.emote'); 861 } 862 863 /** 864 * @param string $userId 865 * @param string $filterId 866 * @return array|string 867 * @throws MatrixException 868 * @throws MatrixHttpLibException 869 * @throws MatrixRequestException 870 */ 871 public function getFilter(string $userId, string $filterId) { 872 $path = sprintf("/user/%s/filter/%s", urlencode($userId), urlencode($filterId)); 873 874 return $this->send('GET', $path); 875 } 876 877 /** 878 * @param string $userId 879 * @param array $filterParams 880 * @return array|string 881 * @throws MatrixException 882 * @throws MatrixHttpLibException 883 * @throws MatrixRequestException 884 */ 885 public function createFilter(string $userId, array $filterParams) { 886 $path = sprintf("/user/%s/filter", urlencode($userId)); 887 888 return $this->send('POST', $path, $filterParams); 889 } 890 891 /** 892 * @param string $method 893 * @param string $path 894 * @param mixed $content 895 * @param array $queryParams 896 * @param array $headers 897 * @param string $apiPath 898 * @param bool $returnJson 899 * @return array|string 900 * @throws MatrixException 901 * @throws MatrixRequestException 902 * @throws MatrixHttpLibException 903 */ 904 private function send(string $method, string $path, $content = null, array $queryParams = [], array $headers = [], 905 $apiPath = self::MATRIX_V2_API_PATH, $returnJson = true) { 906 $options = []; 907 if (!in_array('User-Agent', $headers)) { 908 $headers['User-Agent'] = 'php-matrix-sdk/' . self::VERSION; 909 } 910 911 $method = strtoupper($method); 912 if (!in_array($method, ['GET', 'POST', 'PUT', 'DELETE'])) { 913 throw new MatrixException("Unsupported HTTP method: $method"); 914 } 915 916 if (!in_array('Content-Type', $headers)) { 917 $headers['Content-Type'] = 'application/json'; 918 } 919 920 if ($this->useAuthorizationHeader) { 921 $headers['Authorization'] = sprintf('Bearer %s', $this->token); 922 } else { 923 $queryParams['access_token'] = $this->token; 924 } 925 926 if ($this->identity) { 927 $queryParams['user_id'] = $this->identity; 928 } 929 930 $options = array_merge($options, [ 931 RequestOptions::HEADERS => $headers, 932 RequestOptions::QUERY => $queryParams, 933 RequestOptions::VERIFY => $this->validateCert, 934 RequestOptions::HTTP_ERRORS => FALSE, 935 ]); 936 937 $endpoint = $this->baseUrl . $apiPath . $path; 938 if ($headers['Content-Type'] == "application/json" && $content !== null) { 939 $options[RequestOptions::JSON] = $content; 940 } 941 else { 942 $options[RequestOptions::FORM_PARAMS] = $content; 943 } 944 945 $responseBody = ''; 946 while (true) { 947 try { 948 $response = $this->client->request($method, $endpoint, $options); 949 } catch (GuzzleException $e) { 950 throw new MatrixHttpLibException($e, $method, $endpoint); 951 } 952 953 $responseBody = $response->getBody()->getContents(); 954 955 if ($response->getStatusCode() >= 500) { 956 throw new MatrixUnexpectedResponse($responseBody); 957 } 958 959 if ($response->getStatusCode() != 429) { 960 break; 961 } 962 963 $jsonResponse = json_decode($responseBody, true); 964 $waitTime = array_get($jsonResponse, 'retry_after_ms'); 965 $waitTime = $waitTime ?: array_get($jsonResponse, 'error.retry_after_ms', $this->default429WaitMs); 966 $waitTime /= 1000; 967 sleep($waitTime); 968 } 969 970 if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { 971 throw new MatrixRequestException($response->getStatusCode(), $responseBody); 972 } 973 974 return $returnJson ? json_decode($responseBody, true) : $responseBody; 975 } 976 977 /** 978 * @param $content 979 * @param string $contentType 980 * @param string|null $filename 981 * @return array|string 982 * @throws MatrixException 983 * @throws MatrixHttpLibException 984 * @throws MatrixRequestException 985 */ 986 public function mediaUpload($content, string $contentType, string $filename = null) { 987 $headers = ['Content-Type' => $contentType]; 988 $apiPath = self::MATRIX_V2_MEDIA_PATH . "/upload"; 989 $queryParam = []; 990 if ($filename) { 991 $queryParam['filename'] = $filename; 992 } 993 994 return $this->send('POST', '', $content, $queryParam, $headers, $apiPath); 995 } 996 997 /** 998 * @param string $userId 999 * @return string|null 1000 * @throws MatrixException 1001 * @throws MatrixHttpLibException 1002 * @throws MatrixRequestException 1003 */ 1004 public function getDisplayName(string $userId): ?string { 1005 $content = $this->send("GET", "/profile/$userId/displayname"); 1006 1007 return array_get($content, 'displayname'); 1008 } 1009 1010 /** 1011 * @param string $userId 1012 * @param string $displayName 1013 * @return array|string 1014 * @throws MatrixException 1015 * @throws MatrixHttpLibException 1016 * @throws MatrixRequestException 1017 */ 1018 public function setDisplayName(string $userId, string $displayName) { 1019 $content = ['displayname' => $displayName]; 1020 $path = sprintf('/profile/%s/displayname', urlencode($userId)); 1021 1022 return $this->send('PUT', $path, $content); 1023 } 1024 1025 /** 1026 * @param string $userId 1027 * @return mixed 1028 * @throws MatrixException 1029 * @throws MatrixHttpLibException 1030 * @throws MatrixRequestException 1031 */ 1032 public function getAvatarUrl(string $userId): ?string { 1033 $content = $this->send("GET", "/profile/$userId/avatar_url"); 1034 1035 return array_get($content, 'avatar_url'); 1036 } 1037 1038 /** 1039 * @param string $userId 1040 * @param string $avatarUrl 1041 * @return array|string 1042 * @throws MatrixException 1043 * @throws MatrixHttpLibException 1044 * @throws MatrixRequestException 1045 */ 1046 public function setAvatarUrl(string $userId, string $avatarUrl) { 1047 $content = ['avatar_url' => $avatarUrl]; 1048 $path = sprintf('/profile/%s/avatar_url', urlencode($userId)); 1049 1050 return $this->send('PUT', $path, $content); 1051 } 1052 1053 /** 1054 * @param string $mxcurl 1055 * @return string 1056 * @throws ValidationException 1057 */ 1058 public function getDownloadUrl(string $mxcurl): string { 1059 Util::checkMxcUrl($mxcurl); 1060 1061 return $this->baseUrl . self::MATRIX_V2_MEDIA_PATH . "/download/" . substr($mxcurl, 6); 1062 } 1063 1064 /** 1065 * Download raw media from provided mxc URL. 1066 * 1067 * @param string $mxcurl mxc media URL. 1068 * @param bool $allowRemote Indicates to the server that it should not 1069 * attempt to fetch the media if it is deemed remote. Defaults 1070 * to true if not provided. 1071 * @return string 1072 * @throws MatrixException 1073 * @throws MatrixHttpLibException 1074 * @throws MatrixRequestException 1075 * @throws ValidationException 1076 */ 1077 public function mediaDownload(string $mxcurl, bool $allowRemote = true) { 1078 Util::checkMxcUrl($mxcurl); 1079 $queryParam = []; 1080 if (!$allowRemote) { 1081 $queryParam["allow_remote"] = false; 1082 } 1083 $path = substr($mxcurl, 6); 1084 $apiPath = self::MATRIX_V2_MEDIA_PATH . "/download/"; 1085 1086 return $this->send('GET', $path, null, $queryParam, [], $apiPath, false); 1087 } 1088 1089 /** 1090 * Download raw media thumbnail from provided mxc URL. 1091 * 1092 * @param string $mxcurl mxc media URL 1093 * @param int $width desired thumbnail width 1094 * @param int $height desired thumbnail height 1095 * @param string $method thumb creation method. Must be 1096 * in ['scale', 'crop']. Default 'scale'. 1097 * @param bool $allowRemote indicates to the server that it should not 1098 * attempt to fetch the media if it is deemed remote. Defaults 1099 * to true if not provided. 1100 * @return array|string 1101 * @throws MatrixException 1102 * @throws MatrixHttpLibException 1103 * @throws MatrixRequestException 1104 * @throws ValidationException 1105 */ 1106 public function getThumbnail(string $mxcurl, int $width, int $height, 1107 string $method = 'scale', bool $allowRemote = true) { 1108 Util::checkMxcUrl($mxcurl); 1109 if (!in_array($method, ['scale', 'crop'])) { 1110 throw new ValidationException('Unsupported thumb method ' . $method); 1111 } 1112 $queryParams = [ 1113 "width" => $width, 1114 "height" => $height, 1115 "method" => $method, 1116 ]; 1117 if (!$allowRemote) { 1118 $queryParams["allow_remote"] = false; 1119 } 1120 $path = substr($mxcurl, 6); 1121 $apiPath = self::MATRIX_V2_MEDIA_PATH . "/thumbnail/"; 1122 1123 1124 return $this->send('GET', $path, null, $queryParams, [], $apiPath, false); 1125 } 1126 1127 /** 1128 * Get preview for URL. 1129 * 1130 * @param string $url URL to get a preview 1131 * @param float|null $ts The preferred point in time to return 1132 * a preview for. The server may return a newer 1133 * version if it does not have the requested 1134 * version available. 1135 * @return array|string 1136 * @throws MatrixException 1137 * @throws MatrixHttpLibException 1138 * @throws MatrixRequestException 1139 */ 1140 public function getUrlPreview(string $url, float $ts = null) { 1141 $params = ['url' => $url]; 1142 if ($ts) { 1143 $params['ts'] = $ts; 1144 } 1145 $apiPath = self::MATRIX_V2_MEDIA_PATH . '/preview_url'; 1146 1147 return $this->send('GET', '', null, $params, [], $apiPath); 1148 } 1149 1150 /** 1151 * Get room id from its alias. 1152 * 1153 * @param string $roomAlias The room alias name. 1154 * @return null|string Wanted room's id. 1155 * @throws MatrixException 1156 * @throws MatrixHttpLibException 1157 * @throws MatrixRequestException 1158 */ 1159 public function getRoomId(string $roomAlias): ?string { 1160 $content = $this->send('GET', sprintf("/directory/room/%s", urlencode($roomAlias))); 1161 1162 return array_get($content, 'room_id'); 1163 } 1164 1165 /** 1166 * Set alias to room id 1167 * 1168 * @param string $roomId The room id. 1169 * @param string $roomAlias The room wanted alias name. 1170 * @return array|string 1171 * @throws MatrixException 1172 * @throws MatrixHttpLibException 1173 * @throws MatrixRequestException 1174 */ 1175 public function setRoomAlias(string $roomId, string $roomAlias) { 1176 $content = ['room_id' => $roomId]; 1177 1178 return $this->send('PUT', sprintf("/directory/room/%s", urlencode($roomAlias)), $content); 1179 } 1180 1181 /** 1182 * Remove mapping of an alias 1183 * 1184 * @param string $roomAlias The alias to be removed. 1185 * @return array|string 1186 * @throws MatrixException 1187 * @throws MatrixHttpLibException 1188 * @throws MatrixRequestException 1189 */ 1190 public function removeRoomAlias(string $roomAlias) { 1191 return $this->send('DELETE', sprintf("/directory/room/%s", urlencode($roomAlias))); 1192 } 1193 1194 /** 1195 * Get the list of members for this room. 1196 * 1197 * @param string $roomId The room to get the member events for. 1198 * @return array|string 1199 * @throws MatrixException 1200 * @throws MatrixHttpLibException 1201 * @throws MatrixRequestException 1202 */ 1203 public function getRoomMembers(string $roomId) { 1204 return $this->send('GET', sprintf("/rooms/%s/members", urlencode($roomId))); 1205 } 1206 1207 /** 1208 * Set the rule for users wishing to join the room. 1209 * 1210 * @param string $roomId The room to set the rules for. 1211 * @param string $joinRule The chosen rule. One of: ["public", "knock", "invite", "private"] 1212 * @return array|string 1213 * @throws MatrixException 1214 */ 1215 public function setJoinRule(string $roomId, string $joinRule) { 1216 $content = ['join_rule' => $joinRule]; 1217 1218 return $this->sendStateEvent($roomId, 'm.room.join_rule', $content); 1219 } 1220 1221 /** 1222 * Set the guest access policy of the room. 1223 * 1224 * @param string $roomId The room to set the rules for. 1225 * @param string $guestAccess Wether guests can join. One of: ["can_join", "forbidden"] 1226 * @return array|string 1227 * @throws MatrixException 1228 */ 1229 public function setGuestAccess(string $roomId, string $guestAccess) { 1230 $content = ['guest_access' => $guestAccess]; 1231 1232 return $this->sendStateEvent($roomId, 'm.room.guest_access', $content); 1233 } 1234 1235 /** 1236 * Gets information about all devices for the current user. 1237 * 1238 * @return array|string 1239 * @throws MatrixException 1240 * @throws MatrixHttpLibException 1241 * @throws MatrixRequestException 1242 */ 1243 public function getDevices() { 1244 return $this->send('GET', '/devices'); 1245 } 1246 1247 /** 1248 * Gets information on a single device, by device id. 1249 * 1250 * @param string $deviceId 1251 * @return array|string 1252 * @throws MatrixException 1253 * @throws MatrixHttpLibException 1254 * @throws MatrixRequestException 1255 */ 1256 public function getDevice(string $deviceId) { 1257 return $this->send('GET', sprintf('/devices/%s', urlencode($deviceId))); 1258 } 1259 1260 /** 1261 * Update the display name of a device. 1262 * 1263 * @param string $deviceId The device ID of the device to update. 1264 * @param string $displayName New display name for the device. 1265 * @return array|string 1266 * @throws MatrixException 1267 * @throws MatrixHttpLibException 1268 * @throws MatrixRequestException 1269 */ 1270 public function updateDeviceInfo(string $deviceId, string $displayName) { 1271 $content = ['display_name' => $displayName]; 1272 1273 return $this->send('PUT', sprintf('/devices/%s', urlencode($deviceId)), $content); 1274 } 1275 1276 /** 1277 * Deletes the given device, and invalidates any access token associated with it. 1278 * 1279 * NOTE: This endpoint uses the User-Interactive Authentication API. 1280 * 1281 * @param array $authBody Authentication params. 1282 * @param string $deviceId The device ID of the device to delete. 1283 * @return array|string 1284 * @throws MatrixException 1285 * @throws MatrixHttpLibException 1286 * @throws MatrixRequestException 1287 */ 1288 public function deleteDevice(array $authBody, string $deviceId) { 1289 $content = ['auth' => $authBody]; 1290 1291 return $this->send('DELETE', sprintf('/devices/%s', urlencode($deviceId)), $content); 1292 } 1293 1294 /** 1295 * Bulk deletion of devices. 1296 * 1297 * NOTE: This endpoint uses the User-Interactive Authentication API. 1298 * 1299 * @param array $authBody Authentication params. 1300 * @param array $devices List of device ID"s to delete. 1301 * @return array|string 1302 * @throws MatrixException 1303 * @throws MatrixHttpLibException 1304 * @throws MatrixRequestException 1305 */ 1306 public function deleteDevices($authBody, $devices) { 1307 $content = [ 1308 'auth' => $authBody, 1309 'devices' => $devices 1310 ]; 1311 1312 return $this->send('POST', '/delete_devices', $content); 1313 } 1314 1315 /** 1316 * Publishes end-to-end encryption keys for the device. 1317 * Said device must be the one used when logging in. 1318 * 1319 * @param array $deviceKeys Optional. Identity keys for the device. The required keys are: 1320 * | user_id (str): The ID of the user the device belongs to. Must match the user ID used when logging in. 1321 * | device_id (str): The ID of the device these keys belong to. Must match the device ID used when logging in. 1322 * | algorithms (list<str>): The encryption algorithms supported by this device. 1323 * | keys (dict): Public identity keys. Should be formatted as <algorithm:device_id>: <key>. 1324 * | signatures (dict): Signatures for the device key object. Should be formatted as <user_id>: {<algorithm:device_id>: <key>} 1325 * @param array $oneTimeKeys Optional. One-time public keys. Should be 1326 * formatted as <algorithm:key_id>: <key>, the key format being 1327 * determined by the algorithm. 1328 * @return array|string 1329 * @throws MatrixException 1330 * @throws MatrixHttpLibException 1331 * @throws MatrixRequestException 1332 */ 1333 public function uploadKeys(array $deviceKeys = [], array $oneTimeKeys = []) { 1334 $content = []; 1335 if ($deviceKeys) { 1336 $content['device_keys'] = $deviceKeys; 1337 } 1338 if ($oneTimeKeys) { 1339 $content['one_time_keys'] = $oneTimeKeys; 1340 } 1341 1342 return $this->send('POST', '/keys/upload', $content ?: null); 1343 } 1344 1345 /** 1346 * Query HS for public keys by user and optionally device. 1347 * 1348 * @param array $userDevices The devices whose keys to download. Should be 1349 * formatted as <user_id>: [<device_ids>]. No device_ids indicates 1350 * all devices for the corresponding user. 1351 * @param int $timeout Optional. The time (in milliseconds) to wait when 1352 * downloading keys from remote servers. 1353 * @param string $token Optional. If the client is fetching keys as a result of 1354 * a device update received in a sync request, this should be the 1355 * 'since' token of that sync request, or any later sync token. 1356 * @return array|string 1357 * @throws MatrixException 1358 * @throws MatrixHttpLibException 1359 * @throws MatrixRequestException 1360 */ 1361 public function queryKeys(array $userDevices, int $timeout = null, string $token = null) { 1362 $content = ['device_keys' => $userDevices]; 1363 if ($timeout) { 1364 $content['timeout'] = $timeout; 1365 } 1366 if ($token) { 1367 $content['token'] = $token; 1368 } 1369 1370 return $this->send('POST', "/keys/query", $content); 1371 } 1372 1373 /** 1374 * Claims one-time keys for use in pre-key messages. 1375 * 1376 * @param array $keyRequest The keys to be claimed. Format should be <user_id>: { <device_id>: <algorithm> }. 1377 * @param int $timeout Optional. The time (in ms) to wait when downloading keys from remote servers. 1378 * @return array|string 1379 * @throws MatrixException 1380 * @throws MatrixHttpLibException 1381 * @throws MatrixRequestException 1382 */ 1383 public function claimKeys(array $keyRequest, int $timeout) { 1384 $content = ['one_time_keys' => $keyRequest]; 1385 if ($timeout) { 1386 $content['timeout'] = $timeout; 1387 } 1388 1389 return $this->send('POST', "/keys/claim", $content); 1390 } 1391 1392 /** 1393 * Gets a list of users who have updated their device identity keys. 1394 * 1395 * @param string $fromToken The desired start point of the list. Should be the 1396 * next_batch field from a response to an earlier call to /sync. 1397 * @param string $toToken The desired end point of the list. Should be the next_batch 1398 * field from a recent call to /sync - typically the most recent such call. 1399 * @return array|string 1400 * @throws MatrixException 1401 * @throws MatrixHttpLibException 1402 * @throws MatrixRequestException 1403 */ 1404 public function keyChanges(string $fromToken, string $toToken) { 1405 $params = [ 1406 'from' => $fromToken, 1407 'to' => $toToken, 1408 ]; 1409 1410 return $this->send("GET", "/keys/changes", null, $params); 1411 } 1412 1413 /** 1414 * Sends send-to-device events to a set of client devices. 1415 * 1416 * @param string $eventType The type of event to send. 1417 * @param array $messages The messages to send. Format should be 1418 * <user_id>: {<device_id>: <event_content>}. 1419 * The device ID may also be '*', meaning all known devices for the user. 1420 * @param string|null $txnId Optional. The transaction ID for this event, will be generated automatically otherwise. 1421 * @return array|string 1422 * @throws MatrixException 1423 * @throws MatrixHttpLibException 1424 * @throws MatrixRequestException 1425 */ 1426 public function sendToDevice(string $eventType, array $messages, string $txnId = null) { 1427 $txnId = $txnId ?: $this->makeTxnId(); 1428 $content = ['messages' => $messages]; 1429 $path = sprintf("/sendToDevice/%s/%s", urlencode($eventType), urlencode($txnId)); 1430 1431 return $this->send('PUT', $path, $content); 1432 } 1433 1434 private function makeTxnId(): int { 1435 $txnId = $this->txnId . (int)(microtime(true) * 1000); 1436 $this->txnId++; 1437 1438 return $txnId; 1439 } 1440 1441 /** 1442 * Determine user_id for authenticated user. 1443 * 1444 * @return array 1445 * @throws MatrixException 1446 * @throws MatrixHttpLibException 1447 * @throws MatrixRequestException 1448 */ 1449 public function whoami(): array { 1450 if (!$this->token) { 1451 throw new MatrixException('Authentication required.'); 1452 } 1453 1454 return $this->send('GET', '/account/whoami'); 1455 } 1456 1457 public function setToken(?string $token) { 1458 $this->token = $token; 1459 } 1460 1461 1462} 1463