mantis-matrix-integration – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 office 1 <?php
2  
3 namespace MatrixPhp;
4  
5 use MatrixPhp\Crypto\OlmDevice;
6 use MatrixPhp\Exceptions\MatrixRequestException;
7 use MatrixPhp\Exceptions\MatrixUnexpectedResponse;
8 use MatrixPhp\Exceptions\ValidationException;
9 use phpDocumentor\Reflection\Types\Callable_;
10  
11 //TODO: port OLM bindings
12 define('ENCRYPTION_SUPPORT', false);
13  
14 /**
15 * The client API for Matrix. For the raw HTTP calls, see MatrixHttpApi.
16 *
17 * Examples:
18 *
19 * Create a new user and send a message::
20 *
21 * $client = new MatrixClient("https://matrix.org");
22 * $token = $client->registerWithPassword($username="foobar", $password="monkey");
23 * $room = $client->createRoom("myroom");
24 * $room->sendImage($fileLikeObject);
25 *
26 * Send a message with an already logged in user::
27 *
28 * $client = new MatrixClient("https://matrix.org", $token="foobar", $userId="@foobar:matrix.org");
29 * $client->addListener(func); // NB: event stream callback
30 * $client->rooms[0]->addListener(func); // NB: callbacks just for this room.
31 * $room = $client->joinRoom("#matrix:matrix.org");
32 * $response = $room->sendText("Hello!");
33 * $response = $room->kick("@bob:matrix.org");
34 *
35 * Incoming event callbacks (scopes)::
36 *
37 * function userCallback($user, $incomingEvent);
38 *
39 * function $roomCallback($room, $incomingEvent);
40 *
41 * function globalCallback($incoming_event);
42 *
43 * @package MatrixPhp
44 */
45 class MatrixClient {
46  
47  
48 /**
49 * @var int
50 */
51 protected $cacheLevel;
52  
53 /**
54 * @var bool
55 */
56 protected $encryption;
57  
58 /**
59 * @var MatrixHttpApi
60 */
61 protected $api;
62 /**
63 * @var array
64 */
65 protected $listeners = [];
66 protected $presenceListeners = [];
67 protected $inviteListeners = [];
68 protected $leftListeners = [];
69 protected $ephemeralListeners = [];
70 protected $deviceId;
71 /**
72 * @var OlmDevice
73 */
74 protected $olmDevice;
75 protected $syncToken;
76 protected $syncFilter;
77 protected $syncThread;
78 protected $shouldListen = false;
79 /**
80 * @var int Time to wait before attempting a /sync request after failing.
81 */
82 protected $badSyncTimeoutLimit = 3600;
83 protected $rooms = [];
84 /**
85 * @var array A map from user ID to `User` object.
86 * It is populated automatically while tracking the membership in rooms, and
87 * shouldn't be modified directly.
88 * A `User` object in this array is shared between all `Room`
89 * objects where the corresponding user is joined.
90 */
91 public $users = [];
92 protected $userId;
93 protected $token;
94 protected $hs;
95  
96 /**
97 * MatrixClient constructor.
98 * @param string $baseUrl The url of the HS preceding /_matrix. e.g. (ex: https://localhost:8008 )
99 * @param string|null $token If you have an access token supply it here.
100 * @param bool $validCertCheck Check the homeservers certificate on connections?
101 * @param int $syncFilterLimit
102 * @param int $cacheLevel One of Cache::NONE, Cache::SOME, or Cache::ALL
103 * @param bool $encryption Optional. Whether or not to enable end-to-end encryption support
104 * @param array $encryptionConf Optional. Configuration parameters for encryption.
105 * @throws Exceptions\MatrixException
106 * @throws Exceptions\MatrixHttpLibException
107 * @throws Exceptions\MatrixRequestException
108 * @throws ValidationException
109 */
110 public function __construct(
111 string $baseUrl,
112 ?string $token = null,
113 bool $validCertCheck = true,
114 int $syncFilterLimit = 20,
115 int $cacheLevel = Cache::ALL,
116 $encryption = false,
117 protected $encryptionConf = [],
118 ) {
119 // @phpstan-ignore-next-line
120 if ($encryption && ENCRYPTION_SUPPORT) {
121 throw new ValidationException('Failed to enable encryption. Please make sure the olm library is available.');
122 }
123  
124 $this->api = new MatrixHttpApi($baseUrl, $token);
125 $this->api->validateCertificate($validCertCheck);
126 $this->encryption = $encryption;
127 if (!in_array($cacheLevel, Cache::$levels)) {
128 throw new ValidationException('$cacheLevel must be one of Cache::NONE, Cache::SOME, Cache::ALL');
129 }
130 $this->cacheLevel = $cacheLevel;
131 $this->syncFilter = sprintf('{ "room": { "timeline" : { "limit" : %d } } }', $syncFilterLimit);
132 if ($token) {
133 $response = $this->api->whoami();
134 $this->userId = $response['user_id'];
135 $this->sync();
136 }
137 }
138  
139 /**
140 * Register a guest account on this HS.
141 *
142 * Note: HS must have guest registration enabled.
143 *
144 * @return string|null Access Token
145 * @throws Exceptions\MatrixException
146 */
147 public function registerAsGuest(): ?string {
148 $response = $this->api->register([], 'guest');
149  
150 return $this->postRegistration($response);
151 }
152  
153 /**
154 * Register for a new account on this HS.
155 *
156 * @param string $username Account username
157 * @param string $password Account password
158 * @return string|null Access Token
159 * @throws Exceptions\MatrixException
160 */
161 public function registerWithPassword(string $username, string $password): ?string {
162 $auth = ['type' => 'm.login.dummy'];
163 $response = $this->api->register($auth, 'user', false, $username, $password);
164  
165 return $this->postRegistration($response);
166 }
167  
168 protected function postRegistration(array $response) {
169 $this->userId = array_get($response, 'user_id');
170 $this->token = array_get($response, 'access_token');
171 $this->hs = array_get($response, 'home_server');
172 $this->api->setToken($this->token);
173 $this->sync();
174  
175 return $this->token;
176 }
177  
178 public function login(string $username, string $password, bool $sync = true,
179 int $limit = 10, ?string $deviceId = null): ?string {
180 $response = $this->api->login('m.login.password', [
181 'identifier' => [
182 'type' => 'm.id.user',
183 'user' => $username,
184 ],
185 'user' => $username,
186 'password' => $password,
187 'device_id' => $deviceId
188 ]);
189  
190 return $this->finalizeLogin($response, $sync, $limit);
191 }
192  
193 /**
194 * Log in with a JWT.
195 *
196 * @param string $token JWT token.
197 * @param bool $refreshToken Whether to request a refresh token.
198 * @param bool $sync Indicator whether to sync.
199 * @param int $limit Sync limit.
200 *
201 * @return string Access token.
202 *
203 * @throws \MatrixPhp\Exceptions\MatrixException
204 */
205 public function jwtLogin(string $token, bool $refreshToken = false, bool $sync = true, int $limit = 10): ?string {
206 $response = $this->api->login(
207 'org.matrix.login.jwt',
208 [
209 'token' => $token,
210 'refresh_token' => $refreshToken,
211 ]
212 );
213  
214 return $this->finalizeLogin($response, $sync, $limit);
215 }
216  
217 /**
218 * Finalize login, e.g. after password or JWT login.
219 *
220 * @param array $response Login response array.
221 * @param bool $sync Sync flag.
222 * @param int $limit Sync limit.
223 *
224 * @return string Access token.
225 *
226 * @throws \MatrixPhp\Exceptions\MatrixException
227 * @throws \MatrixPhp\Exceptions\MatrixRequestException
228 */
229 protected function finalizeLogin(array $response, bool $sync, int $limit): string {
230 $this->userId = array_get($response, 'user_id');
231 $this->token = array_get($response, 'access_token');
232 $this->hs = array_get($response, 'home_server');
233 $this->api->setToken($this->token);
234 $this->deviceId = array_get($response, 'device_id');
235  
236 if ($this->encryption) {
237 $this->olmDevice = new OlmDevice($this->api, $this->userId, $this->deviceId, $this->encryptionConf);
238 $this->olmDevice->uploadIdentityKeys();
239 $this->olmDevice->uploadOneTimeKeys();
240 }
241  
242 if ($sync) {
243 $this->syncFilter = sprintf('{ "room": { "timeline" : { "limit" : %d } } }', $limit);
244 $this->sync();
245 }
246  
247 return $this->token;
248 }
249  
250 /**
251 * Logout from the homeserver.
252 *
253 * @throws Exceptions\MatrixException
254 */
255 public function logout() {
256 $this->stopListenerThread();
257 $this->api->logout();
258 }
259  
260 /**
261 * Create a new room on the homeserver.
262 * TODO: move room creation/joining to User class for future application service usage
263 * NOTE: we may want to leave thin wrappers here for convenience
264 *
265 * @param string|null $alias The canonical_alias of the room.
266 * @param bool $isPublic The public/private visibility of the room.
267 * @param array $invitees A set of user ids to invite into the room.
268 * @return Room
269 * @throws Exceptions\MatrixException
270 */
271 public function createRoom(?string $alias = null, bool $isPublic = false, array $invitees = []): Room {
272 $response = $this->api->createRoom($alias, null, $isPublic, $invitees);
273  
274 return $this->mkRoom($response['room_id']);
275 }
276  
277 /**
278 * Join a room.
279 *
280 * @param string $roomIdOrAlias Room ID or an alias.
281 * @return Room
282 * @throws Exceptions\MatrixException
283 */
284 public function joinRoom(string $roomIdOrAlias): Room {
285 $response = $this->api->joinRoom($roomIdOrAlias);
286 $roomId = array_get($response, 'room_id', $roomIdOrAlias);
287  
288 return $this->mkRoom($roomId);
289 }
290  
291 public function getRooms(): array {
292 return $this->rooms;
293 }
294  
295 /**
296 * Add a listener that will send a callback when the client recieves an event.
297 *
298 * @param callable $callback Callback called when an event arrives.
299 * @param string $eventType The event_type to filter for.
300 * @return string Unique id of the listener, can be used to identify the listener.
301 */
302 public function addListener(callable $callback, string $eventType) {
303 $listenerId = uniqid();
304 $this->listeners[] = [
305 'uid' => $listenerId,
306 'callback' => $callback,
307 'event_type' => $eventType,
308 ];
309  
310 return $listenerId;
311 }
312  
313 /**
314 * Remove listener with given uid.
315 *
316 * @param string $uid Unique id of the listener to remove.
317 */
318 public function removeListener(string $uid) {
319 $this->listeners = array_filter($this->listeners, function (array $a) use ($uid) {
320 return $a['uid'] != $uid;
321 });
322 }
323  
324 /**
325 * Add a presence listener that will send a callback when the client receives a presence update.
326 *
327 * @param callable $callback Callback called when a presence update arrives.
328 * @return string Unique id of the listener, can be used to identify the listener.
329 */
330 public function addPresenceListener(callable $callback) {
331 $listenerId = uniqid();
332 $this->presenceListeners[$listenerId] = $callback;
333  
334 return $listenerId;
335 }
336  
337 /**
338 * Remove presence listener with given uid
339 *
340 * @param string $uid Unique id of the listener to remove
341 */
342 public function removePresenceListener(string $uid) {
343 unset($this->presenceListeners[$uid]);
344 }
345  
346 /**
347 * Add an ephemeral listener that will send a callback when the client recieves an ephemeral event.
348 *
349 * @param callable $callback Callback called when an ephemeral event arrives.
350 * @param string|null $eventType Optional. The event_type to filter for.
351 * @return string Unique id of the listener, can be used to identify the listener.
352 */
353 public function addEphemeralListener(callable $callback, ?string $eventType = null) {
354 $listenerId = uniqid();
355 $this->ephemeralListeners[] = [
356 'uid' => $listenerId,
357 'callback' => $callback,
358 'event_type' => $eventType,
359 ];
360  
361 return $listenerId;
362 }
363  
364 /**
365 * Remove ephemeral listener with given uid.
366 *
367 * @param string $uid Unique id of the listener to remove.
368 */
369 public function removeEphemeralListener(string $uid) {
370 $this->ephemeralListeners = array_filter($this->ephemeralListeners, function (array $a) use ($uid) {
371 return $a['uid'] != $uid;
372 });
373 }
374  
375 /**
376 * Add a listener that will send a callback when the client receives an invite.
377 * @param callable $callback Callback called when an invite arrives.
378 */
379 public function addInviteListener(callable $callback) {
380 $this->inviteListeners[] = $callback;
381 }
382  
383 /**
384 * Add a listener that will send a callback when the client has left a room.
385 *
386 * @param callable $callback Callback called when the client has left a room.
387 */
388 public function addLeaveListener(callable $callback) {
389 $this->leftListeners[] = $callback;
390 }
391  
392 public function listenForever(int $timeoutMs = 30000, ?callable $exceptionHandler = null, int $badSyncTimeout = 5) {
393 $tempBadSyncTimeout = $badSyncTimeout;
394 $this->shouldListen = true;
395 // @phpstan-ignore-next-line
396 while ($this->shouldListen) {
397 try {
398 $this->sync($timeoutMs);
399 $tempBadSyncTimeout = $badSyncTimeout;
400 } catch (MatrixRequestException $e) {
401 // TODO: log error
402 if ($e->getHttpCode() >= 500) {
403 sleep($badSyncTimeout);
404 $tempBadSyncTimeout = min($tempBadSyncTimeout * 2, $this->badSyncTimeoutLimit);
405 } elseif (is_callable($exceptionHandler)) {
406 $exceptionHandler($e);
407 } else {
408 throw $e;
409 }
410 } catch (\Exception $e) {
411 if (is_callable($exceptionHandler)) {
412 $exceptionHandler($e);
413 } else {
414 throw $e;
415 }
416 }
417 // TODO: we should also handle MatrixHttpLibException for retry in case no response
418 }
419 }
420  
421 public function startListenerThread(int $timeoutMs = 30000, ?callable $exceptionHandler = null) {
422 // Just no
423 }
424  
425 public function stopListenerThread() {
426 if ($this->syncThread) {
427 $this->shouldListen = false;
428 }
429 }
430  
431 /**
432 * Upload content to the home server and recieve a MXC url.
433 * TODO: move to User class. Consider creating lightweight Media class.
434 *
435 * @param mixed $content The data of the content.
436 * @param string $contentType The mimetype of the content.
437 * @param string|null $filename Optional. Filename of the content.
438 * @return mixed
439 * @throws Exceptions\MatrixException
440 * @throws Exceptions\MatrixHttpLibException
441 * @throws MatrixRequestException If the upload failed for some reason.
442 * @throws MatrixUnexpectedResponse If the homeserver gave a strange response
443 */
444 public function upload($content, string $contentType, ?string $filename = null) {
445 try {
446 $response = $this->api->mediaUpload($content, $contentType, $filename);
447 if (array_key_exists('content_uri', $response)) {
448 return $response['content_uri'];
449 }
450  
451 throw new MatrixUnexpectedResponse('The upload was successful, but content_uri wasn\'t found.');
452 } catch (MatrixRequestException $e) {
453 throw new MatrixRequestException($e->getHttpCode(), 'Upload failed: ' . $e->getMessage());
454 }
455 }
456  
457 /**
458 * @param string $roomId
459 * @return Room
460 * @throws Exceptions\MatrixException
461 * @throws MatrixRequestException
462 */
463 private function mkRoom(string $roomId): Room {
464 $room = new Room($this, $roomId);
465 if ($this->encryption) {
466 try {
467 $event = $this->api->getStateEvent($roomId, "m.room.encryption");
468 if ($event['algorithm'] === "m.megolm.v1.aes-sha2") {
469 $room->enableEncryption();
470 }
471 } catch (MatrixRequestException $e) {
472 if ($e->getHttpCode() != 404) {
473 throw $e;
474 }
475 }
476 }
477 $this->rooms[$roomId] = $room;
478  
479 return $room;
480 }
481  
482 /**
483 * TODO better handling of the blocking I/O caused by update_one_time_key_counts
484 *
485 * @param int $timeoutMs
486 * @throws Exceptions\MatrixException
487 * @throws MatrixRequestException
488 */
489 public function sync(int $timeoutMs = 30000) {
490 $response = $this->api->sync($this->syncToken, $timeoutMs, $this->syncFilter);
491 $this->syncToken = $response['next_batch'];
492  
493 foreach (array_get($response, 'presence.events', []) as $presenceUpdate) {
494 foreach ($this->presenceListeners as $cb) {
495 $cb($presenceUpdate);
496 }
497 }
498 foreach (array_get($response, 'rooms.invite', []) as $roomId => $inviteRoom) {
499 foreach ($this->inviteListeners as $cb) {
500 $cb($roomId, $inviteRoom['invite_state']);
501 }
502 }
503 foreach (array_get($response, 'rooms.leave', []) as $roomId => $leftRoom) {
504 foreach ($this->leftListeners as $cb) {
505 $cb($roomId, $leftRoom);
506 }
507 if (array_key_exists($roomId, $this->rooms)) {
508 unset($this->rooms[$roomId]);
509 }
510 }
511 if ($this->encryption && array_key_exists('device_one_time_keys_count', $response)) {
512 $this->olmDevice->updateOneTimeKeysCounts($response['device_one_time_keys_count']);
513 }
514 foreach (array_get($response, 'rooms.join', []) as $roomId => $syncRoom) {
515 if (!empty($inviteRoom)) {
516 foreach ($this->inviteListeners as $cb) {
517 $cb($roomId, $inviteRoom['invite_state']);
518 }
519 }
520 if (!array_key_exists($roomId, $this->rooms)) {
521 $this->mkRoom($roomId);
522 }
523 $room = $this->rooms[$roomId];
524 // TODO: the rest of this for loop should be in room object method
525 $room->prevBatch = $syncRoom["timeline"]["prev_batch"];
526 foreach (array_get($syncRoom, "state.events", []) as $event) {
527 $event['room_id'] = $roomId;
528 $room->processStateEvent($event);
529 }
530 foreach (array_get($syncRoom, "timeline.events", []) as $event) {
531 $event['room_id'] = $roomId;
532 $room->putEvent($event);
533  
534 // TODO: global listeners can still exist but work by each
535 // $room.listeners[$uuid] having reference to global listener
536  
537 // Dispatch for client (global) listeners
538 foreach ($this->listeners as $listener) {
539 if ($listener['event_type'] == null || $listener['event_type'] == $event['type']) {
540 $listener['callback']($event);
541 }
542 }
543 }
544 foreach (array_get($syncRoom, "ephemeral.events", []) as $event) {
545 $event['room_id'] = $roomId;
546 $room->putEphemeralEvent($event);
547  
548 // Dispatch for client (global) listeners
549 foreach ($this->ephemeralListeners as $listener) {
550 if ($listener['event_type'] == null || $listener['event_type'] == $event['type']) {
551 $listener['callback']($event);
552 }
553 }
554 }
555 }
556 }
557  
558 /**
559 * Remove mapping of an alias
560 *
561 * @param string $roomAlias The alias to be removed.
562 * @return bool True if the alias is removed, false otherwise.
563 * @throws Exceptions\MatrixException
564 * @throws Exceptions\MatrixHttpLibException
565 */
566 public function removeRoomAlias(string $roomAlias): bool {
567 try {
568 $this->api->removeRoomAlias($roomAlias);
569 } catch (MatrixRequestException $e) {
570 return false;
571 }
572  
573 return true;
574 }
575  
576 public function api(): MatrixHttpApi {
577 return $this->api;
578 }
579  
580 public function userId():?string {
581 return $this->userId;
582 }
583  
584 public function cacheLevel() {
585 return $this->cacheLevel;
586 }
587  
588 }