diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 79b76fa0b..f1430055f 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -106,7 +106,7 @@ function buildSignedActivity (byActor: ActorModel, data: Object) { return signJsonLDObject(byActor, activity) as Promise } -function getAPUrl (activity: string | { id: string }) { +function getAPId (activity: string | { id: string }) { if (typeof activity === 'string') return activity return activity.id @@ -123,7 +123,7 @@ function checkUrlsSameHost (url1: string, url2: string) { export { checkUrlsSameHost, - getAPUrl, + getAPId, activityPubContextify, activityPubCollectionPagination, buildSignedActivity diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 2562ead9b..b24590d9d 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -1,26 +1,14 @@ import * as validator from 'validator' import { Activity, ActivityType } from '../../../../shared/models/activitypub' -import { - isActorAcceptActivityValid, - isActorDeleteActivityValid, - isActorFollowActivityValid, - isActorRejectActivityValid, - isActorUpdateActivityValid -} from './actor' -import { isAnnounceActivityValid } from './announce' -import { isActivityPubUrlValid } from './misc' -import { isDislikeActivityValid, isLikeActivityValid } from './rate' -import { isUndoActivityValid } from './undo' -import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' -import { - isVideoFlagValid, - isVideoTorrentDeleteActivityValid, - sanitizeAndCheckVideoTorrentCreateActivity, - sanitizeAndCheckVideoTorrentUpdateActivity -} from './videos' +import { sanitizeAndCheckActorObject } from './actor' +import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' +import { isDislikeActivityValid } from './rate' +import { sanitizeAndCheckVideoCommentObject } from './video-comments' +import { sanitizeAndCheckVideoTorrentObject } from './videos' import { isViewActivityValid } from './view' import { exists } from '../misc' -import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' +import { isCacheFileObjectValid } from './cache-file' +import { isFlagActivityValid } from './flag' function isRootActivityValid (activity: any) { return Array.isArray(activity['@context']) && ( @@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean Reject: checkRejectActivity, Announce: checkAnnounceActivity, Undo: checkUndoActivity, - Like: checkLikeActivity + Like: checkLikeActivity, + View: checkViewActivity, + Flag: checkFlagActivity, + Dislike: checkDislikeActivity } function isActivityValid (activity: any) { @@ -66,47 +57,79 @@ export { // --------------------------------------------------------------------------- +function checkViewActivity (activity: any) { + return isBaseActivityValid(activity, 'View') && + isViewActivityValid(activity) +} + +function checkFlagActivity (activity: any) { + return isBaseActivityValid(activity, 'Flag') && + isFlagActivityValid(activity) +} + +function checkDislikeActivity (activity: any) { + return isBaseActivityValid(activity, 'Dislike') && + isDislikeActivityValid(activity) +} + function checkCreateActivity (activity: any) { - return isViewActivityValid(activity) || - isDislikeActivityValid(activity) || - sanitizeAndCheckVideoTorrentCreateActivity(activity) || - isVideoFlagValid(activity) || - isVideoCommentCreateActivityValid(activity) || - isCacheFileCreateActivityValid(activity) + return isBaseActivityValid(activity, 'Create') && + ( + isViewActivityValid(activity.object) || + isDislikeActivityValid(activity.object) || + isFlagActivityValid(activity.object) || + + isCacheFileObjectValid(activity.object) || + sanitizeAndCheckVideoCommentObject(activity.object) || + sanitizeAndCheckVideoTorrentObject(activity.object) + ) } function checkUpdateActivity (activity: any) { - return isCacheFileUpdateActivityValid(activity) || - sanitizeAndCheckVideoTorrentUpdateActivity(activity) || - isActorUpdateActivityValid(activity) + return isBaseActivityValid(activity, 'Update') && + ( + isCacheFileObjectValid(activity.object) || + sanitizeAndCheckVideoTorrentObject(activity.object) || + sanitizeAndCheckActorObject(activity.object) + ) } function checkDeleteActivity (activity: any) { - return isVideoTorrentDeleteActivityValid(activity) || - isActorDeleteActivityValid(activity) || - isVideoCommentDeleteActivityValid(activity) + // We don't really check objects + return isBaseActivityValid(activity, 'Delete') && + isObjectValid(activity.object) } function checkFollowActivity (activity: any) { - return isActorFollowActivityValid(activity) + return isBaseActivityValid(activity, 'Follow') && + isObjectValid(activity.object) } function checkAcceptActivity (activity: any) { - return isActorAcceptActivityValid(activity) + return isBaseActivityValid(activity, 'Accept') } function checkRejectActivity (activity: any) { - return isActorRejectActivityValid(activity) + return isBaseActivityValid(activity, 'Reject') } function checkAnnounceActivity (activity: any) { - return isAnnounceActivityValid(activity) + return isBaseActivityValid(activity, 'Announce') && + isObjectValid(activity.object) } function checkUndoActivity (activity: any) { - return isUndoActivityValid(activity) + return isBaseActivityValid(activity, 'Undo') && + ( + checkFollowActivity(activity.object) || + checkLikeActivity(activity.object) || + checkDislikeActivity(activity.object) || + checkAnnounceActivity(activity.object) || + checkCreateActivity(activity.object) + ) } function checkLikeActivity (activity: any) { - return isLikeActivityValid(activity) + return isBaseActivityValid(activity, 'Like') && + isObjectValid(activity.object) } diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 070632a20..c05f60f14 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts @@ -73,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) { return isBaseActivityValid(activity, 'Delete') } -function isActorFollowActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Follow') && - isActivityPubUrlValid(activity.object) -} +function sanitizeAndCheckActorObject (object: any) { + normalizeActor(object) -function isActorAcceptActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Accept') -} - -function isActorRejectActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Reject') -} - -function isActorUpdateActivityValid (activity: any) { - normalizeActor(activity.object) - - return isBaseActivityValid(activity, 'Update') && - isActorObjectValid(activity.object) + return isActorObjectValid(object) } function normalizeActor (actor: any) { @@ -139,10 +125,7 @@ export { isActorObjectValid, isActorFollowingCountValid, isActorFollowersCountValid, - isActorFollowActivityValid, - isActorAcceptActivityValid, - isActorRejectActivityValid, isActorDeleteActivityValid, - isActorUpdateActivityValid, + sanitizeAndCheckActorObject, isValidActorHandle } diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts deleted file mode 100644 index 0519c6026..000000000 --- a/server/helpers/custom-validators/activitypub/announce.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' - -function isAnnounceActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Announce') && - ( - isActivityPubUrlValid(activity.object) || - (activity.object && isActivityPubUrlValid(activity.object.id)) - ) -} - -export { - isAnnounceActivityValid -} diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index bd70934c8..e2bd0c55e 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts @@ -1,18 +1,8 @@ -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' +import { isActivityPubUrlValid } from './misc' import { isRemoteVideoUrlValid } from './videos' -import { isDateValid, exists } from '../misc' +import { exists, isDateValid } from '../misc' import { CacheFileObject } from '../../../../shared/models/activitypub/objects' -function isCacheFileCreateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - isCacheFileObjectValid(activity.object) -} - -function isCacheFileUpdateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Update') && - isCacheFileObjectValid(activity.object) -} - function isCacheFileObjectValid (object: CacheFileObject) { return exists(object) && object.type === 'CacheFile' && @@ -22,7 +12,5 @@ function isCacheFileObjectValid (object: CacheFileObject) { } export { - isCacheFileUpdateActivityValid, - isCacheFileCreateActivityValid, isCacheFileObjectValid } diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts new file mode 100644 index 000000000..6452e297c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/flag.ts @@ -0,0 +1,14 @@ +import { isActivityPubUrlValid } from './misc' +import { isVideoAbuseReasonValid } from '../video-abuses' + +function isFlagActivityValid (activity: any) { + return activity.type === 'Flag' && + isVideoAbuseReasonValid(activity.content) && + isActivityPubUrlValid(activity.object) +} + +// --------------------------------------------------------------------------- + +export { + isFlagActivityValid +} diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 4e2c57f04..f1762d11c 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts @@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) { return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && activity.type === type && isActivityPubUrlValid(activity.id) && - exists(activity.actor) && - (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && + isObjectValid(activity.actor) && + isUrlCollectionValid(activity.to) && + isUrlCollectionValid(activity.cc) +} + +function isUrlCollectionValid (collection: any) { + return collection === undefined || + (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t))) +} + +function isObjectValid (object: any) { + return exists(object) && ( - activity.to === undefined || - (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t))) - ) && - ( - activity.cc === undefined || - (Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t))) + isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id) ) } @@ -57,5 +62,6 @@ export { isUrlValid, isActivityPubUrlValid, isBaseActivityValid, - setValidAttributedTo + setValidAttributedTo, + isObjectValid } diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts index e70bd94b8..ba68e8074 100644 --- a/server/helpers/custom-validators/activitypub/rate.ts +++ b/server/helpers/custom-validators/activitypub/rate.ts @@ -1,20 +1,13 @@ -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' - -function isLikeActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Like') && - isActivityPubUrlValid(activity.object) -} +import { isActivityPubUrlValid, isObjectValid } from './misc' function isDislikeActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - activity.object.type === 'Dislike' && - isActivityPubUrlValid(activity.object.actor) && - isActivityPubUrlValid(activity.object.object) + return activity.type === 'Dislike' && + isActivityPubUrlValid(activity.actor) && + isObjectValid(activity.object) } // --------------------------------------------------------------------------- export { - isLikeActivityValid, isDislikeActivityValid } diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts deleted file mode 100644 index 578035893..000000000 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isActorFollowActivityValid } from './actor' -import { isBaseActivityValid } from './misc' -import { isDislikeActivityValid, isLikeActivityValid } from './rate' -import { isAnnounceActivityValid } from './announce' -import { isCacheFileCreateActivityValid } from './cache-file' - -function isUndoActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Undo') && - ( - isActorFollowActivityValid(activity.object) || - isLikeActivityValid(activity.object) || - isDislikeActivityValid(activity.object) || - isAnnounceActivityValid(activity.object) || - isCacheFileCreateActivityValid(activity.object) - ) -} - -export { - isUndoActivityValid -} diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts index 051c4565a..0415db21c 100644 --- a/server/helpers/custom-validators/activitypub/video-comments.ts +++ b/server/helpers/custom-validators/activitypub/video-comments.ts @@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' import { exists, isArray, isDateValid } from '../misc' import { isActivityPubUrlValid, isBaseActivityValid } from './misc' -function isVideoCommentCreateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - sanitizeAndCheckVideoCommentObject(activity.object) -} - function sanitizeAndCheckVideoCommentObject (comment: any) { if (!comment || comment.type !== 'Note') return false @@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) { ) // Only accept public comments } -function isVideoCommentDeleteActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Delete') -} - // --------------------------------------------------------------------------- export { - isVideoCommentCreateActivityValid, - isVideoCommentDeleteActivityValid, sanitizeAndCheckVideoCommentObject } diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 95fe824b9..0f34aab21 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -14,27 +14,11 @@ import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from import { VideoState } from '../../../../shared/models/videos' import { isVideoAbuseReasonValid } from '../video-abuses' -function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { - return isBaseActivityValid(activity, 'Create') && - sanitizeAndCheckVideoTorrentObject(activity.object) -} - function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { return isBaseActivityValid(activity, 'Update') && sanitizeAndCheckVideoTorrentObject(activity.object) } -function isVideoTorrentDeleteActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Delete') -} - -function isVideoFlagValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - activity.object.type === 'Flag' && - isVideoAbuseReasonValid(activity.object.content) && - isActivityPubUrlValid(activity.object.object) -} - function isActivityPubVideoDurationValid (value: string) { // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration return exists(value) && @@ -103,11 +87,8 @@ function isRemoteVideoUrlValid (url: any) { // --------------------------------------------------------------------------- export { - sanitizeAndCheckVideoTorrentCreateActivity, sanitizeAndCheckVideoTorrentUpdateActivity, - isVideoTorrentDeleteActivityValid, isRemoteStringIdentifierValid, - isVideoFlagValid, sanitizeAndCheckVideoTorrentObject, isRemoteVideoUrlValid } diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts index 7a3aca6f5..41d16469f 100644 --- a/server/helpers/custom-validators/activitypub/view.ts +++ b/server/helpers/custom-validators/activitypub/view.ts @@ -1,11 +1,11 @@ -import { isActivityPubUrlValid, isBaseActivityValid } from './misc' +import { isActivityPubUrlValid } from './misc' function isViewActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - activity.object.type === 'View' && - isActivityPubUrlValid(activity.object.actor) && - isActivityPubUrlValid(activity.object.object) + return activity.type === 'View' && + isActivityPubUrlValid(activity.actor) && + isActivityPubUrlValid(activity.object) } + // --------------------------------------------------------------------------- export { diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index edf38bc0a..8215840da 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -4,7 +4,7 @@ import * as url from 'url' import * as uuidv4 from 'uuid/v4' import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' -import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' +import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' @@ -42,7 +42,7 @@ async function getOrCreateActorAndServerAndModel ( recurseIfNeeded = true, updateCollections = false ) { - const actorUrl = getAPUrl(activityActor) + const actorUrl = getAPId(activityActor) let created = false let actor = await fetchActorByUrl(actorUrl, fetchType) diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 605705ad3..ebb275e34 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts @@ -2,7 +2,6 @@ import { ActivityAccept } from '../../../../shared/models/activitypub' import { ActorModel } from '../../../models/activitypub/actor' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { addFetchOutboxJob } from '../actor' -import { Notifier } from '../../notifier' async function processAcceptActivity (activity: ActivityAccept, targetActor: ActorModel, inboxActor?: ActorModel) { if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.') diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 2e04ee843..5f4d793a5 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,36 +1,44 @@ -import { ActivityCreate, CacheFileObject, VideoAbuseState, VideoTorrentObject } from '../../../../shared' -import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' +import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../../shared' import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers' -import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { ActorModel } from '../../../models/activitypub/actor' -import { VideoAbuseModel } from '../../../models/video/video-abuse' import { addVideoComment, resolveThread } from '../video-comments' import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { forwardVideoRelatedActivity } from '../send/utils' -import { Redis } from '../../redis' import { createOrUpdateCacheFile } from '../cache-file' -import { getVideoDislikeActivityPubUrl } from '../url' import { Notifier } from '../../notifier' +import { processViewActivity } from './process-view' +import { processDislikeActivity } from './process-dislike' +import { processFlagActivity } from './process-flag' async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { const activityObject = activity.object const activityType = activityObject.type if (activityType === 'View') { - return processCreateView(byActor, activity) - } else if (activityType === 'Dislike') { - return retryTransactionWrapper(processCreateDislike, byActor, activity) - } else if (activityType === 'Video') { + return processViewActivity(activity, byActor) + } + + if (activityType === 'Dislike') { + return retryTransactionWrapper(processDislikeActivity, activity, byActor) + } + + if (activityType === 'Flag') { + return retryTransactionWrapper(processFlagActivity, activity, byActor) + } + + if (activityType === 'Video') { return processCreateVideo(activity) - } else if (activityType === 'Flag') { - return retryTransactionWrapper(processCreateVideoAbuse, byActor, activityObject as VideoAbuseObject) - } else if (activityType === 'Note') { - return retryTransactionWrapper(processCreateVideoComment, byActor, activity) - } else if (activityType === 'CacheFile') { - return retryTransactionWrapper(processCacheFile, byActor, activity) + } + + if (activityType === 'Note') { + return retryTransactionWrapper(processCreateVideoComment, activity, byActor) + } + + if (activityType === 'CacheFile') { + return retryTransactionWrapper(processCacheFile, activity, byActor) } logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) @@ -55,56 +63,7 @@ async function processCreateVideo (activity: ActivityCreate) { return video } -async function processCreateDislike (byActor: ActorModel, activity: ActivityCreate) { - const dislike = activity.object as DislikeObject - const byAccount = byActor.Account - - if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) - - const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) - - return sequelizeTypescript.transaction(async t => { - const rate = { - type: 'dislike' as 'dislike', - videoId: video.id, - accountId: byAccount.id - } - - const [ , created ] = await AccountVideoRateModel.findOrCreate({ - where: rate, - defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), - transaction: t - }) - if (created === true) await video.increment('dislikes', { transaction: t }) - - if (video.isOwned() && created === true) { - // Don't resend the activity to the sender - const exceptions = [ byActor ] - - await forwardVideoRelatedActivity(activity, t, exceptions, video) - } - }) -} - -async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { - const view = activity.object as ViewObject - - const options = { - videoObject: view.object, - fetchType: 'only-video' as 'only-video' - } - const { video } = await getOrCreateVideoAndAccountAndChannel(options) - - await Redis.Instance.addVideoView(video.id) - - if (video.isOwned()) { - // Don't resend the activity to the sender - const exceptions = [ byActor ] - await forwardVideoRelatedActivity(activity, undefined, exceptions, video) - } -} - -async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { +async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { const cacheFile = activity.object as CacheFileObject const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) @@ -120,32 +79,7 @@ async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) } } -async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateData: VideoAbuseObject) { - logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) - - const account = byActor.Account - if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) - - const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoAbuseToCreateData.object }) - - return sequelizeTypescript.transaction(async t => { - const videoAbuseData = { - reporterAccountId: account.id, - reason: videoAbuseToCreateData.content, - videoId: video.id, - state: VideoAbuseState.PENDING - } - - const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) - videoAbuseInstance.Video = video - - Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) - - logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) - }) -} - -async function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { +async function processCreateVideoComment (activity: ActivityCreate, byActor: ActorModel) { const commentObject = activity.object as VideoCommentObject const byAccount = byActor.Account diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts new file mode 100644 index 000000000..bfd69e07a --- /dev/null +++ b/server/lib/activitypub/process/process-dislike.ts @@ -0,0 +1,52 @@ +import { ActivityCreate, ActivityDislike } from '../../../../shared' +import { DislikeObject } from '../../../../shared/models/activitypub/objects' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { sequelizeTypescript } from '../../../initializers' +import { AccountVideoRateModel } from '../../../models/account/account-video-rate' +import { ActorModel } from '../../../models/activitypub/actor' +import { getOrCreateVideoAndAccountAndChannel } from '../videos' +import { forwardVideoRelatedActivity } from '../send/utils' +import { getVideoDislikeActivityPubUrl } from '../url' + +async function processDislikeActivity (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { + return retryTransactionWrapper(processDislike, activity, byActor) +} + +// --------------------------------------------------------------------------- + +export { + processDislikeActivity +} + +// --------------------------------------------------------------------------- + +async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: ActorModel) { + const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object + const byAccount = byActor.Account + + if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) + + const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject }) + + return sequelizeTypescript.transaction(async t => { + const rate = { + type: 'dislike' as 'dislike', + videoId: video.id, + accountId: byAccount.id + } + + const [ , created ] = await AccountVideoRateModel.findOrCreate({ + where: rate, + defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), + transaction: t + }) + if (created === true) await video.increment('dislikes', { transaction: t }) + + if (video.isOwned() && created === true) { + // Don't resend the activity to the sender + const exceptions = [ byActor ] + + await forwardVideoRelatedActivity(activity, t, exceptions, video) + } + }) +} diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts new file mode 100644 index 000000000..79ce6fb41 --- /dev/null +++ b/server/lib/activitypub/process/process-flag.ts @@ -0,0 +1,49 @@ +import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared' +import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { logger } from '../../../helpers/logger' +import { sequelizeTypescript } from '../../../initializers' +import { ActorModel } from '../../../models/activitypub/actor' +import { VideoAbuseModel } from '../../../models/video/video-abuse' +import { getOrCreateVideoAndAccountAndChannel } from '../videos' +import { Notifier } from '../../notifier' +import { getAPId } from '../../../helpers/activitypub' + +async function processFlagActivity (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { + return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor) +} + +// --------------------------------------------------------------------------- + +export { + processFlagActivity +} + +// --------------------------------------------------------------------------- + +async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: ActorModel) { + const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) + + logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object)) + + const account = byActor.Account + if (!account) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) + + const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object }) + + return sequelizeTypescript.transaction(async t => { + const videoAbuseData = { + reporterAccountId: account.id, + reason: flag.content, + videoId: video.id, + state: VideoAbuseState.PENDING + } + + const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) + videoAbuseInstance.Video = video + + Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) + + logger.info('Remote abuse for video uuid %s created', flag.object) + }) +} diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index a67892440..0cd537187 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -6,9 +6,10 @@ import { ActorModel } from '../../../models/activitypub/actor' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { sendAccept } from '../send' import { Notifier } from '../../notifier' +import { getAPId } from '../../../helpers/activitypub' async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { - const activityObject = activity.object + const activityObject = getAPId(activity.object) return retryTransactionWrapper(processFollow, byActor, activityObject) } diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index e8e97eece..2a04167d7 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts @@ -6,6 +6,7 @@ import { ActorModel } from '../../../models/activitypub/actor' import { forwardVideoRelatedActivity } from '../send/utils' import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getVideoLikeActivityPubUrl } from '../url' +import { getAPId } from '../../../helpers/activitypub' async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { return retryTransactionWrapper(processLikeVideo, byActor, activity) @@ -20,7 +21,7 @@ export { // --------------------------------------------------------------------------- async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) { - const videoUrl = activity.object + const videoUrl = getAPId(activity.object) const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 438a013b6..ed0177a67 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -26,6 +26,10 @@ async function processUndoActivity (activity: ActivityUndo, byActor: ActorModel) } } + if (activityToUndo.type === 'Dislike') { + return retryTransactionWrapper(processUndoDislike, byActor, activity) + } + if (activityToUndo.type === 'Follow') { return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo) } @@ -72,7 +76,9 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) { } async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo) { - const dislike = activity.object.object as DislikeObject + const dislike = activity.object.type === 'Dislike' + ? activity.object + : activity.object.object as DislikeObject const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts new file mode 100644 index 000000000..8f66d3630 --- /dev/null +++ b/server/lib/activitypub/process/process-view.ts @@ -0,0 +1,35 @@ +import { ActorModel } from '../../../models/activitypub/actor' +import { getOrCreateVideoAndAccountAndChannel } from '../videos' +import { forwardVideoRelatedActivity } from '../send/utils' +import { Redis } from '../../redis' +import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' + +async function processViewActivity (activity: ActivityView | ActivityCreate, byActor: ActorModel) { + return processCreateView(activity, byActor) +} + +// --------------------------------------------------------------------------- + +export { + processViewActivity +} + +// --------------------------------------------------------------------------- + +async function processCreateView (activity: ActivityView | ActivityCreate, byActor: ActorModel) { + const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object + + const options = { + videoObject: videoObject, + fetchType: 'only-video' as 'only-video' + } + const { video } = await getOrCreateVideoAndAccountAndChannel(options) + + await Redis.Instance.addVideoView(video.id) + + if (video.isOwned()) { + // Don't resend the activity to the sender + const exceptions = [ byActor ] + await forwardVideoRelatedActivity(activity, undefined, exceptions, video) + } +} diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index 2479d5da2..9dd241402 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts @@ -1,5 +1,5 @@ import { Activity, ActivityType } from '../../../../shared/models/activitypub' -import { checkUrlsSameHost, getAPUrl } from '../../../helpers/activitypub' +import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' import { logger } from '../../../helpers/logger' import { ActorModel } from '../../../models/activitypub/actor' import { processAcceptActivity } from './process-accept' @@ -12,6 +12,9 @@ import { processRejectActivity } from './process-reject' import { processUndoActivity } from './process-undo' import { processUpdateActivity } from './process-update' import { getOrCreateActorAndServerAndModel } from '../actor' +import { processDislikeActivity } from './process-dislike' +import { processFlagActivity } from './process-flag' +import { processViewActivity } from './process-view' const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: ActorModel, inboxActor?: ActorModel) => Promise } = { Create: processCreateActivity, @@ -22,7 +25,10 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac Reject: processRejectActivity, Announce: processAnnounceActivity, Undo: processUndoActivity, - Like: processLikeActivity + Like: processLikeActivity, + Dislike: processDislikeActivity, + Flag: processFlagActivity, + View: processViewActivity } async function processActivities ( @@ -40,7 +46,7 @@ async function processActivities ( continue } - const actorUrl = getAPUrl(activity.actor) + const actorUrl = getAPId(activity.actor) // When we fetch remote data, we don't have signature if (options.signatureActor && actorUrl !== options.signatureActor.url) { diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 170e49238..1767df0ae 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts @@ -11,7 +11,7 @@ import { doRequest } from '../../helpers/requests' import { getOrCreateActorAndServerAndModel } from './actor' import { logger } from '../../helpers/logger' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' -import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' +import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { if (video.privacy === VideoPrivacy.PRIVATE) return undefined @@ -41,7 +41,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) { }) if (!body || !body.actor) throw new Error('Body or body actor is invalid') - const actorUrl = getAPUrl(body.actor) + const actorUrl = getAPId(body.actor) if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) } diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 2cce67f0c..45a2b22ea 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts @@ -9,7 +9,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { logger } from '../../helpers/logger' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' import { doRequest } from '../../helpers/requests' -import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' +import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { ActorModel } from '../../models/activitypub/actor' import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' @@ -26,7 +26,7 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa }) if (!body || !body.actor) throw new Error('Body or body actor is invalid') - const actorUrl = getAPUrl(body.actor) + const actorUrl = getAPId(body.actor) if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) } diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index cbdd981c5..e1e523499 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -28,7 +28,7 @@ import { createRates } from './video-rates' import { addVideoShares, shareVideoByServerAndChannel } from './share' import { AccountModel } from '../../models/account/account' import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' -import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' +import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' import { Notifier } from '../notifier' async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { @@ -155,7 +155,7 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid } async function getOrCreateVideoAndAccountAndChannel (options: { - videoObject: VideoTorrentObject | string, + videoObject: { id: string } | string, syncParam?: SyncParam, fetchType?: VideoFetchByUrlType, allowRefresh?: boolean // true by default @@ -166,7 +166,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: { const allowRefresh = options.allowRefresh !== false // Get video url - const videoUrl = getAPUrl(options.videoObject) + const videoUrl = getAPId(options.videoObject) let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) if (videoFromDatabase) { diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts index 2407ac0b5..c7e014b1f 100644 --- a/server/tests/api/check-params/contact-form.ts +++ b/server/tests/api/check-params/contact-form.ts @@ -46,6 +46,8 @@ describe('Test contact form API validators', function () { }) it('Should not accept a contact form if it is disabled in the configuration', async function () { + this.timeout(10000) + killallServers([ server ]) // Contact form is disabled @@ -54,6 +56,8 @@ describe('Test contact form API validators', function () { }) it('Should not accept a contact form if from email is invalid', async function () { + this.timeout(10000) + killallServers([ server ]) // Email & contact form enabled diff --git a/server/tests/api/server/redundancy.ts b/server/tests/api/server/redundancy.ts deleted file mode 100644 index 8053d0491..000000000 --- a/server/tests/api/server/redundancy.ts +++ /dev/null @@ -1,479 +0,0 @@ -/* tslint:disable:no-unused-expression */ - -import * as chai from 'chai' -import 'mocha' -import { VideoDetails } from '../../../../shared/models/videos' -import { - doubleFollow, - flushAndRunMultipleServers, - getFollowingListPaginationAndSort, - getVideo, - immutableAssign, - killallServers, makeGetRequest, - root, - ServerInfo, - setAccessTokensToServers, unfollow, - uploadVideo, - viewVideo, - wait, - waitUntilLog, - checkVideoFilesWereRemoved, removeVideo -} from '../../../../shared/utils' -import { waitJobs } from '../../../../shared/utils/server/jobs' -import * as magnetUtil from 'magnet-uri' -import { updateRedundancy } from '../../../../shared/utils/server/redundancy' -import { ActorFollow } from '../../../../shared/models/actors' -import { readdir } from 'fs-extra' -import { join } from 'path' -import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' -import { getStats } from '../../../../shared/utils/server/stats' -import { ServerStats } from '../../../../shared/models/server/server-stats.model' - -const expect = chai.expect - -let servers: ServerInfo[] = [] -let video1Server2UUID: string - -function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { - const parsed = magnetUtil.decode(file.magnetUri) - - for (const ws of baseWebseeds) { - const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`) - expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined - } - - expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) -} - -async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { - const config = { - redundancy: { - videos: { - check_interval: '5 seconds', - strategies: [ - immutableAssign({ - min_lifetime: '1 hour', - strategy: strategy, - size: '100KB' - }, additionalParams) - ] - } - } - } - servers = await flushAndRunMultipleServers(3, config) - - // Get the access tokens - await setAccessTokensToServers(servers) - - { - const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) - video1Server2UUID = res.body.video.uuid - - await viewVideo(servers[ 1 ].url, video1Server2UUID) - } - - await waitJobs(servers) - - // Server 1 and server 2 follow each other - await doubleFollow(servers[ 0 ], servers[ 1 ]) - // Server 1 and server 3 follow each other - await doubleFollow(servers[ 0 ], servers[ 2 ]) - // Server 2 and server 3 follow each other - await doubleFollow(servers[ 1 ], servers[ 2 ]) - - await waitJobs(servers) -} - -async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { - if (!videoUUID) videoUUID = video1Server2UUID - - const webseeds = [ - 'http://localhost:9002/static/webseed/' + videoUUID - ] - - for (const server of servers) { - { - const res = await getVideo(server.url, videoUUID) - - const video: VideoDetails = res.body - for (const f of video.files) { - checkMagnetWebseeds(f, webseeds, server) - } - } - } -} - -async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { - const res = await getStats(servers[0].url) - const data: ServerStats = res.body - - expect(data.videosRedundancy).to.have.lengthOf(1) - const stat = data.videosRedundancy[0] - - expect(stat.strategy).to.equal(strategy) - expect(stat.totalSize).to.equal(102400) - expect(stat.totalUsed).to.be.at.least(1).and.below(102401) - expect(stat.totalVideoFiles).to.equal(4) - expect(stat.totalVideos).to.equal(1) -} - -async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { - const res = await getStats(servers[0].url) - const data: ServerStats = res.body - - expect(data.videosRedundancy).to.have.lengthOf(1) - - const stat = data.videosRedundancy[0] - expect(stat.strategy).to.equal(strategy) - expect(stat.totalSize).to.equal(102400) - expect(stat.totalUsed).to.equal(0) - expect(stat.totalVideoFiles).to.equal(0) - expect(stat.totalVideos).to.equal(0) -} - -async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) { - if (!videoUUID) videoUUID = video1Server2UUID - - const webseeds = [ - 'http://localhost:9001/static/webseed/' + videoUUID, - 'http://localhost:9002/static/webseed/' + videoUUID - ] - - for (const server of servers) { - const res = await getVideo(server.url, videoUUID) - - const video: VideoDetails = res.body - - for (const file of video.files) { - checkMagnetWebseeds(file, webseeds, server) - - // Only servers 1 and 2 have the video - if (server.serverNumber !== 3) { - await makeGetRequest({ - url: server.url, - statusCodeExpected: 200, - path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, - contentType: null - }) - } - } - } - - for (const directory of [ 'test1', 'test2' ]) { - const files = await readdir(join(root(), directory, 'videos')) - expect(files).to.have.length.at.least(4) - - for (const resolution of [ 240, 360, 480, 720 ]) { - expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined - } - } -} - -async function enableRedundancyOnServer1 () { - await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) - - const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') - const follows: ActorFollow[] = res.body.data - const server2 = follows.find(f => f.following.host === 'localhost:9002') - const server3 = follows.find(f => f.following.host === 'localhost:9003') - - expect(server3).to.not.be.undefined - expect(server3.following.hostRedundancyAllowed).to.be.false - - expect(server2).to.not.be.undefined - expect(server2.following.hostRedundancyAllowed).to.be.true -} - -async function disableRedundancyOnServer1 () { - await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false) - - const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt') - const follows: ActorFollow[] = res.body.data - const server2 = follows.find(f => f.following.host === 'localhost:9002') - const server3 = follows.find(f => f.following.host === 'localhost:9003') - - expect(server3).to.not.be.undefined - expect(server3.following.hostRedundancyAllowed).to.be.false - - expect(server2).to.not.be.undefined - expect(server2.following.hostRedundancyAllowed).to.be.false -} - -async function cleanServers () { - killallServers(servers) -} - -describe('Test videos redundancy', function () { - - describe('With most-views strategy', function () { - const strategy = 'most-views' - - before(function () { - this.timeout(120000) - - return runServers(strategy) - }) - - it('Should have 1 webseed on the first video', async function () { - await check1WebSeed(strategy) - await checkStatsWith1Webseed(strategy) - }) - - it('Should enable redundancy on server 1', function () { - return enableRedundancyOnServer1() - }) - - it('Should have 2 webseed on the first video', async function () { - this.timeout(40000) - - await waitJobs(servers) - await waitUntilLog(servers[0], 'Duplicated ', 4) - await waitJobs(servers) - - await check2Webseeds(strategy) - await checkStatsWith2Webseed(strategy) - }) - - it('Should undo redundancy on server 1 and remove duplicated videos', async function () { - this.timeout(40000) - - await disableRedundancyOnServer1() - - await waitJobs(servers) - await wait(5000) - - await check1WebSeed(strategy) - - await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) - }) - - after(function () { - return cleanServers() - }) - }) - - describe('With trending strategy', function () { - const strategy = 'trending' - - before(function () { - this.timeout(120000) - - return runServers(strategy) - }) - - it('Should have 1 webseed on the first video', async function () { - await check1WebSeed(strategy) - await checkStatsWith1Webseed(strategy) - }) - - it('Should enable redundancy on server 1', function () { - return enableRedundancyOnServer1() - }) - - it('Should have 2 webseed on the first video', async function () { - this.timeout(40000) - - await waitJobs(servers) - await waitUntilLog(servers[0], 'Duplicated ', 4) - await waitJobs(servers) - - await check2Webseeds(strategy) - await checkStatsWith2Webseed(strategy) - }) - - it('Should unfollow on server 1 and remove duplicated videos', async function () { - this.timeout(40000) - - await unfollow(servers[0].url, servers[0].accessToken, servers[1]) - - await waitJobs(servers) - await wait(5000) - - await check1WebSeed(strategy) - - await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) - }) - - after(function () { - return cleanServers() - }) - }) - - describe('With recently added strategy', function () { - const strategy = 'recently-added' - - before(function () { - this.timeout(120000) - - return runServers(strategy, { min_views: 3 }) - }) - - it('Should have 1 webseed on the first video', async function () { - await check1WebSeed(strategy) - await checkStatsWith1Webseed(strategy) - }) - - it('Should enable redundancy on server 1', function () { - return enableRedundancyOnServer1() - }) - - it('Should still have 1 webseed on the first video', async function () { - this.timeout(40000) - - await waitJobs(servers) - await wait(15000) - await waitJobs(servers) - - await check1WebSeed(strategy) - await checkStatsWith1Webseed(strategy) - }) - - it('Should view 2 times the first video to have > min_views config', async function () { - this.timeout(40000) - - await viewVideo(servers[ 0 ].url, video1Server2UUID) - await viewVideo(servers[ 2 ].url, video1Server2UUID) - - await wait(10000) - await waitJobs(servers) - }) - - it('Should have 2 webseed on the first video', async function () { - this.timeout(40000) - - await waitJobs(servers) - await waitUntilLog(servers[0], 'Duplicated ', 4) - await waitJobs(servers) - - await check2Webseeds(strategy) - await checkStatsWith2Webseed(strategy) - }) - - it('Should remove the video and the redundancy files', async function () { - this.timeout(20000) - - await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID) - - await waitJobs(servers) - - for (const server of servers) { - await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber) - } - }) - - after(function () { - return cleanServers() - }) - }) - - describe('Test expiration', function () { - const strategy = 'recently-added' - - async function checkContains (servers: ServerInfo[], str: string) { - for (const server of servers) { - const res = await getVideo(server.url, video1Server2UUID) - const video: VideoDetails = res.body - - for (const f of video.files) { - expect(f.magnetUri).to.contain(str) - } - } - } - - async function checkNotContains (servers: ServerInfo[], str: string) { - for (const server of servers) { - const res = await getVideo(server.url, video1Server2UUID) - const video: VideoDetails = res.body - - for (const f of video.files) { - expect(f.magnetUri).to.not.contain(str) - } - } - } - - before(async function () { - this.timeout(120000) - - await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) - - await enableRedundancyOnServer1() - }) - - it('Should still have 2 webseeds after 10 seconds', async function () { - this.timeout(40000) - - await wait(10000) - - try { - await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') - } catch { - // Maybe a server deleted a redundancy in the scheduler - await wait(2000) - - await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001') - } - }) - - it('Should stop server 1 and expire video redundancy', async function () { - this.timeout(40000) - - killallServers([ servers[0] ]) - - await wait(15000) - - await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') - }) - - after(function () { - return killallServers([ servers[1], servers[2] ]) - }) - }) - - describe('Test file replacement', function () { - let video2Server2UUID: string - const strategy = 'recently-added' - - before(async function () { - this.timeout(120000) - - await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) - - await enableRedundancyOnServer1() - - await waitJobs(servers) - await waitUntilLog(servers[0], 'Duplicated ', 4) - await waitJobs(servers) - - await check2Webseeds(strategy) - await checkStatsWith2Webseed(strategy) - - const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) - video2Server2UUID = res.body.video.uuid - }) - - it('Should cache video 2 webseed on the first video', async function () { - this.timeout(120000) - - await waitJobs(servers) - - let checked = false - - while (checked === false) { - await wait(1000) - - try { - await check1WebSeed(strategy, video1Server2UUID) - await check2Webseeds(strategy, video2Server2UUID) - - checked = true - } catch { - checked = false - } - } - }) - - after(function () { - return cleanServers() - }) - }) -}) diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index 9858e2b15..aaa6c62f7 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts @@ -75,6 +75,7 @@ describe('Test stats (excluding redundancy)', function () { expect(data.totalLocalVideoComments).to.equal(0) expect(data.totalLocalVideos).to.equal(0) expect(data.totalLocalVideoViews).to.equal(0) + expect(data.totalLocalVideoFilesSize).to.equal(0) expect(data.totalUsers).to.equal(1) expect(data.totalVideoComments).to.equal(1) expect(data.totalVideos).to.equal(1) diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 44cb99efb..89994f665 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -5,12 +5,14 @@ import { DislikeObject } from './objects/dislike-object' import { VideoAbuseObject } from './objects/video-abuse-object' import { VideoCommentObject } from './objects/video-comment-object' import { ViewObject } from './objects/view-object' +import { APObject } from './objects/object.model' export type Activity = ActivityCreate | ActivityUpdate | ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | - ActivityUndo | ActivityLike | ActivityReject + ActivityUndo | ActivityLike | ActivityReject | ActivityView | ActivityDislike | ActivityFlag -export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' +export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' | + 'View' | 'Dislike' | 'Flag' export interface ActivityAudience { to: string[] @@ -59,15 +61,34 @@ export interface ActivityReject extends BaseActivity { export interface ActivityAnnounce extends BaseActivity { type: 'Announce' - object: string | { id: string } + object: APObject } export interface ActivityUndo extends BaseActivity { type: 'Undo', - object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce + object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce } export interface ActivityLike extends BaseActivity { type: 'Like', - object: string + object: APObject +} + +export interface ActivityView extends BaseActivity { + type: 'View', + actor: string + object: APObject +} + +export interface ActivityDislike extends BaseActivity { + id: string + type: 'Dislike' + actor: string + object: APObject +} + +export interface ActivityFlag extends BaseActivity { + type: 'Flag', + content: string, + object: APObject } diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts new file mode 100644 index 000000000..3fd33800a --- /dev/null +++ b/shared/models/activitypub/objects/object.model.ts @@ -0,0 +1 @@ +export type APObject = string | { id: string }