From 7fdb0ead3edcb31719e61aef8bdf6f6b6a140638 Mon Sep 17 00:00:00 2001 From: Sorunome Date: Mon, 5 Jul 2021 10:26:35 +0200 Subject: [PATCH 1/2] Add matrix share --- appinfo/info.xml | 16 +- appinfo/routes.php | 64 +++- css/settings-personal.css | 3 + css/style.css | 9 + img/matrix.svg | 14 + js-old/common.js | 45 +++ js-old/settings-personal.js | 27 ++ js-old/settings-share-admin.js | 8 + lib/AppInfo/Application.php | 14 + lib/Controller/AppController.php | 77 ----- ...igController.php => ElementController.php} | 45 ++- lib/Controller/FileShareController.php | 227 +++++++++++++ lib/Controller/MatrixClient.php | 125 ++++++++ lib/Controller/MatrixController.php | 141 ++++++++ lib/Cron/RoomSyncTask.php | 48 +++ lib/Db/AccountData.php | 45 +++ lib/Db/AccountDataMapper.php | 35 ++ lib/Db/CustomQBMapper.php | 112 +++++++ lib/Db/Room.php | 67 ++++ lib/Db/RoomAccountData.php | 47 +++ lib/Db/RoomAccountDataMapper.php | 35 ++ lib/Db/RoomMapper.php | 52 +++ lib/Db/RoomState.php | 65 ++++ lib/Db/RoomStateMapper.php | 70 ++++ lib/MatrixClient.php | 125 ++++++++ .../Version000000Date20210726173400.php | 186 +++++++++++ lib/RouteConfig.php | 9 + lib/Service/RoomSyncService.php | 303 ++++++++++++++++++ lib/Settings/{Admin.php => ElementAdmin.php} | 4 +- ...minSection.php => ElementAdminSection.php} | 2 +- lib/Settings/Personal.php | 55 ++++ lib/Settings/ShareAdmin.php | 65 ++++ package-lock.json | 1 + .../ElementAdminSettings.vue} | 2 +- .../settings/ShareAdminSettings.vue | 117 +++++++ ...minSettings.js => elementAdminSettings.js} | 6 +- src/shareAdminSettings.js | 37 +++ templates/{index.php => element.php} | 0 templates/settings/admin.php | 5 - templates/settings/element-admin.php | 5 + templates/settings/personal.php | 22 ++ templates/settings/share-admin.php | 5 + webpack.config.js | 3 +- 43 files changed, 2237 insertions(+), 106 deletions(-) create mode 100644 css/settings-personal.css create mode 100644 css/style.css create mode 100644 img/matrix.svg create mode 100644 js-old/common.js create mode 100644 js-old/settings-personal.js create mode 100644 js-old/settings-share-admin.js delete mode 100644 lib/Controller/AppController.php rename lib/Controller/{ConfigController.php => ElementController.php} (75%) create mode 100644 lib/Controller/FileShareController.php create mode 100644 lib/Controller/MatrixClient.php create mode 100644 lib/Controller/MatrixController.php create mode 100644 lib/Cron/RoomSyncTask.php create mode 100644 lib/Db/AccountData.php create mode 100644 lib/Db/AccountDataMapper.php create mode 100644 lib/Db/CustomQBMapper.php create mode 100644 lib/Db/Room.php create mode 100644 lib/Db/RoomAccountData.php create mode 100644 lib/Db/RoomAccountDataMapper.php create mode 100644 lib/Db/RoomMapper.php create mode 100644 lib/Db/RoomState.php create mode 100644 lib/Db/RoomStateMapper.php create mode 100644 lib/MatrixClient.php create mode 100644 lib/Migration/Version000000Date20210726173400.php create mode 100644 lib/RouteConfig.php create mode 100644 lib/Service/RoomSyncService.php rename lib/Settings/{Admin.php => ElementAdmin.php} (95%) rename lib/Settings/{AdminSection.php => ElementAdminSection.php} (97%) create mode 100644 lib/Settings/Personal.php create mode 100644 lib/Settings/ShareAdmin.php rename src/components/{AdminSettings.vue => settings/ElementAdminSettings.vue} (99%) create mode 100644 src/components/settings/ShareAdminSettings.vue rename src/{adminSettings.js => elementAdminSettings.js} (85%) create mode 100644 src/shareAdminSettings.js rename templates/{index.php => element.php} (100%) delete mode 100644 templates/settings/admin.php create mode 100644 templates/settings/element-admin.php create mode 100644 templates/settings/personal.php create mode 100644 templates/settings/share-admin.php diff --git a/appinfo/info.xml b/appinfo/info.xml index c2d24fff..36f31f00 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -2,8 +2,8 @@ riotchat - Element for Nextcloud - Element Web integrated into Nextcloud + Matrix Nextcloud Integration + Integrate Matrix into your Nextcloud 0.7.13 agpl Gary Kim + Sorunome RiotChat social integration @@ -28,13 +29,18 @@ The upstream project can be found at [https://github.com/vector-im/element-web]( - OCA\RiotChat\Settings\Admin - OCA\RiotChat\Settings\AdminSection + OCA\RiotChat\Settings\ElementAdmin + OCA\RiotChat\Settings\ShareAdmin + OCA\RiotChat\Settings\ElementAdminSection + OCA\RiotChat\Settings\Personal + + OCA\RiotChat\Cron\RoomSyncTask + Element - riotchat.app.index + riotchat.element.index app.svg diff --git a/appinfo/routes.php b/appinfo/routes.php index 30a9c93c..51f7782a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -19,13 +19,69 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -return [ + +use OC\Route\Router; +use OCA\RiotChat\RouteConfig; +use OCA\RiotChat\AppInfo\Application; + +$application = \OC::$server->get(Application::class); +$router = $this; + + +// by using a modified RouteConfig to register the routes we can bypass the root url restrictions placed on most apps +$routeConfig = new RouteConfig($application->getContainer(), $router, [ 'routes' => [ - ['name' => 'app#index', 'url' => '/', 'verb' => 'GET'], + // elementweb routes + ['name' => 'element#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'static#index', 'url' => '/riot/', 'verb' => 'GET'], - ['name' => 'config#config', 'url' => '/riot/config.json', 'verb' => 'GET'], + ['name' => 'element#config', 'url' => '/riot/config.json', 'verb' => 'GET'], ['name' => 'static#usercontent', 'url' => '/riot/bundles/{version}/usercontent.js', 'verb' => 'GET'], ['name' => 'static#riot', 'url' => '/riot/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']], ['name' => 'settings#setSetting', 'url' => '/settings/{key}', 'verb' => 'PUT'], + // general personal matrix routes + [ + 'name' => 'matrix#whoami', + 'url' => '/whoami', + 'verb' => 'GET', + ], + [ + 'name' => 'matrix#login', + 'url' => '/login', + 'verb' => 'POST', + ], + [ + 'name' => 'matrix#logout', + 'url' => '/logout', + 'verb' => 'POST', + ], + [ + 'name' => 'matrix#roomSummary', + 'url' => '/room_summary', + 'verb' => 'GET', + ], + // file sharing routes + [ + 'name' => 'fileShare#matrixDownload', + 'url' => '/_matrix/media/r0/download/{mxc}', + 'verb' => 'GET', + 'requirements' => ['mxc' => '.+'], + 'root' => '', + ], + [ + 'name' => 'fileShare#matrixThumbnail', + 'url' => '/_matrix/media/r0/thumbnail/{mxc}', + 'verb' => 'GET', + 'requirements' => ['mxc' => '.+'], + 'root' => '', + ], + [ + 'name' => 'fileShare#matrixEvent', + 'url' => '/_matrix/media/r0/event/{mxc}', + 'verb' => 'GET', + 'requirements' => ['mxc' => '.+'], + 'root' => '', + ], + ] -]; +]); +$routeConfig->register(); diff --git a/css/settings-personal.css b/css/settings-personal.css new file mode 100644 index 00000000..3195a37a --- /dev/null +++ b/css/settings-personal.css @@ -0,0 +1,3 @@ +#matrixSettings { + display: grid; +} diff --git a/css/style.css b/css/style.css new file mode 100644 index 00000000..be68848d --- /dev/null +++ b/css/style.css @@ -0,0 +1,9 @@ +.icon-matrix { + background-image: url('../img/matrix.svg'); +} + +#matrix-integration-room-picker { + position: absolute; + top: 0; + left: 0; +} diff --git a/img/matrix.svg b/img/matrix.svg new file mode 100644 index 00000000..78489615 --- /dev/null +++ b/img/matrix.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/js-old/common.js b/js-old/common.js new file mode 100644 index 00000000..d56dce9e --- /dev/null +++ b/js-old/common.js @@ -0,0 +1,45 @@ +window.addEventListener('DOMContentLoaded', () => { + var appName = 'matrix_integration'; + function url(path) { + return OC.generateUrl('/apps/' + appName + path); + } + function roomPicker(title, callback) { + var $el = $('
'); + $el.append($('')); + $('body').append($el); + + console.log('WAAAAAAAAAAAAAA'); + console.log(url('/room_summary')); + $.getJSON(url('/room_summary'), function(data) { + var rooms = []; + for (var d of data) { + rooms.push($('
  • ').text(d.display_name)); + } + $el.empty().append( + $('
    ').append( + $('
    ').append( + $('

    ').text(title), + $('
      ').append(rooms), + ), + ), + ); + }); + } + + if (OCA.Sharing && OCA.Sharing.ExternalLinkActions) { + OCA.Sharing.ExternalLinkActions.registerAction({ + url: link => `matrixshare:${link}`, + name: t('socialsharing_matrix', 'Share to Matrix'), + icon: 'icon-matrix' + }); + $(document).on('click', 'a[href^="matrixshare:"]', function (e) { + e.preventDefault(); + e.stopPropagation(); + var shareUrl = $(this).attr('href').substr('matrixshare:'.length); + roomPicker('Select a room to share into', function(roomId) { + alert(roomId); + alert('matrix share ' + shareUrl); + }); + }); + } +}); diff --git a/js-old/settings-personal.js b/js-old/settings-personal.js new file mode 100644 index 00000000..37226324 --- /dev/null +++ b/js-old/settings-personal.js @@ -0,0 +1,27 @@ +window.addEventListener('DOMContentLoaded', function() { + var appName = $('#matrixSettings').data('appname'); + function url(path) { + return OC.generateUrl('/apps/' + appName + path); + } + function proccessWhoami(data) { + $('#matrixSettingsLoginForm').hide(); + $('#matrixSettingsLogoutForm').hide(); + if (!data.logged_in) { + $('#matrixSettingsLoginForm').show(); + } else { + $('#matrixSettingsUserId').text(data.user_id); + $('#matrixSettingsLogoutForm').show(); + } + } + $.getJSON(url('/whoami'), proccessWhoami); + $('#matrixSettingsLoginButton').click(function(e) { + e.preventDefault(); + var username = $('#matrixSettingsLoginUsername').val(); + var password = $('#matrixSettingsLoginPassword').val(); + $.post(url('/login'), { username, password }, proccessWhoami); + }); + $('#matrixSettingsLogoutButton').click(function(e) { + e.preventDefault(); + $.post(url('/logout'), {}, proccessWhoami); + }); +}); diff --git a/js-old/settings-share-admin.js b/js-old/settings-share-admin.js new file mode 100644 index 00000000..dd15b32b --- /dev/null +++ b/js-old/settings-share-admin.js @@ -0,0 +1,8 @@ +window.addEventListener('DOMContentLoaded', function() { + var appName = $('#matrixSharingSettings').data('appname'); + + $('#matrixSharingSettings input').change(function() { + OCP.AppConfig.setValue(appName, $(this).attr('name'), this.value); + }); + +}); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 5fd7f6f5..1734863b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -25,7 +25,11 @@ namespace OCA\RiotChat\AppInfo; +use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCP\AppFramework\App; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Util; +use OCP\IConfig; class Application extends App { public const APP_ID = 'riotchat'; @@ -43,10 +47,20 @@ class Application extends App { 'show_labs_settings' => 'true', 'set_custom_permalink' => 'false', 'sso_immediate_redirect' => 'false', + 'share_domain' => '', + 'share_prefix' => '', + 'share_suffix' => '', ]; public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); + + /** @var IEventDispatcher $eventDispatcher */ + $dispatcher = $this->getContainer()->query(IEventDispatcher::class); + $dispatcher->addListener(LoadAdditionalScriptsEvent::class, function(LoadAdditionalScriptsEvent $event) { + Util::addScript(self::APP_ID, 'common'); + Util::addStyle(self::APP_ID, 'style'); + }); } public static function AvailableLabs() { diff --git a/lib/Controller/AppController.php b/lib/Controller/AppController.php deleted file mode 100644 index c0a1de0f..00000000 --- a/lib/Controller/AppController.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * @author Gary Kim - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - - -namespace OCA\RiotChat\Controller; - -use OCA\RiotChat\AppInfo\Application; - -use OC\Security\CSP\ContentSecurityPolicy; -use OCP\IConfig; -use OCP\IInitialStateService; -use OCP\IRequest; -use OCP\AppFramework\Http\FeaturePolicy; -use OCP\AppFramework\Http\TemplateResponse; -use OCP\AppFramework\Controller; - -class AppController extends Controller { - - /** @var IInitialStateService */ - private $initialStateService; - - /** @var IConfig */ - private $config; - - public function __construct($AppName, IRequest $request, IInitialStateService $initialStateService, IConfig $config) { - parent::__construct($AppName, $request); - $this->initialStateService = $initialStateService; - $this->config = $config; - } - - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function index() { - $response = new TemplateResponse('riotchat', 'index'); - - $this->initialStateService->provideInitialState(Application::APP_ID, 'disable_custom_urls', - $this->config->getAppValue(Application::APP_ID, 'disable_custom_urls', Application::AvailableSettings['disable_custom_urls'])); - - $default_server_domain = $this->config->getAppValue(Application::APP_ID, 'base_url', Application::AvailableSettings['base_url']); - $csp = new ContentSecurityPolicy(); - $csp->addAllowedFrameDomain($this->request->getServerHost()); - $csp->addAllowedFrameDomain($default_server_domain); - $response->setContentSecurityPolicy($csp); - - $featurePolicy = new FeaturePolicy(); - $featurePolicy->addAllowedCameraDomain('*'); - $featurePolicy->addAllowedMicrophoneDomain('*'); - - $response->setFeaturePolicy($featurePolicy); - - return $response; - } -} diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ElementController.php similarity index 75% rename from lib/Controller/ConfigController.php rename to lib/Controller/ElementController.php index 5ff5fe9c..80b4788e 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ElementController.php @@ -1,10 +1,9 @@ * @copyright Copyright (c) 2020 Gary Kim - * @copyright Copyright (c) 2019 Robin Appelman * + * @author 2021 Sorunome * @author 2020 Gary Kim * * @license GNU AGPL version 3 or any later version @@ -27,15 +26,22 @@ use OCA\RiotChat\AppInfo\Application; +use OC\Security\CSP\ContentSecurityPolicy; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\FeaturePolicy; +use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\Defaults; use OCP\IConfig; use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; +use OCP\IInitialStateService; + +class ElementController extends Controller { -class ConfigController extends Controller { + /** @var IInitialStateService */ + private $initialStateService; /** @var IL10N */ private $l10n; @@ -50,7 +56,7 @@ class ConfigController extends Controller { private $urlGenerator; /** - * ConfigController constructor. + * ElementController constructor. * * @param IRequest $request * @param IL10N $l10n @@ -58,14 +64,40 @@ class ConfigController extends Controller { * @param Defaults $defaults * @param IURLGenerator $urlGenerator */ - public function __construct(IRequest $request, IL10N $l10n, IConfig $config, Defaults $defaults, IURLGenerator $urlGenerator) { + public function __construct(IRequest $request, IInitialStateService $initialStateService, IL10N $l10n, IConfig $config, Defaults $defaults, IURLGenerator $urlGenerator) { parent::__construct(Application::APP_ID, $request); + $this->initialStateService = $initialStateService; $this->l10n = $l10n; $this->config = $config; $this->defaults = $defaults; $this->urlGenerator = $urlGenerator; } + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function index() { + $response = new TemplateResponse('riotchat', 'element'); + + $this->initialStateService->provideInitialState(Application::APP_ID, 'disable_custom_urls', + $this->config->getAppValue(Application::APP_ID, 'disable_custom_urls', Application::AvailableSettings['disable_custom_urls'])); + + $default_server_domain = $this->config->getAppValue(Application::APP_ID, 'base_url', Application::AvailableSettings['base_url']); + $csp = new ContentSecurityPolicy(); + $csp->addAllowedFrameDomain($this->request->getServerHost()); + $csp->addAllowedFrameDomain($default_server_domain); + $response->setContentSecurityPolicy($csp); + + $featurePolicy = new FeaturePolicy(); + $featurePolicy->addAllowedCameraDomain('*'); + $featurePolicy->addAllowedMicrophoneDomain('*'); + + $response->setFeaturePolicy($featurePolicy); + + return $response; + } + /** * @NoCSRFRequired * @NoAdminRequired @@ -76,7 +108,6 @@ public function config() { return new JSONResponse(json_decode(($custom_json))); } - // TODO: fill in branding from theming $lang = $this->l10n->getLanguageCode(); $config = [ diff --git a/lib/Controller/FileShareController.php b/lib/Controller/FileShareController.php new file mode 100644 index 00000000..9fbcdd37 --- /dev/null +++ b/lib/Controller/FileShareController.php @@ -0,0 +1,227 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Controller; + +use OC_Files; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Controller; +use OCP\Constants; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IConfig; +use OCP\IPreview; +use OCP\IRequest; +use OCP\ISession; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager as ShareManager; +use OCP\Share\IShare; + +class FileShareController extends Controller { + + /** @var IConfig */ + private $config; + + /** @var ShareManager */ + private $shareManager; + + /** @var IPreview */ + private $previewManager; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var string */ + protected $appName; + + public function __construct( + string $appName, + IRequest $request, + IConfig $config, + ShareManager $shareManger, + IPreview $previewManager, + IRootFolder $rootFolder + ) { + parent::__construct($appName, $request); + $this->appName = $appName; + $this->config = $config; + $this->shareManager = $shareManger; + $this->previewManager = $previewManager; + $this->rootFolder = $rootFolder; + } + + private function getAppValue($key, $default = '') { + return $this->config->getAppValue($this->appName, $key, $default); + } + + private function setAppValue($key, $value) { + $this->config->setAppValue($this->appName, $key, $value); + } + + // https://cloud.sorunome.de/s/HybqDm977WJyZTM + + private function getFile($mxc, $isToken = false) { + \OC_User::setIncognitoMode(true); + $token = $mxc; + // due to the magic of MXC URIs we don't need to do any escaping here at all + if (!$isToken) { + $parts = explode('/', $mxc); + if ($parts[0] !== $this->getAppValue('share_domain', $this->config->getSystemValue('trusted_domains')[0])) { + die('nope'); + } + $prefix = $this->getAppValue('share_prefix'); + $suffix = $this->getAppValue('share_suffix'); + if (!str_starts_with($parts[1], $prefix) || !str_ends_with($parts[1], $suffix)) { + die('invalid prefix / suffix'); + } + $token = substr($parts[1], strlen($prefix), strlen($parts[1]) - strlen($prefix) - strlen($suffix)); + } + $share = $this->shareManager->getShareByToken($token); + if (($share->getPermissions() & Constants::PERMISSION_READ) === 0) { + die('no permission'); + } + \OC_Util::tearDownFS(); + \OC_Util::setupFS($share->getShareOwner()); + $node = $share->getNode(); + if ($node instanceof Folder) { + die('folders not supported'); + } + return $node; + } + + private function getEvent($mxc, $isToken = false) { + $f = $this->getFile($mxc, $isToken); + $info = [ + 'mimetype' => $f->getMimeType(), + 'size' => $f->getSize(), + ]; + $msgtype = [ + 'image' => 'm.image', + 'audio' => 'm.audio', + 'video' => 'm.video', + ][explode('/', $info['mimetype'])[0]] ?? 'm.file'; + $path = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $f->getPath(); + if ($msgtype === 'm.image') { + $exif = exif_read_data($path); + $flip = in_array($exif['Orientation'], [2, 4, 6, 8]); + $imgInfo = getimagesize($path); + if ($imgInfo) { + if ($flip) { + $info['w'] = $imgInfo[1]; + $info['h'] = $imgInfo[0]; + } else { + $info['w'] = $imgInfo[0]; + $info['h'] = $imgInfo[1]; + } + } + } + return [ + 'type' => 'm.room.message', + 'content' => [ + 'msgtype' => $msgtype, + 'body' => $f->getName(), + 'info' => $info, + 'url' => 'mxc://' . $mxc, + ], + ]; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @NoSameSiteCookieRequired + * @param string $mxc + */ + public function matrixEvent($mxc) { + return new JSONResponse($this->getEvent($mxc)); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @NoSameSiteCookieRequired + * @param string $mxc + */ + public function matrixDownload($mxc) { + // $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); + // $originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath()); + + $f = $this->getFile($mxc); + $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]); + $response->cacheFor(3600 * 24); + return $response; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @NoSameSiteCookieRequired + * @param string $mxc + * @param string $method + * @param int $w + * @param int $h + */ + public function matrixThumbnail($mxc, $method = 'scale', $w, $h) { + if ($method == NULL) { + $method = 'scale'; + } + if (!in_array($method, ['scale', 'crop'])) { + die('invalid method'); + } + $f = $this->previewManager->getPreview($this->getFile($mxc), $w, $h); + if ($method === 'crop') { + $content = $f->getContent(); + $im = imagecreatefromstring($content); + $width = imagesx($im); + $height = imagesy($im); + if (($w / $h) > ($width / $height)) { + $ratio = $h / $w; + $nh = $height * $ratio; + $im2 = imagecrop($im, ['x' => 0, 'y' => ($height - $nh) / 2, 'width' => $width, 'height' => $nh]); + } else { + $ratio = $w / $h; + $nw = $width * $ratio; + $im2 = imagecrop($im, ['x' => ($width - $nw) / 2, 'y' => 0, 'width' => $nw, 'height' => $height]); + + } + imagedestroy($im); + ob_start(); + imagejpeg($im2); + $buffer = ob_get_contents(); + ob_end_clean(); + imagedestroy($im2); + $resp = new DataDisplayResponse($buffer, Http::STATUS_OK, ['Content-Type' => 'image/jpeg']); + $resp->cacheFor(3600 * 24); + return $resp; + } + $response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]); + $response->cacheFor(3600 * 24); + return $response; + } +} diff --git a/lib/Controller/MatrixClient.php b/lib/Controller/MatrixClient.php new file mode 100644 index 00000000..d0a27f55 --- /dev/null +++ b/lib/Controller/MatrixClient.php @@ -0,0 +1,125 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat; + +class MatrixClient { + private $accessToken; + private $homeserverUrl; + private $userId; + + public function __construct( + string $homeserverUrl = '', + string $accessToken = '' + ) { + $this->homeserverUrl = $homeserverUrl; + $this->accessToken = $accessToken; + } + + public function getAccessToken() { + return $this->accessToken; + } + + public function getHomeserverUrl() { + return $this->homeserverUrl; + } + + public function login($username, $password) { + // make sure we don't have old access tokens / user ids cached + $this->accessToken = NULL; + $this->userId = NULL; + // if our username is an mxid, we need to do a well-known lookup or thelike + if ($username[0] === '@') { + $domain = explode(':', $username, 2)[1]; + $this->homeserverUrl = 'https://' . $domain; + try { + $wk = $this->doRequest('/.well-known/matrix/client'); + $this->homeserverUrl = $wk['m.homeserver']['base_url']; + } catch (Exception $e) { + $this->homeserverUrl = NULL; + } + if (!$this->homeserverUrl) { + $this->homeserverUrl = 'https://' . $domain; + } + } + // ok, now we should have a working homeserver url, if we have right input + $res = $this->doRequest('/_matrix/client/r0/login', 'POST', [ + 'type' => 'm.login.password', + 'identifier' => [ + 'type' => 'm.id.user', + 'user' => $username, + ], + 'password' => $password, + 'initial_device_display_name' => 'Nextcloud Integration', + ]); + if (!$res || !$res['access_token']) { + return false; + } + $this->userId = $res['user_id']; + $this->accessToken = $res['access_token']; + return true; + } + + public function logout() { + $this->doRequest('/_matrix/client/r0/logout', 'POST', []); + $this->accessToken = NULL; + $this->userId = NULL; + } + + public function getUserId() { + if (!$this->userId) { + $this->userId = $this->whoami(); + } + return $this->userId; + } + + public function whoami() { + try { + return $this->doRequest('/_matrix/client/r0/account/whoami')['user_id']; + } catch (Exception $e) { + return NULL; + } + } + + public function uploadFilter($filter) { + return $this->doRequest('/_matrix/client/r0/user/' . urlencode($this->getUserId()) . '/filter', 'POST', $filter)['filter_id']; + } + + public function doRequest($path, $method = 'GET', $body = NULL) { + $url = $this->homeserverUrl . $path; + $ch = curl_init($url); + $headers = []; + if ($body) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + array_push($headers, 'Content-type: application/json'); + } + if ($this->accessToken) { + array_push($headers, 'Authorization: Bearer ' . $this->accessToken); + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + $result = curl_exec($ch); + curl_close($ch); + return json_decode($result, true); + } +} diff --git a/lib/Controller/MatrixController.php b/lib/Controller/MatrixController.php new file mode 100644 index 00000000..a02baccb --- /dev/null +++ b/lib/Controller/MatrixController.php @@ -0,0 +1,141 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Controller; + +use OCA\RiotChat\MatrixClient; +use OCA\RiotChat\Db\AccountDataMapper; +use OCA\RiotChat\Db\RoomAccountDataMapper; +use OCA\RiotChat\Db\RoomMapper; +use OCA\RiotChat\Db\RoomStateMapper; +use OCA\RiotChat\Service\RoomSyncService; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Controller; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserSession; + +class MatrixController extends Controller { + private $config; + private $userSession; + private $matrixClient; + private $accountDataMapper; + private $roomAccountDataMapper; + private $roomMapper; + private $roomStateMapper; + private $roomSyncService; + protected $appName; + + public function __construct( + string $appName, + IRequest $request, + IConfig $config, + IUserSession $userSession, + AccountDataMapper $accountDataMapper, + RoomAccountDataMapper $roomAccountDataMapper, + RoomMapper $roomMapper, + RoomStateMapper $roomStateMapper, + RoomSyncService $roomSyncService + ) { + parent::__construct($appName, $request); + $this->appName = $appName; + $this->config = $config; + $this->userSession = $userSession; + $this->accountDataMapper = $accountDataMapper; + $this->roomAccountDataMapper = $roomAccountDataMapper; + $this->roomMapper = $roomMapper; + $this->roomStateMapper = $roomStateMapper; + $this->roomSyncService = $roomSyncService; + } + + private function getUserValue($key, $default = '') { + return $this->config->getUserValue($this->userSession->getUser()->getUID(), $this->appName, $key, $default); + } + + private function setUserValue($key, $value) { + return $this->config->setUserValue($this->userSession->getUser()->getUID(), $this->appName, $key, $value ?? ''); + } + + private function getMatrixClient() { + if (!$this->matrixClient) { + $accessToken = $this->getUserValue('access_token'); + $homeserverUrl = $this->getUserValue('homeserver_url'); + $this->matrixClient = new MatrixClient($homeserverUrl, $accessToken); + } + return $this->matrixClient; + } + + private function clearUser() { + $this->setUserValue('access_token', NULL); + $this->setUserValue('homeserver_url', NULL); + $this->setUserValue('room_sync_since', NULL); + $userId = $this->userSession->getUser()->getUID(); + $this->accountDataMapper->deleteAll($userId); + $this->roomAccountDataMapper->deleteAll($userId); + $this->roomMapper->deleteAll($userId); + $this->roomStateMapper->deleteAll($userId); + } + + /** + * @param string $username + * @param string $password + */ + public function login($username, $password) { + $cl = $this->getMatrixClient(); + $cl->logout(); + if ($cl->login($username, $password)) { + $this->setUserValue('access_token', $cl->getAccessToken()); + $this->setUserValue('homeserver_url', $cl->getHomeserverUrl()); + } else { + $this->clearUser(); + } + return $this->whoami(); + } + + public function logout() { + $this->getMatrixClient()->logout(); + $this->clearUser(); + return $this->whoami(); + } + + public function whoami() { + $cl = $this->getMatrixClient(); + if (!$cl->getAccessToken()) { + return new JSONResponse(['logged_in' => false]); + } + return new JSONResponse([ + 'logged_in' => true, + 'user_id' => $cl->getUserId(), + ]); + } + + public function roomSummary() { + return new JSONResponse(array_map(function ($r) { + return [ + 'room_id' => $r->getRoomId(), + 'display_name' => $r->getEffectiveName(), + 'avatar_url' => $r->getEffectiveAvatar(), + 'topic' => $r->getEffectiveTopic(), + ]; + }, $this->roomMapper->getAll($this->userSession->getUser()->getUID()))); + } +} diff --git a/lib/Cron/RoomSyncTask.php b/lib/Cron/RoomSyncTask.php new file mode 100644 index 00000000..fe4c77cc --- /dev/null +++ b/lib/Cron/RoomSyncTask.php @@ -0,0 +1,48 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Cron; + +use OC\BackgroundJob\TimedJob; +use OCP\IUserManager; +use OCA\RiotChat\Service\RoomSyncService; + +class RoomSyncTask extends TimedJob { + private $userManager; + private $roomSyncService; + + public function __construct( + IUserManager $userManager, + RoomSyncService $roomSyncService + ) { + $this->userManager = $userManager; + $this->roomSyncService = $roomSyncService; + $this->setInterval(10); // once every 10 seconds + } + + protected function run($arguments) { + $this->userManager->callForAllUsers(function ($user) { + $this->roomSyncService->sync($user->getUID()); + $this->roomSyncService->updateCache($user->getUID()); + }); + } +} diff --git a/lib/Db/AccountData.php b/lib/Db/AccountData.php new file mode 100644 index 00000000..972dabe3 --- /dev/null +++ b/lib/Db/AccountData.php @@ -0,0 +1,45 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\AppFramework\Db\Entity; + +class AccountData extends Entity { + protected $userId; + protected $type; + protected $content; + + public function __construct() { + $this->addType('user_id', 'string'); + $this->addType('type', 'string'); + $this->addType('content', 'string'); + } + + public function jsonSetContent($c) { + $this->setContent(json_encode($c)); + } + + public function jsonGetContent() { + return json_decode($this->getContent(), true); + } +} diff --git a/lib/Db/AccountDataMapper.php b/lib/Db/AccountDataMapper.php new file mode 100644 index 00000000..649a98a0 --- /dev/null +++ b/lib/Db/AccountDataMapper.php @@ -0,0 +1,35 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\IDBConnection; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\RiotChat\Db\CustomQBMapper; + +class AccountDataMapper extends CustomQBMapper { + protected $uniqueColums = ['user_id', 'type']; + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'matrix_int_acc_data'); + } +} diff --git a/lib/Db/CustomQBMapper.php b/lib/Db/CustomQBMapper.php new file mode 100644 index 00000000..95b26a1c --- /dev/null +++ b/lib/Db/CustomQBMapper.php @@ -0,0 +1,112 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\AppFramework\Db\DoesNotExistException; + +class CustomQBMapper extends QBMapper { + private function getEntityId(Entity $entity) { + $qb = $this->db->getQueryBuilder(); + $qb->select('id') + ->from($this->tableName); + foreach ($this->uniqueColums as $col) { + $getter = 'get' . ucfirst($entity->columnToProperty($col)); + $qb->andWhere( + $qb->expr()->eq($col, $qb->createNamedParameter($entity->$getter()), $this->getParameterTypeForProperty($entity, $col)) + ); + } + $res = $this->findOneQuery($qb); + return $res['id']; + } + + public function insertOrUpdate(Entity $entity): Entity { + try { + $properties = $entity->getUpdatedFields(); + if (sizeof($properties) === 0) { + // nothing to do + return $entity; + } + + if ($entity->getId() === NULL) { + return $this->insert($entity); + } else { + return $this->update($entity); + } + } catch (\OCP\DB\Exception $ex) { + if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + if (!$entity->getId()) { + // we need to fetch the id first + $entity->setId($this->getEntityId($entity)); + } + return $this->update($entity); + } + throw $ex; + } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $ex) { + if (!$entity->getId()) { + // we need to fetch the id first + $entity->setId($this->getEntityId($entity)); + } + return $this->update($entity); + } + } + + public function getExisting(Entity $entity): Entity { + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName); + foreach ($this->uniqueColums as $col) { + $getter = 'get' . ucfirst($entity->columnToProperty($col)); + $qb->andWhere( + $qb->expr()->eq($col, $qb->createNamedParameter($entity->$getter()), $this->getParameterTypeForProperty($entity, $col)) + ); + } + return $this->findEntity($qb); + } catch (DoesNotExistException $ex) { + return $entity; + } + } + + public function getAll($userId) : array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ); + return $this->findEntities($qb); + } + + public function deleteAll($userId) { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ) + ->execute(); + } +} diff --git a/lib/Db/Room.php b/lib/Db/Room.php new file mode 100644 index 00000000..efe1aab7 --- /dev/null +++ b/lib/Db/Room.php @@ -0,0 +1,67 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\AppFramework\Db\Entity; + +class Room extends Entity { + protected $userId; + protected $roomId; + protected $membership; + protected $highlightCount; + protected $notificationCount; + protected $joinedMemberCount; + protected $invitedMemberCount; + protected $heroes; + protected $effectiveName; + protected $effectiveAvatar; + protected $effectiveTopic; + protected $nameOutdated; + protected $avatarOutdated; + protected $topicOutdated; + + public function __construct() { + $this->addType('user_id', 'string'); + $this->addType('room_id', 'string'); + $this->addType('membership', 'string'); + $this->addType('highlight_count', 'int'); + $this->addType('notification_count', 'int'); + $this->addType('joined_member_count', 'int'); + $this->addType('invited_member_count', 'int'); + $this->addType('heroes', 'string'); + $this->addType('effective_name', 'string'); + $this->addType('effective_topic', 'string'); + $this->addType('effective_topic', 'string'); + $this->addType('name_outdated', 'boolean'); + $this->addType('avatar_outdated', 'boolean'); + $this->addType('topic_outdated', 'boolean'); + } + + public function jsonSetHeroes($h) { + $this->setHeroes(json_encode($h)); + } + + public function jsonGetHeroes() { + return json_decode($this->getHeroes(), true); + } +} diff --git a/lib/Db/RoomAccountData.php b/lib/Db/RoomAccountData.php new file mode 100644 index 00000000..06a763b9 --- /dev/null +++ b/lib/Db/RoomAccountData.php @@ -0,0 +1,47 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\AppFramework\Db\Entity; + +class RoomAccountData extends Entity { + protected $userId; + protected $roomId; + protected $type; + protected $content; + + public function __construct() { + $this->addType('user_id', 'string'); + $this->addType('room_id', 'string'); + $this->addType('type', 'string'); + $this->addType('content', 'string'); + } + + public function jsonSetContent($c) { + $this->setContent(json_encode($c)); + } + + public function jsonGetContent() { + return json_decode($this->getContent(), true); + } +} diff --git a/lib/Db/RoomAccountDataMapper.php b/lib/Db/RoomAccountDataMapper.php new file mode 100644 index 00000000..16d3da07 --- /dev/null +++ b/lib/Db/RoomAccountDataMapper.php @@ -0,0 +1,35 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\IDBConnection; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\RiotChat\Db\CustomQBMapper; + +class RoomAccountDataMapper extends CustomQBMapper { + protected $uniqueColums = ['user_id', 'room_id', 'type']; + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'matrix_int_room_acc_data'); + } +} diff --git a/lib/Db/RoomMapper.php b/lib/Db/RoomMapper.php new file mode 100644 index 00000000..ed3facb2 --- /dev/null +++ b/lib/Db/RoomMapper.php @@ -0,0 +1,52 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\IDBConnection; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\RiotChat\Db\CustomQBMapper; + +class RoomMapper extends CustomQBMapper { + protected $uniqueColums = ['user_id', 'room_id']; + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'matrix_int_room'); + } + + public function getAllNeedUpdate($userId) : array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('name_outdated', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)), + $qb->expr()->eq('avatar_outdated', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)), + $qb->expr()->eq('topic_outdated', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)) + ) + ); + return $this->findEntities($qb); + } +} diff --git a/lib/Db/RoomState.php b/lib/Db/RoomState.php new file mode 100644 index 00000000..7141185a --- /dev/null +++ b/lib/Db/RoomState.php @@ -0,0 +1,65 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\AppFramework\Db\Entity; + +class RoomState extends Entity { + protected $userId; + protected $roomId; + protected $eventId; + protected $originServerTs; + protected $sender; + protected $type; + protected $unsigned; + protected $content; + protected $stateKey; + + public function __construct() { + $this->addType('user_id', 'string'); + $this->addType('room_id', 'string'); + $this->addType('event_id', 'string'); + $this->addType('origin_server_ts', 'int'); + $this->addType('sender', 'string'); + $this->addType('type', 'string'); + $this->addType('unsigned', 'string'); + $this->addType('content', 'string'); + $this->addType('state_key', 'string'); + } + + public function jsonSetContent($c) { + $this->setContent(json_encode($c)); + } + + public function jsonGetContent() { + return json_decode($this->getContent(), true); + } + + public function jsonSetUnsigned($c) { + $this->setUnsigned(json_encode($c)); + } + + public function jsonGetUnsigned() { + return json_decode($this->getUnsigned(), true); + } +} diff --git a/lib/Db/RoomStateMapper.php b/lib/Db/RoomStateMapper.php new file mode 100644 index 00000000..ec78000f --- /dev/null +++ b/lib/Db/RoomStateMapper.php @@ -0,0 +1,70 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Db; + +use OCP\IDBConnection; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCA\RiotChat\Db\CustomQBMapper; +use OCP\AppFramework\Db\DoesNotExistException; + +class RoomStateMapper extends CustomQBMapper { + protected $uniqueColums = ['user_id', 'room_id', 'type', 'state_key']; + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'matrix_int_room_state'); + } + + public function get(Room $room, $type, $stateKey = '') { + if (!$stateKey) { + $stateKey = ''; + } + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($room->getUserId(), IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->eq('room_id', $qb->createNamedParameter($room->getRoomId(), IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->eq('state_key', $qb->createNamedParameter($stateKey, IQueryBuilder::PARAM_STR)) + ); + try { + return $this->findEntity($qb); + } catch (DoesNotExistException $ex) { + return NULL; + } + } + + public function getContent(Room $room, $type, $stateKey = '') { + $event = $this->get($room, $type, $stateKey); + if (!$event) { + return NULL; + } + return $event->jsonGetContent(); + } +} diff --git a/lib/MatrixClient.php b/lib/MatrixClient.php new file mode 100644 index 00000000..d0a27f55 --- /dev/null +++ b/lib/MatrixClient.php @@ -0,0 +1,125 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat; + +class MatrixClient { + private $accessToken; + private $homeserverUrl; + private $userId; + + public function __construct( + string $homeserverUrl = '', + string $accessToken = '' + ) { + $this->homeserverUrl = $homeserverUrl; + $this->accessToken = $accessToken; + } + + public function getAccessToken() { + return $this->accessToken; + } + + public function getHomeserverUrl() { + return $this->homeserverUrl; + } + + public function login($username, $password) { + // make sure we don't have old access tokens / user ids cached + $this->accessToken = NULL; + $this->userId = NULL; + // if our username is an mxid, we need to do a well-known lookup or thelike + if ($username[0] === '@') { + $domain = explode(':', $username, 2)[1]; + $this->homeserverUrl = 'https://' . $domain; + try { + $wk = $this->doRequest('/.well-known/matrix/client'); + $this->homeserverUrl = $wk['m.homeserver']['base_url']; + } catch (Exception $e) { + $this->homeserverUrl = NULL; + } + if (!$this->homeserverUrl) { + $this->homeserverUrl = 'https://' . $domain; + } + } + // ok, now we should have a working homeserver url, if we have right input + $res = $this->doRequest('/_matrix/client/r0/login', 'POST', [ + 'type' => 'm.login.password', + 'identifier' => [ + 'type' => 'm.id.user', + 'user' => $username, + ], + 'password' => $password, + 'initial_device_display_name' => 'Nextcloud Integration', + ]); + if (!$res || !$res['access_token']) { + return false; + } + $this->userId = $res['user_id']; + $this->accessToken = $res['access_token']; + return true; + } + + public function logout() { + $this->doRequest('/_matrix/client/r0/logout', 'POST', []); + $this->accessToken = NULL; + $this->userId = NULL; + } + + public function getUserId() { + if (!$this->userId) { + $this->userId = $this->whoami(); + } + return $this->userId; + } + + public function whoami() { + try { + return $this->doRequest('/_matrix/client/r0/account/whoami')['user_id']; + } catch (Exception $e) { + return NULL; + } + } + + public function uploadFilter($filter) { + return $this->doRequest('/_matrix/client/r0/user/' . urlencode($this->getUserId()) . '/filter', 'POST', $filter)['filter_id']; + } + + public function doRequest($path, $method = 'GET', $body = NULL) { + $url = $this->homeserverUrl . $path; + $ch = curl_init($url); + $headers = []; + if ($body) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + array_push($headers, 'Content-type: application/json'); + } + if ($this->accessToken) { + array_push($headers, 'Authorization: Bearer ' . $this->accessToken); + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + $result = curl_exec($ch); + curl_close($ch); + return json_decode($result, true); + } +} diff --git a/lib/Migration/Version000000Date20210726173400.php b/lib/Migration/Version000000Date20210726173400.php new file mode 100644 index 00000000..426d64df --- /dev/null +++ b/lib/Migration/Version000000Date20210726173400.php @@ -0,0 +1,186 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; +use OCP\Migration\IOutput; + +class Version000000Date20210726173400 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if (!$schema->hasTable('matrix_int_room')) { + $table = $schema->createTable('matrix_int_room'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('user_id', 'string', [ + 'notnull' => true, + 'length' => 200, + ]); + $table->addColumn('room_id', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('membership', 'text', [ + 'notnull' => true, + 'default' => 'join', + ]); + $table->addColumn('highlight_count', 'integer', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('notification_count', 'integer', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('joined_member_count', 'integer', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('invited_member_count', 'integer', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('heroes', 'text', [ + 'notnull' => true, + 'default' => '[]', + ]); + $table->addColumn('effective_name', 'text', [ + 'notnull' => false, + ]); + $table->addColumn('effective_avatar', 'text', [ + 'notnull' => false, + ]); + $table->addColumn('effective_topic', 'text', [ + 'notnull' => false, + ]); + $table->addColumn('name_outdated', 'boolean', [ + 'notnull' => true, + 'default' => true, + ]); + $table->addColumn('avatar_outdated', 'boolean', [ + 'notnull' => true, + 'default' => true, + ]); + $table->addColumn('topic_outdated', 'boolean', [ + 'notnull' => true, + 'default' => true, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['user_id', 'room_id'], 'matrix_int_r_ur_index'); + } + if (!$schema->hasTable('matrix_int_room_state')) { + $table = $schema->createTable('matrix_int_room_state'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('user_id', 'string', [ + 'notnull' => true, + 'length' => 200, + ]); + $table->addColumn('room_id', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('event_id', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('origin_server_ts', 'bigint', [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('sender', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('type', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('unsigned', 'text', [ + 'notnull' => true, + 'default' => '{}', + ]); + $table->addColumn('content', 'text', [ + 'notnull' => true, + 'default' => '{}', + ]); + $table->addColumn('state_key', 'text', [ + 'notnull' => true, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['user_id', 'room_id', 'type', 'state_key'], 'matrix_int_rs_urts_index'); + } + if (!$schema->hasTable('matrix_int_acc_data')) { + $table = $schema->createTable('matrix_int_acc_data'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('user_id', 'string', [ + 'notnull' => true, + 'length' => 200, + ]); + $table->addColumn('type', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('content', 'text', [ + 'notnull' => true, + 'default' => '{}', + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['user_id', 'type'], 'matrix_int_ad_ut_index'); + } + if (!$schema->hasTable('matrix_int_room_acc_data')) { + $table = $schema->createTable('matrix_int_room_acc_data'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('user_id', 'string', [ + 'notnull' => true, + 'length' => 200, + ]); + $table->addColumn('room_id', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('type', 'text', [ + 'notnull' => true, + ]); + $table->addColumn('content', 'text', [ + 'notnull' => true, + 'default' => '{}', + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['user_id', 'room_id', 'type'], 'matrix_int_rad_urt_index'); + } + return $schema; + } +} diff --git a/lib/RouteConfig.php b/lib/RouteConfig.php new file mode 100644 index 00000000..3b6329ae --- /dev/null +++ b/lib/RouteConfig.php @@ -0,0 +1,9 @@ +rootUrlApps, 'riotchat'); + } +} diff --git a/lib/Service/RoomSyncService.php b/lib/Service/RoomSyncService.php new file mode 100644 index 00000000..3f74f016 --- /dev/null +++ b/lib/Service/RoomSyncService.php @@ -0,0 +1,303 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Service; + +use OCA\RiotChat\MatrixClient; +use OCA\RiotChat\Db\AccountData; +use OCA\RiotChat\Db\AccountDataMapper; +use OCA\RiotChat\Db\RoomAccountData; +use OCA\RiotChat\Db\RoomAccountDataMapper; +use OCA\RiotChat\Db\Room; +use OCA\RiotChat\Db\RoomMapper; +use OCA\RiotChat\Db\RoomState; +use OCA\RiotChat\Db\RoomStateMapper; +use OCP\IConfig; + +function is_key_type(&$arr, $key, $type) { + return is_array($arr) && array_key_exists($key, $arr) && ('is_' . $type)($arr[$key]); +} + +class RoomSyncService { + private $appName; + private $config; + private $accountDataMapper; + private $roomAccountDataMapper; + private $roomMapper; + private $roomStateMapper; + private $syncFilter = [ + 'presence' => [ + 'limit' => 0, + 'types' => ['none'], + ], + 'room' => [ + 'ephemeral' => [ + 'limit' => 0, + 'types' => ['none'], + ], + 'timeline' => [ + 'limit' => 0, + 'types' => ['none'], + 'lazy_load_members' => true, + ], + 'state' => [ + 'lazy_load_members' => true, + ], + ], + ]; + + public function __construct( + string $appName, + IConfig $config, + AccountDataMapper $accountDataMapper, + RoomAccountDataMapper $roomAccountDataMapper, + RoomMapper $roomMapper, + RoomStateMapper $roomStateMapper + ) { + $this->appName = $appName; + $this->config = $config; + $this->accountDataMapper = $accountDataMapper; + $this->roomAccountDataMapper = $roomAccountDataMapper; + $this->roomMapper = $roomMapper; + $this->roomStateMapper = $roomStateMapper; + } + + private function getUserValue($userId, $key, $default = '') { + return $this->config->getUserValue($userId, $this->appName, $key, $default); + } + + private function setUserValue($userId, $key, $value) { + return $this->config->setUserValue($userId, $this->appName, $key, $value ?? ''); + } + + public function sync($userId) { + $accessToken = $this->getUserValue($userId, 'access_token'); + $homeserverUrl = $this->getUserValue($userId, 'homeserver_url'); + if (!$accessToken || !$homeserverUrl) { + return; + } + $cl = new MatrixClient($homeserverUrl, $accessToken); + $filterId = $cl->uploadFilter($this->syncFilter); + $since = $this->getUserValue($userId, 'room_sync_since'); + $query = [ + 'set_presence' => 'offline', + 'full_state' => 'false', + 'filter' => $filterId, + ]; + if ($since) { + $query['since'] = $since; + } + $response = $cl->doRequest('/_matrix/client/r0/sync?' . http_build_query($query)); + if (!is_array($response)) { + return; + } + if (is_array($response['account_data']) && is_array($response['account_data']['events'])) { + foreach ($response['account_data']['events'] as $event) { + if ( + !is_key_type($event, 'type', 'string') || + !is_key_type($event, 'content', 'array') + ) { + continue; + } + $accountData = new AccountData(); + $accountData->setUserId($userId); + $accountData->setType($event['type']); + $accountData->jsonSetContent($event['content']); + $this->accountDataMapper->insertOrUpdate($accountData); + } + } + if (is_array($response['rooms'])) { + foreach (['join', 'leave', 'invite', 'knock'] as $membership) { + if (!is_key_type($response['rooms'], $membership, 'array')) { + continue; + } + foreach ($response['rooms'][$membership] as $roomId => $room) { + if (!is_string($roomId) || !is_array($room)) { + continue; + } + $roomObj = new Room(); + $roomObj->setUserId($userId); + $roomObj->setRoomId($roomId); + $roomObj = $this->roomMapper->getExisting($roomObj); + $roomObj->setMembership($membership); + if (is_key_type($room, 'summary', 'array')) { + $summary = $room['summary']; + if (is_key_type($summary, 'm.heroes', 'array')) { + $roomObj->jsonSetHeroes($summary['m.heroes']); + $roomObj->setNameOutdated(true); + } + if (is_key_type($summary, 'm.joined_member_count', 'int')) { + $roomObj->setJoinedMemberCount($summary['m.joined_member_count']); + $roomObj->setNameOutdated(true); + } + if (is_key_type($summary, 'm.invited_member_count', 'int')) { + $roomObj->setInvitedMemberCount($summary['m.invited_member_count']); + } + } + if (is_key_type($room, 'unread_notifications', 'array')) { + $unread = $room['unread_notifications']; + if (is_key_type($unread, 'highlight_count', 'int')) { + $roomObj->setHighlightCount($unread['highlight_count']); + } + if (is_key_type($unread, 'notification_count', 'int')) { + $roomObj->setNotificationCount($unread['notification_count']); + } + } + if (is_key_type($room, 'account_data', 'array') && is_key_type($room['account_data'], 'events', 'array')) { + foreach ($room['account_data']['events'] as $event) { + if ( + !is_array($event) || + !is_key_type($event, 'type', 'string') || + !is_key_type($event, 'content', 'array') + ) { + continue; + } + $roomAccountData = new RoomAccountData(); + $roomAccountData->setUserId($userId); + $roomAccountData->setRoomId($roomId); + $roomAccountData->setType($event['type']); + $roomAccountData->jsonSetContent($event['content']); + $this->roomAccountDataMapper->insertOrUpdate($roomAccountData); + } + } + foreach (['invite_state', 'state', 'timeline'] as $stateSource) { + if (!is_key_type($room, $stateSource, 'array') || !is_key_type($room[$stateSource], 'events', 'array')) { + continue; + } + foreach ($room[$stateSource]['events'] as $event) { + if ( + !is_key_type($event, 'type', 'string') || + !is_key_type($event, 'state_key', 'string') || + !is_key_type($event, 'sender', 'string') || + !is_key_type($event, 'content', 'array') || + !is_key_type($event, 'event_id', 'string') + ) { + continue; + } + if (in_array($event['type'], ['m.room.name', 'm.room.member', 'm.room.canonical_alias'])) { + $roomObj->setNameOutdated(true); + } + if (in_array($event['type'], ['m.room.avatar', 'm.room.member'])) { + $roomObj->setAvatarOutdated(true); + } + if (in_array($event['type'], ['m.room.topic'])) { + $roomObj->setTopicOutdated(true); + } + $roomState = new RoomState(); + $roomState->setUserId($userId); + $roomState->setRoomId($roomId); + $roomState->setEventId($event['event_id']); + if (is_key_type($event, 'origin_server_ts', 'int')) { + $roomState->setOriginServerTs($event['origin_server_ts']); + } + $roomState->setSender($event['sender']); + $roomState->setType($event['type']); + if (is_key_type($event, 'unsigned', 'array')) { + $roomState->jsonSetUnsigned($event['unsigned']); + } + $roomState->jsonSetContent($event['content']); + $roomState->setStateKey($event['state_key']); + $this->roomStateMapper->insertOrUpdate($roomState); + } + } + $this->roomMapper->insertOrUpdate($roomObj); + } + } + } + if (is_key_type($response, 'next_batch', 'string')) { + $this->setUserValue($userId, 'room_sync_since', $response['next_batch']); + } + } + + public function updateCache($userId) { + $rooms = $this->roomMapper->getAllNeedUpdate($userId); + foreach ($rooms as $room) { + if ($room->getNameOutdated()) { + // name outdated + $event = $this->roomStateMapper->getContent($room, 'm.room.name'); + if (is_key_type($event, 'name', 'string') && $event['name']) { + $room->setEffectiveName($event['name']); + } else { + $event = $this->roomStateMapper->getContent($room, 'm.room.canonical_alias'); + if (is_key_type($event, 'alias', 'string') && $event['alias']) { + $room->setEffectiveName($event['alias']); + } else { + // ok....time to go off of heroes + $heroes = $room->jsonGetHeroes(); + $heroeNames = []; + foreach ($heroes as $hero) { + if (!is_string($hero)) { + continue; + } + $event = $this->roomStateMapper->getContent($room, 'm.room.member', $hero); + if (is_key_type($event, 'displayname', 'string')) { + $heroeNames[] = $event['displayname']; + } else { + $heroeNames[] = $hero; + } + } + if (sizeof($heroeNames) === 0) { + $room->setEffectiveName('Empty Room'); + } else { + $totalMembers = $room->getJoinedMemberCount() + $room->getInvitedMemberCount(); + $totalHeroes = sizeof($heroeNames); + $nameStr = implode(', ', $heroeNames); + if ($totalMembers - 1 > $totalHeroes && $totalMembers > 1) { + $room->setEffectiveName($nameStr . ' and ' . ($totalMembers - $totalHeroes) . ' others'); + } else { + $room->setEffectiveName($nameStr); + } + } + } + } + $room->setNameOutdated('false'); + } + if ($room->getAvatarOutdated()) { + // avatar outdated + $event = $this->roomStateMapper->getContent($room, 'm.room.avatar'); + if (is_key_type($event, 'url', 'string')) { + $room->setEffectiveAvatar($event['url']); + } else { + $heroes = $room->jsonGetHeroes(); + if (sizeof($heroes) === 1) { + $event = $this->roomStateMapper->getContent($room, 'm.room.member', $heroes[0]); + if (is_key_type($event, 'avatar_url', 'string')) { + $room->setEffectiveAvatar($event['avatar_url']); + } else { + $room->setEffectiveAvatar(''); + } + } else { + $room->setEffectiveAvatar(''); + } + } + $room->setAvatarOutdated('false'); + } + if ($room->getTopicOutdated()) { + // topic outdated + $event = $this->roomStateMapper->getContent($room, 'm.room.topic'); + $room->setEffectiveTopic(is_key_type($event, 'topic', 'string') ? $event['topic'] : ''); + $room->setTopicOutdated('false'); + } + $this->roomMapper->update($room); + } + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/ElementAdmin.php similarity index 95% rename from lib/Settings/Admin.php rename to lib/Settings/ElementAdmin.php index 1976c9d3..c7242a69 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/ElementAdmin.php @@ -30,7 +30,7 @@ use OCP\IUserSession; use OCP\Settings\ISettings; -class Admin implements ISettings { +class ElementAdmin implements ISettings { /** @var IConfig */ private $config; @@ -67,7 +67,7 @@ public function getForm() { $labstr['lab_' . $k] = $this->config->getAppValue(Application::APP_ID, 'lab_' . $k, 'disable'); } $this->initialStateService->provideInitialState(Application::APP_ID, 'labs', json_encode($labstr)); - return new TemplateResponse(Application::APP_ID, 'settings/admin'); + return new TemplateResponse(Application::APP_ID, 'settings/element-admin'); } /** diff --git a/lib/Settings/AdminSection.php b/lib/Settings/ElementAdminSection.php similarity index 97% rename from lib/Settings/AdminSection.php rename to lib/Settings/ElementAdminSection.php index 7d00765c..20a54a7d 100644 --- a/lib/Settings/AdminSection.php +++ b/lib/Settings/ElementAdminSection.php @@ -27,7 +27,7 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; -class AdminSection implements IIconSection { +class ElementAdminSection implements IIconSection { /** @var IURLGenerator */ private $urlGenerator; diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php new file mode 100644 index 00000000..de59ef50 --- /dev/null +++ b/lib/Settings/Personal.php @@ -0,0 +1,55 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Settings; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\Settings\ISettings; + +class Personal implements ISettings { + private $config; + protected $appName; + + public function __construct( + string $appName, + IConfig $config + ) { + $this->appName = $appName; + $this->config = $config; + } + + public function getForm() { + $parameters = [ + 'appName' => $this->appName, + ]; + return new TemplateResponse($this->appName, 'settings/personal', $parameters, ''); + } + + public function getSection() { + return 'personal-info'; + } + + public function getPriority() { + return 70; + } +} diff --git a/lib/Settings/ShareAdmin.php b/lib/Settings/ShareAdmin.php new file mode 100644 index 00000000..fa04c692 --- /dev/null +++ b/lib/Settings/ShareAdmin.php @@ -0,0 +1,65 @@ + + * + * @author 2021 Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\RiotChat\Settings; + +use OCA\RiotChat\AppInfo\Application; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\Settings\ISettings; +use OCP\IInitialStateService; + +class ShareAdmin implements ISettings { + private $config; + private $initialStateService; + + public function __construct( + IConfig $config, + IInitialStateService $initialStateService + ) { + $this->config = $config; + $this->initialStateService = $initialStateService; + } + + private function getAppValue($key, $default = '') { + return $this->config->getAppValue(Application::APP_ID, $key, $default); + } + + private function setAppValue($key, $value) { + $this->config->setAppValue(Application::APP_ID, $key, $value); + } + + public function getForm() { + $this->initialStateService->provideInitialState(Application::APP_ID, 'share_domain', $this->getAppValue('share_domain', $this->config->getSystemValue('trusted_domains')[0])); + $this->initialStateService->provideInitialState(Application::APP_ID, 'share_prefix', $this->getAppValue('share_prefix')); + $this->initialStateService->provideInitialState(Application::APP_ID, 'share_suffix', $this->getAppValue('share_suffix')); + return new TemplateResponse(Application::APP_ID, 'settings/share-admin'); + } + + public function getSection() { + return 'sharing'; + } + + public function getPriority() { + return 70; + } +} diff --git a/package-lock.json b/package-lock.json index e4ed4bfd..a7d1ee4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "riotchat", "version": "0.0.1", "license": "AGPL-3.0-or-later", "dependencies": { diff --git a/src/components/AdminSettings.vue b/src/components/settings/ElementAdminSettings.vue similarity index 99% rename from src/components/AdminSettings.vue rename to src/components/settings/ElementAdminSettings.vue index 529f38c7..3d593769 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/settings/ElementAdminSettings.vue @@ -210,7 +210,7 @@ import { loadState } from '@nextcloud/initial-state'; import { SettingsSection, Tooltip } from '@nextcloud/vue'; export default { - name: "AdminSettings", + name: "ElementAdminSettings", components: { SettingsSection, }, diff --git a/src/components/settings/ShareAdminSettings.vue b/src/components/settings/ShareAdminSettings.vue new file mode 100644 index 00000000..48f8ff4c --- /dev/null +++ b/src/components/settings/ShareAdminSettings.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/adminSettings.js b/src/elementAdminSettings.js similarity index 85% rename from src/adminSettings.js rename to src/elementAdminSettings.js index ba721a9d..79080c33 100644 --- a/src/adminSettings.js +++ b/src/elementAdminSettings.js @@ -21,7 +21,7 @@ */ import Vue from 'vue'; -import AdminSettings from "./components/AdminSettings"; +import ElementAdminSettings from "./components/settings/ElementAdminSettings"; document.addEventListener('DOMContentLoaded', main); @@ -31,7 +31,7 @@ function main () { Vue.prototype.OC = window.OC; Vue.prototype.OCA = window.OCA; - const View = Vue.extend(AdminSettings); + const View = Vue.extend(ElementAdminSettings); const view = new View(); - view.$mount('#riot-chat-settings'); + view.$mount('#riotchat-element-admin-settings'); } diff --git a/src/shareAdminSettings.js b/src/shareAdminSettings.js new file mode 100644 index 00000000..25e00cf9 --- /dev/null +++ b/src/shareAdminSettings.js @@ -0,0 +1,37 @@ +/** + * @copyright Copyright (c) 2021 Sorunome + * + * @author Sorunome + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import Vue from 'vue'; +import ShareAdminSettings from "./components/settings/ShareAdminSettings"; + +document.addEventListener('DOMContentLoaded', main); + +function main () { + Vue.prototype.t = t; + Vue.prototype.n = n; + Vue.prototype.OC = window.OC; + Vue.prototype.OCA = window.OCA; + + const View = Vue.extend(ShareAdminSettings); + const view = new View(); + view.$mount('#riotchat-share-admin-settings'); +} diff --git a/templates/index.php b/templates/element.php similarity index 100% rename from templates/index.php rename to templates/element.php diff --git a/templates/settings/admin.php b/templates/settings/admin.php deleted file mode 100644 index 14964247..00000000 --- a/templates/settings/admin.php +++ /dev/null @@ -1,5 +0,0 @@ - - -
      diff --git a/templates/settings/element-admin.php b/templates/settings/element-admin.php new file mode 100644 index 00000000..7a30f4ba --- /dev/null +++ b/templates/settings/element-admin.php @@ -0,0 +1,5 @@ + + +
      diff --git a/templates/settings/personal.php b/templates/settings/personal.php new file mode 100644 index 00000000..b084b17b --- /dev/null +++ b/templates/settings/personal.php @@ -0,0 +1,22 @@ + + +
      +

      + t('Matrix Account')); ?> +

      + + +
      diff --git a/templates/settings/share-admin.php b/templates/settings/share-admin.php new file mode 100644 index 00000000..71e746d9 --- /dev/null +++ b/templates/settings/share-admin.php @@ -0,0 +1,5 @@ + + +
      diff --git a/webpack.config.js b/webpack.config.js index 7c4fb7d3..3c6e1228 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,8 @@ const webpack = require('webpack'); module.exports = { entry: { - adminSettings: path.join(__dirname, 'src', 'adminSettings.js'), + elementAdminSettings: path.join(__dirname, 'src', 'elementAdminSettings.js'), + shareAdminSettings: path.join(__dirname, 'src', 'shareAdminSettings.js'), main: path.join(__dirname, 'src', 'main.js'), }, output: { From bdb9f89814519c90600f113c2ebfcb7b52d4828f Mon Sep 17 00:00:00 2001 From: Gary Kim Date: Wed, 28 Jul 2021 08:01:33 -0400 Subject: [PATCH 2/2] Start Migration to Vue Signed-off-by: Gary Kim --- .editorconfig | 4 + lib/AppInfo/Application.php | 2 + lib/Settings/{ElementAdmin.php => Admin.php} | 3 +- ...ementAdminSection.php => AdminSection.php} | 2 +- lib/Settings/Personal.php | 15 +- lib/Settings/ShareAdmin.php | 1 + ...ementAdminSettings.js => adminSettings.js} | 6 +- ...entAdminSettings.vue => AdminSettings.vue} | 0 src/components/settings/PersonalSettings.vue | 131 ++++++++++++++++++ .../settings/ShareAdminSettings.vue | 94 ++++++------- src/personalSettings.js | 37 +++++ templates/settings/admin.php | 5 + templates/settings/element-admin.php | 5 - templates/settings/personal.php | 20 +-- webpack.config.js | 5 +- 15 files changed, 236 insertions(+), 94 deletions(-) rename lib/Settings/{ElementAdmin.php => Admin.php} (96%) rename lib/Settings/{ElementAdminSection.php => AdminSection.php} (97%) rename src/{elementAdminSettings.js => adminSettings.js} (85%) rename src/components/settings/{ElementAdminSettings.vue => AdminSettings.vue} (100%) create mode 100644 src/components/settings/PersonalSettings.vue create mode 100644 src/personalSettings.js create mode 100644 templates/settings/admin.php delete mode 100644 templates/settings/element-admin.php diff --git a/.editorconfig b/.editorconfig index 3e787db9..69b7b806 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,7 @@ indent_style = space [*.json] indent_size = 4 indent_style = space + +[*.vue] +indent_size = 4 +indent_style = space diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 1734863b..d9f161ef 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -47,6 +47,8 @@ class Application extends App { 'show_labs_settings' => 'true', 'set_custom_permalink' => 'false', 'sso_immediate_redirect' => 'false', + + // Default is set in the OCA\RiotChat\Settings\ShareAdmin class 'share_domain' => '', 'share_prefix' => '', 'share_suffix' => '', diff --git a/lib/Settings/ElementAdmin.php b/lib/Settings/Admin.php similarity index 96% rename from lib/Settings/ElementAdmin.php rename to lib/Settings/Admin.php index c7242a69..40de6b04 100644 --- a/lib/Settings/ElementAdmin.php +++ b/lib/Settings/Admin.php @@ -30,7 +30,7 @@ use OCP\IUserSession; use OCP\Settings\ISettings; -class ElementAdmin implements ISettings { +class Admin implements ISettings { /** @var IConfig */ private $config; @@ -58,6 +58,7 @@ public function __construct(IConfig $config, IUserSession $user, IInitialStateSe */ public function getForm() { foreach (Application::AvailableSettings as $key => $default) { + // TODO: Don't send non-Element related settings here $data = $this->config->getAppValue(Application::APP_ID, $key, $default); $this->initialStateService->provideInitialState(Application::APP_ID, $key, $data); } diff --git a/lib/Settings/ElementAdminSection.php b/lib/Settings/AdminSection.php similarity index 97% rename from lib/Settings/ElementAdminSection.php rename to lib/Settings/AdminSection.php index 20a54a7d..7d00765c 100644 --- a/lib/Settings/ElementAdminSection.php +++ b/lib/Settings/AdminSection.php @@ -27,7 +27,7 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; -class ElementAdminSection implements IIconSection { +class AdminSection implements IIconSection { /** @var IURLGenerator */ private $urlGenerator; diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index de59ef50..c41ca5db 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -22,27 +22,16 @@ namespace OCA\RiotChat\Settings; +use OCA\RiotChat\AppInfo\Application; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; use OCP\Settings\ISettings; class Personal implements ISettings { - private $config; - protected $appName; - public function __construct( - string $appName, - IConfig $config - ) { - $this->appName = $appName; - $this->config = $config; - } public function getForm() { - $parameters = [ - 'appName' => $this->appName, - ]; - return new TemplateResponse($this->appName, 'settings/personal', $parameters, ''); + return new TemplateResponse(Application::APP_ID, 'settings/personal'); } public function getSection() { diff --git a/lib/Settings/ShareAdmin.php b/lib/Settings/ShareAdmin.php index fa04c692..bb4d0c4c 100644 --- a/lib/Settings/ShareAdmin.php +++ b/lib/Settings/ShareAdmin.php @@ -52,6 +52,7 @@ public function getForm() { $this->initialStateService->provideInitialState(Application::APP_ID, 'share_domain', $this->getAppValue('share_domain', $this->config->getSystemValue('trusted_domains')[0])); $this->initialStateService->provideInitialState(Application::APP_ID, 'share_prefix', $this->getAppValue('share_prefix')); $this->initialStateService->provideInitialState(Application::APP_ID, 'share_suffix', $this->getAppValue('share_suffix')); + return new TemplateResponse(Application::APP_ID, 'settings/share-admin'); } diff --git a/src/elementAdminSettings.js b/src/adminSettings.js similarity index 85% rename from src/elementAdminSettings.js rename to src/adminSettings.js index 79080c33..a8720dd8 100644 --- a/src/elementAdminSettings.js +++ b/src/adminSettings.js @@ -21,7 +21,7 @@ */ import Vue from 'vue'; -import ElementAdminSettings from "./components/settings/ElementAdminSettings"; +import AdminSettings from "./components/settings/AdminSettings"; document.addEventListener('DOMContentLoaded', main); @@ -31,7 +31,7 @@ function main () { Vue.prototype.OC = window.OC; Vue.prototype.OCA = window.OCA; - const View = Vue.extend(ElementAdminSettings); + const View = Vue.extend(AdminSettings); const view = new View(); - view.$mount('#riotchat-element-admin-settings'); + view.$mount('#riotchat-admin-settings'); } diff --git a/src/components/settings/ElementAdminSettings.vue b/src/components/settings/AdminSettings.vue similarity index 100% rename from src/components/settings/ElementAdminSettings.vue rename to src/components/settings/AdminSettings.vue diff --git a/src/components/settings/PersonalSettings.vue b/src/components/settings/PersonalSettings.vue new file mode 100644 index 00000000..07c10d27 --- /dev/null +++ b/src/components/settings/PersonalSettings.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/src/components/settings/ShareAdminSettings.vue b/src/components/settings/ShareAdminSettings.vue index 48f8ff4c..53f2660d 100644 --- a/src/components/settings/ShareAdminSettings.vue +++ b/src/components/settings/ShareAdminSettings.vue @@ -1,6 +1,6 @@