1
0
Fork 0

Search video channel handle/uri

This commit is contained in:
Chocobozzz 2018-08-24 11:04:02 +02:00
parent aa55a4da42
commit f5b0af50c8
18 changed files with 303 additions and 65 deletions

View file

@ -1,6 +1,6 @@
<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" />

View file

@ -1,13 +1,13 @@
<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>

View file

@ -7,14 +7,14 @@
<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>
@ -23,7 +23,7 @@
<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>

View file

@ -11,11 +11,4 @@
.actor-name {
flex-grow: 1;
}
my-subscribe-button {
/deep/ span[role=button] {
padding: 7px 12px;
font-size: 16px;
}
}
}

View file

@ -27,14 +27,14 @@
</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>

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

@ -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<FetchRemoteActorResult> {
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<FetchRemoteActorResu
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)
@ -307,12 +313,15 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
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
}
}
}
@ -355,7 +364,14 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
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

View file

@ -325,15 +325,13 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
},
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<ActorFollowModel> {
required: true
},
{
model: AccountModel,
model: AccountModel.unscoped(),
required: true,
include: [
{

View file

@ -50,7 +50,9 @@ export const unusedActorAttributesForAPI = [
'sharedInboxUrl',
'followersUrl',
'followingUrl',
'url'
'url',
'createdAt',
'updatedAt'
]
@DefaultScope({

View file

@ -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 ])

View file

@ -1,2 +1,3 @@
import './search-activitypub-video-channels'
import './search-activitypub-videos'
import './search-videos'

View file

@ -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()
}
})
})

View file

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

View file

@ -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
}

View file

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