1
0
Fork 0

Add subscriptions endpoints to REST API

This commit is contained in:
Chocobozzz 2018-08-16 15:25:20 +02:00
parent 4bda2e47bb
commit 06a05d5f47
36 changed files with 1039 additions and 94 deletions

View File

@ -3,9 +3,10 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
import { logger } from '../../helpers/logger'
import { processActivities } from '../../lib/activitypub/process/process'
import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
import { ActorModel } from '../../models/activitypub/actor'
import { VideoChannelModel } from '../../models/video/video-channel'
import { AccountModel } from '../../models/account/account'
const inboxRouter = express.Router()
@ -23,6 +24,13 @@ inboxRouter.post('/accounts/:name/inbox',
asyncMiddleware(activityPubValidator),
asyncMiddleware(inboxController)
)
inboxRouter.post('/video-channels/:name/inbox',
signatureValidator,
asyncMiddleware(checkSignature),
asyncMiddleware(localVideoChannelValidator),
asyncMiddleware(activityPubValidator),
asyncMiddleware(inboxController)
)
// ---------------------------------------------------------------------------
@ -49,16 +57,16 @@ async function inboxController (req: express.Request, res: express.Response, nex
activities = activities.filter(a => isActivityValid(a))
logger.debug('We keep %d activities.', activities.length, { activities })
let specificActor: ActorModel = undefined
let accountOrChannel: VideoChannelModel | AccountModel
if (res.locals.account) {
specificActor = res.locals.account
accountOrChannel = res.locals.account
} else if (res.locals.videoChannel) {
specificActor = res.locals.videoChannel
accountOrChannel = res.locals.videoChannel
}
logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url)
await processActivities(activities, res.locals.signature.actor, specificActor)
await processActivities(activities, res.locals.signature.actor, accountOrChannel ? accountOrChannel.Actor : undefined)
res.status(204).end()
}

View File

