// SPDX-License-Identifier: AGPL-3.0-or-later namespace OCA\UPschooling\Service; use Aryess\PhpMatrixSdk\Exceptions\MatrixException; use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException; use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException; use Aryess\PhpMatrixSdk\Exceptions\ValidationException; use Aryess\PhpMatrixSdk\MatrixClient; use Aryess\PhpMatrixSdk\Room; use OCA\UPschooling\Db\MatrixUser; use OCA\UPschooling\Exceptions\RoomNotFoundException; use OCP\IConfig; use Psr\Log\LoggerInterface; class MatrixService { /** @var LoggerInterface */ private $logger; /** @var IConfig */ private $config; /** @var MatrixClient */ private $client; /** @var string */ private $registrationSecret; /** @var string Matrix server URL */ private $serverUrl; /** @var string Matrix server part */ private $server; /** @var string Matrix admin user */ private $superuser; /** @var string Matrix authentication token */ private $token; /** * @throws MatrixRequestException * @throws MatrixHttpLibException * @throws ValidationException * @throws MatrixException */ public function __construct(IConfig $config, LoggerInterface $logger) { $this->logger = $logger; $this->config = $config; $this->registrationSecret = $this->config->getSystemValueString( "upschooling.matrix_registration_secret", "oyYh_iEJ7Aim.iB+ye.Xk;Gl3iHFab5*8K,zv~IulT85P=c-38" ); $this->serverUrl = $this->config->getSystemValueString( "upschooling.matrix_server_url", "http://localhost:8008" ); $this->server = $this->config->getSystemValueString( "upschooling.matrix_server", "synapse" ); $this->superuser = $this->config->getSystemValueString( "upschooling.matrix_superuser", "upschooling" ); $this->token = $this->config->getSystemValueString("upschooling.matrix_auth_token"); if ($this->token != "") { $this->client = new MatrixClient($this->serverUrl, $this->token); $this->logger->debug("Using previous login as " . $this->superuser . " on server " . $this->serverUrl); } else { $this->client = new MatrixClient($this->serverUrl); $token = $this->client->login($this->superuser, "secret", true); $this->logger->debug("Logged in as " . $this->superuser . " on server " . $this->serverUrl); $this->config->setSystemValue("upschooling.matrix_auth_token", $token); } $this->checkRateLimit(); } /** * @param string $roomId a room id of an existing and joined room. * @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property. * @param array $content the contents as a JSON serializable array. * @throws RoomNotFoundException * @throws MatrixException */ public function setProperty(string $roomId, string $eventType, array $content) { $room = $this->findRoom($roomId); $room->sendStateEvent($eventType, $content); $this->logger->debug( "Set property " . $eventType . " on room " . $roomId, array("room" => $roomId, "key" => $eventType, "value" => $content) ); } /** * @param string $roomId a room id of an existing and joined room. * @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property. * @return array the contents of the room state. * @throws MatrixException * @throws RoomNotFoundException */ public function getProperty(string $roomId, string $eventType): array { $this->findRoom($roomId); // make sure the room exists/is joined $content = $this->client->api()->getStateEvent($roomId, $eventType); $this->logger->debug( "Got property " . $eventType . " from room " . $roomId, array("room" => $roomId, "key" => $eventType, "value" => $content) ); return $content; } /** * @param string $roomId a room id of an existing and joined room. * @throws RoomNotFoundException * @returns int the origin server timestamp of the most recent event. */ public function getLastEventDate(string $roomId): int { $room = $this->findRoom($roomId); $events = $room->getEvents(); if (count($events) === 0) { $this->logger->debug("Did not have any events for room " . $roomId); return 0; } else { $timestamp = array_get($events[0], 'origin_server_ts', 1); if ($timestamp === 1) { $this->logger->debug("Could not find origin_server_ts in last event of room " . $roomId); } else { $this->logger->debug("Last event in room " . $roomId . " was at " . $timestamp); } return $timestamp; } } /** * Registers a new matrix user with the local matrix server. * * @throws MatrixException * @throws MatrixHttpLibException * @throws MatrixRequestException * @return MatrixUser user object without id or Nextcloud user id set. */ public function registerNewUser(): MatrixUser { $nonceResponse = $this->client->api()->send( 'GET', '/register', null, [], [], '/_synapse/admin/v1', true ); $randUsername = trim(str_replace(["/", "+"], ".", base64_encode(random_bytes(6))), "="); $username = "upschooling_" . $randUsername; $password = base64_encode(random_bytes(32)); $hmacData = $nonceResponse["nonce"] . "\x00" . $username . "\x00" . $password . "\x00notadmin"; $hmac = hash_hmac("sha1", $hmacData, $this->registrationSecret, false); $registrationResponse = $this->client->api()->send( 'POST', '/register', array( "nonce" => $nonceResponse["nonce"], "username" => $username, "password" => $password, "displayname" => "UPschooling Support User " . $randUsername, "admin" => false, "mac" => $hmac, ), [], [], '/_synapse/admin/v1', true ); $matrixUser = new MatrixUser(); $matrixUser->setMatrixUser($registrationResponse["user_id"]); $matrixUser->setMatrixToken($registrationResponse["access_token"]); $this->logger->debug("Created a new user: " . $matrixUser->getMatrixUser()); return $matrixUser; } /** * @param string $roomId a room id of an existing and joined room. * @throws RoomNotFoundException * @returns Room the room object, if found. */ private function findRoom(string $roomId): Room { foreach ($this->client->getRooms() as $room) { if ($room->getRoomId() === $roomId) { $this->logger->debug("Found room " . $roomId . " on matrix client"); return $room; } } $this->logger->error("Room " . $roomId . " was not found on matrix client"); throw new RoomNotFoundException(); } /** * Creates a new room. * * @throws MatrixException * @return string the room id of the newly created room. */ public function createRoom(): string { $roomId = $this->client->createRoom()->getRoomId(); $this->logger->debug("Created a new room: " . $roomId); return $roomId; } /** * @return string the public matrix server url. */ public function getServerUrl(): string { return $this->serverUrl; } private function checkRateLimit() { $fullSuperuserId = "@" . $this->superuser . ":" . $this->server; $rateLimitResponse = $this->client->api()->send( 'GET', '/users/' . rawurlencode($fullSuperuserId) . '/override_ratelimit', null, [], [], '/_synapse/admin/v1', true ); if (array_has($rateLimitResponse, "messages_per_second") && array_has($rateLimitResponse, "burst_count")) { $this->logger->debug("Ratelimit setting found for " . $this->superuser); } else { $this->client->api()->send( 'POST', '/users/' . rawurlencode($fullSuperuserId) . '/override_ratelimit', array( "messages_per_second" => 0, "burst_count" => 0, ), [], [], '/_synapse/admin/v1', true ); $this->logger->debug("No ratelimiting for " . $this->superuser); } } public function inviteUser(string $roomId, string $matrixUserId) { $room = $this->client->joinRoom($roomId); $room->inviteUser($matrixUserId); } }