Implement Matrix as data backend
This commit is contained in:
parent
1f14932b28
commit
01212543fe
8
composer.lock
generated
8
composer.lock
generated
|
@ -12,12 +12,12 @@
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/bziemons/matrix-php-sdk.git",
|
"url": "https://github.com/bziemons/matrix-php-sdk.git",
|
||||||
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c"
|
"reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/54aa76a82237b1abea6b6d7157b03b39d207846c",
|
"url": "https://api.github.com/repos/bziemons/matrix-php-sdk/zipball/f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
|
||||||
"reference": "54aa76a82237b1abea6b6d7157b03b39d207846c",
|
"reference": "f0d759e56bf7013c1b0d9936acd1406ebcfcecb9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/bziemons/matrix-php-sdk/tree/feature/guzzle7-update"
|
"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",
|
"name": "guzzlehttp/guzzle",
|
||||||
|
|
|
@ -45,8 +45,7 @@ class TicketApiController extends ApiController
|
||||||
|
|
||||||
public function create(string $title, string $content): DataResponse
|
public function create(string $title, string $content): DataResponse
|
||||||
{
|
{
|
||||||
return new DataResponse($this->service->create($title, $content,
|
return new DataResponse($this->service->create($title, $content, $this->userId));
|
||||||
$this->userId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(int $id, string $title, string $content): DataResponse
|
public function update(int $id, string $title, string $content): DataResponse
|
||||||
|
|
|
@ -8,12 +8,14 @@ use OCP\AppFramework\Db\Entity;
|
||||||
class MatrixUser extends Entity implements JsonSerializable {
|
class MatrixUser extends Entity implements JsonSerializable {
|
||||||
protected $userId;
|
protected $userId;
|
||||||
protected $matrixUser;
|
protected $matrixUser;
|
||||||
|
protected $matrixToken;
|
||||||
|
|
||||||
public function jsonSerialize(): array {
|
public function jsonSerialize(): array {
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'userId' => $this->userId,
|
'userId' => $this->userId,
|
||||||
'matrixUser' => $this->matrixUser,
|
'matrixUser' => $this->matrixUser,
|
||||||
|
'matrixToken' => $this->matrixToken,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,20 +22,14 @@ class TicketMapper extends QBMapper
|
||||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
* @throws DoesNotExistException
|
* @throws DoesNotExistException
|
||||||
*/
|
*/
|
||||||
public function find(int $id, string $userId): MatrixTicket
|
public function find(int $id, MatrixUser $matrixUser): MatrixTicket
|
||||||
{
|
{
|
||||||
/* @var $qb IQueryBuilder */
|
/* @var $qb IQueryBuilder */
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
$qb->select('*')
|
$qb->select('*')
|
||||||
->from('upschooling_tickets', 't')
|
->from('upschooling_tickets')
|
||||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||||
->innerJoin(
|
->andWhere($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
|
||||||
'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)));
|
|
||||||
return $this->findEntity($qb);
|
return $this->findEntity($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,19 +37,13 @@ class TicketMapper extends QBMapper
|
||||||
* @param string $userId
|
* @param string $userId
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function findAll(string $userId): array
|
public function findAll(MatrixUser $matrixUser): array
|
||||||
{
|
{
|
||||||
/* @var $qb IQueryBuilder */
|
/* @var $qb IQueryBuilder */
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
$qb->select('*')
|
$qb->select('*')
|
||||||
->from('upschooling_users', 'u')
|
->from('upschooling_tickets')
|
||||||
->where($qb->expr()->eq('u.user_id', $qb->createNamedParameter($userId)))
|
->where($qb->expr()->in($matrixUser->getMatrixUser(), ['matrix_assisted_user', 'matrix_helper_user']));
|
||||||
->innerJoin(
|
|
||||||
'u',
|
|
||||||
'upschooling_tickets',
|
|
||||||
't',
|
|
||||||
$qb->expr()->in('u.matrix_user', ['t.matrix_assisted_user', 't.matrix_helper_user'])
|
|
||||||
);
|
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
lib/Exceptions/RoomNotFoundException.php
Normal file
13
lib/Exceptions/RoomNotFoundException.php
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\UPschooling\Exceptions;
|
||||||
|
|
||||||
|
use OC\OCS\Exception;
|
||||||
|
use OC\OCS\Result;
|
||||||
|
|
||||||
|
class RoomNotFoundException extends Exception {
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct(new Result(null, 404, 'Room not found'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,22 +29,22 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
|
||||||
]);
|
]);
|
||||||
$table->addColumn('matrix_room', 'string', [
|
$table->addColumn('matrix_room', 'string', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'length' => 63,
|
'length' => 200,
|
||||||
]);
|
]);
|
||||||
$table->addColumn('matrix_assisted_user', 'string', [
|
$table->addColumn('matrix_assisted_user', 'string', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'length' => 63,
|
'length' => 200,
|
||||||
]);
|
]);
|
||||||
$table->addColumn('matrix_helper_user', 'string', [
|
$table->addColumn('matrix_helper_user', 'string', [
|
||||||
'notnull' => false,
|
'notnull' => false,
|
||||||
'length' => 63,
|
'length' => 200,
|
||||||
]);
|
]);
|
||||||
// could be optimized by having a "localUser" with foreign key to users.id,
|
// could be optimized by having a "localUser" with foreign key to users.id,
|
||||||
// but that would create consistency issues
|
// but that would create consistency issues
|
||||||
|
|
||||||
$table->addColumn('status', 'string', [
|
$table->addColumn('status', 'string', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'length' => 100,
|
'length' => 63,
|
||||||
]);
|
]);
|
||||||
$table->addColumn('version', 'integer', [
|
$table->addColumn('version', 'integer', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
|
@ -63,11 +63,15 @@ class Version000000Date20210918151800 extends SimpleMigrationStep {
|
||||||
]);
|
]);
|
||||||
$table->addColumn('user_id', 'string', [
|
$table->addColumn('user_id', 'string', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'length' => 100,
|
'length' => 200,
|
||||||
]);
|
]);
|
||||||
$table->addColumn('matrix_user', 'string', [
|
$table->addColumn('matrix_user', 'string', [
|
||||||
'notnull' => true,
|
'notnull' => true,
|
||||||
'length' => 63,
|
'length' => 200,
|
||||||
|
]);
|
||||||
|
$table->addColumn('matrix_token', 'string', [
|
||||||
|
'notnull' => true,
|
||||||
|
'length' => 200,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$table->setPrimaryKey(['id']);
|
$table->setPrimaryKey(['id']);
|
||||||
|
|
|
@ -3,9 +3,13 @@
|
||||||
namespace OCA\UPschooling\Service;
|
namespace OCA\UPschooling\Service;
|
||||||
|
|
||||||
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
|
||||||
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
|
||||||
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
|
||||||
|
use Aryess\PhpMatrixSdk\Exceptions\ValidationException;
|
||||||
use Aryess\PhpMatrixSdk\MatrixClient;
|
use Aryess\PhpMatrixSdk\MatrixClient;
|
||||||
use Aryess\PhpMatrixSdk\Room;
|
use Aryess\PhpMatrixSdk\Room;
|
||||||
|
use OCA\UPschooling\Db\MatrixUser;
|
||||||
|
use OCA\UPschooling\Exceptions\RoomNotFoundException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class MatrixService
|
class MatrixService
|
||||||
|
@ -14,58 +18,160 @@ class MatrixService
|
||||||
/** @var LoggerInterface */
|
/** @var LoggerInterface */
|
||||||
private $logger;
|
private $logger;
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
private $channel;
|
|
||||||
|
|
||||||
/** @var MatrixClient */
|
/** @var MatrixClient */
|
||||||
private $client;
|
private $client;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $token;
|
private $registrationSecret = "~d9fJpPKDZIV67A7=tPCvok:=fTBLV;MFf=9FRxtAazW@-GwSo";
|
||||||
|
|
||||||
/** @var Room */
|
|
||||||
private $room;
|
|
||||||
|
|
||||||
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)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a property on a channel.
|
* @throws MatrixRequestException
|
||||||
*
|
* @throws MatrixHttpLibException
|
||||||
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
|
* @throws ValidationException
|
||||||
* @param array $content the contents as a JSON serializable array.
|
|
||||||
* @throws MatrixException
|
* @throws MatrixException
|
||||||
*/
|
*/
|
||||||
public function setProperty(string $eventType, array $content)
|
public function __construct(LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
$this->room->sendStateEvent($eventType, $content);
|
$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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @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.
|
* @param string $eventType a unique property identifier with reverse domain notation, e.g. com.example.property.
|
||||||
* @return array the contents of the room state.
|
* @return array the contents of the room state.
|
||||||
* @throws MatrixException|MatrixRequestException
|
* @throws MatrixException
|
||||||
|
* @throws RoomNotFoundException
|
||||||
*/
|
*/
|
||||||
public function getProperty(string $eventType): array
|
public function getProperty(string $roomId, string $eventType): array
|
||||||
{
|
{
|
||||||
// first parameter should be $this->room->roomId
|
$this->findRoom($roomId); // make sure the room exists/is joined
|
||||||
return $this->client->api()->getStateEvent($this->channel, $eventType);
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace OCA\UPschooling\Service;
|
|
||||||
|
|
||||||
class NoteNotFound extends \Exception {
|
|
||||||
}
|
|
|
@ -2,76 +2,113 @@
|
||||||
|
|
||||||
namespace OCA\UPschooling\Service;
|
namespace OCA\UPschooling\Service;
|
||||||
|
|
||||||
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixException;
|
||||||
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixHttpLibException;
|
||||||
|
use Aryess\PhpMatrixSdk\Exceptions\MatrixRequestException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use OCA\UPschooling\Db\MatrixTicket;
|
use OCA\UPschooling\Db\MatrixTicket;
|
||||||
|
use OCA\UPschooling\Db\MatrixUser;
|
||||||
use OCA\UPschooling\Db\TicketMapper;
|
use OCA\UPschooling\Db\TicketMapper;
|
||||||
|
use OCA\UPschooling\Db\UserMapper;
|
||||||
|
use OCA\UPschooling\Exceptions\RoomNotFoundException;
|
||||||
use OCP\AppFramework\Db\DoesNotExistException;
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||||
|
|
||||||
class TicketService {
|
class TicketService {
|
||||||
|
|
||||||
/** @var TicketMapper */
|
/** @var MatrixService */
|
||||||
private $mapper;
|
|
||||||
|
|
||||||
private $matrix;
|
private $matrix;
|
||||||
|
|
||||||
public function __construct(MatrixService $matrix, TicketMapper $mapper) {
|
/** @var TicketMapper */
|
||||||
|
private $ticketMapper;
|
||||||
|
|
||||||
|
/** @var UserMapper */
|
||||||
|
private $userMapper;
|
||||||
|
|
||||||
|
public function __construct(MatrixService $matrix, TicketMapper $ticketMapper, UserMapper $userMapper) {
|
||||||
$this->matrix = $matrix;
|
$this->matrix = $matrix;
|
||||||
$this->mapper = $mapper;
|
$this->ticketMapper = $ticketMapper;
|
||||||
|
$this->userMapper = $userMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAll(string $userId): array {
|
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 {
|
public function find($id, $userId): array {
|
||||||
if ($e instanceof DoesNotExistException ||
|
return $this->resolveTicket($this->ticketMapper->find($id, $this->getOrCreateUser($userId)));
|
||||||
$e instanceof MultipleObjectsReturnedException) {
|
|
||||||
throw new NoteNotFound($e->getMessage());
|
|
||||||
} else {
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function find($id, $userId) {
|
public function create($title, $content, $userId): array {
|
||||||
try {
|
$matrixUser = $this->getOrCreateUser($userId);
|
||||||
return $this->mapper->find($id, $userId);
|
// FIXME: room must be joined on the remote support-platform Nextcloud
|
||||||
|
$roomId = $this->matrix->createRoom();
|
||||||
// in order to be able to plug in different storage backends like files
|
$this->matrix->setProperty($roomId, "upschooling.ticket", array(
|
||||||
// for instance it is a good idea to turn storage related exceptions
|
// id is to be set from the remote support-platform Nextcloud
|
||||||
// into service related exceptions so controllers and service users
|
"title" => $title,
|
||||||
// have to deal with only one type of exception
|
"description" => $content,
|
||||||
} catch (Exception $e) {
|
));
|
||||||
$this->handleException($e);
|
$ticket = new MatrixTicket();
|
||||||
}
|
$ticket->setMatrixRoom($roomId);
|
||||||
}
|
$ticket->setMatrixAssistedUser($matrixUser->getMatrixUser());
|
||||||
|
$ticket->setStatus("open");
|
||||||
public function create($title, $content, $userId) {
|
$ticket->setVersion(1);
|
||||||
$note = new MatrixTicket();
|
return $this->resolveTicket($this->ticketMapper->insert($ticket));
|
||||||
$note->setTitle($title);
|
|
||||||
$note->setContent($content);
|
|
||||||
$note->setUserId($userId);
|
|
||||||
return $this->mapper->insert($note);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update($id, $title, $content, $userId) {
|
public function update($id, $title, $content, $userId) {
|
||||||
try {
|
throw new Exception("Not implemented");
|
||||||
$note = $this->mapper->find($id, $userId);
|
|
||||||
$note->setTitle($title);
|
|
||||||
$note->setContent($content);
|
|
||||||
return $this->mapper->update($note);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$this->handleException($e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($id, $userId) {
|
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 {
|
try {
|
||||||
$note = $this->mapper->find($id, $userId);
|
$matrixTicketContent = $this->matrix->getProperty($ticket->getMatrixRoom(), "upschooling.ticket");
|
||||||
$this->mapper->delete($note);
|
$ticketId = array_get($matrixTicketContent, "id", $ticket->getId());
|
||||||
return $note;
|
$title = array_get($matrixTicketContent, "title", "Untitled");
|
||||||
} catch (Exception $e) {
|
$description = array_get($matrixTicketContent, "description", "");
|
||||||
$this->handleException($e);
|
$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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ DIR="${0%/*}"
|
||||||
podman rm -if synapse
|
podman rm -if synapse
|
||||||
podman rm -if nextcloud
|
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
|
podman exec nextcloud chown -R 33 /var/www/html/custom_apps
|
||||||
"$DIR/podman-reown.sh"
|
"$DIR/podman-reown.sh"
|
||||||
podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'
|
podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'
|
||||||
|
|
31
src/App.vue
31
src/App.vue
|
@ -38,6 +38,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
currentTicket: undefined,
|
currentTicket: undefined,
|
||||||
|
|
||||||
|
ticketsFetched: false,
|
||||||
tickets: [],
|
tickets: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -45,19 +46,43 @@ export default {
|
||||||
$route: 'fetchTickets',
|
$route: 'fetchTickets',
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.createExampleContent()
|
||||||
this.fetchTickets()
|
this.fetchTickets()
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
fetchTickets() {
|
||||||
axios.get(
|
axios.get(
|
||||||
'api/v1/tickets',
|
'api/v1/tickets',
|
||||||
{ headers: { Accept: 'application/json' } }
|
{ headers: { Accept: 'application/json' } }
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
|
this.ticketsFetched = true
|
||||||
if (response.data.length !== 0) {
|
if (response.data.length !== 0) {
|
||||||
this.tickets.push(response.data)
|
this.tickets = response.data
|
||||||
|
console.debug(this.tickets) // FIXME
|
||||||
} else {
|
} else {
|
||||||
console.warn('Empty ticket list :(')
|
console.debug('Empty ticket list :(')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('API did not return array: ', response)
|
console.error('API did not return array: ', response)
|
||||||
|
@ -69,7 +94,7 @@ export default {
|
||||||
console.debug('upschooling', 'saveTicket', ticketId, data)
|
console.debug('upschooling', 'saveTicket', ticketId, data)
|
||||||
},
|
},
|
||||||
openTicket(ticketId) {
|
openTicket(ticketId) {
|
||||||
this.currentTicket = this.tickets.find((obj) => obj.id === ticketId)
|
this.currentTicket = this.tickets.find((obj) => obj.ticketId === ticketId)
|
||||||
},
|
},
|
||||||
deselectTicket() {
|
deselectTicket() {
|
||||||
this.currentTicket = null
|
this.currentTicket = null
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h2>Ticket "ding"</h2>
|
<h2>Ticket "ding"</h2>
|
||||||
<KeyValueTable :data-rows="{Name: ticket.title, Status: ticket.status, Ablaufdatum: '<<gestern>>'}" />
|
<KeyValueTable :data-rows="{Id: ticket.ticketId, Name: ticket.title, Status: ticket.status, Geaendert: toLocaleDate(ticket.lastModified)}" />
|
||||||
<br>
|
<br>
|
||||||
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
|
<label for="description">{{ t('upschooling', 'Beschreibung') }}</label>
|
||||||
<textarea id="description" v-model.trim.lazy="description" rows="15" />
|
<textarea id="description" v-model.trim.lazy="description" rows="15" />
|
||||||
<br>
|
<br>
|
||||||
<button @click="save">
|
<button @click="save">
|
||||||
Speichern
|
{{ t('upschooling', 'Speichern') }}
|
||||||
</button>
|
</button>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="placeholder" />
|
<div class="placeholder" />
|
||||||
|
@ -41,8 +41,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toLocaleDate(timestamp) {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString()
|
||||||
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.$emit('save-ticket', this.ticket.id, {}) // TODO: give it only the changed data
|
this.$emit('save-ticket', this.ticket.ticketId, {}) // TODO: give it only the changed data
|
||||||
},
|
},
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
|
|
|
@ -7,32 +7,32 @@
|
||||||
<th id="headerName" class="column-name">
|
<th id="headerName" class="column-name">
|
||||||
<div id="headerName-container">
|
<div id="headerName-container">
|
||||||
<a class="name sort columntitle" data-sort="name">
|
<a class="name sort columntitle" data-sort="name">
|
||||||
<span>Titel (#Ticket-Nummer)</span>
|
<span>{{ t('upschooling', 'Titel (#Ticket-Nummer)') }}</span>
|
||||||
<span class="sort-indicator icon-triangle-n" />
|
<span class="sort-indicator icon-triangle-n" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th id="headerStatus" class="column-status">
|
<th id="headerStatus" class="column-status">
|
||||||
<a class="status sort columntitle" data-sort="status">
|
<a class="status sort columntitle" data-sort="status">
|
||||||
<span>Status</span>
|
<span>{{ t('upschooling', 'Status') }}</span>
|
||||||
<span class="sort-indicator hidden icon-triangle-s" />
|
<span class="sort-indicator hidden icon-triangle-s" />
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th id="headerDate" class="column-mtime">
|
<th id="headerDate" class="column-mtime">
|
||||||
<a id="modified" class="columntitle" data-sort="mtime">
|
<a id="modified" class="columntitle" data-sort="mtime">
|
||||||
<span>Geändert</span>
|
<span>{{ t('upschooling', 'Zuletzt Geändert') }}</span>
|
||||||
<span class="sort-indicator hidden icon-triangle-s" />
|
<span class="sort-indicator hidden icon-triangle-s" />
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="tickettbody">
|
<tbody id="tickettbody">
|
||||||
<tr v-for="item in tickets" :key="item.id">
|
<tr v-for="item in tickets" :key="item.ticketId">
|
||||||
<td class="filename ui-draggable ui-draggable-handle">
|
<td class="filename ui-draggable ui-draggable-handle">
|
||||||
<a class="name" :href="'#ticket-' + item.id" @click="openTicket(item.id)">
|
<a class="name" :href="'#ticket-' + item.ticketId" @click="openTicket(item.ticketId)">
|
||||||
<span class="nametext">
|
<span class="nametext">
|
||||||
<span class="innernametext">{{ item.title }}</span>
|
<span class="innernametext">{{ item.title }}</span>
|
||||||
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.id }})</span>
|
<span class="ticket-number"> (<span class="icon icon-ticket" />#{{ item.ticketId }})</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -42,11 +42,10 @@
|
||||||
<td class="date">
|
<td class="date">
|
||||||
<span
|
<span
|
||||||
class="modified live-relative-timestamp"
|
class="modified live-relative-timestamp"
|
||||||
title=""
|
:title="toLocaleDate(item.lastModified)"
|
||||||
data-timestamp="1628157115000"
|
:data-timestamp="item.lastModified"
|
||||||
style="color:rgb(81,81,81)"
|
style="color:rgb(81,81,81)">
|
||||||
data-original-title="5. August 2021 11:51">
|
{{ toLocaleDate(item.lastModified) }}
|
||||||
vor 16 Tagen <!-- ToDo -->
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -66,6 +65,10 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toLocaleDate(timestamp) {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString()
|
||||||
|
},
|
||||||
openTicket(ticketId) {
|
openTicket(ticketId) {
|
||||||
this.$emit('open-ticket', ticketId)
|
this.$emit('open-ticket', ticketId)
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,8 @@ namespace Unit\Service;
|
||||||
|
|
||||||
use OCA\UPschooling\Db\MatrixTicket;
|
use OCA\UPschooling\Db\MatrixTicket;
|
||||||
use OCA\UPschooling\Db\TicketMapper;
|
use OCA\UPschooling\Db\TicketMapper;
|
||||||
|
use OCA\UPschooling\Db\UserMapper;
|
||||||
use OCA\UPschooling\Service\MatrixService;
|
use OCA\UPschooling\Service\MatrixService;
|
||||||
use OCA\UPschooling\Service\NoteNotFound;
|
|
||||||
use OCA\UPschooling\Service\TicketService;
|
use OCA\UPschooling\Service\TicketService;
|
||||||
use OCP\AppFramework\Db\DoesNotExistException;
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
@ -20,17 +20,23 @@ class NoteServiceTest extends TestCase {
|
||||||
private $matrixService;
|
private $matrixService;
|
||||||
|
|
||||||
/** @var TicketMapper */
|
/** @var TicketMapper */
|
||||||
private $mapper;
|
private $ticketMapper;
|
||||||
|
|
||||||
|
/** @var UserMapper */
|
||||||
|
private $userMapper;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $userId = 'john';
|
private $userId = 'john';
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
$this->mapper = $this->getMockBuilder(TicketMapper::class)
|
$this->ticketMapper = $this->getMockBuilder(TicketMapper::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
$this->userMapper = $this->getMockBuilder(UserMapper::class)
|
||||||
->disableOriginalConstructor()
|
->disableOriginalConstructor()
|
||||||
->getMock();
|
->getMock();
|
||||||
$this->matrixService = new MatrixService(new NullLogger());
|
$this->matrixService = new MatrixService(new NullLogger());
|
||||||
$this->service = new TicketService($this->matrixService, $this->mapper);
|
$this->service = new TicketService($this->matrixService, $this->ticketMapper, $this->userMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdate() {
|
public function testUpdate() {
|
||||||
|
@ -40,7 +46,7 @@ class NoteServiceTest extends TestCase {
|
||||||
'title' => 'yo',
|
'title' => 'yo',
|
||||||
'content' => 'nope'
|
'content' => 'nope'
|
||||||
]);
|
]);
|
||||||
$this->mapper->expects($this->once())
|
$this->ticketMapper->expects($this->once())
|
||||||
->method('find')
|
->method('find')
|
||||||
->with($this->equalTo(3))
|
->with($this->equalTo(3))
|
||||||
->will($this->returnValue($note));
|
->will($this->returnValue($note));
|
||||||
|
@ -49,7 +55,7 @@ class NoteServiceTest extends TestCase {
|
||||||
$updatedNote = MatrixTicket::fromRow(['id' => 3]);
|
$updatedNote = MatrixTicket::fromRow(['id' => 3]);
|
||||||
$updatedNote->setTitle('title');
|
$updatedNote->setTitle('title');
|
||||||
$updatedNote->setContent('content');
|
$updatedNote->setContent('content');
|
||||||
$this->mapper->expects($this->once())
|
$this->ticketMapper->expects($this->once())
|
||||||
->method('update')
|
->method('update')
|
||||||
->with($this->equalTo($updatedNote))
|
->with($this->equalTo($updatedNote))
|
||||||
->will($this->returnValue($updatedNote));
|
->will($this->returnValue($updatedNote));
|
||||||
|
@ -60,9 +66,9 @@ class NoteServiceTest extends TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUpdateNotFound() {
|
public function testUpdateNotFound() {
|
||||||
$this->expectException(NoteNotFound::class);
|
// $this->expectException(NoteNotFound::class);
|
||||||
// test the correct status code if no note is found
|
// test the correct status code if no note is found
|
||||||
$this->mapper->expects($this->once())
|
$this->ticketMapper->expects($this->once())
|
||||||
->method('find')
|
->method('find')
|
||||||
->with($this->equalTo(3))
|
->with($this->equalTo(3))
|
||||||
->will($this->throwException(new DoesNotExistException('')));
|
->will($this->throwException(new DoesNotExistException('')));
|
||||||
|
|
Loading…
Reference in a new issue