Add subscriptions endpoints to REST API
This commit is contained in:
parent
4bda2e47bb
commit
06a05d5f47
|
@ -3,9 +3,10 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi
|
||||||
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
|
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { processActivities } from '../../lib/activitypub/process/process'
|
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 { 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()
|
const inboxRouter = express.Router()
|
||||||
|
|
||||||
|
@ -23,6 +24,13 @@ inboxRouter.post('/accounts/:name/inbox',
|
||||||
asyncMiddleware(activityPubValidator),
|
asyncMiddleware(activityPubValidator),
|
||||||
asyncMiddleware(inboxController)
|
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))
|
activities = activities.filter(a => isActivityValid(a))
|
||||||
logger.debug('We keep %d activities.', activities.length, { activities })
|
logger.debug('We keep %d activities.', activities.length, { activities })
|
||||||
|
|
||||||
let specificActor: ActorModel = undefined
|
let accountOrChannel: VideoChannelModel | AccountModel
|
||||||
if (res.locals.account) {
|
if (res.locals.account) {
|
||||||
specificActor = res.locals.account
|
accountOrChannel = res.locals.account
|
||||||
} else if (res.locals.videoChannel) {
|
} 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)
|
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()
|
res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,12 @@ import { activityPubCollectionPagination, activityPubContextify } from '../../he
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { announceActivityData, createActivityData } from '../../lib/activitypub/send'
|
import { announceActivityData, createActivityData } from '../../lib/activitypub/send'
|
||||||
import { buildAudience } from '../../lib/activitypub/audience'
|
import { buildAudience } from '../../lib/activitypub/audience'
|
||||||
import { asyncMiddleware, localAccountValidator } from '../../middlewares'
|
import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares'
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
import { activityPubResponse } from './utils'
|
import { activityPubResponse } from './utils'
|
||||||
|
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||||
|
|
||||||
const outboxRouter = express.Router()
|
const outboxRouter = express.Router()
|
||||||
|
|
||||||
|
@ -18,6 +19,11 @@ outboxRouter.get('/accounts/:name/outbox',
|
||||||
asyncMiddleware(outboxController)
|
asyncMiddleware(outboxController)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
outboxRouter.get('/video-channels/:name/outbox',
|
||||||
|
localVideoChannelValidator,
|
||||||
|
asyncMiddleware(outboxController)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -27,9 +33,9 @@ export {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const account: AccountModel = res.locals.account
|
const accountOrVideoChannel: AccountModel | VideoChannelModel = res.locals.account || res.locals.videoChannel
|
||||||
const actor = account.Actor
|
const actor = accountOrVideoChannel.Actor
|
||||||
const actorOutboxUrl = account.Actor.url + '/outbox'
|
const actorOutboxUrl = actor.url + '/outbox'
|
||||||
|
|
||||||
logger.info('Receiving outbox request for %s.', actorOutboxUrl)
|
logger.info('Receiving outbox request for %s.', actorOutboxUrl)
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
|
includeLocalVideos: false,
|
||||||
categoryOneOf: req.query.categoryOneOf,
|
categoryOneOf: req.query.categoryOneOf,
|
||||||
licenceOneOf: req.query.licenceOneOf,
|
licenceOneOf: req.query.licenceOneOf,
|
||||||
languageOneOf: req.query.languageOneOf,
|
languageOneOf: req.query.languageOneOf,
|
||||||
|
|
|
@ -36,7 +36,10 @@ export { searchRouter }
|
||||||
async function searchVideos (req: express.Request, res: express.Response) {
|
async function searchVideos (req: express.Request, res: express.Response) {
|
||||||
const query: VideosSearchQuery = req.query
|
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)
|
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as express from 'express'
|
||||||
import { UserRight } from '../../../../shared/models/users'
|
import { UserRight } from '../../../../shared/models/users'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers'
|
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
|
||||||
import { sendUndoFollow } from '../../../lib/activitypub/send'
|
import { sendUndoFollow } from '../../../lib/activitypub/send'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
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) {
|
async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const hosts = req.body.hosts as string[]
|
const hosts = req.body.hosts as string[]
|
||||||
|
const follower = await getServerActor()
|
||||||
|
|
||||||
for (const host of hosts) {
|
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))
|
.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 })
|
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()
|
return res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPassw
|
||||||
import { UserModel } from '../../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
|
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
|
||||||
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
||||||
import { videosRouter } from '../videos'
|
|
||||||
import { meRouter } from './me'
|
import { meRouter } from './me'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
const auditLogger = auditLoggerFactory('users')
|
||||||
|
@ -41,7 +40,7 @@ const loginRateLimiter = new RateLimit({
|
||||||
})
|
})
|
||||||
|
|
||||||
const usersRouter = express.Router()
|
const usersRouter = express.Router()
|
||||||
videosRouter.use('/', meRouter)
|
usersRouter.use('/', meRouter)
|
||||||
|
|
||||||
usersRouter.get('/',
|
usersRouter.get('/',
|
||||||
authenticate,
|
authenticate,
|
||||||
|
|
|
@ -7,23 +7,35 @@ import { sendUpdateActor } from '../../../lib/activitypub/send'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
commonVideosFiltersValidator,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
userSubscriptionAddValidator,
|
||||||
|
userSubscriptionRemoveValidator,
|
||||||
usersUpdateMeValidator,
|
usersUpdateMeValidator,
|
||||||
usersVideoRatingValidator
|
usersVideoRatingValidator
|
||||||
} from '../../../middlewares'
|
} 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 { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
||||||
import { UserModel } from '../../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
|
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 { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
|
||||||
import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
|
import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
|
||||||
import { updateActorAvatarFile } from '../../../lib/avatar'
|
import { updateActorAvatarFile } from '../../../lib/avatar'
|
||||||
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
||||||
import { VideoImportModel } from '../../../models/video/video-import'
|
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')
|
const auditLogger = auditLoggerFactory('users-me')
|
||||||
|
|
||||||
|
@ -83,6 +95,40 @@ meRouter.post('/me/avatar/pick',
|
||||||
asyncMiddleware(updateMyAvatar)
|
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 {
|
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) {
|
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const user = res.locals.oauth.token.User as UserModel
|
const user = res.locals.oauth.token.User as UserModel
|
||||||
const resultList = await VideoModel.listUserVideosForApi(
|
const resultList = await VideoModel.listUserVideosForApi(
|
||||||
|
@ -150,7 +252,7 @@ async function getUserVideoRating (req: express.Request, res: express.Response,
|
||||||
videoId,
|
videoId,
|
||||||
rating
|
rating
|
||||||
}
|
}
|
||||||
res.json(json)
|
return res.json(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMe (req: express.Request, res: express.Response) {
|
async function deleteMe (req: express.Request, res: express.Response) {
|
||||||
|
@ -207,9 +309,5 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
|
||||||
oldUserAuditView
|
oldUserAuditView
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return res.json({ avatar: avatar.toFormattedJSON() })
|
||||||
.json({
|
|
||||||
avatar: avatar.toFormattedJSON()
|
|
||||||
})
|
|
||||||
.end()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,6 +215,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
|
includeLocalVideos: false,
|
||||||
categoryOneOf: req.query.categoryOneOf,
|
categoryOneOf: req.query.categoryOneOf,
|
||||||
licenceOneOf: req.query.licenceOneOf,
|
licenceOneOf: req.query.licenceOneOf,
|
||||||
languageOneOf: req.query.languageOneOf,
|
languageOneOf: req.query.languageOneOf,
|
||||||
|
|
|
@ -414,6 +414,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
|
includeLocalVideos: true,
|
||||||
categoryOneOf: req.query.categoryOneOf,
|
categoryOneOf: req.query.categoryOneOf,
|
||||||
licenceOneOf: req.query.licenceOneOf,
|
licenceOneOf: req.query.licenceOneOf,
|
||||||
languageOneOf: req.query.languageOneOf,
|
languageOneOf: req.query.languageOneOf,
|
||||||
|
|
|
@ -96,6 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
|
||||||
start,
|
start,
|
||||||
count: FEEDS.COUNT,
|
count: FEEDS.COUNT,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
|
includeLocalVideos: true,
|
||||||
nsfw,
|
nsfw,
|
||||||
filter: req.query.filter,
|
filter: req.query.filter,
|
||||||
withFiles: true,
|
withFiles: true,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||||
import { exists } from '../misc'
|
import { exists } from '../misc'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
|
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
|
||||||
|
import { isHostValid } from '../servers'
|
||||||
|
|
||||||
function isActorEndpointsObjectValid (endpointObject: any) {
|
function isActorEndpointsObjectValid (endpointObject: any) {
|
||||||
return isActivityPubUrlValid(endpointObject.sharedInbox)
|
return isActivityPubUrlValid(endpointObject.sharedInbox)
|
||||||
|
@ -109,6 +110,15 @@ function normalizeActor (actor: any) {
|
||||||
return
|
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 {
|
export {
|
||||||
|
@ -126,5 +136,6 @@ export {
|
||||||
isActorAcceptActivityValid,
|
isActorAcceptActivityValid,
|
||||||
isActorRejectActivityValid,
|
isActorRejectActivityValid,
|
||||||
isActorDeleteActivityValid,
|
isActorDeleteActivityValid,
|
||||||
isActorUpdateActivityValid
|
isActorUpdateActivityValid,
|
||||||
|
isValidActorHandle
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import * as validator from 'validator'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||||
import { exists } from './misc'
|
import { exists } from './misc'
|
||||||
|
import { Response } from 'express'
|
||||||
|
|
||||||
const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS
|
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))
|
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) {
|
async function isVideoChannelExist (id: string, res: express.Response) {
|
||||||
let videoChannel: VideoChannelModel
|
let videoChannel: VideoChannelModel
|
||||||
if (validator.isInt(id)) {
|
if (validator.isInt(id)) {
|
||||||
|
@ -28,6 +35,20 @@ async function isVideoChannelExist (id: string, res: express.Response) {
|
||||||
videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id)
|
videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return processVideoChannelExist(videoChannel, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
isLocalVideoChannelNameExist,
|
||||||
|
isVideoChannelDescriptionValid,
|
||||||
|
isVideoChannelNameValid,
|
||||||
|
isVideoChannelSupportValid,
|
||||||
|
isVideoChannelExist
|
||||||
|
}
|
||||||
|
|
||||||
|
function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
|
||||||
if (!videoChannel) {
|
if (!videoChannel) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
.json({ error: 'Video channel not found' })
|
.json({ error: 'Video channel not found' })
|
||||||
|
@ -39,12 +60,3 @@ async function isVideoChannelExist (id: string, res: express.Response) {
|
||||||
res.locals.videoChannel = videoChannel
|
res.locals.videoChannel = videoChannel
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
isVideoChannelDescriptionValid,
|
|
||||||
isVideoChannelNameValid,
|
|
||||||
isVideoChannelSupportValid,
|
|
||||||
isVideoChannelExist
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { CONFIG, REMOTE_SCHEME } from '../../initializers'
|
||||||
import { sanitizeHost } from '../core-utils'
|
import { sanitizeHost } from '../core-utils'
|
||||||
import { exists } from './misc'
|
import { exists } from './misc'
|
||||||
|
|
||||||
function isWebfingerResourceValid (value: string) {
|
function isWebfingerLocalResourceValid (value: string) {
|
||||||
if (!exists(value)) return false
|
if (!exists(value)) return false
|
||||||
if (value.startsWith('acct:') === false) return false
|
if (value.startsWith('acct:') === false) return false
|
||||||
|
|
||||||
|
@ -17,5 +17,5 @@ function isWebfingerResourceValid (value: string) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isWebfingerResourceValid
|
isWebfingerLocalResourceValid
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,17 @@ const webfinger = new WebFinger({
|
||||||
request_timeout: 3000
|
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)
|
const actor = await ActorModel.loadByNameAndHost(name, host)
|
||||||
if (actor) return actor.url
|
if (actor) return actor.url
|
||||||
|
|
||||||
return getUrlFromWebfinger(name, host)
|
return getUrlFromWebfinger(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUrlFromWebfinger (name: string, host: string) {
|
async function getUrlFromWebfinger (uri: string) {
|
||||||
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
|
const webfingerData: WebFingerData = await webfingerLookup(uri)
|
||||||
return getLinkOrThrow(webfingerData)
|
return getLinkOrThrow(webfingerData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ const PAGINATION = {
|
||||||
// Sortable columns per schema
|
// Sortable columns per schema
|
||||||
const SORTABLE_COLUMNS = {
|
const SORTABLE_COLUMNS = {
|
||||||
USERS: [ 'id', 'username', 'createdAt' ],
|
USERS: [ 'id', 'username', 'createdAt' ],
|
||||||
|
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
|
||||||
ACCOUNTS: [ 'createdAt' ],
|
ACCOUNTS: [ 'createdAt' ],
|
||||||
JOBS: [ 'createdAt' ],
|
JOBS: [ 'createdAt' ],
|
||||||
VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
|
VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
|
||||||
|
|
|
@ -352,7 +352,7 @@ async function refreshActorIfNeeded (actor: ActorModel): Promise<ActorModel> {
|
||||||
if (!actor.isOutdated()) return actor
|
if (!actor.isOutdated()) return actor
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
|
const actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
|
||||||
const result = await fetchRemoteActor(actorUrl)
|
const result = await fetchRemoteActor(actorUrl)
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
logger.warn('Cannot fetch remote actor in refresh actor.')
|
logger.warn('Cannot fetch remote actor in refresh actor.')
|
||||||
|
|
|
@ -10,6 +10,11 @@ async function sendAccept (actorFollow: ActorFollowModel) {
|
||||||
const follower = actorFollow.ActorFollower
|
const follower = actorFollow.ActorFollower
|
||||||
const me = actorFollow.ActorFollowing
|
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)
|
logger.info('Creating job to accept follower %s.', follower.url)
|
||||||
|
|
||||||
const followUrl = getActorFollowActivityPubUrl(actorFollow)
|
const followUrl = getActorFollowActivityPubUrl(actorFollow)
|
||||||
|
|
|
@ -33,6 +33,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, 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)
|
const url = getVideoAbuseActivityPubUrl(videoAbuse)
|
||||||
|
|
||||||
logger.info('Creating job to send video abuse %s.', url)
|
logger.info('Creating job to send video abuse %s.', url)
|
||||||
|
|
|
@ -9,6 +9,9 @@ function sendFollow (actorFollow: ActorFollowModel) {
|
||||||
const me = actorFollow.ActorFollower
|
const me = actorFollow.ActorFollower
|
||||||
const following = actorFollow.ActorFollowing
|
const following = actorFollow.ActorFollowing
|
||||||
|
|
||||||
|
// Same server as ours
|
||||||
|
if (!following.serverId) return
|
||||||
|
|
||||||
logger.info('Creating job to send follow request to %s.', following.url)
|
logger.info('Creating job to send follow request to %s.', following.url)
|
||||||
|
|
||||||
const url = getActorFollowActivityPubUrl(actorFollow)
|
const url = getActorFollowActivityPubUrl(actorFollow)
|
||||||
|
|
|
@ -24,6 +24,9 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
|
||||||
const me = actorFollow.ActorFollower
|
const me = actorFollow.ActorFollower
|
||||||
const following = actorFollow.ActorFollowing
|
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)
|
logger.info('Creating job to send an unfollow request to %s.', following.url)
|
||||||
|
|
||||||
const followUrl = getActorFollowActivityPubUrl(actorFollow)
|
const followUrl = getActorFollowActivityPubUrl(actorFollow)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { JobQueue } from '../../job-queue'
|
import { JobQueue } from '../../job-queue'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { getActorsInvolvedInVideo } from '../audience'
|
import { getActorsInvolvedInVideo } from '../audience'
|
||||||
|
import { getServerActor } from '../../../helpers/utils'
|
||||||
|
|
||||||
async function forwardVideoRelatedActivity (
|
async function forwardVideoRelatedActivity (
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
|
@ -118,14 +119,28 @@ async function computeFollowerUris (toActorFollower: ActorModel[], actorsExcepti
|
||||||
const toActorFollowerIds = toActorFollower.map(a => a.id)
|
const toActorFollowerIds = toActorFollower.map(a => a.id)
|
||||||
|
|
||||||
const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
|
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)
|
return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) {
|
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)
|
return Array.from(toActorSharedInboxesSet)
|
||||||
.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
|
.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 ])
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as Bull from 'bull'
|
import * as Bull from 'bull'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { getServerActor } from '../../../helpers/utils'
|
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 { sendFollow } from '../../activitypub/send'
|
||||||
import { sanitizeHost } from '../../../helpers/core-utils'
|
import { sanitizeHost } from '../../../helpers/core-utils'
|
||||||
import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
|
import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
|
||||||
|
@ -11,6 +11,8 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
|
|
||||||
export type ActivitypubFollowPayload = {
|
export type ActivitypubFollowPayload = {
|
||||||
|
followerActorId: number
|
||||||
|
name: string
|
||||||
host: string
|
host: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,10 +24,10 @@ async function processActivityPubFollow (job: Bull.Job) {
|
||||||
|
|
||||||
const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
|
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 targetActor = await getOrCreateActorAndServerAndModel(actorUrl)
|
||||||
|
|
||||||
const fromActor = await getServerActor()
|
const fromActor = await ActorModel.load(payload.followerActorId)
|
||||||
|
|
||||||
return retryTransactionWrapper(follow, fromActor, targetActor)
|
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.')
|
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 => {
|
return sequelizeTypescript.transaction(async t => {
|
||||||
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
|
const [ actorFollow ] = await ActorFollowModel.findOrCreate({
|
||||||
where: {
|
where: {
|
||||||
|
@ -49,7 +54,7 @@ function follow (fromActor: ActorModel, targetActor: ActorModel) {
|
||||||
targetActorId: targetActor.id
|
targetActorId: targetActor.id
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
state: 'pending',
|
state,
|
||||||
actorId: fromActor.id,
|
actorId: fromActor.id,
|
||||||
targetActorId: targetActor.id
|
targetActorId: targetActor.id
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { isTestInstance } from '../../helpers/core-utils'
|
||||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
|
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { getServerActor } from '../../helpers/utils'
|
import { getServerActor } from '../../helpers/utils'
|
||||||
import { CONFIG } from '../../initializers'
|
import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
|
||||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ const removeFollowingValidator = [
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const serverActor = await getServerActor()
|
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) {
|
if (!follow) {
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -6,6 +6,7 @@ export * from './follows'
|
||||||
export * from './feeds'
|
export * from './feeds'
|
||||||
export * from './sort'
|
export * from './sort'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
|
export * from './user-subscriptions'
|
||||||
export * from './videos'
|
export * from './videos'
|
||||||
export * from './video-abuses'
|
export * from './video-abuses'
|
||||||
export * from './video-blacklist'
|
export * from './video-blacklist'
|
||||||
|
|
|
@ -14,6 +14,7 @@ const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACK
|
||||||
const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
|
const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
|
||||||
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
|
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
|
||||||
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
|
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 usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
||||||
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_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 videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
|
||||||
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
|
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
|
||||||
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
|
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
|
||||||
|
const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -42,5 +44,6 @@ export {
|
||||||
followersSortValidator,
|
followersSortValidator,
|
||||||
followingSortValidator,
|
followingSortValidator,
|
||||||
jobsSortValidator,
|
jobsSortValidator,
|
||||||
videoCommentThreadsSortValidator
|
videoCommentThreadsSortValidator,
|
||||||
|
userSubscriptionsSortValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
|
@ -4,6 +4,7 @@ import { UserRight } from '../../../shared'
|
||||||
import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
|
import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
|
||||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
|
isLocalVideoChannelNameExist,
|
||||||
isVideoChannelDescriptionValid,
|
isVideoChannelDescriptionValid,
|
||||||
isVideoChannelExist,
|
isVideoChannelExist,
|
||||||
isVideoChannelNameValid,
|
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 {
|
export {
|
||||||
|
@ -107,7 +121,8 @@ export {
|
||||||
videoChannelsAddValidator,
|
videoChannelsAddValidator,
|
||||||
videoChannelsUpdateValidator,
|
videoChannelsUpdateValidator,
|
||||||
videoChannelsRemoveValidator,
|
videoChannelsRemoveValidator,
|
||||||
videoChannelsGetValidator
|
videoChannelsGetValidator,
|
||||||
|
localVideoChannelValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { query } from 'express-validator/check'
|
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 { logger } from '../../helpers/logger'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
import { getHostWithPort } from '../../helpers/express-utils'
|
import { getHostWithPort } from '../../helpers/express-utils'
|
||||||
|
|
||||||
const webfingerValidator = [
|
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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking webfinger parameters', { parameters: req.query })
|
logger.debug('Checking webfinger parameters', { parameters: req.query })
|
||||||
|
|
|
@ -2,8 +2,21 @@ import * as Bluebird from 'bluebird'
|
||||||
import { values } from 'lodash'
|
import { values } from 'lodash'
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model,
|
AfterCreate,
|
||||||
Table, UpdatedAt
|
AfterDestroy,
|
||||||
|
AfterUpdate,
|
||||||
|
AllowNull,
|
||||||
|
BelongsTo,
|
||||||
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
DataType,
|
||||||
|
Default,
|
||||||
|
ForeignKey,
|
||||||
|
IsInt,
|
||||||
|
Max,
|
||||||
|
Model,
|
||||||
|
Table,
|
||||||
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { FollowState } from '../../../shared/models/actors'
|
import { FollowState } from '../../../shared/models/actors'
|
||||||
import { AccountFollow } from '../../../shared/models/actors/follow.model'
|
import { AccountFollow } from '../../../shared/models/actors/follow.model'
|
||||||
|
@ -14,6 +27,7 @@ import { FOLLOW_STATES } from '../../initializers/constants'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
import { getSort } from '../utils'
|
import { getSort } from '../utils'
|
||||||
import { ActorModel } from './actor'
|
import { ActorModel } from './actor'
|
||||||
|
import { VideoChannelModel } from '../video/video-channel'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'actorFollow',
|
tableName: 'actorFollow',
|
||||||
|
@ -151,7 +165,32 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
return ActorFollowModel.findOne(query)
|
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 = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
actorId
|
actorId
|
||||||
|
@ -162,20 +201,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
required: true,
|
required: true,
|
||||||
as: 'ActorFollower'
|
as: 'ActorFollower'
|
||||||
},
|
},
|
||||||
{
|
actorFollowingPartInclude
|
||||||
model: ActorModel,
|
|
||||||
required: true,
|
|
||||||
as: 'ActorFollowing',
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: ServerModel,
|
|
||||||
required: true,
|
|
||||||
where: {
|
|
||||||
host: targetHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
transaction: t
|
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) {
|
static listFollowersForApi (id: number, start: number, count: number, sort: string) {
|
||||||
const query = {
|
const query = {
|
||||||
distinct: true,
|
distinct: true,
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
import {
|
import {
|
||||||
AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Is, Model, Scopes, Table,
|
AllowNull,
|
||||||
UpdatedAt, Default, DataType
|
BeforeDestroy,
|
||||||
|
BelongsTo,
|
||||||
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
DataType,
|
||||||
|
Default,
|
||||||
|
DefaultScope,
|
||||||
|
ForeignKey,
|
||||||
|
HasMany,
|
||||||
|
Is,
|
||||||
|
Model,
|
||||||
|
Scopes,
|
||||||
|
Table,
|
||||||
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { ActivityPubActor } from '../../../shared/models/activitypub'
|
import { ActivityPubActor } from '../../../shared/models/activitypub'
|
||||||
import { VideoChannel } from '../../../shared/models/videos'
|
import { VideoChannel } from '../../../shared/models/videos'
|
||||||
import {
|
import {
|
||||||
isVideoChannelDescriptionValid, isVideoChannelNameValid,
|
isVideoChannelDescriptionValid,
|
||||||
|
isVideoChannelNameValid,
|
||||||
isVideoChannelSupportValid
|
isVideoChannelSupportValid
|
||||||
} from '../../helpers/custom-validators/video-channels'
|
} from '../../helpers/custom-validators/video-channels'
|
||||||
import { logger } from '../../helpers/logger'
|
|
||||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
|
@ -241,6 +254,23 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
.findById(id, options)
|
.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 {
|
toFormattedJSON (): VideoChannel {
|
||||||
const actor = this.Actor.toFormattedJSON()
|
const actor = this.Actor.toFormattedJSON()
|
||||||
const videoChannel = {
|
const videoChannel = {
|
||||||
|
@ -251,8 +281,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
isLocal: this.Actor.isOwned(),
|
isLocal: this.Actor.isOwned(),
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
ownerAccount: undefined,
|
ownerAccount: undefined
|
||||||
videos: undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
|
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
|
||||||
|
|
|
@ -133,6 +133,7 @@ export enum ScopeNames {
|
||||||
|
|
||||||
type AvailableForListOptions = {
|
type AvailableForListOptions = {
|
||||||
actorId: number,
|
actorId: number,
|
||||||
|
includeLocalVideos: boolean,
|
||||||
filter?: VideoFilter,
|
filter?: VideoFilter,
|
||||||
categoryOneOf?: number[],
|
categoryOneOf?: number[],
|
||||||
nsfw?: boolean,
|
nsfw?: boolean,
|
||||||
|
@ -201,6 +202,15 @@ type AvailableForListOptions = {
|
||||||
|
|
||||||
// Force actorId to be a number to avoid SQL injections
|
// Force actorId to be a number to avoid SQL injections
|
||||||
const actorIdNumber = parseInt(options.actorId.toString(), 10)
|
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...
|
// 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> = {
|
const query: IFindOptions<VideoModel> = {
|
||||||
|
@ -214,12 +224,6 @@ type AvailableForListOptions = {
|
||||||
'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
|
'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
|
||||||
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
|
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
|
||||||
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
|
'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 ' +
|
' UNION ALL ' +
|
||||||
'SELECT "video"."id" AS "id" FROM "video" ' +
|
'SELECT "video"."id" AS "id" FROM "video" ' +
|
||||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
||||||
|
@ -227,6 +231,7 @@ type AvailableForListOptions = {
|
||||||
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
|
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
|
||||||
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
|
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
|
||||||
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
|
'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
|
||||||
|
localVideosReq +
|
||||||
')'
|
')'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -825,6 +830,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
count: number,
|
count: number,
|
||||||
sort: string,
|
sort: string,
|
||||||
nsfw: boolean,
|
nsfw: boolean,
|
||||||
|
includeLocalVideos: boolean,
|
||||||
withFiles: boolean,
|
withFiles: boolean,
|
||||||
categoryOneOf?: number[],
|
categoryOneOf?: number[],
|
||||||
licenceOneOf?: number[],
|
licenceOneOf?: number[],
|
||||||
|
@ -833,7 +839,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
tagsAllOf?: string[],
|
tagsAllOf?: string[],
|
||||||
filter?: VideoFilter,
|
filter?: VideoFilter,
|
||||||
accountId?: number,
|
accountId?: number,
|
||||||
videoChannelId?: number
|
videoChannelId?: number,
|
||||||
|
actorId?: number
|
||||||
}) {
|
}) {
|
||||||
const query = {
|
const query = {
|
||||||
offset: options.start,
|
offset: options.start,
|
||||||
|
@ -841,11 +848,12 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
order: getSort(options.sort)
|
order: getSort(options.sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverActor = await getServerActor()
|
const actorId = options.actorId || (await getServerActor()).id
|
||||||
|
|
||||||
const scopes = {
|
const scopes = {
|
||||||
method: [
|
method: [
|
||||||
ScopeNames.AVAILABLE_FOR_LIST, {
|
ScopeNames.AVAILABLE_FOR_LIST, {
|
||||||
actorId: serverActor.id,
|
actorId,
|
||||||
nsfw: options.nsfw,
|
nsfw: options.nsfw,
|
||||||
categoryOneOf: options.categoryOneOf,
|
categoryOneOf: options.categoryOneOf,
|
||||||
licenceOneOf: options.licenceOneOf,
|
licenceOneOf: options.licenceOneOf,
|
||||||
|
@ -855,7 +863,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
filter: options.filter,
|
filter: options.filter,
|
||||||
withFiles: options.withFiles,
|
withFiles: options.withFiles,
|
||||||
accountId: options.accountId,
|
accountId: options.accountId,
|
||||||
videoChannelId: options.videoChannelId
|
videoChannelId: options.videoChannelId,
|
||||||
|
includeLocalVideos: options.includeLocalVideos
|
||||||
} as AvailableForListOptions
|
} as AvailableForListOptions
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -871,6 +880,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async searchAndPopulateAccountAndServer (options: {
|
static async searchAndPopulateAccountAndServer (options: {
|
||||||
|
includeLocalVideos: boolean
|
||||||
search?: string
|
search?: string
|
||||||
start?: number
|
start?: number
|
||||||
count?: number
|
count?: number
|
||||||
|
@ -955,6 +965,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
method: [
|
method: [
|
||||||
ScopeNames.AVAILABLE_FOR_LIST, {
|
ScopeNames.AVAILABLE_FOR_LIST, {
|
||||||
actorId: serverActor.id,
|
actorId: serverActor.id,
|
||||||
|
includeLocalVideos: options.includeLocalVideos,
|
||||||
nsfw: options.nsfw,
|
nsfw: options.nsfw,
|
||||||
categoryOneOf: options.categoryOneOf,
|
categoryOneOf: options.categoryOneOf,
|
||||||
licenceOneOf: options.licenceOneOf,
|
licenceOneOf: options.licenceOneOf,
|
||||||
|
|
|
@ -12,3 +12,4 @@ import './video-comments'
|
||||||
import './videos'
|
import './videos'
|
||||||
import './video-imports'
|
import './video-imports'
|
||||||
import './search'
|
import './search'
|
||||||
|
import './user-subscriptions'
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -6,6 +6,7 @@ import './server/follows'
|
||||||
import './server/jobs'
|
import './server/jobs'
|
||||||
import './videos/video-comments'
|
import './videos/video-comments'
|
||||||
import './users/users-multiple-servers'
|
import './users/users-multiple-servers'
|
||||||
|
import './users/user-subscriptions'
|
||||||
import './server/handle-down'
|
import './server/handle-down'
|
||||||
import './videos/video-schedule-update'
|
import './videos/video-schedule-update'
|
||||||
import './videos/video-imports'
|
import './videos/video-imports'
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue