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