From fde0f22df28a206c80e539b00e36e605d418bcb6 Mon Sep 17 00:00:00 2001 From: Israt Jahan Farzana Date: Thu, 17 Aug 2023 17:09:48 +0100 Subject: [PATCH] Mastodon username search (#1988) --- cypress/e2e/folder.cy.js | 371 +++++++++++++++---------------- cypress/e2e/layout_dataset.cy.js | 5 +- lib/Widget/MastodonProvider.php | 68 ++++-- modules/mastodon.xml | 7 +- 4 files changed, 246 insertions(+), 205 deletions(-) diff --git a/cypress/e2e/folder.cy.js b/cypress/e2e/folder.cy.js index 5e8ac2c8c0..8b0d2a82fd 100644 --- a/cypress/e2e/folder.cy.js +++ b/cypress/e2e/folder.cy.js @@ -1,7 +1,7 @@ /* * Copyright (C) 2023 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -19,189 +19,188 @@ * along with Xibo. If not, see . */ -describe('Folders', function () { - - beforeEach(function () { - cy.login(); - }); - - it('creating a new folder and rename it', () => { - cy.visit('/folders/view'); - cy.contains('Root Folder').rightclick(); - cy.contains('Create').should('be.visible').click(); - - cy.visit('/folders/view'); - cy.contains('New Folder').should('be.visible').rightclick(); - cy.contains('Rename').type('Folder123{enter}'); - }); - - it('Moving an image from Root Folder to another folder', () => { - // Create and alias for load folders - cy.server(); - cy.route('/library?*').as('mediaLoad'); - - // Go to library - cy.visit('/library/view'); - cy.get('#media').type('child_folder_media'); - - cy.wait('@mediaLoad'); - cy.get('#libraryItems tbody tr').should('have.length', 1); - - cy.get('#libraryItems tr:first-child .dropdown-toggle').click(); - cy.get('#libraryItems tr:first-child .library_button_selectfolder').click(); - - cy.get('#container-folder-form-tree>ul>li>i').click(); - cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click(); - cy.contains('ChildFolder').click(); - cy.get('.save-button').click(); - }); - - it('Sharing', () => { - cy.server(); - - // Create and alias for load folders - cy.route('/folders').as('loadFolders'); - - // Create and alias for load user permissions for folders - cy.route('/user/permissions/Folder/*').as('permissionsFolders'); - - cy.visit('/folders/view'); - - cy.wait('@loadFolders'); - - cy.contains('ShareFolder').rightclick(); - cy.get('ul.jstree-contextmenu >li:nth-child(6) > a').click(); // Click on Share Link - cy.get('#name').type('folder_user'); - - cy.wait('@permissionsFolders'); - - cy.get('#permissionsTable tbody tr').should('have.length', 1); - cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user'); - cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(2)> input').click(); - cy.get('.save-button').click(); - }); - - it('Set Home Folders for a user', () => { - cy.server(); - // Create and alias for load users - cy.route('/user*').as('loadUsers'); - - cy.visit('/user/view'); - cy.get('#userName').type('folder_user'); - - cy.wait('@loadUsers'); - cy.get('#users tbody tr').should('have.length', 1); - cy.get('#users tr:first-child .dropdown-toggle').click(); - cy.get('#users tr:first-child .user_button_set_home').click(); - cy.get('#home-folder').should('be.visible'); - cy.contains('FolderHome').click({force: true} ); - cy.get('.save-button').click(); - - // Check - cy.visit('/user/view'); - cy.get('#userName').clear(); - cy.get('#userName').type('folder_user'); - - cy.wait('@loadUsers'); - cy.get('#users tbody tr').should('have.length', 1); - cy.get('#users tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user'); - cy.get('#users tbody tr:nth-child(1) td:nth-child(3)').contains('FolderHome'); - }); - - it('Remove an empty folder', () => { - // Create and alias for load folders - cy.server(); - cy.route('/folders').as('loadFolders'); - - cy.visit('/folders/view'); - cy.contains('EmptyFolder').rightclick(); - cy.contains('Remove').click(); - - cy.visit('/folders/view'); - cy.wait('@loadFolders'); - cy.contains('EmptyFolder').should('not.exist'); - }); - - it('cannot remove a folder with content', () => { - // Create and alias for load folders - cy.server(); - cy.route('/folders').as('loadFolders'); - - cy.visit('/folders/view'); - cy.contains('FolderWithContent').rightclick(); - cy.contains('Remove').click(); - - // Check folder still exists - cy.visit('/folders/view'); - - cy.wait('@loadFolders'); - cy.contains('FolderWithContent').should('exist'); - }); - - it('search a media in a folder', () => { - // Create and alias for load folders - cy.server(); - cy.route('/folders').as('loadFolders'); - - // Go to library - cy.visit('/library/view'); - - cy.wait('@loadFolders'); - - // Click on All folders - cy.get('#folder-tree-clear-selection-button').click(); - cy.get('.jstree-ocl').click(); - cy.contains('FolderWithImage').click(); - - cy.get('#libraryItems tbody tr').should('have.length', 1); - cy.get('#libraryItems tbody').contains('media_for_search_in_folder'); - }); - - it('Hide Folder tree', () => { - // Go to library - cy.visit('/library/view'); - // The Folder tree is open by default on a grid - cy.get('#folder-tree-select-folder-button').click(); - // clicking on the folder icon hides it - cy.get('#grid-folder-filter').should('have.css', 'display', 'none'); - }); - - it('Move folders and Merge', () => { - // Move a folder (MoveFromFolder) to another (MoveToFolder) and merge - cy.server(); - cy.route('/library?*').as('mediaLoad'); - // Create and alias for load folders - cy.route('/folders').as('loadFolders'); - - // Go to folders - cy.visit('/folders/view'); - cy.contains('MoveFromFolder').rightclick(); - cy.contains('Move Folder').click(); - - cy.get('#container-folder-form-tree>ul>li>i').click(); - cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click(); - cy.contains('MoveToFolder').click({force: true}); - cy.get('.form-check input').click(); - cy.get('.save-button').click(); - - // Validate test34 image exist in MoveToFolder - cy.visit('/folders/view'); - - cy.wait('@loadFolders'); - cy.contains('MoveFromFolder').should('not.exist'); - - // Validate test34 image exist in MoveToFolder - // Go to library - cy.visit('/library/view'); - cy.wait('@mediaLoad'); - cy.wait('@loadFolders'); - - // Click on All folders from Folder Tree - cy.get('#folder-tree-clear-selection-button').click(); - cy.get('.jstree-ocl').click(); - cy.contains('MoveToFolder').click(); - - cy.wait('@mediaLoad'); - cy.get('#libraryItems tbody').contains('test34'); - }); +describe('Folders', function() { + beforeEach(function() { + cy.login(); + }); + + it('creating a new folder and rename it', () => { + cy.visit('/folders/view'); + cy.contains('Root Folder').rightclick(); + cy.contains('Create').should('be.visible').click(); + + cy.visit('/folders/view'); + cy.contains('New Folder').should('be.visible').rightclick(); + cy.contains('Rename').type('Folder123{enter}'); + }); + + it('Moving an image from Root Folder to another folder', () => { + // Create and alias for load folders + cy.intercept('/library?*').as('mediaLoad'); + cy.intercept('/user/pref').as('userPref'); + + // Go to library + cy.visit('/library/view'); + cy.get('#media').type('child_folder_media'); + + cy.wait('@mediaLoad'); + cy.get('#libraryItems tbody tr').should('have.length', 1); + cy.wait('@mediaLoad'); + cy.wait('@userPref'); + cy.get('#datatable-container').should('contain', 'child_folder_media'); + + cy.get('#libraryItems tr:first-child .dropdown-toggle').click(); + cy.get('#libraryItems tr:first-child .library_button_selectfolder').click(); + + cy.get('#container-folder-form-tree>ul>li>i').click(); + cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click(); + cy.contains('ChildFolder').click(); + cy.get('.save-button').click(); + }); + + it('Sharing', () => { + // Create and alias for load folders + cy.intercept('/folders').as('loadFolders'); + + // Create and alias for load user permissions for folders + cy.intercept('/user/permissions/Folder/*').as('permissionsFolders'); + + cy.visit('/folders/view'); + + cy.wait('@loadFolders'); + + cy.contains('ShareFolder').rightclick(); + cy.get('ul.jstree-contextmenu >li:nth-child(6) > a').click(); // Click on Share Link + cy.get('#name').type('folder_user'); + + cy.wait('@permissionsFolders'); + + cy.get('#permissionsTable tbody tr').should('have.length', 1); + cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user'); + cy.get('#permissionsTable tbody tr:nth-child(1) td:nth-child(2)> input').click(); + cy.get('.save-button').click(); + }); + + it('Set Home Folders for a user', () => { + // Create and alias for load users + cy.intercept('/user*').as('loadUsers'); + + cy.visit('/user/view'); + cy.get('#userName').type('folder_user'); + + cy.wait('@loadUsers'); + cy.get('#users tbody tr').should('have.length', 1); + cy.get('#users tr:first-child .dropdown-toggle').click(); + cy.get('#users tr:first-child .user_button_set_home').click(); + cy.get('#home-folder').should('be.visible'); + cy.contains('FolderHome').click({force: true} ); + cy.get('.save-button').click(); + + // Check + cy.visit('/user/view'); + cy.get('#userName').clear(); + cy.get('#userName').type('folder_user'); + + cy.wait('@loadUsers'); + cy.get('#users tbody tr').should('have.length', 1); + cy.get('#users tbody tr:nth-child(1) td:nth-child(1)').contains('folder_user'); + cy.get('#users tbody tr:nth-child(1) td:nth-child(3)').contains('FolderHome'); + }); + + it('Remove an empty folder', () => { + // Create and alias for load folders + cy.intercept('/folders').as('loadFolders'); + + cy.visit('/folders/view'); + cy.contains('EmptyFolder').rightclick(); + cy.contains('Remove').click(); + + cy.visit('/folders/view'); + cy.wait('@loadFolders'); + cy.contains('EmptyFolder').should('not.exist'); + }); + + it('cannot remove a folder with content', () => { + // Create and alias for load folders + cy.intercept('/folders').as('loadFolders'); + + cy.visit('/folders/view'); + cy.contains('FolderWithContent').rightclick(); + cy.contains('Remove').click(); + + // Check folder still exists + cy.visit('/folders/view'); + + cy.wait('@loadFolders'); + cy.contains('FolderWithContent').should('exist'); + }); + + it('search a media in a folder', () => { + // Create and alias for load folders + cy.intercept('/folders').as('loadFolders'); + cy.intercept('/library?*').as('mediaLoad'); + cy.intercept('/user/pref').as('userPref'); + + // Go to library + cy.visit('/library/view'); + + cy.wait('@loadFolders'); + cy.wait('@mediaLoad'); + cy.wait('@userPref'); + + // Click on All folders + cy.get('#folder-tree-clear-selection-button').click(); + cy.get('.jstree-ocl').click(); + cy.contains('FolderWithImage').click(); + + cy.get('#libraryItems tbody tr').should('have.length', 1); + cy.get('#libraryItems tbody').contains('media_for_search_in_folder'); + }); + + it('Hide Folder tree', () => { + // Go to library + cy.visit('/library/view'); + // The Folder tree is open by default on a grid + cy.get('#folder-tree-select-folder-button').click(); + // clicking on the folder icon hides it + cy.get('#grid-folder-filter').should('have.css', 'display', 'none'); + }); + + it('Move folders and Merge', () => { + // Move a folder (MoveFromFolder) to another (MoveToFolder) and merge + cy.intercept('/library?*').as('mediaLoad'); + // Create and alias for load folders + cy.intercept('/folders').as('loadFolders'); + + // Go to folders + cy.visit('/folders/view'); + cy.contains('MoveFromFolder').rightclick(); + cy.contains('Move Folder').click(); + + cy.get('#container-folder-form-tree>ul>li>i').click(); + cy.get('#container-folder-form-tree>ul>li:not(.jstree-loading)>i').click(); + cy.contains('MoveToFolder').click({force: true}); + cy.get('.form-check input').click(); + cy.get('.save-button').click(); + + // Validate test34 image exist in MoveToFolder + cy.visit('/folders/view'); + + cy.wait('@loadFolders'); + cy.contains('MoveFromFolder').should('not.exist'); + + // Validate test34 image exist in MoveToFolder + // Go to library + cy.visit('/library/view'); + cy.wait('@mediaLoad'); + cy.wait('@loadFolders'); + + // Click on All folders from Folder Tree + cy.get('#folder-tree-clear-selection-button').click(); + cy.get('.jstree-ocl').click(); + cy.contains('MoveToFolder').click(); + + cy.wait('@mediaLoad'); + cy.get('#libraryItems tbody').contains('test34'); + }); }); diff --git a/cypress/e2e/layout_dataset.cy.js b/cypress/e2e/layout_dataset.cy.js index 12f168da89..7b83fc0158 100644 --- a/cypress/e2e/layout_dataset.cy.js +++ b/cypress/e2e/layout_dataset.cy.js @@ -57,10 +57,7 @@ describe('Layout Designer', function() { cy.get('.select2-container--open').contains('test'); cy.get('.select2-container--open .select2-results > ul > li').should('have.length', 1); cy.get('.select2-container--open .select2-results > ul > li:first').contains('test').click(); - - cy.get('[contenteditable="true" ]').should('exist'); - cy.get('.cke_editable_inline').clear(); - cy.get('.cke_editable_inline').type('No data to show').trigger('change'); + cy.get('[name="lowerLimit"]').clear().type('1'); cy.get('[name="upperLimit"]').clear().type('10'); cy.get('.order-clause-row > :nth-child(2) > .form-control').select('Text', {force: true}); diff --git a/lib/Widget/MastodonProvider.php b/lib/Widget/MastodonProvider.php index 786044f1c7..5ca78a4478 100644 --- a/lib/Widget/MastodonProvider.php +++ b/lib/Widget/MastodonProvider.php @@ -23,6 +23,8 @@ namespace Xibo\Widget; use Carbon\Carbon; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; use Xibo\Widget\DataType\SocialMedia; use Xibo\Widget\Provider\DataProviderInterface; @@ -45,42 +47,55 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn try { $httpOptions = [ 'timeout' => 20, // wait no more than 20 seconds - 'query' => [ - 'limit' => $dataProvider->getProperty('numItems', 15) - ] + ]; + + $queryOptions = [ + 'limit' => $dataProvider->getProperty('numItems', 15) ]; if ($dataProvider->getProperty('searchOn', 'all') === 'local') { - $httpOptions['query']['local'] = true; + $queryOptions['local'] = true; } elseif ($dataProvider->getProperty('searchOn', 'all') === 'remote') { - $httpOptions['query']['remote'] = true; + $queryOptions['remote'] = true; } // Media Only if ($dataProvider->getProperty('onlyMedia', 0)) { - $httpOptions['query']['only_media'] = true; + $queryOptions['only_media'] = true; } if (!empty($dataProvider->getProperty('serverUrl', ''))) { - $uri = $dataProvider->getProperty('serverUrl', ''); + $uri = $dataProvider->getProperty('serverUrl'); } - // Hashtag: When empty we should do a public search, when filled we should do a hashtag search + // Hashtag $hashtag = trim($dataProvider->getProperty('hashtag', '')); - if (!empty($hashtag)) { - $uri = rtrim($uri, '/').'/api/v1/timelines/tag/'. trim($hashtag, '#'); + + // when username is provided do not search in public timeline + if (!empty($dataProvider->getProperty('userName', ''))) { + // username search: get account ID, always returns one record + $accountId = $this->getAccountId($uri, $dataProvider->getProperty('userName'), $dataProvider); + $queryOptions['tagged'] = trim($hashtag, '#'); + $uri = rtrim($uri, '/') . '/api/v1/accounts/' . $accountId . '/statuses?'; } else { - $uri = rtrim($uri, '/').'/api/v1/timelines/public'; + // Hashtag: When empty we should do a public search, when filled we should do a hashtag search + if (!empty($hashtag)) { + $uri = rtrim($uri, '/') . '/api/v1/timelines/tag/' . trim($hashtag, '#'); + } else { + $uri = rtrim($uri, '/') . '/api/v1/timelines/public'; + } } - $this->getLog()->debug('Mastodon: uri: ' . $uri . ' httpOptions: '. json_encode($httpOptions)); - $response = $dataProvider ->getGuzzleClient($httpOptions) - ->get($uri); + ->get($uri, [ + 'query' => $queryOptions + ]); $result = json_decode($response->getBody()->getContents(), true); + $this->getLog()->debug('Mastodon: uri: ' . $uri . ' httpOptions: ' . json_encode($httpOptions)); + $this->getLog()->debug('Mastodon: count: ' . count($result)); // Expiry time for any media that is downloaded @@ -157,4 +172,29 @@ public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon { return null; } + + /** + * Get Mastodon Account Id from username + * @throws GuzzleException + */ + private function getAccountId(string $uri, string $username, DataProviderInterface $dataProvider) + { + $uri = rtrim($uri, '/').'/api/v1/accounts/lookup?'; + + $httpOptions = [ + 'timeout' => 20, // wait no more than 20 seconds + 'query' => [ + 'acct' => $username + ], + ]; + $response = $dataProvider + ->getGuzzleClient($httpOptions) + ->get($uri); + + $result = json_decode($response->getBody()->getContents(), true); + + $this->getLog()->debug('Mastodon: getAccountId: ID ' . $result['id']); + + return $result['id']; + } } diff --git a/modules/mastodon.xml b/modules/mastodon.xml index 5b340b88fa..20d426c9a7 100644 --- a/modules/mastodon.xml +++ b/modules/mastodon.xml @@ -26,7 +26,7 @@ \Xibo\Widget\MastodonProvider mastodon social-media - %hashtag%_%numItems%_%searchOn%_%onlyMedia%_%serverUrl% + %hashtag%_%numItems%_%searchOn%_%onlyMedia%_%serverUrl%_%userName% 1 1 1 @@ -70,6 +70,11 @@ Leave empty to use the one from settings. + + Username + Provide Mastodon username to get public statuses from the account. + + Count The number of posts to return (default = 15).