diff options
author | Chocobozzz <me@florianbigard.com> | 2023-06-05 15:51:16 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-06-29 10:18:00 +0200 |
commit | cefe22cf7c5286af1eb0e7a19937e741e2c2f58a (patch) | |
tree | e07607fb9f3a1e1ba91b07b701ad6f599a9b24bf | |
parent | f987425bd192614d7086b029019a3aae32e90516 (diff) | |
download | PeerTube-cefe22cf7c5286af1eb0e7a19937e741e2c2f58a.tar.gz PeerTube-cefe22cf7c5286af1eb0e7a19937e741e2c2f58a.tar.zst PeerTube-cefe22cf7c5286af1eb0e7a19937e741e2c2f58a.zip |
Fetch remote AP objects if only id is specified
19 files changed, 684 insertions, 585 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 | |||
4 | import { activityPubContextify } from '@server/lib/activitypub/context' | 4 | import { activityPubContextify } from '@server/lib/activitypub/context' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' | 6 | import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' |
7 | import { VideoCommentObject } from '@shared/models' | ||
7 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' | 8 | import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' |
8 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 9 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' | 10 | import { 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 @@ | |||
1 | import { ActivityType } from '@shared/models' | 1 | import { doJSONRequest } from '@server/helpers/requests' |
2 | import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models' | ||
2 | 3 | ||
3 | function getAPId (object: string | { id: string }) { | 4 | function 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 | ||
36 | async 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 | |||
35 | export { | 46 | export { |
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 @@ | |||
1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 1 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
2 | import { MVideoPlaylistFullSummary } from '@server/types/models' | 2 | import { MVideoPlaylistFullSummary } from '@server/types/models' |
3 | import { APObject } from '@shared/models' | 3 | import { APObjectId } from '@shared/models' |
4 | import { getAPId } from '../activity' | 4 | import { getAPId } from '../activity' |
5 | import { createOrUpdateVideoPlaylist } from './create-update' | 5 | import { createOrUpdateVideoPlaylist } from './create-update' |
6 | import { scheduleRefreshIfNeeded } from './refresh' | 6 | import { scheduleRefreshIfNeeded } from './refresh' |
7 | import { fetchRemoteVideoPlaylist } from './shared' | 7 | import { fetchRemoteVideoPlaylist } from './shared' |
8 | 8 | ||
9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { | 9 | async 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 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | 1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' |
2 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' | 4 | import { |
5 | AbuseObject, | ||
6 | ActivityCreate, | ||
7 | ActivityCreateObject, | ||
8 | ActivityObject, | ||
9 | CacheFileObject, | ||
10 | PlaylistObject, | ||
11 | VideoCommentObject, | ||
12 | VideoObject, | ||
13 | WatchActionObject | ||
14 | } from '@shared/models' | ||
5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 15 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
6 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 17 | import { sequelizeTypescript } from '../../../initializers/database' |
8 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 18 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 19 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
10 | import { Notifier } from '../../notifier' | 20 | import { Notifier } from '../../notifier' |
21 | import { fetchAPObject } from '../activity' | ||
11 | import { createOrUpdateCacheFile } from '../cache-file' | 22 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' | 23 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' |
13 | import { createOrUpdateVideoPlaylist } from '../playlists' | 24 | import { createOrUpdateVideoPlaylist } from '../playlists' |
@@ -15,35 +26,35 @@ import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | |||
15 | import { resolveThread } from '../video-comments' | 26 | import { resolveThread } from '../video-comments' |
16 | import { getOrCreateAPVideo } from '../videos' | 27 | import { getOrCreateAPVideo } from '../videos' |
17 | 28 | ||
18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 29 | async 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 | ||
61 | async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | 72 | async 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 | ||
72 | async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { | 81 | async 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 | ||
90 | async function processCreateWatchAction (activity: ActivityCreate) { | 101 | async 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 | ||
103 | async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { | 112 | async 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 | ||
147 | async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) { | 160 | async 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 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' | 2 | import { ActivityDislike } from '@shared/models' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { sequelizeTypescript } from '../../../initializers/database' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
@@ -7,7 +7,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
7 | import { MActorSignature } from '../../../types/models' | 7 | import { MActorSignature } from '../../../types/models' |
8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 8 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
9 | 9 | ||
10 | async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { | 10 | async 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 | ||
23 | async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) { | 23 | async 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' | |||
3 | import { VideoModel } from '@server/models/video/video' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | 4 | import { VideoCommentModel } from '@server/models/video/video-comment' |
5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 5 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' |
6 | import { AbuseObject, AbuseState, ActivityCreate, ActivityFlag } from '@shared/models' | 6 | import { AbuseState, ActivityFlag } from '@shared/models' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { sequelizeTypescript } from '../../../initializers/database' | 9 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity' | |||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' | 12 | import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' |
13 | 13 | ||
14 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 14 | async 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 | ||
28 | async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { | 28 | async 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 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | 1 | import { VideoModel } from '@server/models/video/video' |
2 | import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' | 2 | import { |
3 | import { 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' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 12 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers/database' | 14 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -11,10 +19,11 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc | |||
11 | import { VideoShareModel } from '../../../models/video/video-share' | 19 | import { VideoShareModel } from '../../../models/video/video-share' |
12 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 20 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
13 | import { MActorSignature } from '../../../types/models' | 21 | import { MActorSignature } from '../../../types/models' |
22 | import { fetchAPObject } from '../activity' | ||
14 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 23 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
15 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' | 24 | import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' |
16 | 25 | ||
17 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | 26 | async 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 | ||
56 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { | 67 | async 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 | ||
81 | async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { | 92 | async 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 | ||
110 | async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { | 119 | async 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 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 1 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
2 | import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -10,16 +10,18 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
10 | import { ActorModel } from '../../../models/actor/actor' | 10 | import { ActorModel } from '../../../models/actor/actor' |
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MActorFull, MActorSignature } from '../../../types/models' | 12 | import { MActorFull, MActorSignature } from '../../../types/models' |
13 | import { fetchAPObject } from '../activity' | ||
13 | import { APActorUpdater } from '../actors/updater' | 14 | import { APActorUpdater } from '../actors/updater' |
14 | import { createOrUpdateCacheFile } from '../cache-file' | 15 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { createOrUpdateVideoPlaylist } from '../playlists' | 16 | import { createOrUpdateVideoPlaylist } from '../playlists' |
16 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 17 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
17 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' | 18 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' |
18 | 19 | ||
19 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 20 | async 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 | ||
55 | async function processUpdateVideo (activity: ActivityUpdate) { | 57 | async 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 | ||
75 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { | 77 | async 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 | ||
99 | async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { | 103 | async 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 | ||
108 | async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { | 110 | async 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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { |
4 | ActivityAudience, | ||
5 | ActivityCreate, | ||
6 | ActivityCreateObject, | ||
7 | ContextType, | ||
8 | VideoCommentObject, | ||
9 | VideoPlaylistPrivacy, | ||
10 | VideoPrivacy | ||
11 | } from '@shared/models' | ||
4 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 12 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
5 | import { VideoCommentModel } from '../../../models/video/video-comment' | 13 | import { VideoCommentModel } from '../../../models/video/video-comment' |
6 | import { | 14 | import { |
@@ -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 | ||
171 | function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { | 179 | function 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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { | 2 | import { 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' | ||
12 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
13 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
14 | import { | 5 | import { |
@@ -128,12 +119,12 @@ export { | |||
128 | 119 | ||
129 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
130 | 121 | ||
131 | function undoActivityData ( | 122 | function 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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 3 | import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { AccountModel } from '../../../models/account/account' | 5 | import { AccountModel } from '../../../models/account/account' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
@@ -137,7 +137,12 @@ export { | |||
137 | 137 | ||
138 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
139 | 139 | ||
140 | function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate { | 140 | function 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' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' | 5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' |
6 | import { APObject } from '@shared/models' | 6 | import { APObjectId } from '@shared/models' |
7 | import { getAPId } from '../activity' | 7 | import { getAPId } from '../activity' |
8 | import { refreshVideoIfNeeded } from './refresh' | 8 | import { refreshVideoIfNeeded } from './refresh' |
9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | 9 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' |
@@ -15,21 +15,21 @@ type GetVideoResult <T> = Promise<{ | |||
15 | }> | 15 | }> |
16 | 16 | ||
17 | type GetVideoParamAll = { | 17 | type 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 | ||
24 | type GetVideoParamImmutable = { | 24 | type 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 | ||
31 | type GetVideoParamOther = { | 31 | type 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' | |||
6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' | 6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' |
7 | 7 | ||
8 | describe('Test follows', function () { | 8 | describe('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 | }) |
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index fd5d38316..10cf53ead 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts | |||
@@ -1,20 +1,34 @@ | |||
1 | import { ActivityPubActor } from './activitypub-actor' | 1 | import { ActivityPubActor } from './activitypub-actor' |
2 | import { ActivityPubSignature } from './activitypub-signature' | 2 | import { ActivityPubSignature } from './activitypub-signature' |
3 | import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects' | 3 | import { |
4 | import { AbuseObject } from './objects/abuse-object' | 4 | ActivityFlagReasonObject, |
5 | import { DislikeObject } from './objects/dislike-object' | 5 | ActivityObject, |
6 | import { APObject } from './objects/object.model' | 6 | APObjectId, |
7 | import { PlaylistObject } from './objects/playlist-object' | 7 | CacheFileObject, |
8 | import { VideoCommentObject } from './objects/video-comment-object' | 8 | PlaylistObject, |
9 | VideoCommentObject, | ||
10 | VideoObject, | ||
11 | WatchActionObject | ||
12 | } from './objects' | ||
13 | |||
14 | export type ActivityUpdateObject = | ||
15 | Extract<ActivityObject, VideoObject | CacheFileObject | PlaylistObject | ActivityPubActor | string> | ActivityPubActor | ||
16 | |||
17 | // Cannot Extract from Activity because of circular reference | ||
18 | export type ActivityUndoObject = | ||
19 | ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate<CacheFileObject | string> | ActivityAnnounce | ||
20 | |||
21 | export type ActivityCreateObject = | ||
22 | Extract<ActivityObject, VideoObject | CacheFileObject | WatchActionObject | VideoCommentObject | PlaylistObject | string> | ||
9 | 23 | ||
10 | export type Activity = | 24 | export type Activity = |
11 | ActivityCreate | | 25 | ActivityCreate<ActivityCreateObject> | |
12 | ActivityUpdate | | 26 | ActivityUpdate<ActivityUpdateObject> | |
13 | ActivityDelete | | 27 | ActivityDelete | |
14 | ActivityFollow | | 28 | ActivityFollow | |
15 | ActivityAccept | | 29 | ActivityAccept | |
16 | ActivityAnnounce | | 30 | ActivityAnnounce | |
17 | ActivityUndo | | 31 | ActivityUndo<ActivityUndoObject> | |
18 | ActivityLike | | 32 | ActivityLike | |
19 | ActivityReject | | 33 | ActivityReject | |
20 | ActivityView | | 34 | ActivityView | |
@@ -50,19 +64,19 @@ export interface BaseActivity { | |||
50 | signature?: ActivityPubSignature | 64 | signature?: ActivityPubSignature |
51 | } | 65 | } |
52 | 66 | ||
53 | export interface ActivityCreate extends BaseActivity { | 67 | export interface ActivityCreate <T extends ActivityCreateObject> extends BaseActivity { |
54 | type: 'Create' | 68 | type: 'Create' |
55 | object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | WatchActionObject | 69 | object: T |
56 | } | 70 | } |
57 | 71 | ||
58 | export interface ActivityUpdate extends BaseActivity { | 72 | export interface ActivityUpdate <T extends ActivityUpdateObject> extends BaseActivity { |
59 | type: 'Update' | 73 | type: 'Update' |
60 | object: VideoObject | ActivityPubActor | CacheFileObject | PlaylistObject | 74 | object: T |
61 | } | 75 | } |
62 | 76 | ||
63 | export interface ActivityDelete extends BaseActivity { | 77 | export interface ActivityDelete extends BaseActivity { |
64 | type: 'Delete' | 78 | type: 'Delete' |
65 | object: string | { id: string } | 79 | object: APObjectId |
66 | } | 80 | } |
67 | 81 | ||
68 | export interface ActivityFollow extends BaseActivity { | 82 | export interface ActivityFollow extends BaseActivity { |
@@ -82,23 +96,23 @@ export interface ActivityReject extends BaseActivity { | |||
82 | 96 | ||
83 | export interface ActivityAnnounce extends BaseActivity { | 97 | export interface ActivityAnnounce extends BaseActivity { |
84 | type: 'Announce' | 98 | type: 'Announce' |
85 | object: APObject | 99 | object: APObjectId |
86 | } | 100 | } |
87 | 101 | ||
88 | export interface ActivityUndo extends BaseActivity { | 102 | export interface ActivityUndo <T extends ActivityUndoObject> extends BaseActivity { |
89 | type: 'Undo' | 103 | type: 'Undo' |
90 | object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce | 104 | object: T |
91 | } | 105 | } |
92 | 106 | ||
93 | export interface ActivityLike extends BaseActivity { | 107 | export interface ActivityLike extends BaseActivity { |
94 | type: 'Like' | 108 | type: 'Like' |
95 | object: APObject | 109 | object: APObjectId |
96 | } | 110 | } |
97 | 111 | ||
98 | export interface ActivityView extends BaseActivity { | 112 | export interface ActivityView extends BaseActivity { |
99 | type: 'View' | 113 | type: 'View' |
100 | actor: string | 114 | actor: string |
101 | object: APObject | 115 | object: APObjectId |
102 | 116 | ||
103 | // If sending a "viewer" event | 117 | // If sending a "viewer" event |
104 | expires?: string | 118 | expires?: string |
@@ -108,13 +122,13 @@ export interface ActivityDislike extends BaseActivity { | |||
108 | id: string | 122 | id: string |
109 | type: 'Dislike' | 123 | type: 'Dislike' |
110 | actor: string | 124 | actor: string |
111 | object: APObject | 125 | object: APObjectId |
112 | } | 126 | } |
113 | 127 | ||
114 | export interface ActivityFlag extends BaseActivity { | 128 | export interface ActivityFlag extends BaseActivity { |
115 | type: 'Flag' | 129 | type: 'Flag' |
116 | content: string | 130 | content: string |
117 | object: APObject | APObject[] | 131 | object: APObjectId | APObjectId[] |
118 | tag?: ActivityFlagReasonObject[] | 132 | tag?: ActivityFlagReasonObject[] |
119 | startAt?: number | 133 | startAt?: number |
120 | endAt?: number | 134 | endAt?: number |
diff --git a/shared/models/activitypub/objects/activitypub-object.ts b/shared/models/activitypub/objects/activitypub-object.ts new file mode 100644 index 000000000..faeac2618 --- /dev/null +++ b/shared/models/activitypub/objects/activitypub-object.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import { AbuseObject } from './abuse-object' | ||
2 | import { CacheFileObject } from './cache-file-object' | ||
3 | import { PlaylistObject } from './playlist-object' | ||
4 | import { VideoCommentObject } from './video-comment-object' | ||
5 | import { VideoObject } from './video-object' | ||
6 | import { WatchActionObject } from './watch-action-object' | ||
7 | |||
8 | export type ActivityObject = | ||
9 | VideoObject | | ||
10 | AbuseObject | | ||
11 | VideoCommentObject | | ||
12 | CacheFileObject | | ||
13 | PlaylistObject | | ||
14 | WatchActionObject | | ||
15 | string | ||
16 | |||
17 | export type APObjectId = string | { id: string } | ||
diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts deleted file mode 100644 index 7218fb784..000000000 --- a/shared/models/activitypub/objects/dislike-object.ts +++ /dev/null | |||
@@ -1,6 +0,0 @@ | |||
1 | export interface DislikeObject { | ||
2 | id: string | ||
3 | type: 'Dislike' | ||
4 | actor: string | ||
5 | object: string | ||
6 | } | ||
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index a2e040b32..753e02003 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | export * from './abuse-object' | 1 | export * from './abuse-object' |
2 | export * from './activitypub-object' | ||
2 | export * from './cache-file-object' | 3 | export * from './cache-file-object' |
3 | export * from './common-objects' | 4 | export * from './common-objects' |
4 | export * from './dislike-object' | ||
5 | export * from './object.model' | ||
6 | export * from './playlist-element-object' | 5 | export * from './playlist-element-object' |
7 | export * from './playlist-object' | 6 | export * from './playlist-object' |
8 | export * from './video-comment-object' | 7 | export * from './video-comment-object' |
diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts deleted file mode 100644 index 3fd33800a..000000000 --- a/shared/models/activitypub/objects/object.model.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export type APObject = string | { id: string } | ||