diff --git a/composer.lock b/composer.lock index 9eb05ab..e7ebad7 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/bziemons/matrix-php-sdk.git", - "reference": "54aa76a82237b1abea6b6d7157b03b39d207846c" + "reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/54aa76a82237b1abea6b6d7157b03b39d207846c", - "reference": "54aa76a82237b1abea6b6d7157b03b39d207846c", + "url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/f0d759e56bf7013c1b0d9936acd1406ebcfcecb9", + "reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9", "shasum": "" }, "require": { @@ -73,7 +73,7 @@ "support": { "source": "https://github.com/bziemons/matrix-php-sdk/tree/feature/guzzle7-update" }, - "time": "2021-09-18T10:11:20+00:00" + "time": "2021-09-19T12:54:20+00:00" }, { "name": "guzzlehttp/guzzle", diff --git a/lib/Controller/TicketApiController.php b/lib/Controller/TicketApiController.php index 603ecac..04c46ce 100644 --- a/lib/Controller/TicketApiController.php +++ b/lib/Controller/TicketApiController.php @@ -45,8 +45,7 @@ class TicketApiController extends ApiController public function create(string $title, string $content): DataResponse { - return new DataResponse($this->service->create($title, $content, - $this->userId)); + return new DataResponse($this->service->create($title, $content, $this->userId)); } public function update(int $id, string $title, string $content): DataResponse diff --git a/lib/Db/MatrixUser.php b/lib/Db/MatrixUser.php index 5af1881..a9594b8 100644 --- a/lib/Db/MatrixUser.php +++ b/lib/Db/MatrixUser.php @@ -8,12 +8,14 @@ use OCP\AppFramework\Db\Entity; class MatrixUser extends Entity implements JsonSerializable { protected $userId; protected $matrixUser; + protected $matrixToken; public function jsonSerialize(): array { return [ 'id' => $this->id, 'userId' => $this->userId, 'matrixUser' => $this->matrixUser, + 'matrixToken' => $this->matrixToken, ]; } } diff --git a/lib/Db/TicketMapper.php b/lib/Db/TicketMapper.php index d55d96f..98a0a06 100644 --- a/lib/Db/TicketMapper.php +++ b/lib/Db/TicketMapper.php @@ -22,20 +22,14 @@ class TicketMapper extends QBMapper * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws DoesNotExistException */ - public function find(int $id, string $userId): MatrixTicket + public function find(int $id, MatrixUser $matrixUser): MatrixTicket { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('upschooling_tickets', 't') + ->from('upschooling_tickets') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) - ->innerJoin( - 't', - 'upschooling_users', - 'u', - $qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user']) - ) - ->andWhere($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId))); + ->andWhere($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user'])); return $this->findEntity($qb); } @@ -43,19 +37,13 @@ class TicketMapper extends QBMapper * @param string $userId * @return array */ - public function findAll(string $userId): array + public function findAll(MatrixUser $matrixUser): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from('upschooling_users', 'u') - ->where($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId))) - ->innerJoin( - 'u', - 'upschooling_tickets', - 't', - $qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user']) - ); + ->from('upschooling_tickets') + ->where($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user'])); return $this->findEntities($qb); } } diff --git a/lib/Exceptions/RoomNotFoundException.php b/lib/Exceptions/RoomNotFoundException.php new file mode 100644 index 0000000..9f3000c --- /dev/null +++ b/lib/Exceptions/RoomNotFoundException.php @@ -0,0 +1,13 @@ +addColumn('matrix_room', 'string', [ 'notnull' => true, - 'length' => 63, + 'length' => 200, ]); $table->addColumn('matrix_assisted_user', 'string', [ 'notnull' => true, - 'length' => 63, + 'length' => 200, ]); $table->addColumn('matrix_helper_user', 'string', [ 'notnull' => false, - 'length' => 63, + 'length' => 200, ]); // could be optimized by having a "localUser" with foreign key to users.id, // but that would create consistency issues $table->addColumn('status', 'string', [ 'notnull' => true, - 'length' => 100, + 'length' => 63, ]); $table->addColumn('version', 'integer', [ 'notnull' => true, @@ -63,11 +63,15 @@ class Version000000Date20210918151800 extends SimpleMigrationStep { ]); $table->addColumn('user_id', 'string', [ 'notnull' => true, - 'length' => 100, + 'length' => 200, ]); $table->addColumn('matrix_user', 'string', [ 'notnull' => true, - 'length' => 63, + 'length' => 200, + ]); + $table->addColumn('matrix_token', 'string', [ + 'notnull' => true, + 'length' => 200, ]); $table->setPrimaryKey(['id']); diff --git a/lib/Service/MatrixService.php b/lib/Service/MatrixService.php index 7b7c96c..d62406c 100644 --- a/lib/Service/MatrixService.php +++ b/lib/Service/MatrixService.php @@ -3,69 +3,175 @@ 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 Psr\Log\LoggerInterface; class MatrixService { - /** @var LoggerInterface */ - private $logger; + /** @var LoggerInterface */ + private $logger; - /** @var string */ - private $channel; + /** @var MatrixClient */ + private $client; - /** @var MatrixClient */ - private $client; + /** @var string */ + private $registrationSecret = "~d9fJpPKDZIV67A7=tPCvok:=fTBLV;MFf=9FRxtAazW@-GwSo"; - /** @var string */ - private $token; + /** + * @throws MatrixRequestException + * @throws MatrixHttpLibException + * @throws ValidationException + * @throws MatrixException + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + $serverUrl = "http://synapse:8008"; + $user = "upschooling"; + $this->client = new MatrixClient($serverUrl); + $this->client->login($user, "secret", true); + $this->logger->debug("Logged in as " . $user . " on server " . $serverUrl); + } - /** @var Room */ - private $room; + /** + * @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) + ); + } - public function __construct(LoggerInterface $logger) { - $this->logger = $logger; - $this->channel = "#issue:synapse"; - $this->client = new MatrixClient("http://synapse:8008"); - $this->token = $this->client->login("upschooling", "secret"); -// try { -// $this->room = $this->client->createRoom($this->channel, false, array()); -// } catch (MatrixException $createE) { -// try { -// $this->room = $this->client->joinRoom($this->channel); -// } catch (MatrixException $e) { -// $this->logger->error( -// "Could not create room ".$this->channel, -// array('exception' => $e, 'causedBy' => $createE) -// ); -// } -// } - } + /** + * @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; + } - /** - * Sets a property on a channel. - * - * @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 MatrixException - */ - public function setProperty(string $eventType, array $content) - { - $this->room->sendStateEvent($eventType, $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; + } + } - /** - * @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|MatrixRequestException - */ - public function getProperty(string $eventType): array - { - // first parameter should be $this->room->roomId - return $this->client->api()->getStateEvent($this->channel, $eventType); - } + /** + * 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; + } } diff --git a/lib/Service/NoteNotFound.php b/lib/Service/NoteNotFound.php deleted file mode 100644 index a1c1a08..0000000 --- a/lib/Service/NoteNotFound.php +++ /dev/null @@ -1,6 +0,0 @@ -matrix = $matrix; - $this->mapper = $mapper; + $this->ticketMapper = $ticketMapper; + $this->userMapper = $userMapper; } public function findAll(string $userId): array { - return $this->mapper->findAll($userId); + $dbTickets = $this->ticketMapper->findAll($this->getOrCreateUser($userId)); + return array_map(function ($ticket) { return $this->resolveTicket($ticket); }, $dbTickets); } - private function handleException(Exception $e): void { - if ($e instanceof DoesNotExistException || - $e instanceof MultipleObjectsReturnedException) { - throw new NoteNotFound($e->getMessage()); - } else { - throw $e; - } + public function find($id, $userId): array { + return $this->resolveTicket($this->ticketMapper->find($id, $this->getOrCreateUser($userId))); } - public function find($id, $userId) { - try { - return $this->mapper->find($id, $userId); - - // in order to be able to plug in different storage backends like files - // for instance it is a good idea to turn storage related exceptions - // into service related exceptions so controllers and service users - // have to deal with only one type of exception - } catch (Exception $e) { - $this->handleException($e); - } - } - - public function create($title, $content, $userId) { - $note = new MatrixTicket(); - $note->setTitle($title); - $note->setContent($content); - $note->setUserId($userId); - return $this->mapper->insert($note); + public function create($title, $content, $userId): array { + $matrixUser = $this->getOrCreateUser($userId); + // FIXME: room must be joined on the remote support-platform Nextcloud + $roomId = $this->matrix->createRoom(); + $this->matrix->setProperty($roomId, "upschooling.ticket", array( + // id is to be set from the remote support-platform Nextcloud + "title" => $title, + "description" => $content, + )); + $ticket = new MatrixTicket(); + $ticket->setMatrixRoom($roomId); + $ticket->setMatrixAssistedUser($matrixUser->getMatrixUser()); + $ticket->setStatus("open"); + $ticket->setVersion(1); + return $this->resolveTicket($this->ticketMapper->insert($ticket)); } public function update($id, $title, $content, $userId) { - try { - $note = $this->mapper->find($id, $userId); - $note->setTitle($title); - $note->setContent($content); - return $this->mapper->update($note); - } catch (Exception $e) { - $this->handleException($e); - } + throw new Exception("Not implemented"); } public function delete($id, $userId) { + throw new Exception("Not implemented"); + } + + /** + * @param MatrixTicket $ticket the database object. + * @return array a JSON serializable representation of the resolved ticket, for the frontend. + */ + private function resolveTicket(MatrixTicket $ticket): array + { try { - $note = $this->mapper->find($id, $userId); - $this->mapper->delete($note); - return $note; - } catch (Exception $e) { - $this->handleException($e); + $matrixTicketContent = $this->matrix->getProperty($ticket->getMatrixRoom(), "upschooling.ticket"); + $ticketId = array_get($matrixTicketContent, "id", $ticket->getId()); + $title = array_get($matrixTicketContent, "title", "Untitled"); + $description = array_get($matrixTicketContent, "description", ""); + $lastModified = $this->matrix->getLastEventDate($ticket->getMatrixRoom()); + return array( + 'ticketId' => $ticketId, + 'status' => $ticket->getStatus(), + 'lastModified' => $lastModified, + 'title' => $title, + 'description' => $description, + ); + } catch (MatrixException | RoomNotFoundException $e) { + return array( + 'ticketId' => $ticket->getId(), + 'status' => 'error', + ); + } + } + + /** + * @param $userId string Nextcloud user id + * @throws MatrixException + * @throws MatrixHttpLibException + * @throws MatrixRequestException + * @throws MultipleObjectsReturnedException + * @throws \OCP\DB\Exception + */ + private function getOrCreateUser(string $userId): MatrixUser + { + try { + return $this->userMapper->find($userId); + } catch (DoesNotExistException $e) { + $matrixUser = $this->matrix->registerNewUser(); + $matrixUser->setUserId($userId); + $this->userMapper->insert($matrixUser); + return $matrixUser; } } } diff --git a/podman-run.sh b/podman-run.sh index 316c054..b94b937 100755 --- a/podman-run.sh +++ b/podman-run.sh @@ -9,7 +9,7 @@ DIR="${0%/*}" podman rm -if synapse podman rm -if nextcloud -podman run -d --name=nextcloud -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud +podman run -d --name=nextcloud -p 8080:80 -p 8008:8008 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud podman exec nextcloud chown -R 33 /var/www/html/custom_apps "$DIR/podman-reown.sh" podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer' diff --git a/src/App.vue b/src/App.vue index 7047a8a..efff633 100644 --- a/src/App.vue +++ b/src/App.vue @@ -38,6 +38,7 @@ export default { */ currentTicket: undefined, + ticketsFetched: false, tickets: [], } }, @@ -45,19 +46,43 @@ export default { $route: 'fetchTickets', }, created() { + this.createExampleContent() this.fetchTickets() }, methods: { + createExampleContent() { + // this obviously shouldn't survive version 1.0 + axios.post( + 'api/v1/tickets', + { title: 'Erstes Ticket', content: 'Erstes Beispiel' }, + { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } }, + ).then((response) => { + if (this.ticketsFetched === false) { + this.tickets.push(response.data) + } + }).catch(console.error) + axios.post( + 'api/v1/tickets', + { title: 'Zweites Ticket', content: 'Zweites Beispiel' }, + { headers: { 'Content-Type': 'application/json', Accept: 'application/json' } }, + ).then((response) => { + if (this.ticketsFetched === false) { + this.tickets.push(response.data) + } + }).catch(console.error) + }, fetchTickets() { axios.get( 'api/v1/tickets', { headers: { Accept: 'application/json' } } ).then((response) => { if (Array.isArray(response.data)) { + this.ticketsFetched = true if (response.data.length !== 0) { - this.tickets.push(response.data) + this.tickets = response.data + console.debug(this.tickets) // FIXME } else { - console.warn('Empty ticket list :(') + console.debug('Empty ticket list :(') } } else { console.error('API did not return array: ', response) @@ -69,7 +94,7 @@ export default { console.debug('upschooling', 'saveTicket', ticketId, data) }, openTicket(ticketId) { - this.currentTicket = this.tickets.find((obj) => obj.id === ticketId) + this.currentTicket = this.tickets.find((obj) => obj.ticketId === ticketId) }, deselectTicket() { this.currentTicket = null diff --git a/src/Ticket.vue b/src/Ticket.vue index b8edc4b..c017b36 100644 --- a/src/Ticket.vue +++ b/src/Ticket.vue @@ -9,13 +9,13 @@

Ticket "ding"

- +