Add subscriptions endpoints to REST API
This commit is contained in:
parent
4bda2e47bb
commit
06a05d5f47
36 changed files with 1039 additions and 94 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() })
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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' ],
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
58
server/middlewares/validators/user-subscriptions.ts
Normal file
58
server/middlewares/validators/user-subscriptions.ts
Normal 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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
|
@ -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
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -12,3 +12,4 @@ import './video-comments'
|
|||
import './videos'
|
||||
import './video-imports'
|
||||
import './search'
|
||||
import './user-subscriptions'
|
||||
|
|
220
server/tests/api/check-params/user-subscriptions.ts
Normal file
220
server/tests/api/check-params/user-subscriptions.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
})
|
|
@ -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'
|
||||
|
|
312
server/tests/api/users/user-subscriptions.ts
Normal file
312
server/tests/api/users/user-subscriptions.ts
Normal 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)
|
||||
})
|
||||
})
|
57
server/tests/utils/users/user-subscriptions.ts
Normal file
57
server/tests/utils/users/user-subscriptions.ts
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue