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