aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts3
-rw-r--r--server/controllers/activitypub/outbox.ts1
-rw-r--r--server/lib/activitypub/activity.ts14
-rw-r--r--server/lib/activitypub/playlists/get.ts4
-rw-r--r--server/lib/activitypub/process/process-create.ts58
-rw-r--r--server/lib/activitypub/process/process-dislike.ts11
-rw-r--r--server/lib/activitypub/process/process-flag.ts8
-rw-r--r--server/lib/activitypub/process/process-undo.ts43
-rw-r--r--server/lib/activitypub/process/process-update.ts36
-rw-r--r--server/lib/activitypub/send/send-create.ts23
-rw-r--r--server/lib/activitypub/send/send-undo.ts19
-rw-r--r--server/lib/activitypub/send/send-update.ts9
-rw-r--r--server/lib/activitypub/videos/get.ts8
-rw-r--r--server/tests/api/server/follows.ts949
14 files changed, 631 insertions, 555 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 166fc2a22..c47c61f52 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -4,6 +4,7 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect
4import { activityPubContextify } from '@server/lib/activitypub/context' 4import { activityPubContextify } from '@server/lib/activitypub/context'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' 6import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models'
7import { VideoCommentObject } from '@shared/models'
7import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 8import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
8import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 9import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' 10import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants'
@@ -353,7 +354,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
353 videoCommentObject = audiencify(videoCommentObject, audience) 354 videoCommentObject = audiencify(videoCommentObject, audience)
354 355
355 if (req.path.endsWith('/activity')) { 356 if (req.path.endsWith('/activity')) {
356 const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) 357 const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience)
357 return activityPubResponse(activityPubContextify(data, 'Comment'), res) 358 return activityPubResponse(activityPubContextify(data, 'Comment'), res)
358 } 359 }
359 } 360 }
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
index 681a5660c..4175cf276 100644
--- a/server/controllers/activitypub/outbox.ts
+++ b/server/controllers/activitypub/outbox.ts
@@ -63,6 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number
63 63
64 activities.push(announceActivity) 64 activities.push(announceActivity)
65 } else { 65 } else {
66 // FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0
66 const videoObject = await video.toActivityPubObject() 67 const videoObject = await video.toActivityPubObject()
67 const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) 68 const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience)
68 69
diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts
index 1f6ec221e..0fed3e8fd 100644
--- a/server/lib/activitypub/activity.ts
+++ b/server/lib/activitypub/activity.ts
@@ -1,4 +1,5 @@
1import { ActivityType } from '@shared/models' 1import { doJSONRequest } from '@server/helpers/requests'
2import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models'
2 3
3function getAPId (object: string | { id: string }) { 4function getAPId (object: string | { id: string }) {
4 if (typeof object === 'string') return object 5 if (typeof object === 'string') return object
@@ -32,8 +33,19 @@ function buildAvailableActivities (): ActivityType[] {
32 ] 33 ]
33} 34}
34 35
36async function fetchAPObject <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) {
37 if (typeof object === 'string') {
38 const { body } = await doJSONRequest<Exclude<T, string>>(object, { activityPub: true })
39
40 return body
41 }
42
43 return object as Exclude<T, string>
44}
45
35export { 46export {
36 getAPId, 47 getAPId,
48 fetchAPObject,
37 getActivityStreamDuration, 49 getActivityStreamDuration,
38 buildAvailableActivities, 50 buildAvailableActivities,
39 getDurationFromActivityStream 51 getDurationFromActivityStream
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts
index bfaf52cc9..c34554d69 100644
--- a/server/lib/activitypub/playlists/get.ts
+++ b/server/lib/activitypub/playlists/get.ts
@@ -1,12 +1,12 @@
1import { VideoPlaylistModel } from '@server/models/video/video-playlist' 1import { VideoPlaylistModel } from '@server/models/video/video-playlist'
2import { MVideoPlaylistFullSummary } from '@server/types/models' 2import { MVideoPlaylistFullSummary } from '@server/types/models'
3import { APObject } from '@shared/models' 3import { APObjectId } from '@shared/models'
4import { getAPId } from '../activity' 4import { getAPId } from '../activity'
5import { createOrUpdateVideoPlaylist } from './create-update' 5import { createOrUpdateVideoPlaylist } from './create-update'
6import { scheduleRefreshIfNeeded } from './refresh' 6import { scheduleRefreshIfNeeded } from './refresh'
7import { fetchRemoteVideoPlaylist } from './shared' 7import { fetchRemoteVideoPlaylist } from './shared'
8 8
9async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { 9async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise<MVideoPlaylistFullSummary> {
10 const playlistUrl = getAPId(playlistObjectArg) 10 const playlistUrl = getAPId(playlistObjectArg)
11 11
12 const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) 12 const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl)
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 1e6e8956c..e89d1ab45 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -1,13 +1,24 @@
1import { isBlockedByServerOrAccount } from '@server/lib/blocklist' 1import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
2import { isRedundancyAccepted } from '@server/lib/redundancy' 2import { isRedundancyAccepted } from '@server/lib/redundancy'
3import { VideoModel } from '@server/models/video/video' 3import { VideoModel } from '@server/models/video/video'
4import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' 4import {
5 AbuseObject,
6 ActivityCreate,
7 ActivityCreateObject,
8 ActivityObject,
9 CacheFileObject,
10 PlaylistObject,
11 VideoCommentObject,
12 VideoObject,
13 WatchActionObject
14} from '@shared/models'
5import { retryTransactionWrapper } from '../../../helpers/database-utils' 15import { retryTransactionWrapper } from '../../../helpers/database-utils'
6import { logger } from '../../../helpers/logger' 16import { logger } from '../../../helpers/logger'
7import { sequelizeTypescript } from '../../../initializers/database' 17import { sequelizeTypescript } from '../../../initializers/database'
8import { APProcessorOptions } from '../../../types/activitypub-processor.model' 18import { APProcessorOptions } from '../../../types/activitypub-processor.model'
9import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' 19import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models'
10import { Notifier } from '../../notifier' 20import { Notifier } from '../../notifier'
21import { fetchAPObject } from '../activity'
11import { createOrUpdateCacheFile } from '../cache-file' 22import { createOrUpdateCacheFile } from '../cache-file'
12import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' 23import { createOrUpdateLocalVideoViewer } from '../local-video-viewer'
13import { createOrUpdateVideoPlaylist } from '../playlists' 24import { createOrUpdateVideoPlaylist } from '../playlists'
@@ -15,35 +26,35 @@ import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
15import { resolveThread } from '../video-comments' 26import { resolveThread } from '../video-comments'
16import { getOrCreateAPVideo } from '../videos' 27import { getOrCreateAPVideo } from '../videos'
17 28
18async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { 29async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
19 const { activity, byActor } = options 30 const { activity, byActor } = options
20 31
21 // Only notify if it is not from a fetcher job 32 // Only notify if it is not from a fetcher job
22 const notify = options.fromFetch !== true 33 const notify = options.fromFetch !== true
23 const activityObject = activity.object 34 const activityObject = await fetchAPObject<Exclude<ActivityObject, AbuseObject>>(activity.object)
24 const activityType = activityObject.type 35 const activityType = activityObject.type
25 36
26 if (activityType === 'Video') { 37 if (activityType === 'Video') {
27 return processCreateVideo(activity, notify) 38 return processCreateVideo(activityObject, notify)
28 } 39 }
29 40
30 if (activityType === 'Note') { 41 if (activityType === 'Note') {
31 // Comments will be fetched from videos 42 // Comments will be fetched from videos
32 if (options.fromFetch) return 43 if (options.fromFetch) return
33 44
34 return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) 45 return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify)
35 } 46 }
36 47
37 if (activityType === 'WatchAction') { 48 if (activityType === 'WatchAction') {
38 return retryTransactionWrapper(processCreateWatchAction, activity) 49 return retryTransactionWrapper(processCreateWatchAction, activityObject)
39 } 50 }
40 51
41 if (activityType === 'CacheFile') { 52 if (activityType === 'CacheFile') {
42 return retryTransactionWrapper(processCreateCacheFile, activity, byActor) 53 return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor)
43 } 54 }
44 55
45 if (activityType === 'Playlist') { 56 if (activityType === 'Playlist') {
46 return retryTransactionWrapper(processCreatePlaylist, activity, byActor) 57 return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor)
47 } 58 }
48 59
49 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 60 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@@ -58,9 +69,7 @@ export {
58 69
59// --------------------------------------------------------------------------- 70// ---------------------------------------------------------------------------
60 71
61async function processCreateVideo (activity: ActivityCreate, notify: boolean) { 72async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) {
62 const videoToCreateData = activity.object as VideoObject
63
64 const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } 73 const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false }
65 const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) 74 const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
66 75
@@ -69,11 +78,13 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
69 return video 78 return video
70} 79}
71 80
72async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { 81async function processCreateCacheFile (
82 activity: ActivityCreate<CacheFileObject | string>,
83 cacheFile: CacheFileObject,
84 byActor: MActorSignature
85) {
73 if (await isRedundancyAccepted(activity, byActor) !== true) return 86 if (await isRedundancyAccepted(activity, byActor) !== true) return
74 87
75 const cacheFile = activity.object as CacheFileObject
76
77 const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) 88 const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
78 89
79 await sequelizeTypescript.transaction(async t => { 90 await sequelizeTypescript.transaction(async t => {
@@ -87,9 +98,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor
87 } 98 }
88} 99}
89 100
90async function processCreateWatchAction (activity: ActivityCreate) { 101async function processCreateWatchAction (watchAction: WatchActionObject) {
91 const watchAction = activity.object as WatchActionObject
92
93 if (watchAction.actionStatus !== 'CompletedActionStatus') return 102 if (watchAction.actionStatus !== 'CompletedActionStatus') return
94 103
95 const video = await VideoModel.loadByUrl(watchAction.object) 104 const video = await VideoModel.loadByUrl(watchAction.object)
@@ -100,8 +109,12 @@ async function processCreateWatchAction (activity: ActivityCreate) {
100 }) 109 })
101} 110}
102 111
103async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { 112async function processCreateVideoComment (
104 const commentObject = activity.object as VideoCommentObject 113 activity: ActivityCreate<VideoCommentObject | string>,
114 commentObject: VideoCommentObject,
115 byActor: MActorSignature,
116 notify: boolean
117) {
105 const byAccount = byActor.Account 118 const byAccount = byActor.Account
106 119
107 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) 120 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
@@ -144,8 +157,11 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc
144 if (created && notify) Notifier.Instance.notifyOnNewComment(comment) 157 if (created && notify) Notifier.Instance.notifyOnNewComment(comment)
145} 158}
146 159
147async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) { 160async function processCreatePlaylist (
148 const playlistObject = activity.object as PlaylistObject 161 activity: ActivityCreate<PlaylistObject | string>,
162 playlistObject: PlaylistObject,
163 byActor: MActorSignature
164) {
149 const byAccount = byActor.Account 165 const byAccount = byActor.Account
150 166
151 if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) 167 if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url)
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
index 44e349b22..4e270f917 100644
--- a/server/lib/activitypub/process/process-dislike.ts
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -1,5 +1,5 @@
1import { VideoModel } from '@server/models/video/video' 1import { VideoModel } from '@server/models/video/video'
2import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' 2import { ActivityDislike } from '@shared/models'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers/database' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
@@ -7,7 +7,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model'
7import { MActorSignature } from '../../../types/models' 7import { MActorSignature } from '../../../types/models'
8import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' 8import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
9 9
10async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { 10async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
11 const { activity, byActor } = options 11 const { activity, byActor } = options
12 return retryTransactionWrapper(processDislike, activity, byActor) 12 return retryTransactionWrapper(processDislike, activity, byActor)
13} 13}
@@ -20,11 +20,8 @@ export {
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
23async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) { 23async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
24 const dislikeObject = activity.type === 'Dislike' 24 const dislikeObject = activity.object
25 ? activity.object
26 : (activity.object as DislikeObject).object
27
28 const byAccount = byActor.Account 25 const byAccount = byActor.Account
29 26
30 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) 27 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 10f58ef27..bea285670 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -3,7 +3,7 @@ import { AccountModel } from '@server/models/account/account'
3import { VideoModel } from '@server/models/video/video' 3import { VideoModel } from '@server/models/video/video'
4import { VideoCommentModel } from '@server/models/video/video-comment' 4import { VideoCommentModel } from '@server/models/video/video-comment'
5import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' 5import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
6import { AbuseObject, AbuseState, ActivityCreate, ActivityFlag } from '@shared/models' 6import { AbuseState, ActivityFlag } from '@shared/models'
7import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { sequelizeTypescript } from '../../../initializers/database' 9import { sequelizeTypescript } from '../../../initializers/database'
@@ -11,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity'
11import { APProcessorOptions } from '../../../types/activitypub-processor.model' 11import { APProcessorOptions } from '../../../types/activitypub-processor.model'
12import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' 12import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
13 13
14async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 14async function processFlagActivity (options: APProcessorOptions<ActivityFlag>) {
15 const { activity, byActor } = options 15 const { activity, byActor } = options
16 16
17 return retryTransactionWrapper(processCreateAbuse, activity, byActor) 17 return retryTransactionWrapper(processCreateAbuse, activity, byActor)
@@ -25,9 +25,7 @@ export {
25 25
26// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
27 27
28async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { 28async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) {
29 const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
30
31 const account = byActor.Account 29 const account = byActor.Account
32 if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) 30 if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
33 31
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 99423a72b..25f68724d 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -1,6 +1,14 @@
1import { VideoModel } from '@server/models/video/video' 1import { VideoModel } from '@server/models/video/video'
2import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' 2import {
3import { DislikeObject } from '../../../../shared/models/activitypub/objects' 3 ActivityAnnounce,
4 ActivityCreate,
5 ActivityDislike,
6 ActivityFollow,
7 ActivityLike,
8 ActivityUndo,
9 ActivityUndoObject,
10 CacheFileObject
11} from '../../../../shared/models/activitypub'
4import { retryTransactionWrapper } from '../../../helpers/database-utils' 12import { retryTransactionWrapper } from '../../../helpers/database-utils'
5import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
6import { sequelizeTypescript } from '../../../initializers/database' 14import { sequelizeTypescript } from '../../../initializers/database'
@@ -11,10 +19,11 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc
11import { VideoShareModel } from '../../../models/video/video-share' 19import { VideoShareModel } from '../../../models/video/video-share'
12import { APProcessorOptions } from '../../../types/activitypub-processor.model' 20import { APProcessorOptions } from '../../../types/activitypub-processor.model'
13import { MActorSignature } from '../../../types/models' 21import { MActorSignature } from '../../../types/models'
22import { fetchAPObject } from '../activity'
14import { forwardVideoRelatedActivity } from '../send/shared/send-utils' 23import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
15import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' 24import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
16 25
17async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { 26async function processUndoActivity (options: APProcessorOptions<ActivityUndo<ActivityUndoObject>>) {
18 const { activity, byActor } = options 27 const { activity, byActor } = options
19 const activityToUndo = activity.object 28 const activityToUndo = activity.object
20 29
@@ -23,8 +32,10 @@ async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
23 } 32 }
24 33
25 if (activityToUndo.type === 'Create') { 34 if (activityToUndo.type === 'Create') {
26 if (activityToUndo.object.type === 'CacheFile') { 35 const objectToUndo = await fetchAPObject<CacheFileObject>(activityToUndo.object)
27 return retryTransactionWrapper(processUndoCacheFile, byActor, activity) 36
37 if (objectToUndo.type === 'CacheFile') {
38 return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo)
28 } 39 }
29 } 40 }
30 41
@@ -53,8 +64,8 @@ export {
53 64
54// --------------------------------------------------------------------------- 65// ---------------------------------------------------------------------------
55 66
56async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { 67async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo<ActivityLike>) {
57 const likeActivity = activity.object as ActivityLike 68 const likeActivity = activity.object
58 69
59 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) 70 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
60 // We don't care about likes of remote videos 71 // We don't care about likes of remote videos
@@ -78,12 +89,10 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo
78 }) 89 })
79} 90}
80 91
81async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { 92async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo<ActivityDislike>) {
82 const dislike = activity.object.type === 'Dislike' 93 const dislikeActivity = activity.object
83 ? activity.object
84 : activity.object.object as DislikeObject
85 94
86 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object }) 95 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object })
87 // We don't care about likes of remote videos 96 // We don't care about likes of remote videos
88 if (!onlyVideo.isOwned()) return 97 if (!onlyVideo.isOwned()) return
89 98
@@ -91,7 +100,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
91 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 100 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
92 101
93 const video = await VideoModel.loadFull(onlyVideo.id, t) 102 const video = await VideoModel.loadFull(onlyVideo.id, t)
94 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t) 103 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t)
95 if (!rate || rate.type !== 'dislike') { 104 if (!rate || rate.type !== 'dislike') {
96 logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) 105 logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id)
97 return 106 return
@@ -107,9 +116,11 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
107 116
108// --------------------------------------------------------------------------- 117// ---------------------------------------------------------------------------
109 118
110async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { 119async function processUndoCacheFile (
111 const cacheFileObject = activity.object.object as CacheFileObject 120 byActor: MActorSignature,
112 121 activity: ActivityUndo<ActivityCreate<CacheFileObject>>,
122 cacheFileObject: CacheFileObject
123) {
113 const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) 124 const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
114 125
115 return sequelizeTypescript.transaction(async t => { 126 return sequelizeTypescript.transaction(async t => {
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 4afdbd430..9caa74e04 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -1,5 +1,5 @@
1import { isRedundancyAccepted } from '@server/lib/redundancy' 1import { isRedundancyAccepted } from '@server/lib/redundancy'
2import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' 2import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
5import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 5import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -10,16 +10,18 @@ import { sequelizeTypescript } from '../../../initializers/database'
10import { ActorModel } from '../../../models/actor/actor' 10import { ActorModel } from '../../../models/actor/actor'
11import { APProcessorOptions } from '../../../types/activitypub-processor.model' 11import { APProcessorOptions } from '../../../types/activitypub-processor.model'
12import { MActorFull, MActorSignature } from '../../../types/models' 12import { MActorFull, MActorSignature } from '../../../types/models'
13import { fetchAPObject } from '../activity'
13import { APActorUpdater } from '../actors/updater' 14import { APActorUpdater } from '../actors/updater'
14import { createOrUpdateCacheFile } from '../cache-file' 15import { createOrUpdateCacheFile } from '../cache-file'
15import { createOrUpdateVideoPlaylist } from '../playlists' 16import { createOrUpdateVideoPlaylist } from '../playlists'
16import { forwardVideoRelatedActivity } from '../send/shared/send-utils' 17import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
17import { APVideoUpdater, getOrCreateAPVideo } from '../videos' 18import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
18 19
19async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
20 const { activity, byActor } = options 21 const { activity, byActor } = options
21 22
22 const objectType = activity.object.type 23 const object = await fetchAPObject(activity.object)
24 const objectType = object.type
23 25
24 if (objectType === 'Video') { 26 if (objectType === 'Video') {
25 return retryTransactionWrapper(processUpdateVideo, activity) 27 return retryTransactionWrapper(processUpdateVideo, activity)
@@ -28,17 +30,17 @@ async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate
28 if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { 30 if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') {
29 // We need more attributes 31 // We need more attributes
30 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) 32 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url)
31 return retryTransactionWrapper(processUpdateActor, byActorFull, activity) 33 return retryTransactionWrapper(processUpdateActor, byActorFull, object)
32 } 34 }
33 35
34 if (objectType === 'CacheFile') { 36 if (objectType === 'CacheFile') {
35 // We need more attributes 37 // We need more attributes
36 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url) 38 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url)
37 return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) 39 return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity, object)
38 } 40 }
39 41
40 if (objectType === 'Playlist') { 42 if (objectType === 'Playlist') {
41 return retryTransactionWrapper(processUpdatePlaylist, byActor, activity) 43 return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object)
42 } 44 }
43 45
44 return undefined 46 return undefined
@@ -52,7 +54,7 @@ export {
52 54
53// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
54 56
55async function processUpdateVideo (activity: ActivityUpdate) { 57async function processUpdateVideo (activity: ActivityUpdate<VideoObject | string>) {
56 const videoObject = activity.object as VideoObject 58 const videoObject = activity.object as VideoObject
57 59
58 if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { 60 if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) {
@@ -72,11 +74,13 @@ async function processUpdateVideo (activity: ActivityUpdate) {
72 return updater.update(activity.to) 74 return updater.update(activity.to)
73} 75}
74 76
75async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { 77async function processUpdateCacheFile (
78 byActor: MActorSignature,
79 activity: ActivityUpdate<CacheFileObject | string>,
80 cacheFileObject: CacheFileObject
81) {
76 if (await isRedundancyAccepted(activity, byActor) !== true) return 82 if (await isRedundancyAccepted(activity, byActor) !== true) return
77 83
78 const cacheFileObject = activity.object as CacheFileObject
79
80 if (!isCacheFileObjectValid(cacheFileObject)) { 84 if (!isCacheFileObjectValid(cacheFileObject)) {
81 logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) 85 logger.debug('Cache file object sent by update is not valid.', { cacheFileObject })
82 return undefined 86 return undefined
@@ -96,19 +100,19 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ
96 } 100 }
97} 101}
98 102
99async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { 103async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) {
100 const actorObject = activity.object as ActivityPubActor
101
102 logger.debug('Updating remote account "%s".', actorObject.url) 104 logger.debug('Updating remote account "%s".', actorObject.url)
103 105
104 const updater = new APActorUpdater(actorObject, actor) 106 const updater = new APActorUpdater(actorObject, actor)
105 return updater.update() 107 return updater.update()
106} 108}
107 109
108async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { 110async function processUpdatePlaylist (
109 const playlistObject = activity.object as PlaylistObject 111 byActor: MActorSignature,
112 activity: ActivityUpdate<PlaylistObject | string>,
113 playlistObject: PlaylistObject
114) {
110 const byAccount = byActor.Account 115 const byAccount = byActor.Account
111
112 if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) 116 if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url)
113 117
114 await createOrUpdateVideoPlaylist(playlistObject, activity.to) 118 await createOrUpdateVideoPlaylist(playlistObject, activity.to)
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 0e996ab80..2cd4db14d 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -1,6 +1,14 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
3import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' 3import {
4 ActivityAudience,
5 ActivityCreate,
6 ActivityCreateObject,
7 ContextType,
8 VideoCommentObject,
9 VideoPlaylistPrivacy,
10 VideoPrivacy
11} from '@shared/models'
4import { logger, loggerTagsFactory } from '../../../helpers/logger' 12import { logger, loggerTagsFactory } from '../../../helpers/logger'
5import { VideoCommentModel } from '../../../models/video/video-comment' 13import { VideoCommentModel } from '../../../models/video/video-comment'
6import { 14import {
@@ -107,7 +115,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction:
107 115
108 const byActor = comment.Account.Actor 116 const byActor = comment.Account.Actor
109 const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) 117 const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction)
110 const commentObject = comment.toActivityPubObject(threadParentComments) 118 const commentObject = comment.toActivityPubObject(threadParentComments) as VideoCommentObject
111 119
112 const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) 120 const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction)
113 // Add the actor that commented too 121 // Add the actor that commented too
@@ -168,7 +176,12 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction:
168 }) 176 })
169} 177}
170 178
171function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { 179function buildCreateActivity <T extends ActivityCreateObject> (
180 url: string,
181 byActor: MActorLight,
182 object: T,
183 audience?: ActivityAudience
184): ActivityCreate<T> {
172 if (!audience) audience = getAudience(byActor) 185 if (!audience) audience = getAudience(byActor)
173 186
174 return audiencify( 187 return audiencify(
@@ -176,7 +189,9 @@ function buildCreateActivity (url: string, byActor: MActorLight, object: any, au
176 type: 'Create' as 'Create', 189 type: 'Create' as 'Create',
177 id: url + '/activity', 190 id: url + '/activity',
178 actor: byActor.url, 191 actor: byActor.url,
179 object: audiencify(object, audience) 192 object: typeof object === 'string'
193 ? object
194 : audiencify(object, audience)
180 }, 195 },
181 audience 196 audience
182 ) 197 )
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index b8eb47ff6..b0b48c9c4 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -1,14 +1,5 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { 2import { ActivityAudience, ActivityDislike, ActivityLike, ActivityUndo, ActivityUndoObject, ContextType } from '@shared/models'
3 ActivityAnnounce,
4 ActivityAudience,
5 ActivityCreate,
6 ActivityDislike,
7 ActivityFollow,
8 ActivityLike,
9 ActivityUndo,
10 ContextType
11} from '@shared/models'
12import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
13import { VideoModel } from '../../../models/video/video' 4import { VideoModel } from '../../../models/video/video'
14import { 5import {
@@ -128,12 +119,12 @@ export {
128 119
129// --------------------------------------------------------------------------- 120// ---------------------------------------------------------------------------
130 121
131function undoActivityData ( 122function undoActivityData <T extends ActivityUndoObject> (
132 url: string, 123 url: string,
133 byActor: MActorAudience, 124 byActor: MActorAudience,
134 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 125 object: T,
135 audience?: ActivityAudience 126 audience?: ActivityAudience
136): ActivityUndo { 127): ActivityUndo<T> {
137 if (!audience) audience = getAudience(byActor) 128 if (!audience) audience = getAudience(byActor)
138 129
139 return audiencify( 130 return audiencify(
@@ -151,7 +142,7 @@ async function sendUndoVideoRelatedActivity (options: {
151 byActor: MActor 142 byActor: MActor
152 video: MVideoAccountLight 143 video: MVideoAccountLight
153 url: string 144 url: string
154 activity: ActivityFollow | ActivityCreate | ActivityAnnounce 145 activity: ActivityUndoObject
155 contextType: ContextType 146 contextType: ContextType
156 transaction: Transaction 147 transaction: Transaction
157}) { 148}) {
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 3d2b437e4..f3fb741c6 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -1,6 +1,6 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
3import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' 3import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { AccountModel } from '../../../models/account/account' 5import { AccountModel } from '../../../models/account/account'
6import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
@@ -137,7 +137,12 @@ export {
137 137
138// --------------------------------------------------------------------------- 138// ---------------------------------------------------------------------------
139 139
140function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate { 140function buildUpdateActivity (
141 url: string,
142 byActor: MActorLight,
143 object: ActivityUpdateObject,
144 audience?: ActivityAudience
145): ActivityUpdate<ActivityUpdateObject> {
141 if (!audience) audience = getAudience(byActor) 146 if (!audience) audience = getAudience(byActor)
142 147
143 return audiencify( 148 return audiencify(
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts
index 14ba55034..92387c5d4 100644
--- a/server/lib/activitypub/videos/get.ts
+++ b/server/lib/activitypub/videos/get.ts
@@ -3,7 +3,7 @@ import { logger } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' 4import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders'
5import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' 5import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
6import { APObject } from '@shared/models' 6import { APObjectId } from '@shared/models'
7import { getAPId } from '../activity' 7import { getAPId } from '../activity'
8import { refreshVideoIfNeeded } from './refresh' 8import { refreshVideoIfNeeded } from './refresh'
9import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' 9import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
@@ -15,21 +15,21 @@ type GetVideoResult <T> = Promise<{
15}> 15}>
16 16
17type GetVideoParamAll = { 17type GetVideoParamAll = {
18 videoObject: APObject 18 videoObject: APObjectId
19 syncParam?: SyncParam 19 syncParam?: SyncParam
20 fetchType?: 'all' 20 fetchType?: 'all'
21 allowRefresh?: boolean 21 allowRefresh?: boolean
22} 22}
23 23
24type GetVideoParamImmutable = { 24type GetVideoParamImmutable = {
25 videoObject: APObject 25 videoObject: APObjectId
26 syncParam?: SyncParam 26 syncParam?: SyncParam
27 fetchType: 'only-immutable-attributes' 27 fetchType: 'only-immutable-attributes'
28 allowRefresh: false 28 allowRefresh: false
29} 29}
30 30
31type GetVideoParamOther = { 31type GetVideoParamOther = {
32 videoObject: APObject 32 videoObject: APObjectId
33 syncParam?: SyncParam 33 syncParam?: SyncParam
34 fetchType?: 'all' | 'only-video' 34 fetchType?: 'all' | 'only-video'
35 allowRefresh?: boolean 35 allowRefresh?: boolean
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index 2a5fff82b..e3e4605ee 100644
--- a/server/tests/api/server/follows.ts
+++ b/server/tests/api/server/follows.ts
@@ -6,611 +6,636 @@ import { Video, VideoPrivacy } from '@shared/models'
6import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' 6import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands'
7 7
8describe('Test follows', function () { 8describe('Test follows', function () {
9 let servers: PeerTubeServer[] = []
10 9
11 before(async function () { 10 describe('Complex follow', function () {
12 this.timeout(120000) 11 let servers: PeerTubeServer[] = []
13 12
14 servers = await createMultipleServers(3) 13 before(async function () {
14 this.timeout(120000)
15 15
16 // Get the access tokens 16 servers = await createMultipleServers(3)
17 await setAccessTokensToServers(servers)
18 })
19 17
20 describe('Data propagation after follow', function () { 18 // Get the access tokens
19 await setAccessTokensToServers(servers)
20 })
21 21
22 it('Should not have followers/followings', async function () { 22 describe('Data propagation after follow', function () {
23 for (const server of servers) {
24 const bodies = await Promise.all([
25 server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
26 server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
27 ])
28 23
29 for (const body of bodies) { 24 it('Should not have followers/followings', async function () {
30 expect(body.total).to.equal(0) 25 for (const server of servers) {
26 const bodies = await Promise.all([
27 server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
28 server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
29 ])
31 30
32 const follows = body.data 31 for (const body of bodies) {
33 expect(follows).to.be.an('array') 32 expect(body.total).to.equal(0)
34 expect(follows).to.have.lengthOf(0) 33
34 const follows = body.data
35 expect(follows).to.be.an('array')
36 expect(follows).to.have.lengthOf(0)
37 }
35 } 38 }
36 } 39 })
37 }) 40
41 it('Should have server 1 following root account of server 2 and server 3', async function () {
42 this.timeout(30000)
38 43
39 it('Should have server 1 following root account of server 2 and server 3', async function () { 44 await servers[0].follows.follow({
40 this.timeout(30000) 45 hosts: [ servers[2].url ],
46 handles: [ 'root@' + servers[1].host ]
47 })
41 48
42 await servers[0].follows.follow({ 49 await waitJobs(servers)
43 hosts: [ servers[2].url ],
44 handles: [ 'root@' + servers[1].host ]
45 }) 50 })
46 51
47 await waitJobs(servers) 52 it('Should have 2 followings on server 1', async function () {
48 }) 53 const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' })
54 expect(body.total).to.equal(2)
49 55
50 it('Should have 2 followings on server 1', async function () { 56 let follows = body.data
51 const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) 57 expect(follows).to.be.an('array')
52 expect(body.total).to.equal(2) 58 expect(follows).to.have.lengthOf(1)
53 59
54 let follows = body.data 60 const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' })
55 expect(follows).to.be.an('array') 61 follows = follows.concat(body2.data)
56 expect(follows).to.have.lengthOf(1)
57 62
58 const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) 63 const server2Follow = follows.find(f => f.following.host === servers[1].host)
59 follows = follows.concat(body2.data) 64 const server3Follow = follows.find(f => f.following.host === servers[2].host)
60 65
61 const server2Follow = follows.find(f => f.following.host === servers[1].host) 66 expect(server2Follow).to.not.be.undefined
62 const server3Follow = follows.find(f => f.following.host === servers[2].host) 67 expect(server2Follow.following.name).to.equal('root')
68 expect(server2Follow.state).to.equal('accepted')
63 69
64 expect(server2Follow).to.not.be.undefined 70 expect(server3Follow).to.not.be.undefined
65 expect(server2Follow.following.name).to.equal('root') 71 expect(server3Follow.following.name).to.equal('peertube')
66 expect(server2Follow.state).to.equal('accepted') 72 expect(server3Follow.state).to.equal('accepted')
73 })
67 74
68 expect(server3Follow).to.not.be.undefined 75 it('Should have 0 followings on server 2 and 3', async function () {
69 expect(server3Follow.following.name).to.equal('peertube') 76 for (const server of [ servers[1], servers[2] ]) {
70 expect(server3Follow.state).to.equal('accepted') 77 const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
71 }) 78 expect(body.total).to.equal(0)
72 79
73 it('Should have 0 followings on server 2 and 3', async function () { 80 const follows = body.data
74 for (const server of [ servers[1], servers[2] ]) { 81 expect(follows).to.be.an('array')
75 const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) 82 expect(follows).to.have.lengthOf(0)
76 expect(body.total).to.equal(0) 83 }
84 })
85
86 it('Should have 1 followers on server 3', async function () {
87 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
88 expect(body.total).to.equal(1)
77 89
78 const follows = body.data 90 const follows = body.data
79 expect(follows).to.be.an('array') 91 expect(follows).to.be.an('array')
80 expect(follows).to.have.lengthOf(0) 92 expect(follows).to.have.lengthOf(1)
81 } 93 expect(follows[0].follower.host).to.equal(servers[0].host)
82 }) 94 })
83 95
84 it('Should have 1 followers on server 3', async function () { 96 it('Should have 0 followers on server 1 and 2', async function () {
85 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) 97 for (const server of [ servers[0], servers[1] ]) {
86 expect(body.total).to.equal(1) 98 const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
99 expect(body.total).to.equal(0)
87 100
88 const follows = body.data 101 const follows = body.data
89 expect(follows).to.be.an('array') 102 expect(follows).to.be.an('array')
90 expect(follows).to.have.lengthOf(1) 103 expect(follows).to.have.lengthOf(0)
91 expect(follows[0].follower.host).to.equal(servers[0].host) 104 }
92 }) 105 })
93 106
94 it('Should have 0 followers on server 1 and 2', async function () { 107 it('Should search/filter followings on server 1', async function () {
95 for (const server of [ servers[0], servers[1] ]) { 108 const sort = 'createdAt'
96 const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) 109 const start = 0
97 expect(body.total).to.equal(0) 110 const count = 1
98 111
99 const follows = body.data 112 {
100 expect(follows).to.be.an('array') 113 const search = ':' + servers[1].port
101 expect(follows).to.have.lengthOf(0)
102 }
103 })
104 114
105 it('Should search/filter followings on server 1', async function () { 115 {
106 const sort = 'createdAt' 116 const body = await servers[0].follows.getFollowings({ start, count, sort, search })
107 const start = 0 117 expect(body.total).to.equal(1)
108 const count = 1
109 118
110 { 119 const follows = body.data
111 const search = ':' + servers[1].port 120 expect(follows).to.have.lengthOf(1)
121 expect(follows[0].following.host).to.equal(servers[1].host)
122 }
112 123
113 { 124 {
114 const body = await servers[0].follows.getFollowings({ start, count, sort, search }) 125 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' })
115 expect(body.total).to.equal(1) 126 expect(body.total).to.equal(1)
127 expect(body.data).to.have.lengthOf(1)
128 }
116 129
117 const follows = body.data 130 {
118 expect(follows).to.have.lengthOf(1) 131 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
119 expect(follows[0].following.host).to.equal(servers[1].host) 132 expect(body.total).to.equal(1)
120 } 133 expect(body.data).to.have.lengthOf(1)
134 }
121 135
122 { 136 {
123 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) 137 const body = await servers[0].follows.getFollowings({
124 expect(body.total).to.equal(1) 138 start,
125 expect(body.data).to.have.lengthOf(1) 139 count,
140 sort,
141 search,
142 state: 'accepted',
143 actorType: 'Application'
144 })
145 expect(body.total).to.equal(0)
146 expect(body.data).to.have.lengthOf(0)
147 }
148
149 {
150 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
151 expect(body.total).to.equal(0)
152 expect(body.data).to.have.lengthOf(0)
153 }
126 } 154 }
127 155
128 { 156 {
129 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) 157 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' })
130 expect(body.total).to.equal(1) 158 expect(body.total).to.equal(1)
131 expect(body.data).to.have.lengthOf(1) 159 expect(body.data).to.have.lengthOf(1)
132 } 160 }
133 161
134 { 162 {
135 const body = await servers[0].follows.getFollowings({ 163 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' })
136 start,
137 count,
138 sort,
139 search,
140 state: 'accepted',
141 actorType: 'Application'
142 })
143 expect(body.total).to.equal(0) 164 expect(body.total).to.equal(0)
144 expect(body.data).to.have.lengthOf(0)
145 }
146 165
147 {
148 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
149 expect(body.total).to.equal(0)
150 expect(body.data).to.have.lengthOf(0) 166 expect(body.data).to.have.lengthOf(0)
151 } 167 }
152 } 168 })
153 169
154 { 170 it('Should search/filter followers on server 2', async function () {
155 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) 171 const start = 0
156 expect(body.total).to.equal(1) 172 const count = 5
157 expect(body.data).to.have.lengthOf(1) 173 const sort = 'createdAt'
158 }
159 174
160 { 175 {
161 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) 176 const search = servers[0].port + ''
162 expect(body.total).to.equal(0)
163 177
164 expect(body.data).to.have.lengthOf(0) 178 {
165 } 179 const body = await servers[2].follows.getFollowers({ start, count, sort, search })
166 }) 180 expect(body.total).to.equal(1)
167 181
168 it('Should search/filter followers on server 2', async function () { 182 const follows = body.data
169 const start = 0 183 expect(follows).to.have.lengthOf(1)
170 const count = 5 184 expect(follows[0].following.host).to.equal(servers[2].host)
171 const sort = 'createdAt' 185 }
172 186
173 { 187 {
174 const search = servers[0].port + '' 188 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' })
189 expect(body.total).to.equal(1)
190 expect(body.data).to.have.lengthOf(1)
191 }
175 192
176 { 193 {
177 const body = await servers[2].follows.getFollowers({ start, count, sort, search }) 194 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
178 expect(body.total).to.equal(1) 195 expect(body.total).to.equal(0)
196 expect(body.data).to.have.lengthOf(0)
197 }
179 198
180 const follows = body.data 199 {
181 expect(follows).to.have.lengthOf(1) 200 const body = await servers[2].follows.getFollowers({
182 expect(follows[0].following.host).to.equal(servers[2].host) 201 start,
183 } 202 count,
203 sort,
204 search,
205 state: 'accepted',
206 actorType: 'Application'
207 })
208 expect(body.total).to.equal(1)
209 expect(body.data).to.have.lengthOf(1)
210 }
184 211
185 { 212 {
186 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) 213 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' })
187 expect(body.total).to.equal(1) 214 expect(body.total).to.equal(0)
188 expect(body.data).to.have.lengthOf(1) 215 expect(body.data).to.have.lengthOf(0)
216 }
189 } 217 }
190 218
191 { 219 {
192 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) 220 const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' })
193 expect(body.total).to.equal(0) 221 expect(body.total).to.equal(0)
194 expect(body.data).to.have.lengthOf(0)
195 }
196 222
197 { 223 const follows = body.data
198 const body = await servers[2].follows.getFollowers({ 224 expect(follows).to.have.lengthOf(0)
199 start,
200 count,
201 sort,
202 search,
203 state: 'accepted',
204 actorType: 'Application'
205 })
206 expect(body.total).to.equal(1)
207 expect(body.data).to.have.lengthOf(1)
208 } 225 }
226 })
209 227
210 { 228 it('Should have the correct follows counts', async function () {
211 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) 229 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
212 expect(body.total).to.equal(0) 230 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
213 expect(body.data).to.have.lengthOf(0) 231 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
214 } 232
215 } 233 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
234 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
235 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
236 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
237
238 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
239 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
240 })
241
242 it('Should unfollow server 3 on server 1', async function () {
243 this.timeout(15000)
244
245 await servers[0].follows.unfollow({ target: servers[2] })
216 246
217 { 247 await waitJobs(servers)
218 const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) 248 })
249
250 it('Should not follow server 3 on server 1 anymore', async function () {
251 const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' })
252 expect(body.total).to.equal(1)
253
254 const follows = body.data
255 expect(follows).to.be.an('array')
256 expect(follows).to.have.lengthOf(1)
257
258 expect(follows[0].following.host).to.equal(servers[1].host)
259 })
260
261 it('Should not have server 1 as follower on server 3 anymore', async function () {
262 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
219 expect(body.total).to.equal(0) 263 expect(body.total).to.equal(0)
220 264
221 const follows = body.data 265 const follows = body.data
266 expect(follows).to.be.an('array')
222 expect(follows).to.have.lengthOf(0) 267 expect(follows).to.have.lengthOf(0)
223 } 268 })
224 })
225 269
226 it('Should have the correct follows counts', async function () { 270 it('Should have the correct follows counts after the unfollow', async function () {
227 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) 271 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
228 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) 272 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
229 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) 273 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
230 274
231 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) 275 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
232 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 276 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
233 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) 277 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
234 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
235 278
236 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 279 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 })
237 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) 280 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
238 }) 281 })
239 282
240 it('Should unfollow server 3 on server 1', async function () { 283 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
241 this.timeout(15000) 284 this.timeout(160000)
242 285
243 await servers[0].follows.unfollow({ target: servers[2] }) 286 await servers[1].videos.upload({ attributes: { name: 'server2' } })
287 await servers[2].videos.upload({ attributes: { name: 'server3' } })
244 288
245 await waitJobs(servers) 289 await waitJobs(servers)
246 })
247 290
248 it('Should not follow server 3 on server 1 anymore', async function () { 291 {
249 const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) 292 const { total, data } = await servers[0].videos.list()
250 expect(body.total).to.equal(1) 293 expect(total).to.equal(1)
294 expect(data[0].name).to.equal('server2')
295 }
251 296
252 const follows = body.data 297 {
253 expect(follows).to.be.an('array') 298 const { total, data } = await servers[1].videos.list()
254 expect(follows).to.have.lengthOf(1) 299 expect(total).to.equal(1)
300 expect(data[0].name).to.equal('server2')
301 }
255 302
256 expect(follows[0].following.host).to.equal(servers[1].host) 303 {
257 }) 304 const { total, data } = await servers[2].videos.list()
305 expect(total).to.equal(1)
306 expect(data[0].name).to.equal('server3')
307 }
308 })
258 309
259 it('Should not have server 1 as follower on server 3 anymore', async function () { 310 it('Should remove account follow', async function () {
260 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) 311 this.timeout(15000)
261 expect(body.total).to.equal(0)
262 312
263 const follows = body.data 313 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
264 expect(follows).to.be.an('array')
265 expect(follows).to.have.lengthOf(0)
266 })
267 314
268 it('Should have the correct follows counts after the unfollow', async function () { 315 await waitJobs(servers)
269 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 316 })
270 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
271 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
272 317
273 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 318 it('Should have removed the account follow', async function () {
274 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) 319 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
275 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) 320 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
276 321
277 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) 322 {
278 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) 323 const { total, data } = await servers[0].follows.getFollowings()
279 }) 324 expect(total).to.equal(0)
325 expect(data).to.have.lengthOf(0)
326 }
280 327
281 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { 328 {
282 this.timeout(160000) 329 const { total, data } = await servers[0].videos.list()
330 expect(total).to.equal(0)
331 expect(data).to.have.lengthOf(0)
332 }
333 })
283 334
284 await servers[1].videos.upload({ attributes: { name: 'server2' } }) 335 it('Should follow a channel', async function () {
285 await servers[2].videos.upload({ attributes: { name: 'server3' } }) 336 this.timeout(15000)
286 337
287 await waitJobs(servers) 338 await servers[0].follows.follow({
339 handles: [ 'root_channel@' + servers[1].host ]
340 })
288 341
289 { 342 await waitJobs(servers)
290 const { total, data } = await servers[0].videos.list()
291 expect(total).to.equal(1)
292 expect(data[0].name).to.equal('server2')
293 }
294 343
295 { 344 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
296 const { total, data } = await servers[1].videos.list() 345 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
297 expect(total).to.equal(1)
298 expect(data[0].name).to.equal('server2')
299 }
300 346
301 { 347 {
302 const { total, data } = await servers[2].videos.list() 348 const { total, data } = await servers[0].follows.getFollowings()
303 expect(total).to.equal(1) 349 expect(total).to.equal(1)
304 expect(data[0].name).to.equal('server3') 350 expect(data).to.have.lengthOf(1)
305 } 351 }
352
353 {
354 const { total, data } = await servers[0].videos.list()
355 expect(total).to.equal(1)
356 expect(data).to.have.lengthOf(1)
357 }
358 })
306 }) 359 })
307 360
308 it('Should remove account follow', async function () { 361 describe('Should propagate data on a new server follow', function () {
309 this.timeout(15000) 362 let video4: Video
310 363
311 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) 364 before(async function () {
365 this.timeout(240000)
312 366
313 await waitJobs(servers) 367 const video4Attributes = {
314 }) 368 name: 'server3-4',
369 category: 2,
370 nsfw: true,
371 licence: 6,
372 tags: [ 'tag1', 'tag2', 'tag3' ]
373 }
315 374
316 it('Should have removed the account follow', async function () { 375 await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
317 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) 376 await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
318 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
319 377
320 { 378 const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes })
321 const { total, data } = await servers[0].follows.getFollowings()
322 expect(total).to.equal(0)
323 expect(data).to.have.lengthOf(0)
324 }
325 379
326 { 380 await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
327 const { total, data } = await servers[0].videos.list() 381 await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
328 expect(total).to.equal(0)
329 expect(data).to.have.lengthOf(0)
330 }
331 })
332 382
333 it('Should follow a channel', async function () { 383 {
334 this.timeout(15000) 384 const userAccessToken = await servers[2].users.generateUserAndToken('captain')
335 385
336 await servers[0].follows.follow({ 386 await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' })
337 handles: [ 'root_channel@' + servers[1].host ] 387 await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' })
338 }) 388 }
339 389
340 await waitJobs(servers) 390 {
391 await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' })
341 392
342 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) 393 await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
343 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) 394 await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
395 await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' })
396 }
344 397
345 { 398 {
346 const { total, data } = await servers[0].follows.getFollowings() 399 const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' })
347 expect(total).to.equal(1) 400 await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
348 expect(data).to.have.lengthOf(1)
349 }
350 401
351 { 402 const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
352 const { total, data } = await servers[0].videos.list()
353 expect(total).to.equal(1)
354 expect(data).to.have.lengthOf(1)
355 }
356 })
357 })
358 403
359 describe('Should propagate data on a new server follow', function () { 404 await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
360 let video4: Video
361 405
362 before(async function () { 406 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId })
363 this.timeout(120000) 407 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId })
408 }
364 409
365 const video4Attributes = { 410 await servers[2].captions.add({
366 name: 'server3-4', 411 language: 'ar',
367 category: 2, 412 videoId: video4CreateResult.id,
368 nsfw: true, 413 fixture: 'subtitle-good2.vtt'
369 licence: 6, 414 })
370 tags: [ 'tag1', 'tag2', 'tag3' ]
371 }
372 415
373 await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) 416 await waitJobs(servers)
374 await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
375 const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes })
376 await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
377 await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
378 417
379 { 418 // Server 1 follows server 3
380 const userAccessToken = await servers[2].users.generateUserAndToken('captain') 419 await servers[0].follows.follow({ hosts: [ servers[2].url ] })
381 420
382 await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) 421 await waitJobs(servers)
383 await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) 422 })
384 }
385 423
386 { 424 it('Should have the correct follows counts', async function () {
387 await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) 425 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
426 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
427 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
428 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
388 429
389 await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) 430 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
390 await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) 431 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
391 await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) 432 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
392 } 433 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
393 434
394 { 435 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
395 const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) 436 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
396 await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) 437 })
397 438
398 const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) 439 it('Should have propagated videos', async function () {
440 const { total, data } = await servers[0].videos.list()
441 expect(total).to.equal(7)
442
443 const video2 = data.find(v => v.name === 'server3-2')
444 video4 = data.find(v => v.name === 'server3-4')
445 const video6 = data.find(v => v.name === 'server3-6')
446
447 expect(video2).to.not.be.undefined
448 expect(video4).to.not.be.undefined
449 expect(video6).to.not.be.undefined
450
451 const isLocal = false
452 const checkAttributes = {
453 name: 'server3-4',
454 category: 2,
455 licence: 6,
456 language: 'zh',
457 nsfw: true,
458 description: 'my super description',
459 support: 'my super support text',
460 account: {
461 name: 'root',
462 host: servers[2].host
463 },
464 isLocal,
465 commentsEnabled: true,
466 downloadEnabled: true,
467 duration: 5,
468 tags: [ 'tag1', 'tag2', 'tag3' ],
469 privacy: VideoPrivacy.PUBLIC,
470 likes: 1,
471 dislikes: 1,
472 channel: {
473 displayName: 'Main root channel',
474 name: 'root_channel',
475 description: '',
476 isLocal
477 },
478 fixture: 'video_short.webm',
479 files: [
480 {
481 resolution: 720,
482 size: 218910
483 }
484 ]
485 }
486 await completeVideoCheck({
487 server: servers[0],
488 originServer: servers[2],
489 videoUUID: video4.uuid,
490 attributes: checkAttributes
491 })
492 })
399 493
400 await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) 494 it('Should have propagated comments', async function () {
495 const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' })
401 496
402 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) 497 expect(total).to.equal(2)
403 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) 498 expect(data).to.be.an('array')
404 } 499 expect(data).to.have.lengthOf(2)
405 500
406 await servers[2].captions.add({ 501 {
407 language: 'ar', 502 const comment = data[0]
408 videoId: video4CreateResult.id, 503 expect(comment.inReplyToCommentId).to.be.null
409 fixture: 'subtitle-good2.vtt' 504 expect(comment.text).equal('my super first comment')
410 }) 505 expect(comment.videoId).to.equal(video4.id)
506 expect(comment.id).to.equal(comment.threadId)
507 expect(comment.account.name).to.equal('root')
508 expect(comment.account.host).to.equal(servers[2].host)
509 expect(comment.totalReplies).to.equal(3)
510 expect(dateIsValid(comment.createdAt as string)).to.be.true
511 expect(dateIsValid(comment.updatedAt as string)).to.be.true
512
513 const threadId = comment.threadId
514
515 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId })
516 expect(tree.comment.text).equal('my super first comment')
517 expect(tree.children).to.have.lengthOf(2)
518
519 const firstChild = tree.children[0]
520 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
521 expect(firstChild.children).to.have.lengthOf(1)
522
523 const childOfFirstChild = firstChild.children[0]
524 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
525 expect(childOfFirstChild.children).to.have.lengthOf(0)
526
527 const secondChild = tree.children[1]
528 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
529 expect(secondChild.children).to.have.lengthOf(0)
530 }
411 531
412 await waitJobs(servers) 532 {
533 const deletedComment = data[1]
534 expect(deletedComment).to.not.be.undefined
535 expect(deletedComment.isDeleted).to.be.true
536 expect(deletedComment.deletedAt).to.not.be.null
537 expect(deletedComment.text).to.equal('')
538 expect(deletedComment.inReplyToCommentId).to.be.null
539 expect(deletedComment.account).to.be.null
540 expect(deletedComment.totalReplies).to.equal(2)
541 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
542
543 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId })
544 const [ commentRoot, deletedChildRoot ] = tree.children
545
546 expect(deletedChildRoot).to.not.be.undefined
547 expect(deletedChildRoot.comment.isDeleted).to.be.true
548 expect(deletedChildRoot.comment.deletedAt).to.not.be.null
549 expect(deletedChildRoot.comment.text).to.equal('')
550 expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
551 expect(deletedChildRoot.comment.account).to.be.null
552 expect(deletedChildRoot.children).to.have.lengthOf(1)
553
554 const answerToDeletedChild = deletedChildRoot.children[0]
555 expect(answerToDeletedChild.comment).to.not.be.undefined
556 expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id)
557 expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted')
558 expect(answerToDeletedChild.comment.account.name).to.equal('root')
559
560 expect(commentRoot.comment).to.not.be.undefined
561 expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
562 expect(commentRoot.comment.text).to.equal('answer to deleted')
563 expect(commentRoot.comment.account.name).to.equal('root')
564 }
565 })
413 566
414 // Server 1 follows server 3 567 it('Should have propagated captions', async function () {
415 await servers[0].follows.follow({ hosts: [ servers[2].url ] }) 568 const body = await servers[0].captions.list({ videoId: video4.id })
569 expect(body.total).to.equal(1)
570 expect(body.data).to.have.lengthOf(1)
416 571
417 await waitJobs(servers) 572 const caption1 = body.data[0]
418 }) 573 expect(caption1.language.id).to.equal('ar')
574 expect(caption1.language.label).to.equal('Arabic')
575 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
576 await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
577 })
419 578
420 it('Should have the correct follows counts', async function () { 579 it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
421 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) 580 this.timeout(5000)
422 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
423 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
424 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
425 581
426 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 582 await servers[0].follows.unfollow({ target: servers[2] })
427 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
428 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
429 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
430 583
431 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) 584 await waitJobs(servers)
432 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
433 })
434 585
435 it('Should have propagated videos', async function () { 586 const { total } = await servers[0].videos.list()
436 const { total, data } = await servers[0].videos.list() 587 expect(total).to.equal(1)
437 expect(total).to.equal(7)
438
439 const video2 = data.find(v => v.name === 'server3-2')
440 video4 = data.find(v => v.name === 'server3-4')
441 const video6 = data.find(v => v.name === 'server3-6')
442
443 expect(video2).to.not.be.undefined
444 expect(video4).to.not.be.undefined
445 expect(video6).to.not.be.undefined
446
447 const isLocal = false
448 const checkAttributes = {
449 name: 'server3-4',
450 category: 2,
451 licence: 6,
452 language: 'zh',
453 nsfw: true,
454 description: 'my super description',
455 support: 'my super support text',
456 account: {
457 name: 'root',
458 host: servers[2].host
459 },
460 isLocal,
461 commentsEnabled: true,
462 downloadEnabled: true,
463 duration: 5,
464 tags: [ 'tag1', 'tag2', 'tag3' ],
465 privacy: VideoPrivacy.PUBLIC,
466 likes: 1,
467 dislikes: 1,
468 channel: {
469 displayName: 'Main root channel',
470 name: 'root_channel',
471 description: '',
472 isLocal
473 },
474 fixture: 'video_short.webm',
475 files: [
476 {
477 resolution: 720,
478 size: 218910
479 }
480 ]
481 }
482 await completeVideoCheck({
483 server: servers[0],
484 originServer: servers[2],
485 videoUUID: video4.uuid,
486 attributes: checkAttributes
487 }) 588 })
488 }) 589 })
489 590
490 it('Should have propagated comments', async function () { 591 after(async function () {
491 const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) 592 await cleanupTests(servers)
492
493 expect(total).to.equal(2)
494 expect(data).to.be.an('array')
495 expect(data).to.have.lengthOf(2)
496
497 {
498 const comment = data[0]
499 expect(comment.inReplyToCommentId).to.be.null
500 expect(comment.text).equal('my super first comment')
501 expect(comment.videoId).to.equal(video4.id)
502 expect(comment.id).to.equal(comment.threadId)
503 expect(comment.account.name).to.equal('root')
504 expect(comment.account.host).to.equal(servers[2].host)
505 expect(comment.totalReplies).to.equal(3)
506 expect(dateIsValid(comment.createdAt as string)).to.be.true
507 expect(dateIsValid(comment.updatedAt as string)).to.be.true
508
509 const threadId = comment.threadId
510
511 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId })
512 expect(tree.comment.text).equal('my super first comment')
513 expect(tree.children).to.have.lengthOf(2)
514
515 const firstChild = tree.children[0]
516 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
517 expect(firstChild.children).to.have.lengthOf(1)
518
519 const childOfFirstChild = firstChild.children[0]
520 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
521 expect(childOfFirstChild.children).to.have.lengthOf(0)
522
523 const secondChild = tree.children[1]
524 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
525 expect(secondChild.children).to.have.lengthOf(0)
526 }
527
528 {
529 const deletedComment = data[1]
530 expect(deletedComment).to.not.be.undefined
531 expect(deletedComment.isDeleted).to.be.true
532 expect(deletedComment.deletedAt).to.not.be.null
533 expect(deletedComment.text).to.equal('')
534 expect(deletedComment.inReplyToCommentId).to.be.null
535 expect(deletedComment.account).to.be.null
536 expect(deletedComment.totalReplies).to.equal(2)
537 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
538
539 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId })
540 const [ commentRoot, deletedChildRoot ] = tree.children
541
542 expect(deletedChildRoot).to.not.be.undefined
543 expect(deletedChildRoot.comment.isDeleted).to.be.true
544 expect(deletedChildRoot.comment.deletedAt).to.not.be.null
545 expect(deletedChildRoot.comment.text).to.equal('')
546 expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
547 expect(deletedChildRoot.comment.account).to.be.null
548 expect(deletedChildRoot.children).to.have.lengthOf(1)
549
550 const answerToDeletedChild = deletedChildRoot.children[0]
551 expect(answerToDeletedChild.comment).to.not.be.undefined
552 expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id)
553 expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted')
554 expect(answerToDeletedChild.comment.account.name).to.equal('root')
555
556 expect(commentRoot.comment).to.not.be.undefined
557 expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
558 expect(commentRoot.comment.text).to.equal('answer to deleted')
559 expect(commentRoot.comment.account.name).to.equal('root')
560 }
561 }) 593 })
594 })
562 595
563 it('Should have propagated captions', async function () { 596 describe('Simple data propagation propagate data on a new channel follow', function () {
564 const body = await servers[0].captions.list({ videoId: video4.id }) 597 let servers: PeerTubeServer[] = []
565 expect(body.total).to.equal(1)
566 expect(body.data).to.have.lengthOf(1)
567 598
568 const caption1 = body.data[0] 599 before(async function () {
569 expect(caption1.language.id).to.equal('ar') 600 this.timeout(120000)
570 expect(caption1.language.label).to.equal('Arabic')
571 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
572 await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
573 })
574 601
575 it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { 602 servers = await createMultipleServers(3)
576 this.timeout(5000) 603 await setAccessTokensToServers(servers)
577 604
578 await servers[0].follows.unfollow({ target: servers[2] }) 605 await servers[0].videos.upload({ attributes: { name: 'video to add' } })
579 606
580 await waitJobs(servers) 607 await waitJobs(servers)
581 608
582 const { total } = await servers[0].videos.list() 609 for (const server of [ servers[1], servers[2] ]) {
583 expect(total).to.equal(1) 610 const video = await server.videos.find({ name: 'video to add' })
611 expect(video).to.not.exist
612 }
584 }) 613 })
585 })
586
587 describe('Should propagate data on a new channel follow', function () {
588 614
589 before(async function () { 615 it('Should have propagated video after new channel follow', async function () {
590 this.timeout(60000) 616 this.timeout(60000)
591 617
592 await servers[2].videos.upload({ attributes: { name: 'server3-7' } }) 618 await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] })
593 619
594 await waitJobs(servers) 620 await waitJobs(servers)
595 621
596 const video = await servers[0].videos.find({ name: 'server3-7' }) 622 const video = await servers[1].videos.find({ name: 'video to add' })
597 expect(video).to.not.exist 623 expect(video).to.exist
598 }) 624 })
599 625
600 it('Should have propagated channel video', async function () { 626 it('Should have propagated video after new account follow', async function () {
601 this.timeout(60000) 627 this.timeout(60000)
602 628
603 await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] }) 629 await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] })
604 630
605 await waitJobs(servers) 631 await waitJobs(servers)
606 632
607 const video = await servers[0].videos.find({ name: 'server3-7' }) 633 const video = await servers[2].videos.find({ name: 'video to add' })
608
609 expect(video).to.exist 634 expect(video).to.exist
610 }) 635 })
611 })
612 636
613 after(async function () { 637 after(async function () {
614 await cleanupTests(servers) 638 await cleanupTests(servers)
639 })
615 }) 640 })
616}) 641})