<div *ngIf="account" class="row">
<a
- *ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.name ]"
+ *ngFor="let videoChannel of videoChannels" [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]"
class="video-channel" i18n-title title="See this video channel"
>
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
<div *ngFor="let videoChannel of videoChannels" class="video-channel">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
</a>
<div class="video-channel-info">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]" class="video-channel-names" i18n-title title="Go to the channel">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Go to the channel">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
- <div class="video-channel-name">{{ videoChannel.name }}</div>
+ <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
</a>
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
<div class="video-channels">
<div *ngFor="let videoChannel of videoChannels" class="video-channel">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
</a>
<div class="video-channel-info">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]" class="video-channel-names" i18n-title title="Go to the channel">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Go to the channel">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
- <div class="video-channel-name">{{ videoChannel.name }}</div>
+ <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
</a>
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
<div class="video-channel-buttons">
<my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
- <my-edit-button [routerLink]="[ 'update', videoChannel.name ]"></my-edit-button>
+ <my-edit-button [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
</div>
</div>
</div>
.actor-name {
flex-grow: 1;
}
-
- my-subscribe-button {
- /deep/ span[role=button] {
- padding: 7px 12px;
- font-size: 16px;
- }
- }
}
\ No newline at end of file
</div>
<div *ngFor="let videoChannel of videoChannels" class="entry video-channel">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
</a>
<div class="video-channel-info">
- <a [routerLink]="[ '/video-channels', videoChannel.name ]" class="video-channel-names">
+ <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
- <div class="video-channel-name">{{ videoChannel.name }}</div>
+ <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
</a>
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
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'
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 {
params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp
- .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.name + '/videos', { params })
+ .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
.pipe(
switchMap(res => this.extractVideos(res)),
catchError(err => this.restExtractor.handleError(err))
videoChannelsSearchSortValidator,
setDefaultSearchSort,
optionalAuthenticate,
- commonVideosFiltersValidator,
videoChannelsSearchValidator,
asyncMiddleware(searchVideoChannels)
)
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)
}
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 (isUserAbleToSearchRemoteURI(res)) {
- let uri = search
- if (isHandleSearch) uri = await loadActorUrlOrGetFromWebfinger(search)
+ if (isWebfingerSearch) uri = await loadActorUrlOrGetFromWebfinger(search)
- const actor = await getOrCreateActorAndServerAndModel(uri)
- videoChannel = actor.VideoChannel
+ if (isUserAbleToSearchRemoteURI(res)) {
+ 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({
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)
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,
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)
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 = {
// 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
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) {
avatarName?: string
attributedTo: ActivityPubAttributedTo[]
}
-async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
+async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
const options = {
uri: actorUrl,
method: 'GET',
if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
- return undefined
+ return { result: undefined, statusCode: requestResult.response.statusCode }
}
const followersCount = await fetchActorTotalItems(actorJSON.followers)
const name = actorJSON.name || actorJSON.preferredUsername
return {
- actor,
- name,
- avatarName,
- summary: actorJSON.summary,
- support: actorJSON.support,
- attributedTo: actorJSON.attributedTo
+ statusCode: requestResult.response.statusCode,
+ result: {
+ actor,
+ name,
+ avatarName,
+ summary: actorJSON.summary,
+ support: actorJSON.support,
+ attributedTo: actorJSON.attributedTo
+ }
}
}
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
},
include: [
{
- attributes: {
- exclude: unusedActorAttributesForAPI
- },
- model: ActorModel,
+ attributes: [ 'id' ],
+ model: ActorModel.unscoped(),
as: 'ActorFollowing',
required: true,
include: [
{
- model: VideoChannelModel,
+ model: VideoChannelModel.unscoped(),
required: true,
include: [
{
required: true
},
{
- model: AccountModel,
+ model: AccountModel.unscoped(),
required: true,
include: [
{
'sharedInboxUrl',
'followersUrl',
'followingUrl',
- 'url'
+ 'url',
+ 'createdAt',
+ 'updatedAt'
]
@DefaultScope({
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
// ---------------------------------------------------------------
})
describe('When searching videos', function () {
+ const path = '/api/v1/search/videos/'
+
const query = {
search: 'coucou'
}
})
})
+ 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 ])
+import './search-activitypub-video-channels'
import './search-activitypub-videos'
import './search-videos'
--- /dev/null
+/* 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()
+ }
+ })
+})
}
{
+ // Without token
const res = await searchVideo(servers[0].url, 'http://localhost:9002/videos/watch/' + videoServer2UUID)
expect(res.body.total).to.equal(0)
--- /dev/null
+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
+}
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
.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)
.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)