Procházet zdrojové kódy

Prepare Dislike/Flag/View fixes

For now we Create these activities, but we should just send them
directly.

This fix handles correctly direct Dislikes/Flags/Views, we'll implement
the sending correctly these activities in the next peertube version
Chocobozzz před 5 roky
rodič
revize
848f499def
30 změnil soubory, kde provedl 330 přidání a 755 odebrání
  1. 2 2
      server/helpers/activitypub.ts
  2. 61 38
      server/helpers/custom-validators/activitypub/activity.ts
  3. 4 21
      server/helpers/custom-validators/activitypub/actor.ts
  4. 0 13
      server/helpers/custom-validators/activitypub/announce.ts
  5. 2 14
      server/helpers/custom-validators/activitypub/cache-file.ts
  6. 14 0
      server/helpers/custom-validators/activitypub/flag.ts
  7. 15 9
      server/helpers/custom-validators/activitypub/misc.ts
  8. 4 11
      server/helpers/custom-validators/activitypub/rate.ts
  9. 0 20
      server/helpers/custom-validators/activitypub/undo.ts
  10. 0 11
      server/helpers/custom-validators/activitypub/video-comments.ts
  11. 0 19
      server/helpers/custom-validators/activitypub/videos.ts
  12. 5 5
      server/helpers/custom-validators/activitypub/view.ts
  13. 2 2
      server/lib/activitypub/actor.ts
  14. 0 1
      server/lib/activitypub/process/process-accept.ts
  15. 26 92
      server/lib/activitypub/process/process-create.ts
  16. 52 0
      server/lib/activitypub/process/process-dislike.ts
  17. 49 0
      server/lib/activitypub/process/process-flag.ts
  18. 2 1
      server/lib/activitypub/process/process-follow.ts
  19. 2 1
      server/lib/activitypub/process/process-like.ts
  20. 7 1
      server/lib/activitypub/process/process-undo.ts
  21. 35 0
      server/lib/activitypub/process/process-view.ts
  22. 9 3
      server/lib/activitypub/process/process.ts
  23. 2 2
      server/lib/activitypub/share.ts
  24. 2 2
      server/lib/activitypub/video-rates.ts
  25. 3 3
      server/lib/activitypub/videos.ts
  26. 4 0
      server/tests/api/check-params/contact-form.ts
  27. 0 479
      server/tests/api/server/redundancy.ts
  28. 1 0
      server/tests/api/server/stats.ts
  29. 26 5
      shared/models/activitypub/activity.ts
  30. 1 0
      shared/models/activitypub/objects/object.model.ts

+ 2 - 2
server/helpers/activitypub.ts

@@ -106,7 +106,7 @@ function buildSignedActivity (byActor: ActorModel, data: Object) {
   return signJsonLDObject(byActor, activity) as Promise<Activity>
 }
 
-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

+ 61 - 38
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)
 }

+ 4 - 21
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 isActorAcceptActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Accept')
-}
-
-function isActorRejectActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Reject')
-}
-
-function isActorUpdateActivityValid (activity: any) {
-  normalizeActor(activity.object)
+function sanitizeAndCheckActorObject (object: any) {
+  normalizeActor(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
 }

+ 0 - 13
server/helpers/custom-validators/activitypub/announce.ts

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

+ 2 - 14
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
 }

+ 14 - 0
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
+}

+ 15 - 9
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)) &&
-    (
-      activity.to === undefined ||
-      (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t)))
-    ) &&
+    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.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
 }

+ 4 - 11
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
 }

+ 0 - 20
server/helpers/custom-validators/activitypub/undo.ts

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

+ 0 - 11
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
 }
 

+ 0 - 19
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
 }

+ 5 - 5
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 {

+ 2 - 2
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)

+ 0 - 1
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.')

+ 26 - 92
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
 

+ 52 - 0
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)
+    }
+  })
+}

+ 49 - 0
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)
+  })
+}

+ 2 - 1
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)
 }

+ 2 - 1
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)

+ 7 - 1
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 })
 

+ 35 - 0
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)
+  }
+}

+ 9 - 3
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<any> } = {
   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) {

+ 2 - 2
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}`)
       }

+ 2 - 2
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}`)
       }

+ 3 - 3
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) {

+ 4 - 0
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

+ 0 - 479
server/tests/api/server/redundancy.ts

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

+ 1 - 0
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)

+ 26 - 5
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
 }

+ 1 - 0
shared/models/activitypub/objects/object.model.ts

@@ -0,0 +1 @@
+export type APObject = string | { id: string }