diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index 114a9e517..c3ef1d894 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html @@ -1,6 +1,6 @@
Avatar diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html index 3752de49f..2d76990f7 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html @@ -1,13 +1,13 @@
- + Avatar
- +
{{ videoChannel.displayName }}
-
{{ videoChannel.name }}
+
{{ videoChannel.nameWithHost }}
{{ videoChannel.followersCount }} subscribers
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index 548645a76..df74b19b6 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html @@ -7,14 +7,14 @@
- + Avatar
- +
{{ videoChannel.displayName }}
-
{{ videoChannel.name }}
+
{{ videoChannel.nameWithHost }}
{{ videoChannel.followersCount }} subscribers
@@ -23,7 +23,7 @@
- +
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index a63b1ec06..711b1839d 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss @@ -11,11 +11,4 @@ .actor-name { flex-grow: 1; } - - my-subscribe-button { - /deep/ span[role=button] { - padding: 7px 12px; - font-size: 16px; - } - } } \ No newline at end of file diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index 83d014987..0d09ebbe6 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html @@ -27,14 +27,14 @@
- + Avatar
- +
{{ videoChannel.displayName }}
-
{{ videoChannel.name }}
+
{{ videoChannel.nameWithHost }}
{{ videoChannel.followersCount }} subscribers
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 1a934c8e2..558db9543 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -4,14 +4,7 @@ import { Injectable } from '@angular/core' import { Observable } from 'rxjs' import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared' import { ResultList } from '../../../../../shared/models/result-list.model' -import { - UserVideoRate, - UserVideoRateUpdate, - VideoChannel, - VideoFilter, - VideoRateType, - VideoUpdate -} from '../../../../../shared/models/videos' +import { UserVideoRate, UserVideoRateUpdate, VideoFilter, VideoRateType, VideoUpdate } from '../../../../../shared/models/videos' import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' import { environment } from '../../../environments/environment' import { ComponentPagination } from '../rest/component-pagination.model' @@ -28,6 +21,7 @@ import { AccountService } from '@app/shared/account/account.service' import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' import { ServerService } from '@app/core' import { UserSubscriptionService } from '@app/shared/user-subscription' +import { VideoChannel } from '@app/shared/video-channel/video-channel.model' @Injectable() export class VideoService { @@ -151,7 +145,7 @@ export class VideoService { params = this.restService.addRestGetParams(params, pagination, sort) return this.authHttp - .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.name + '/videos', { params }) + .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) .pipe( switchMap(res => this.extractVideos(res)), catchError(err => this.restExtractor.handleError(err)) diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index 87aa5d76f..959d79855 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts @@ -41,7 +41,6 @@ searchRouter.get('/video-channels', videoChannelsSearchSortValidator, setDefaultSearchSort, optionalAuthenticate, - commonVideosFiltersValidator, videoChannelsSearchValidator, asyncMiddleware(searchVideoChannels) ) @@ -59,9 +58,9 @@ function searchVideoChannels (req: express.Request, res: express.Response) { const isURISearch = search.startsWith('http://') || search.startsWith('https://') const parts = search.split('@') - const isHandleSearch = parts.length === 2 && parts.every(p => p.indexOf(' ') === -1) + const isWebfingerSearch = parts.length === 2 && parts.every(p => p.indexOf(' ') === -1) - if (isURISearch || isHandleSearch) return searchVideoChannelURI(search, isHandleSearch, res) + if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) return searchVideoChannelsDB(query, res) } @@ -81,17 +80,21 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr return res.json(getFormattedObjects(resultList.data, resultList.total)) } -async function searchVideoChannelURI (search: string, isHandleSearch: boolean, res: express.Response) { +async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { let videoChannel: VideoChannelModel + let uri = search + + if (isWebfingerSearch) uri = await loadActorUrlOrGetFromWebfinger(search) if (isUserAbleToSearchRemoteURI(res)) { - let uri = search - if (isHandleSearch) uri = await loadActorUrlOrGetFromWebfinger(search) - - const actor = await getOrCreateActorAndServerAndModel(uri) - videoChannel = actor.VideoChannel + try { + const actor = await getOrCreateActorAndServerAndModel(uri) + videoChannel = actor.VideoChannel + } catch (err) { + logger.info('Cannot search remote video channel %s.', uri, { err }) + } } else { - videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(search) + videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(uri) } return res.json({ @@ -138,7 +141,7 @@ async function searchVideoURI (url: string, res: express.Response) { const result = await getOrCreateVideoAndAccountAndChannel(url, syncParam) video = result ? result.video : undefined } catch (err) { - logger.info('Cannot search remote video %s.', url) + logger.info('Cannot search remote video %s.', url, { err }) } } else { video = await VideoModel.loadByUrlAndPopulateAccount(url) diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts index 5c60de10c..10fcec462 100644 --- a/server/helpers/webfinger.ts +++ b/server/helpers/webfinger.ts @@ -3,6 +3,7 @@ import { WebFingerData } from '../../shared' import { ActorModel } from '../models/activitypub/actor' import { isTestInstance } from './core-utils' import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' +import { CONFIG } from '../initializers' const webfinger = new WebFinger({ webfist_fallback: false, @@ -13,8 +14,14 @@ const webfinger = new WebFinger({ async function loadActorUrlOrGetFromWebfinger (uri: string) { const [ name, host ] = uri.split('@') + let actor: ActorModel + + if (host === CONFIG.WEBSERVER.HOST) { + actor = await ActorModel.loadLocalByName(name) + } else { + actor = await ActorModel.loadByNameAndHost(name, host) + } - const actor = await ActorModel.loadByNameAndHost(name, host) if (actor) return actor.url return getUrlFromWebfinger(uri) diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9beb9b7c2..a0dd78f42 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -44,7 +44,7 @@ const SORTABLE_COLUMNS = { FOLLOWING: [ 'createdAt' ], VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ], - VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName' ] + VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ] } const OAUTH_LIFETIME = { diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 9922229d2..22e1c9f19 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -48,7 +48,7 @@ async function getOrCreateActorAndServerAndModel (activityActor: string | Activi // We don't have this actor in our database, fetch it on remote if (!actor) { - const result = await fetchRemoteActor(actorUrl) + const { result } = await fetchRemoteActor(actorUrl) if (result === undefined) throw new Error('Cannot fetch remote actor.') // Create the attributed to actor @@ -70,7 +70,13 @@ async function getOrCreateActorAndServerAndModel (activityActor: string | Activi actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) } - return retryTransactionWrapper(refreshActorIfNeeded, actor) + if (actor.Account) actor.Account.Actor = actor + if (actor.VideoChannel) actor.VideoChannel.Actor = actor + + actor = await retryTransactionWrapper(refreshActorIfNeeded, actor) + if (!actor) throw new Error('Actor ' + actor.url + ' does not exist anymore.') + + return actor } function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { @@ -264,7 +270,7 @@ type FetchRemoteActorResult = { avatarName?: string attributedTo: ActivityPubAttributedTo[] } -async function fetchRemoteActor (actorUrl: string): Promise { +async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { const options = { uri: actorUrl, method: 'GET', @@ -281,7 +287,7 @@ async function fetchRemoteActor (actorUrl: string): Promise { try { const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) - const result = await fetchRemoteActor(actorUrl) + const { result, statusCode } = await fetchRemoteActor(actorUrl) + + if (statusCode === 404) { + logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) + actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() + return undefined + } + if (result === undefined) { logger.warn('Cannot fetch remote actor in refresh actor.') return actor diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index ebb2d47c2..8bc095997 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts @@ -325,15 +325,13 @@ export class ActorFollowModel extends Model { }, include: [ { - attributes: { - exclude: unusedActorAttributesForAPI - }, - model: ActorModel, + attributes: [ 'id' ], + model: ActorModel.unscoped(), as: 'ActorFollowing', required: true, include: [ { - model: VideoChannelModel, + model: VideoChannelModel.unscoped(), required: true, include: [ { @@ -344,7 +342,7 @@ export class ActorFollowModel extends Model { required: true }, { - model: AccountModel, + model: AccountModel.unscoped(), required: true, include: [ { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index e16bd5d79..119d0c1da 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -50,7 +50,9 @@ export const unusedActorAttributesForAPI = [ 'sharedInboxUrl', 'followersUrl', 'followingUrl', - 'url' + 'url', + 'createdAt', + 'updatedAt' ] @DefaultScope({ diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index d35eac7fe..eabf602ac 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts @@ -6,7 +6,6 @@ import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' describe('Test videos API validator', function () { - const path = '/api/v1/search/videos/' let server: ServerInfo // --------------------------------------------------------------- @@ -20,6 +19,8 @@ describe('Test videos API validator', function () { }) describe('When searching videos', function () { + const path = '/api/v1/search/videos/' + const query = { search: 'coucou' } @@ -111,6 +112,30 @@ describe('Test videos API validator', function () { }) }) + describe('When searching video channels', function () { + const path = '/api/v1/search/video-channels/' + + const query = { + search: 'coucou' + } + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, null, query) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, null, query) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, null, query) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, query, statusCodeExpected: 200 }) + }) + }) + after(async function () { killallServers([ server ]) diff --git a/server/tests/api/search/index.ts b/server/tests/api/search/index.ts index 64b3d0910..d573c8a9e 100644 --- a/server/tests/api/search/index.ts +++ b/server/tests/api/search/index.ts @@ -1,2 +1,3 @@ +import './search-activitypub-video-channels' import './search-activitypub-videos' import './search-videos' diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts new file mode 100644 index 000000000..512cb32fd --- /dev/null +++ b/server/tests/api/search/search-activitypub-video-channels.ts @@ -0,0 +1,176 @@ +/* tslint:disable:no-unused-expression */ + +import * as chai from 'chai' +import 'mocha' +import { + addVideoChannel, + createUser, + deleteVideoChannel, + flushAndRunMultipleServers, + flushTests, + getVideoChannelsList, + killallServers, + ServerInfo, + setAccessTokensToServers, + updateMyUser, + updateVideoChannel, + uploadVideo, + userLogin, + wait +} from '../../utils' +import { waitJobs } from '../../utils/server/jobs' +import { VideoChannel } from '../../../../shared/models/videos' +import { searchVideoChannel } from '../../utils/search/video-channels' + +const expect = chai.expect + +describe('Test a ActivityPub video channels search', function () { + let servers: ServerInfo[] + let userServer2Token: string + + before(async function () { + this.timeout(120000) + + await flushTests() + + servers = await flushAndRunMultipleServers(2) + + await setAccessTokensToServers(servers) + + { + await createUser(servers[0].url, servers[0].accessToken, 'user1_server1', 'password') + const channel = { + name: 'channel1_server1', + displayName: 'Channel 1 server 1' + } + await addVideoChannel(servers[0].url, servers[0].accessToken, channel) + } + + { + const user = { username: 'user1_server2', password: 'password' } + await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) + userServer2Token = await userLogin(servers[1], user) + + const channel = { + name: 'channel1_server2', + displayName: 'Channel 1 server 2' + } + const resChannel = await addVideoChannel(servers[1].url, userServer2Token, channel) + const channelId = resChannel.body.videoChannel.id + + await uploadVideo(servers[1].url, userServer2Token, { name: 'video 1 server 2', channelId }) + await uploadVideo(servers[1].url, userServer2Token, { name: 'video 2 server 2', channelId }) + } + + await waitJobs(servers) + }) + + it('Should not find a remote video channel', async function () { + { + const search = 'http://localhost:9002/video-channels/channel1_server3' + const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) + + expect(res.body.total).to.equal(0) + expect(res.body.data).to.be.an('array') + expect(res.body.data).to.have.lengthOf(0) + } + + { + // Without token + const search = 'http://localhost:9002/video-channels/channel1_server2' + const res = await searchVideoChannel(servers[0].url, search) + + expect(res.body.total).to.equal(0) + expect(res.body.data).to.be.an('array') + expect(res.body.data).to.have.lengthOf(0) + } + }) + + it('Should search a local video channel', async function () { + const searches = [ + 'http://localhost:9001/video-channels/channel1_server1', + 'channel1_server1@localhost:9001' + ] + + for (const search of searches) { + const res = await searchVideoChannel(servers[ 0 ].url, search) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.be.an('array') + expect(res.body.data).to.have.lengthOf(1) + expect(res.body.data[ 0 ].name).to.equal('channel1_server1') + expect(res.body.data[ 0 ].displayName).to.equal('Channel 1 server 1') + } + }) + + it('Should search a remote video channel with URL or handle', async function () { + const searches = [ + 'http://localhost:9002/video-channels/channel1_server2', + 'channel1_server2@localhost:9002' + ] + + for (const search of searches) { + const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.be.an('array') + expect(res.body.data).to.have.lengthOf(1) + expect(res.body.data[ 0 ].name).to.equal('channel1_server2') + expect(res.body.data[ 0 ].displayName).to.equal('Channel 1 server 2') + } + }) + + it('Should not list this remote video channel', async function () { + const res = await getVideoChannelsList(servers[0].url, 0, 5) + expect(res.body.total).to.equal(3) + expect(res.body.data).to.have.lengthOf(3) + expect(res.body.data[0].name).to.equal('channel1_server1') + expect(res.body.data[1].name).to.equal('user1_server1_channel') + expect(res.body.data[2].name).to.equal('root_channel') + }) + + it('Should update video channel of server 2, and refresh it on server 1', async function () { + this.timeout(60000) + + await updateVideoChannel(servers[1].url, userServer2Token, 'channel1_server2', { displayName: 'channel updated' }) + await updateMyUser({ url: servers[1].url, accessToken: userServer2Token, displayName: 'user updated' }) + + await waitJobs(servers) + // Expire video channel + await wait(10000) + + const search = 'http://localhost:9002/video-channels/channel1_server2' + const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken) + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + + const videoChannel: VideoChannel = res.body.data[0] + expect(videoChannel.displayName).to.equal('channel updated') + + // We don't return the owner account for now + // expect(videoChannel.ownerAccount.displayName).to.equal('user updated') + }) + + it('Should delete video channel of server 2, and delete it on server 1', async function () { + this.timeout(60000) + + await deleteVideoChannel(servers[1].url, userServer2Token, 'channel1_server2') + + await waitJobs(servers) + // Expire video + await wait(10000) + + const res = await searchVideoChannel(servers[0].url, 'http://localhost:9002/video-channels/channel1_server2', servers[0].accessToken) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + }) + + after(async function () { + killallServers(servers) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index 6dc792696..28f4fac50 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts @@ -59,6 +59,7 @@ describe('Test a ActivityPub videos search', function () { } { + // Without token const res = await searchVideo(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID) expect(res.body.total).to.equal(0) diff --git a/server/tests/utils/search/video-channels.ts b/server/tests/utils/search/video-channels.ts new file mode 100644 index 000000000..0532134ae --- /dev/null +++ b/server/tests/utils/search/video-channels.ts @@ -0,0 +1,22 @@ +import { makeGetRequest } from '../requests/requests' + +function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = 200) { + const path = '/api/v1/search/video-channels' + + return makeGetRequest({ + url, + path, + query: { + sort: '-createdAt', + search + }, + token, + statusCodeExpected + }) +} + +// --------------------------------------------------------------------------- + +export { + searchVideoChannel +} diff --git a/server/tests/utils/videos/video-channels.ts b/server/tests/utils/videos/video-channels.ts index 1eea22b31..092985777 100644 --- a/server/tests/utils/videos/video-channels.ts +++ b/server/tests/utils/videos/video-channels.ts @@ -54,12 +54,12 @@ function addVideoChannel ( function updateVideoChannel ( url: string, token: string, - channelId: number | string, + channelName: string, attributes: VideoChannelUpdate, expectedStatus = 204 ) { const body = {} - const path = '/api/v1/video-channels/' + channelId + const path = '/api/v1/video-channels/' + channelName if (attributes.displayName) body['displayName'] = attributes.displayName if (attributes.description) body['description'] = attributes.description @@ -73,8 +73,8 @@ function updateVideoChannel ( .expect(expectedStatus) } -function deleteVideoChannel (url: string, token: string, channelId: number | string, expectedStatus = 204) { - const path = '/api/v1/video-channels/' + channelId +function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = 204) { + const path = '/api/v1/video-channels/' + channelName return request(url) .delete(path) @@ -83,8 +83,8 @@ function deleteVideoChannel (url: string, token: string, channelId: number | str .expect(expectedStatus) } -function getVideoChannel (url: string, channelId: number | string) { - const path = '/api/v1/video-channels/' + channelId +function getVideoChannel (url: string, channelName: string) { + const path = '/api/v1/video-channels/' + channelName return request(url) .get(path)