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 @@
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 @@
-
+
-
+
{{ 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)