@ -5,11 +5,12 @@ import { activityPubCollectionPagination, activityPubContextify } from '../../he
import { logger } from '../../helpers/logger'
import { announceActivityData, createActivityData } from '../../lib/activitypub/send'
import { buildAudience } from '../../lib/activitypub/audience'
import { asyncMiddleware, localAccountValidator } from '../../middlewares'
import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
import { VideoModel } from '../../models/video/video'
import { activityPubResponse } from './utils'
import { VideoChannelModel } from '../../models/video/video-channel'
const outboxRouter = express.Router()
@ -18,6 +19,11 @@ outboxRouter.get('/accounts/:name/outbox',
asyncMiddleware(outboxController)
)
outboxRouter.get('/video-channels/:name/outbox',
localVideoChannelValidator,
asyncMiddleware(outboxController)
)
// ---------------------------------------------------------------------------
export {
@ -27,9 +33,9 @@ export {
// ---------------------------------------------------------------------------
async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
const account: AccountModel = res.locals.account
const actor = account.Actor
const actorOutboxUrl = account.Actor.url + '/outbox'
const accountOrVideoChannel: AccountModel | VideoChannelModel = res.locals.account || res.locals.videoChannel
const actor = accountOrVideoChannel.Actor
const actorOutboxUrl = actor.url + '/outbox'
logger.info('Receiving outbox request for %s.', actorOutboxUrl)

View File

@ -78,6 +78,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
includeLocalVideos: false,
categoryOneOf: req.query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf,
languageOneOf: req.query.languageOneOf,

View File

@ -36,7 +36,10 @@ export { searchRouter }
async function searchVideos (req: express.Request, res: express.Response) {
const query: VideosSearchQuery = req.query
const options = Object.assign(query, { nsfw: buildNSFWFilter(res, query.nsfw) })
const options = Object.assign(query, {
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw)
})
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
return res.json(getFormattedObjects(resultList.data, resultList.total))

View File

@ -2,7 +2,7 @@ import * as express from 'express'
import { UserRight } from '../../../../shared/models/users'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import { sequelizeTypescript } from '../../../initializers'
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
import { sendUndoFollow } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
@ -74,9 +74,16 @@ async function listFollowers (req: express.Request, res: express.Response, next:
async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) {
const hosts = req.body.hosts as string[]
const follower = await getServerActor()
for (const host of hosts) {
JobQueue.Instance.createJob({ type: 'activitypub-follow', payload: { host } })
const payload = {
host,
name: SERVER_ACTOR_NAME,
followerActorId: follower.id
}
JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
.catch(err => logger.error('Cannot create follow job for %s.', host, err))
}
@ -92,11 +99,5 @@ async function removeFollow (req: express.Request, res: express.Response, next:
await follow.destroy({ transaction: t })
})
// Destroy the actor that will destroy video channels, videos and video files too
// This could be long so don't wait this task
const following = follow.ActorFollowing
following.destroy()
.catch(err => logger.error('Cannot destroy actor that we do not follow anymore %s.', following.url, { err }))
return res.status(204).end()
}

View File

@ -29,7 +29,6 @@ import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPassw
import { UserModel } from '../../../models/account/user'
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
import { videosRouter } from '../videos'
import { meRouter } from './me'
const auditLogger = auditLoggerFactory('users')
@ -41,7 +40,7 @@ const loginRateLimiter = new RateLimit({
})
const usersRouter = express.Router()
videosRouter.use('/', meRouter)
usersRouter.use('/', meRouter)
usersRouter.get('/',
authenticate,

View File

@ -7,23 +7,35 @@ import { sendUpdateActor } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
authenticate,
commonVideosFiltersValidator,
paginationValidator,
setDefaultPagination,
setDefaultSort,
userSubscriptionAddValidator,
userSubscriptionRemoveValidator,
usersUpdateMeValidator,
usersVideoRatingValidator
} from '../../../middlewares'
import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
import {
deleteMeValidator,
userSubscriptionsSortValidator,
videoImportsSortValidator,
videosSortValidator
} from '../../../middlewares/validators'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { UserModel } from '../../../models/account/user'
import { VideoModel } from '../../../models/video/video'
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
import { createReqFiles } from '../../../helpers/express-utils'
import { buildNSFWFilter, createReqFiles } from '../../../helpers/express-utils'
import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
import { updateActorAvatarFile } from '../../../lib/avatar'
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
import { VideoImportModel } from '../../../models/video/video-import'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { JobQueue } from '../../../lib/job-queue'
import { logger } from '../../../helpers/logger'
const auditLogger = auditLoggerFactory('users-me')
@ -83,6 +95,40 @@ meRouter.post('/me/avatar/pick',
asyncMiddleware(updateMyAvatar)
)
// ##### Subscriptions part #####
meRouter.get('/me/subscriptions',
authenticate,
paginationValidator,
userSubscriptionsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(getUserSubscriptions)
)
meRouter.post('/me/subscriptions',
authenticate,
userSubscriptionAddValidator,
asyncMiddleware(addUserSubscription)
)
meRouter.delete('/me/subscriptions/:uri',
authenticate,
userSubscriptionRemoveValidator,
asyncMiddleware(deleteUserSubscription)
)
meRouter.get('/me/subscriptions/videos',
authenticate,
authenticate,
paginationValidator,
videosSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideosFiltersValidator,
asyncMiddleware(getUserSubscriptionVideos)
)
// ---------------------------------------------------------------------------
export {
@ -91,6 +137,62 @@ export {
// ---------------------------------------------------------------------------
async function addUserSubscription (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User as UserModel
const [ name, host ] = req.body.uri.split('@')
const payload = {
name,
host,
followerActorId: user.Account.Actor.id
}
JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
.catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err))
return res.status(204).end()
}
async function deleteUserSubscription (req: express.Request, res: express.Response) {
const subscription: ActorFollowModel = res.locals.subscription
await sequelizeTypescript.transaction(async t => {
return subscription.destroy({ transaction: t })
})
return res.type('json').status(204).end()
}
async function getUserSubscriptions (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User as UserModel
const actorId = user.Account.Actor.id
const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.User as UserModel
const resultList = await VideoModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
includeLocalVideos: false,
categoryOneOf: req.query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf,
languageOneOf: req.query.languageOneOf,
tagsOneOf: req.query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf,
nsfw: buildNSFWFilter(res, req.query.nsfw),
filter: req.query.filter as VideoFilter,
withFiles: false,
actorId: user.Account.Actor.id
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.User as UserModel
const resultList = await VideoModel.listUserVideosForApi(
@ -150,7 +252,7 @@ async function getUserVideoRating (req: express.Request, res: express.Response,
videoId,
rating
}
res.json(json)
return res.json(json)
}
async function deleteMe (req: express.Request, res: express.Response) {
@ -207,9 +309,5 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
oldUserAuditView
)
return res
.json({
avatar: avatar.toFormattedJSON()
})
.end()
return res.json({ avatar: avatar.toFormattedJSON() })
}

View File

@ -215,6 +215,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
includeLocalVideos: false,
categoryOneOf: req.query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf,
languageOneOf: req.query.languageOneOf,

View File

@ -414,6 +414,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
includeLocalVideos: true,
categoryOneOf: req.query.categoryOneOf,
licenceOneOf: req.query.licenceOneOf,
languageOneOf: req.query.languageOneOf,

View File

@ -96,6 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
start,
count: FEEDS.COUNT,
sort: req.query.sort,
includeLocalVideos: true,
nsfw,
filter: req.query.filter,
withFiles: true,

View File

@ -3,6 +3,7 @@ import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { exists } from '../misc'
import { truncate } from 'lodash'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { isHostValid } from '../servers'
function isActorEndpointsObjectValid (endpointObject: any) {
return isActivityPubUrlValid(endpointObject.sharedInbox)
@ -109,6 +110,15 @@ function normalizeActor (actor: any) {
return
}
function isValidActorHandle (handle: string) {
if (!exists(handle)) return false
const parts = handle.split('@')
if (parts.length !== 2) return false
return isHostValid(parts[1])
}
// ---------------------------------------------------------------------------
export {
@ -126,5 +136,6 @@ export {
isActorAcceptActivityValid,
isActorRejectActivityValid,
isActorDeleteActivityValid,
isActorUpdateActivityValid
isActorUpdateActivityValid,
isValidActorHandle
}

View File

@ -5,6 +5,7 @@ import * as validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { VideoChannelModel } from '../../models/video/video-channel'
import { exists } from './misc'
import { Response } from 'express'
const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
@ -20,6 +21,12 @@ function isVideoChannelSupportValid (value: string) {
return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT))
}
async function isLocalVideoChannelNameExist (name: string, res: Response) {
const videoChannel = await VideoChannelModel.loadLocalByName(name)
return processVideoChannelExist(videoChannel, res)
}
async function isVideoChannelExist (id: string, res: express.Response) {
let videoChannel: VideoChannelModel
if (validator.isInt(id)) {
@ -28,10 +35,24 @@ async function isVideoChannelExist (id: string, res: express.Response) {
videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id)
}
return processVideoChannelExist(videoChannel, res)
}
// ---------------------------------------------------------------------------
export {
isLocalVideoChannelNameExist,
isVideoChannelDescriptionValid,
isVideoChannelNameValid,
isVideoChannelSupportValid,
isVideoChannelExist
}
function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
if (!videoChannel) {
res.status(404)
.json({ error: 'Video channel not found' })
.end()
.json({ error: 'Video channel not found' })
.end()
return false
}
@ -39,12 +60,3 @@ async function isVideoChannelExist (id: string, res: express.Response) {
res.locals.videoChannel = videoChannel
return true
}
// ---------------------------------------------------------------------------
export {
isVideoChannelDescriptionValid,
isVideoChannelNameValid,
isVideoChannelSupportValid,
isVideoChannelExist
}

View File

@ -2,7 +2,7 @@ import { CONFIG, REMOTE_SCHEME } from '../../initializers'
import { sanitizeHost } from '../core-utils'
import { exists } from './misc'
function isWebfingerResourceValid (value: string) {
function isWebfingerLocalResourceValid (value: string) {
if (!exists(value)) return false
if (value.startsWith('acct:') === false) return false
@ -17,5 +17,5 @@ function isWebfingerResourceValid (value: string) {
// ---------------------------------------------------------------------------
export {
isWebfingerResourceValid
isWebfingerLocalResourceValid
}

View File

@ -11,15 +11,17 @@ const webfinger = new WebFinger({
request_timeout: 3000
})
async function loadActorUrlOrGetFromWebfinger (name: string, host: string) {
async function loadActorUrlOrGetFromWebfinger (uri: string) {
const [ name, host ] = uri.split('@')
const actor = await ActorModel.loadByNameAndHost(name, host)
if (actor) return actor.url
return getUrlFromWebfinger(name, host)
return getUrlFromWebfinger(uri)
}
async function getUrlFromWebfinger (name: string, host: string) {
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
async function getUrlFromWebfinger (uri: string) {
const webfingerData: WebFingerData = await webfingerLookup(uri)
return getLinkOrThrow(webfingerData)
}

View File

@ -31,6 +31,7 @@ const PAGINATION = {
// Sortable columns per schema
const SORTABLE_COLUMNS = {
USERS: [ 'id', 'username', 'createdAt' ],
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
ACCOUNTS: [ 'createdAt' ],
JOBS: [ 'createdAt' ],
VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],

View File

@ -352,7 +352,7 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
if (!actor.isOutdated()) return actor
try {
const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
const result = await fetchRemoteActor(actorUrl)
if (result === undefined) {
logger.warn('Cannot fetch remote actor in refresh actor.')

View File

@ -10,6 +10,11 @@ async function sendAccept (actorFollow: ActorFollowModel) {
const follower = actorFollow.ActorFollower
const me = actorFollow.ActorFollowing
if (!follower.serverId) { // This should never happen
logger.warn('Do not sending accept to local follower.')
return
}
logger.info('Creating job to accept follower %s.', follower.url)
const followUrl = getActorFollowActivityPubUrl(actorFollow)

View File

@ -33,6 +33,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
}
async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) {
if (!video.VideoChannel.Account.Actor.serverId) return // Local
const url = getVideoAbuseActivityPubUrl(videoAbuse)
logger.info('Creating job to send video abuse %s.', url)

View File

@ -9,6 +9,9 @@ function sendFollow (actorFollow: ActorFollowModel) {
const me = actorFollow.ActorFollower
const following = actorFollow.ActorFollowing
// Same server as ours
if (!following.serverId) return
logger.info('Creating job to send follow request to %s.', following.url)
const url = getActorFollowActivityPubUrl(actorFollow)

View File

@ -24,6 +24,9 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
const me = actorFollow.ActorFollower
const following = actorFollow.ActorFollowing
// Same server as ours
if (!following.serverId) return
logger.info('Creating job to send an unfollow request to %s.', following.url)
const followUrl = getActorFollowActivityPubUrl(actorFollow)

View File

@ -6,6 +6,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { JobQueue } from '../../job-queue'
import { VideoModel } from '../../../models/video/video'
import { getActorsInvolvedInVideo } from '../audience'
import { getServerActor } from '../../../helpers/utils'
async function forwardVideoRelatedActivity (
activity: Activity,
@ -118,14 +119,28 @@ async function computeFollowerUris (toActorFollower: ActorModel[], actorsExcepti
const toActorFollowerIds = toActorFollower.map(a => a.id)
const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl || f.inboxUrl)
const sharedInboxesException = await buildSharedInboxesException(actorsException)
return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
}
async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) {
const toActorSharedInboxesSet = new Set(toActors.map(a => a.sharedInboxUrl || a.inboxUrl))
const serverActor = await getServerActor()
const targetUrls = toActors
.filter(a => a.id !== serverActor.id) // Don't send to ourselves
.map(a => a.sharedInboxUrl || a.inboxUrl)
const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl || f.inboxUrl)
const toActorSharedInboxesSet = new Set(targetUrls)
const sharedInboxesException = await buildSharedInboxesException(actorsException)
return Array.from(toActorSharedInboxesSet)
.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
}
async function buildSharedInboxesException (actorsException: ActorModel[]) {
const serverActor = await getServerActor()
return actorsException
.map(f => f.sharedInboxUrl || f.inboxUrl)
.concat([ serverActor.sharedInboxUrl ])
}

View File

@ -1,7 +1,7 @@
import * as Bull from 'bull'
import { logger } from '../../../helpers/logger'
import { getServerActor } from '../../../helpers/utils'
import { REMOTE_SCHEME, sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
import { REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers'
import { sendFollow } from '../../activitypub/send'
import { sanitizeHost } from '../../../helpers/core-utils'
import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
@ -11,6 +11,8 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { ActorModel } from '../../../models/activitypub/actor'
export type ActivitypubFollowPayload = {
followerActorId: number
name: string
host: string
}
@ -22,10 +24,10 @@ async function processActivityPubFollow (job: Bull.Job) {
const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
const actorUrl = await loadActorUrlOrGetFromWebfinger(SERVER_ACTOR_NAME, sanitizedHost)
const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
const targetActor = await getOrCreateActorAndServerAndModel(actorUrl)
const fromActor = await getServerActor()
const fromActor = await ActorModel.load(payload.followerActorId)
return retryTransactionWrapper(follow, fromActor, targetActor)
}
@ -42,6 +44,9 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) {
throw new Error('Follower is the same than target actor.')
}
// Same server, direct accept
const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending'
return sequelizeTypescript.transaction(async t => {
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
where: {
@ -49,7 +54,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) {
targetActorId: targetActor.id
},
defaults: {
state: 'pending',
state,
actorId: fromActor.id,
targetActorId: targetActor.id
},

View File

@ -4,7 +4,7 @@ import { isTestInstance } from '../../helpers/core-utils'
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
import { logger } from '../../helpers/logger'
import { getServerActor } from '../../helpers/utils'
import { CONFIG } from '../../initializers'
import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { areValidationErrors } from './utils'
@ -38,7 +38,7 @@ const removeFollowingValidator = [
if (areValidationErrors(req, res)) return
const serverActor = await getServerActor()
const follow = await ActorFollowModel.loadByActorAndTargetHost(serverActor.id, req.params.host)
const follow = await ActorFollowModel.loadByActorAndTargetNameAndHost(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
if (!follow) {
return res

View File

@ -6,6 +6,7 @@ export * from './follows'
export * from './feeds'
export * from './sort'
export * from './users'
export * from './user-subscriptions'
export * from './videos'
export * from './video-abuses'
export * from './video-blacklist'

View File

@ -14,6 +14,7 @@ const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACK
const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@ -27,6 +28,7 @@ const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
// ---------------------------------------------------------------------------
@ -42,5 +44,6 @@ export {
followersSortValidator,
followingSortValidator,
jobsSortValidator,
videoCommentThreadsSortValidator
videoCommentThreadsSortValidator,
userSubscriptionsSortValidator
}

View File

@ -0,0 +1,58 @@
import * as express from 'express'
import 'express-validator'
import { body, param } from 'express-validator/check'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
import { UserModel } from '../../models/account/user'
import { CONFIG } from '../../initializers'
const userSubscriptionAddValidator = [
body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking userSubscriptionAddValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
return next()
}
]
const userSubscriptionRemoveValidator = [
param('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to unfollow'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking unfollow parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
let [ name, host ] = req.params.uri.split('@')
if (host === CONFIG.WEBSERVER.HOST) host = null
const user: UserModel = res.locals.oauth.token.User
const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHost(user.Account.Actor.id, name, host)
if (!subscription) {
return res
.status(404)
.json({
error: `Subscription ${req.params.uri} not found.`
})
.end()
}
res.locals.subscription = subscription
return next()
}
]
// ---------------------------------------------------------------------------
export {
userSubscriptionAddValidator,
userSubscriptionRemoveValidator
}
// ---------------------------------------------------------------------------

View File

@ -4,6 +4,7 @@ import { UserRight } from '../../../shared'
import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import {
isLocalVideoChannelNameExist,
isVideoChannelDescriptionValid,
isVideoChannelExist,
isVideoChannelNameValid,
@ -100,6 +101,19 @@ const videoChannelsGetValidator = [
}
]
const localVideoChannelValidator = [
param('name').custom(isVideoChannelNameValid).withMessage('Should have a valid video channel name'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isLocalVideoChannelNameExist(req.params.name, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
@ -107,7 +121,8 @@ export {
videoChannelsAddValidator,
videoChannelsUpdateValidator,
videoChannelsRemoveValidator,
videoChannelsGetValidator
videoChannelsGetValidator,
localVideoChannelValidator
}
// ---------------------------------------------------------------------------

View File

@ -1,13 +1,13 @@
import * as express from 'express'
import { query } from 'express-validator/check'
import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger'
import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
import { logger } from '../../helpers/logger'
import { ActorModel } from '../../models/activitypub/actor'
import { areValidationErrors } from './utils'
import { getHostWithPort } from '../../helpers/express-utils'
const webfingerValidator = [
query('resource').custom(isWebfingerResourceValid).withMessage('Should have a valid webfinger resource'),
query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking webfinger parameters', { parameters: req.query })

View File

@ -2,8 +2,21 @@ import * as Bluebird from 'bluebird'
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
import {
AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model,
Table, UpdatedAt
AfterCreate,
AfterDestroy,
AfterUpdate,
AllowNull,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
ForeignKey,
IsInt,
Max,
Model,
Table,
UpdatedAt
} from 'sequelize-typescript'
import { FollowState } from '../../../shared/models/actors'
import { AccountFollow } from '../../../shared/models/actors/follow.model'
@ -14,6 +27,7 @@ import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server'
import { getSort } from '../utils'
import { ActorModel } from './actor'
import { VideoChannelModel } from '../video/video-channel'
@Table({
tableName: 'actorFollow',
@ -151,7 +165,32 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
return ActorFollowModel.findOne(query)
}
static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
const actorFollowingPartInclude = {
model: ActorModel,
required: true,
as: 'ActorFollowing',
where: {
preferredUsername: targetName
}
}
if (targetHost === null) {
actorFollowingPartInclude.where['serverId'] = null
} else {
Object.assign(actorFollowingPartInclude, {
include: [
{
model: ServerModel,
required: true,
where: {
host: targetHost
}
}
]
})
}
const query = {
where: {
actorId
@ -162,20 +201,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
required: true,
as: 'ActorFollower'
},
{
model: ActorModel,
required: true,
as: 'ActorFollowing',
include: [
{
model: ServerModel,
required: true,
where: {
host: targetHost
}
}
]
}
actorFollowingPartInclude
],
transaction: t
}
@ -216,6 +242,39 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
})
}
static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
offset: start,
limit: count,
order: getSort(sort),
where: {
actorId: id
},
include: [
{
model: ActorModel,
as: 'ActorFollowing',
required: true,
include: [
{
model: VideoChannelModel,
required: true
}
]
}
]
}
return ActorFollowModel.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows.map(r => r.ActorFollowing.VideoChannel),
total: count
}
})
}
static listFollowersForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,

View File

@ -1,14 +1,27 @@
import {
AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Is, Model, Scopes, Table,
UpdatedAt, Default, DataType
AllowNull,
BeforeDestroy,
BelongsTo,
Column,
CreatedAt,
DataType,
Default,
DefaultScope,
ForeignKey,
HasMany,
Is,
Model,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript'
import { ActivityPubActor } from '../../../shared/models/activitypub'
import { VideoChannel } from '../../../shared/models/videos'
import {
isVideoChannelDescriptionValid, isVideoChannelNameValid,
isVideoChannelDescriptionValid,
isVideoChannelNameValid,
isVideoChannelSupportValid
} from '../../helpers/custom-validators/video-channels'
import { logger } from '../../helpers/logger'
import { sendDeleteActor } from '../../lib/activitypub/send'
import { AccountModel } from '../account/account'
import { ActorModel } from '../activitypub/actor'
@ -241,6 +254,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
.findById(id, options)
}
static loadLocalByName (name: string) {
const query = {
include: [
{
model: ActorModel,
required: true,
where: {
preferredUsername: name,
serverId: null
}
}
]
}
return VideoChannelModel.findOne(query)
}
toFormattedJSON (): VideoChannel {
const actor = this.Actor.toFormattedJSON()
const videoChannel = {
@ -251,8 +281,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
isLocal: this.Actor.isOwned(),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
ownerAccount: undefined,
videos: undefined
ownerAccount: undefined
}
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()

View File

@ -133,6 +133,7 @@ export enum ScopeNames {
type AvailableForListOptions = {
actorId: number,
includeLocalVideos: boolean,
filter?: VideoFilter,
categoryOneOf?: number[],
nsfw?: boolean,
@ -201,6 +202,15 @@ type AvailableForListOptions = {
// Force actorId to be a number to avoid SQL injections
const actorIdNumber = parseInt(options.actorId.toString(), 10)
let localVideosReq = ''
if (options.includeLocalVideos === true) {
localVideosReq = ' UNION ALL ' +
'SELECT "video"."id" AS "id" FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
'WHERE "actor"."serverId" IS NULL'
}
// FIXME: It would be more efficient to use a CTE so we join AFTER the filters, but sequelize does not support it...
const query: IFindOptions<VideoModel> = {
@ -214,12 +224,6 @@ type AvailableForListOptions = {
'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
' UNION ' +
'SELECT "video"."id" AS "id" FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
'WHERE "actor"."serverId" IS NULL ' +
' UNION ALL ' +
'SELECT "video"."id" AS "id" FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
@ -227,6 +231,7 @@ type AvailableForListOptions = {
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
localVideosReq +
')'
)
},
@ -825,6 +830,7 @@ export class VideoModel extends Model<VideoModel> {
count: number,
sort: string,
nsfw: boolean,
includeLocalVideos: boolean,
withFiles: boolean,
categoryOneOf?: number[],
licenceOneOf?: number[],
@ -833,7 +839,8 @@ export class VideoModel extends Model<VideoModel> {
tagsAllOf?: string[],
filter?: VideoFilter,
accountId?: number,
videoChannelId?: number
videoChannelId?: number,
actorId?: number
}) {
const query = {
offset: options.start,
@ -841,11 +848,12 @@ export class VideoModel extends Model<VideoModel> {
order: getSort(options.sort)
}
const serverActor = await getServerActor()
const actorId = options.actorId || (await getServerActor()).id
const scopes = {
method: [
ScopeNames.AVAILABLE_FOR_LIST, {
actorId: serverActor.id,
actorId,
nsfw: options.nsfw,
categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf,
@ -855,7 +863,8 @@ export class VideoModel extends Model<VideoModel> {
filter: options.filter,
withFiles: options.withFiles,
accountId: options.accountId,
videoChannelId: options.videoChannelId
videoChannelId: options.videoChannelId,
includeLocalVideos: options.includeLocalVideos
} as AvailableForListOptions
]
}
@ -871,6 +880,7 @@ export class VideoModel extends Model<VideoModel> {
}
static async searchAndPopulateAccountAndServer (options: {
includeLocalVideos: boolean
search?: string
start?: number
count?: number
@ -955,6 +965,7 @@ export class VideoModel extends Model<VideoModel> {
method: [
ScopeNames.AVAILABLE_FOR_LIST, {
actorId: serverActor.id,
includeLocalVideos: options.includeLocalVideos,
nsfw: options.nsfw,
categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf,

View File

@ -12,3 +12,4 @@ import './video-comments'
import './videos'
import './video-imports'
import './search'
import './user-subscriptions'

View File

@ -0,0 +1,220 @@
/* tslint:disable:no-unused-expression */
import 'mocha'
import {
createUser,
flushTests,
getMyUserInformation,
killallServers,
makeDeleteRequest,
makeGetRequest,
makePostBodyRequest,
runServer,
ServerInfo,
setAccessTokensToServers,
userLogin
} from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
describe('Test user subscriptions API validators', function () {
const path = '/api/v1/users/me/subscriptions'
let server: ServerInfo
let userAccessToken = ''
let userChannelUUID: string
// ---------------------------------------------------------------
before(async function () {
this.timeout(30000)
await flushTests()
server = await runServer(1)
await setAccessTokensToServers([ server ])
const user = {
username: 'user1',
password: 'my super password'
}
await createUser(server.url, server.accessToken, user.username, user.password)
userAccessToken = await userLogin(server, user)
{
const res = await getMyUserInformation(server.url, server.accessToken)
userChannelUUID = res.body.videoChannels[ 0 ].uuid
}
})
describe('When listing my subscriptions', function () {
it('Should fail with a bad start pagination', async function () {
await checkBadStartPagination(server.url, path, server.accessToken)
})
it('Should fail with a bad count pagination', async function () {
await checkBadCountPagination(server.url, path, server.accessToken)
})
it('Should fail with an incorrect sort', async function () {
await checkBadSortPagination(server.url, path, server.accessToken)
})
it('Should fail with a non authenticated user', async function () {
await makeGetRequest({
url: server.url,
path,
statusCodeExpected: 401
})
})
it('Should success with the correct parameters', async function () {
await await makeGetRequest({
url: server.url,
path,
token: userAccessToken,
statusCodeExpected: 200
})
})
})
describe('When listing my subscriptions videos', function () {
const path = '/api/v1/users/me/subscriptions/videos'
it('Should fail with a bad start pagination', async function () {
await checkBadStartPagination(server.url, path, server.accessToken)
})
it('Should fail with a bad count pagination', async function () {
await checkBadCountPagination(server.url, path, server.accessToken)
})
it('Should fail with an incorrect sort', async function () {
await checkBadSortPagination(server.url, path, server.accessToken)
})
it('Should fail with a non authenticated user', async function () {
await makeGetRequest({
url: server.url,
path,
statusCodeExpected: 401
})
})
it('Should success with the correct parameters', async function () {
await await makeGetRequest({
url: server.url,
path,
token: userAccessToken,
statusCodeExpected: 200
})
})
})
describe('When adding a subscription', function () {
it('Should fail with a non authenticated user', async function () {
await makePostBodyRequest({
url: server.url,
path,
fields: { uri: userChannelUUID + '@localhost:9001' },
statusCodeExpected: 401
})
})
it('Should fail with bad URIs', async function () {
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields: { uri: 'root' },
statusCodeExpected: 400
})
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields: { uri: 'root@' },
statusCodeExpected: 400
})
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields: { uri: 'root@hello@' },
statusCodeExpected: 400
})
})
it('Should success with the correct parameters', async function () {
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields: { uri: userChannelUUID + '@localhost:9001' },
statusCodeExpected: 204
})
})
})
describe('When removing a subscription', function () {
it('Should fail with a non authenticated user', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/' + userChannelUUID + '@localhost:9001',
statusCodeExpected: 401
})
})
it('Should fail with bad URIs', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/root',
token: server.accessToken,
statusCodeExpected: 400
})
await makeDeleteRequest({
url: server.url,
path: path + '/root@',
token: server.accessToken,
statusCodeExpected: 400
})
await makeDeleteRequest({
url: server.url,
path: path + '/root@hello@',
token: server.accessToken,
statusCodeExpected: 400
})
})
it('Should fail with an unknown subscription', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/root1@localhost:9001',
token: server.accessToken,
statusCodeExpected: 404
})
})
it('Should success with the correct parameters', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/' + userChannelUUID + '@localhost:9001',
token: server.accessToken,
statusCodeExpected: 204
})
})
})
after(async function () {
killallServers([ server ])
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -6,6 +6,7 @@ import './server/follows'
import './server/jobs'
import './videos/video-comments'
import './users/users-multiple-servers'
import './users/user-subscriptions'
import './server/handle-down'
import './videos/video-schedule-update'
import './videos/video-imports'

View File

@ -0,0 +1,312 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, userLogin } from '../../utils'
import { getMyUserInformation, killallServers, ServerInfo, uploadVideo } from '../../utils/index'
import { setAccessTokensToServers } from '../../utils/users/login'
import { Video, VideoChannel } from '../../../../shared/models/videos'
import { waitJobs } from '../../utils/server/jobs'
import {
addUserSubscription,
listUserSubscriptions,
listUserSubscriptionVideos,
removeUserSubscription
} from '../../utils/users/user-subscriptions'
const expect = chai.expect
describe('Test users subscriptions', function () {
let servers: ServerInfo[] = []
const users: { accessToken: string, videoChannelName: string }[] = []
let rootChannelNameServer1: string
before(async function () {
this.timeout(120000)
servers = await flushAndRunMultipleServers(3)
// Get the access tokens
await setAccessTokensToServers(servers)
// Server 1 and server 2 follow each other
await doubleFollow(servers[0], servers[1])
const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
rootChannelNameServer1 = res.body.videoChannels[0].name
{
for (const server of servers) {
const user = { username: 'user' + server.serverNumber, password: 'password' }
await createUser(server.url, server.accessToken, user.username, user.password)
const accessToken = await userLogin(server, user)
const res = await getMyUserInformation(server.url, accessToken)
const videoChannels: VideoChannel[] = res.body.videoChannels
users.push({ accessToken, videoChannelName: videoChannels[0].name })
const videoName1 = 'video 1-' + server.serverNumber
await uploadVideo(server.url, accessToken, { name: videoName1 })
const videoName2 = 'video 2-' + server.serverNumber
await uploadVideo(server.url, accessToken, { name: videoName2 })
}
}
await waitJobs(servers)
})
it('Should display videos of server 2 on server 1', async function () {
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(4)
})
it('User of server 1 should follow user of server 3 and root of server 1', async function () {
this.timeout(30000)
await addUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003')
await addUserSubscription(servers[0].url, users[0].accessToken, rootChannelNameServer1 + '@localhost:9001')
await waitJobs(servers)
await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
await waitJobs(servers)
})
it('Should not display videos of server 3 on server 1', async function () {
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(4)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
expect(video.name).to.not.contain('video server 3 added after follow')
}
})
it('Should list subscriptions', async function () {
{
const res = await listUserSubscriptions(servers[0].url, 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)
}
{
const res = await listUserSubscriptions(servers[0].url, users[0].accessToken)
expect(res.body.total).to.equal(2)
const subscriptions: VideoChannel[] = res.body.data
expect(subscriptions).to.be.an('array')
expect(subscriptions).to.have.lengthOf(2)
expect(subscriptions[0].name).to.equal(users[2].videoChannelName)
expect(subscriptions[1].name).to.equal(rootChannelNameServer1)
}
})
it('Should list subscription videos', async function () {
{
const res = await listUserSubscriptionVideos(servers[0].url, 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)
}
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(3)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(3)
expect(videos[0].name).to.equal('video 1-3')
expect(videos[1].name).to.equal('video 2-3')
expect(videos[2].name).to.equal('video server 3 added after follow')
}
})
it('Should upload a video by root on server 1 and see it in the subscription videos', async function () {
this.timeout(30000)
const videoName = 'video server 1 added after follow'
await uploadVideo(servers[0].url, servers[0].accessToken, { name: videoName })
await waitJobs(servers)
{
const res = await listUserSubscriptionVideos(servers[0].url, 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)
}
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(4)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(4)
expect(videos[0].name).to.equal('video 1-3')
expect(videos[1].name).to.equal('video 2-3')
expect(videos[2].name).to.equal('video server 3 added after follow')
expect(videos[3].name).to.equal('video server 1 added after follow')
}
{
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(5)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
expect(video.name).to.not.contain('video server 3 added after follow')
}
}
})
it('Should have server 1 follow server 3 and display server 3 videos', async function () {
this.timeout(30000)
await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
await waitJobs(servers)
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(8)
const names = [ '1-3', '2-3', 'video server 3 added after follow' ]
for (const name of names) {
const video = res.body.data.find(v => v.name.indexOf(name) === -1)
expect(video).to.not.be.undefined
}
})
it('Should remove follow server 1 -> server 3 and hide server 3 videos', async function () {
this.timeout(30000)
await unfollow(servers[0].url, servers[0].accessToken, servers[2])
await waitJobs(servers)
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(5)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
expect(video.name).to.not.contain('video server 3 added after follow')
}
})
it('Should still list subscription videos', async function () {
{
const res = await listUserSubscriptionVideos(servers[0].url, 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)
}
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(4)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(4)
expect(videos[0].name).to.equal('video 1-3')
expect(videos[1].name).to.equal('video 2-3')
expect(videos[2].name).to.equal('video server 3 added after follow')
expect(videos[3].name).to.equal('video server 1 added after follow')
}
})
it('Should remove user of server 3 subscription', async function () {
await removeUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003')
await waitJobs(servers)
})
it('Should not display its videos anymore', async function () {
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(1)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(1)
expect(videos[0].name).to.equal('video server 1 added after follow')
}
})
it('Should remove the root subscription and not display the videos anymore', async function () {
await removeUserSubscription(servers[0].url, users[0].accessToken, rootChannelNameServer1 + '@localhost:9001')
await waitJobs(servers)
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(0)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(0)
}
})
it('Should correctly display public videos on server 1', async function () {
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(5)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
expect(video.name).to.not.contain('video server 3 added after follow')
}
})
it('Should follow user of server 3 again', async function () {
this.timeout(30000)
await addUserSubscription(servers[0].url, users[0].accessToken, users[2].videoChannelName + '@localhost:9003')
await waitJobs(servers)
{
const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
expect(res.body.total).to.equal(3)
const videos: Video[] = res.body.data
expect(videos).to.be.an('array')
expect(videos).to.have.lengthOf(3)
expect(videos[0].name).to.equal('video 1-3')
expect(videos[1].name).to.equal('video 2-3')
expect(videos[2].name).to.equal('video server 3 added after follow')
}
{
const res = await getVideosList(servers[0].url)
expect(res.body.total).to.equal(5)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
expect(video.name).to.not.contain('video server 3 added after follow')
}
}
})
after(async function () {
killallServers(servers)
})
})

View File

@ -0,0 +1,57 @@
import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../'
function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = 204) {
const path = '/api/v1/users/me/subscriptions'
return makePostBodyRequest({
url,
path,
token,
statusCodeExpected,
fields: { uri: targetUri }
})
}
function listUserSubscriptions (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
const path = '/api/v1/users/me/subscriptions'
return makeGetRequest({
url,
path,
token,
statusCodeExpected,
query: { sort }
})
}
function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
const path = '/api/v1/users/me/subscriptions/videos'
return makeGetRequest({
url,
path,
token,
statusCodeExpected,
query: { sort }
})
}
function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) {
const path = '/api/v1/users/me/subscriptions/' + uri
return makeDeleteRequest({
url,
path,
token,
statusCodeExpected
})
}
// ---------------------------------------------------------------------------
export {
addUserSubscription,
listUserSubscriptions,
listUserSubscriptionVideos,
removeUserSubscription
}