diff options
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/activitypub/actor.ts | 13 | ||||
-rw-r--r-- | server/lib/activitypub/cache-file.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/crawl.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 162 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 19 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 15 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-create.ts | 23 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-delete.ts | 21 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-update.ts | 30 | ||||
-rw-r--r-- | server/lib/activitypub/url.ts | 12 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/activitypub-http-fetcher.ts | 11 |
11 files changed, 301 insertions, 9 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index a3f379b76..f77df8b78 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -44,6 +44,7 @@ async function getOrCreateActorAndServerAndModel ( | |||
44 | ) { | 44 | ) { |
45 | const actorUrl = getAPId(activityActor) | 45 | const actorUrl = getAPId(activityActor) |
46 | let created = false | 46 | let created = false |
47 | let accountPlaylistsUrl: string | ||
47 | 48 | ||
48 | let actor = await fetchActorByUrl(actorUrl, fetchType) | 49 | let actor = await fetchActorByUrl(actorUrl, fetchType) |
49 | // Orphan actor (not associated to an account of channel) so recreate it | 50 | // Orphan actor (not associated to an account of channel) so recreate it |
@@ -70,7 +71,8 @@ async function getOrCreateActorAndServerAndModel ( | |||
70 | 71 | ||
71 | try { | 72 | try { |
72 | // Don't recurse another time | 73 | // Don't recurse another time |
73 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) | 74 | const recurseIfNeeded = false |
75 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded) | ||
74 | } catch (err) { | 76 | } catch (err) { |
75 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) | 77 | logger.error('Cannot get or create account attributed to video channel ' + actor.url) |
76 | throw new Error(err) | 78 | throw new Error(err) |
@@ -79,6 +81,7 @@ async function getOrCreateActorAndServerAndModel ( | |||
79 | 81 | ||
80 | actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) | 82 | actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) |
81 | created = true | 83 | created = true |
84 | accountPlaylistsUrl = result.playlists | ||
82 | } | 85 | } |
83 | 86 | ||
84 | if (actor.Account) actor.Account.Actor = actor | 87 | if (actor.Account) actor.Account.Actor = actor |
@@ -92,6 +95,12 @@ async function getOrCreateActorAndServerAndModel ( | |||
92 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 95 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) |
93 | } | 96 | } |
94 | 97 | ||
98 | // We created a new account: fetch the playlists | ||
99 | if (created === true && actor.Account && accountPlaylistsUrl) { | ||
100 | const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } | ||
101 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | ||
102 | } | ||
103 | |||
95 | return actorRefreshed | 104 | return actorRefreshed |
96 | } | 105 | } |
97 | 106 | ||
@@ -342,6 +351,7 @@ type FetchRemoteActorResult = { | |||
342 | name: string | 351 | name: string |
343 | summary: string | 352 | summary: string |
344 | support?: string | 353 | support?: string |
354 | playlists?: string | ||
345 | avatarName?: string | 355 | avatarName?: string |
346 | attributedTo: ActivityPubAttributedTo[] | 356 | attributedTo: ActivityPubAttributedTo[] |
347 | } | 357 | } |
@@ -398,6 +408,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
398 | avatarName, | 408 | avatarName, |
399 | summary: actorJSON.summary, | 409 | summary: actorJSON.summary, |
400 | support: actorJSON.support, | 410 | support: actorJSON.support, |
411 | playlists: actorJSON.playlists, | ||
401 | attributedTo: actorJSON.attributedTo | 412 | attributedTo: actorJSON.attributedTo |
402 | } | 413 | } |
403 | } | 414 | } |
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index 9a40414bb..597003135 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' | 1 | import { CacheFileObject } from '../../../shared/index' |
2 | import { VideoModel } from '../../models/video/video' | 2 | import { VideoModel } from '../../models/video/video' |
3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 3 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
4 | import { Transaction } from 'sequelize' | 4 | import { Transaction } from 'sequelize' |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 1b9b14c2e..2675524c6 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -4,7 +4,7 @@ import { logger } from '../../helpers/logger' | |||
4 | import * as Bluebird from 'bluebird' | 4 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | 6 | ||
7 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { | 7 | async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => (Promise<any> | Bluebird<any>)) { |
8 | logger.info('Crawling ActivityPub data on %s.', uri) | 8 | logger.info('Crawling ActivityPub data on %s.', uri) |
9 | 9 | ||
10 | const options = { | 10 | const options = { |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts new file mode 100644 index 000000000..c9b428c92 --- /dev/null +++ b/server/lib/activitypub/playlist.ts | |||
@@ -0,0 +1,162 @@ | |||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
2 | import { crawlCollectionPage } from './crawl' | ||
3 | import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' | ||
4 | import { AccountModel } from '../../models/account/account' | ||
5 | import { isArray } from '../../helpers/custom-validators/misc' | ||
6 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
7 | import { logger } from '../../helpers/logger' | ||
8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
9 | import { doRequest, downloadImage } from '../../helpers/requests' | ||
10 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
11 | import * as Bluebird from 'bluebird' | ||
12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
13 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
14 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
15 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | ||
16 | import { VideoModel } from '../../models/video/video' | ||
17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | ||
18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
19 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
20 | |||
21 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | ||
22 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED | ||
23 | |||
24 | return { | ||
25 | name: playlistObject.name, | ||
26 | description: playlistObject.content, | ||
27 | privacy, | ||
28 | url: playlistObject.id, | ||
29 | uuid: playlistObject.uuid, | ||
30 | ownerAccountId: byAccount.id, | ||
31 | videoChannelId: null | ||
32 | } | ||
33 | } | ||
34 | |||
35 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: VideoPlaylistModel, video: VideoModel) { | ||
36 | return { | ||
37 | position: elementObject.position, | ||
38 | url: elementObject.id, | ||
39 | startTimestamp: elementObject.startTimestamp || null, | ||
40 | stopTimestamp: elementObject.stopTimestamp || null, | ||
41 | videoPlaylistId: videoPlaylist.id, | ||
42 | videoId: video.id | ||
43 | } | ||
44 | } | ||
45 | |||
46 | async function createAccountPlaylists (playlistUrls: string[], account: AccountModel) { | ||
47 | await Bluebird.map(playlistUrls, async playlistUrl => { | ||
48 | try { | ||
49 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | ||
50 | if (exists === true) return | ||
51 | |||
52 | // Fetch url | ||
53 | const { body } = await doRequest<PlaylistObject>({ | ||
54 | uri: playlistUrl, | ||
55 | json: true, | ||
56 | activityPub: true | ||
57 | }) | ||
58 | |||
59 | if (!isPlaylistObjectValid(body)) { | ||
60 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | ||
61 | } | ||
62 | |||
63 | if (!isArray(body.to)) { | ||
64 | throw new Error('Playlist does not have an audience.') | ||
65 | } | ||
66 | |||
67 | return createOrUpdateVideoPlaylist(body, account, body.to) | ||
68 | } catch (err) { | ||
69 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err }) | ||
70 | } | ||
71 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
72 | } | ||
73 | |||
74 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | ||
75 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | ||
76 | |||
77 | if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) { | ||
78 | const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) | ||
79 | |||
80 | if (actor.VideoChannel) { | ||
81 | playlistAttributes.videoChannelId = actor.VideoChannel.id | ||
82 | } else { | ||
83 | logger.warn('Attributed to of video playlist %s is not a video channel.', playlistObject.id, { playlistObject }) | ||
84 | } | ||
85 | } | ||
86 | |||
87 | const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true }) | ||
88 | |||
89 | let accItems: string[] = [] | ||
90 | await crawlCollectionPage<string>(playlistObject.id, items => { | ||
91 | accItems = accItems.concat(items) | ||
92 | |||
93 | return Promise.resolve() | ||
94 | }) | ||
95 | |||
96 | // Empty playlists generally do not have a miniature, so skip it | ||
97 | if (accItems.length !== 0) { | ||
98 | try { | ||
99 | await generateThumbnailFromUrl(playlist, playlistObject.icon) | ||
100 | } catch (err) { | ||
101 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | return resetVideoPlaylistElements(accItems, playlist) | ||
106 | } | ||
107 | |||
108 | // --------------------------------------------------------------------------- | ||
109 | |||
110 | export { | ||
111 | createAccountPlaylists, | ||
112 | playlistObjectToDBAttributes, | ||
113 | playlistElementObjectToDBAttributes, | ||
114 | createOrUpdateVideoPlaylist | ||
115 | } | ||
116 | |||
117 | // --------------------------------------------------------------------------- | ||
118 | |||
119 | async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) { | ||
120 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | ||
121 | |||
122 | await Bluebird.map(elementUrls, async elementUrl => { | ||
123 | try { | ||
124 | // Fetch url | ||
125 | const { body } = await doRequest<PlaylistElementObject>({ | ||
126 | uri: elementUrl, | ||
127 | json: true, | ||
128 | activityPub: true | ||
129 | }) | ||
130 | |||
131 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | ||
132 | |||
133 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | ||
134 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | ||
135 | } | ||
136 | |||
137 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: { id: body.url }, fetchType: 'only-video' }) | ||
138 | |||
139 | elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video)) | ||
140 | } catch (err) { | ||
141 | logger.warn('Cannot add playlist element %s.', elementUrl, { err }) | ||
142 | } | ||
143 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
144 | |||
145 | await sequelizeTypescript.transaction(async t => { | ||
146 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | ||
147 | |||
148 | for (const element of elementsToCreate) { | ||
149 | await VideoPlaylistElementModel.create(element, { transaction: t }) | ||
150 | } | ||
151 | }) | ||
152 | |||
153 | logger.info('Reset playlist %s with %s elements.', playlist.url, elementsToCreate.length) | ||
154 | |||
155 | return undefined | ||
156 | } | ||
157 | |||
158 | function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) { | ||
159 | const thumbnailName = playlist.getThumbnailName() | ||
160 | |||
161 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) | ||
162 | } | ||
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 5f4d793a5..e882669ce 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -12,6 +12,8 @@ import { Notifier } from '../../notifier' | |||
12 | import { processViewActivity } from './process-view' | 12 | import { processViewActivity } from './process-view' |
13 | import { processDislikeActivity } from './process-dislike' | 13 | import { processDislikeActivity } from './process-dislike' |
14 | import { processFlagActivity } from './process-flag' | 14 | import { processFlagActivity } from './process-flag' |
15 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
16 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
15 | 17 | ||
16 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 18 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
17 | const activityObject = activity.object | 19 | const activityObject = activity.object |
@@ -38,7 +40,11 @@ async function processCreateActivity (activity: ActivityCreate, byActor: ActorMo | |||
38 | } | 40 | } |
39 | 41 | ||
40 | if (activityType === 'CacheFile') { | 42 | if (activityType === 'CacheFile') { |
41 | return retryTransactionWrapper(processCacheFile, activity, byActor) | 43 | return retryTransactionWrapper(processCreateCacheFile, activity, byActor) |
44 | } | ||
45 | |||
46 | if (activityType === 'Playlist') { | ||
47 | return retryTransactionWrapper(processCreatePlaylist, activity, byActor) | ||
42 | } | 48 | } |
43 | 49 | ||
44 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 50 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -63,7 +69,7 @@ async function processCreateVideo (activity: ActivityCreate) { | |||
63 | return video | 69 | return video |
64 | } | 70 | } |
65 | 71 | ||
66 | async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { | 72 | async function processCreateCacheFile (activity: ActivityCreate, byActor: ActorModel) { |
67 | const cacheFile = activity.object as CacheFileObject | 73 | const cacheFile = activity.object as CacheFileObject |
68 | 74 | ||
69 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 75 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
@@ -98,3 +104,12 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Act | |||
98 | 104 | ||
99 | if (created === true) Notifier.Instance.notifyOnNewComment(comment) | 105 | if (created === true) Notifier.Instance.notifyOnNewComment(comment) |
100 | } | 106 | } |
107 | |||
108 | async function processCreatePlaylist (activity: ActivityCreate, byActor: ActorModel) { | ||
109 | const playlistObject = activity.object as PlaylistObject | ||
110 | const byAccount = byActor.Account | ||
111 | |||
112 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | ||
113 | |||
114 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | ||
115 | } | ||
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index c6b42d846..0b96ba352 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -12,6 +12,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-vali | |||
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
13 | import { createOrUpdateCacheFile } from '../cache-file' | 13 | import { createOrUpdateCacheFile } from '../cache-file' |
14 | import { forwardVideoRelatedActivity } from '../send/utils' | 14 | import { forwardVideoRelatedActivity } from '../send/utils' |
15 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
16 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
15 | 17 | ||
16 | async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorModel) { | 18 | async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorModel) { |
17 | const objectType = activity.object.type | 19 | const objectType = activity.object.type |
@@ -32,6 +34,10 @@ async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorMo | |||
32 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) | 34 | return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) |
33 | } | 35 | } |
34 | 36 | ||
37 | if (objectType === 'Playlist') { | ||
38 | return retryTransactionWrapper(processUpdatePlaylist, byActor, activity) | ||
39 | } | ||
40 | |||
35 | return undefined | 41 | return undefined |
36 | } | 42 | } |
37 | 43 | ||
@@ -135,3 +141,12 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) | |||
135 | throw err | 141 | throw err |
136 | } | 142 | } |
137 | } | 143 | } |
144 | |||
145 | async function processUpdatePlaylist (byActor: ActorModel, activity: ActivityUpdate) { | ||
146 | const playlistObject = activity.object as PlaylistObject | ||
147 | const byAccount = byActor.Account | ||
148 | |||
149 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | ||
150 | |||
151 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | ||
152 | } | ||
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index ef20e404c..bacdb97e3 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -8,6 +8,9 @@ import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unic | |||
8 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 8 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
9 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
10 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 10 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
11 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | ||
12 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
13 | import { getServerActor } from '../../../helpers/utils' | ||
11 | 14 | ||
12 | async function sendCreateVideo (video: VideoModel, t: Transaction) { | 15 | async function sendCreateVideo (video: VideoModel, t: Transaction) { |
13 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 16 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
@@ -34,6 +37,25 @@ async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, file | |||
34 | }) | 37 | }) |
35 | } | 38 | } |
36 | 39 | ||
40 | async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transaction) { | ||
41 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined | ||
42 | |||
43 | logger.info('Creating job to send create video playlist of %s.', playlist.url) | ||
44 | |||
45 | const byActor = playlist.OwnerAccount.Actor | ||
46 | const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) | ||
47 | |||
48 | const object = await playlist.toActivityPubObject() | ||
49 | const createActivity = buildCreateActivity(playlist.url, byActor, object, audience) | ||
50 | |||
51 | const serverActor = await getServerActor() | ||
52 | const toFollowersOf = [ byActor, serverActor ] | ||
53 | |||
54 | if (playlist.VideoChannel) toFollowersOf.push(playlist.VideoChannel.Actor) | ||
55 | |||
56 | return broadcastToFollowers(createActivity, byActor, toFollowersOf, t) | ||
57 | } | ||
58 | |||
37 | async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) { | 59 | async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) { |
38 | logger.info('Creating job to send comment %s.', comment.url) | 60 | logger.info('Creating job to send comment %s.', comment.url) |
39 | 61 | ||
@@ -92,6 +114,7 @@ export { | |||
92 | sendCreateVideo, | 114 | sendCreateVideo, |
93 | buildCreateActivity, | 115 | buildCreateActivity, |
94 | sendCreateVideoComment, | 116 | sendCreateVideoComment, |
117 | sendCreateVideoPlaylist, | ||
95 | sendCreateCacheFile | 118 | sendCreateCacheFile |
96 | } | 119 | } |
97 | 120 | ||
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index 18969433a..016811e60 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts | |||
@@ -8,6 +8,8 @@ import { getDeleteActivityPubUrl } from '../url' | |||
8 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 8 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
9 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' | 9 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' |
10 | import { logger } from '../../../helpers/logger' | 10 | import { logger } from '../../../helpers/logger' |
11 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | ||
12 | import { getServerActor } from '../../../helpers/utils' | ||
11 | 13 | ||
12 | async function sendDeleteVideo (video: VideoModel, transaction: Transaction) { | 14 | async function sendDeleteVideo (video: VideoModel, transaction: Transaction) { |
13 | logger.info('Creating job to broadcast delete of video %s.', video.url) | 15 | logger.info('Creating job to broadcast delete of video %s.', video.url) |
@@ -64,12 +66,29 @@ async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Trans | |||
64 | return unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl) | 66 | return unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl) |
65 | } | 67 | } |
66 | 68 | ||
69 | async function sendDeleteVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) { | ||
70 | logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url) | ||
71 | |||
72 | const byActor = videoPlaylist.OwnerAccount.Actor | ||
73 | |||
74 | const url = getDeleteActivityPubUrl(videoPlaylist.url) | ||
75 | const activity = buildDeleteActivity(url, videoPlaylist.url, byActor) | ||
76 | |||
77 | const serverActor = await getServerActor() | ||
78 | const toFollowersOf = [ byActor, serverActor ] | ||
79 | |||
80 | if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor) | ||
81 | |||
82 | return broadcastToFollowers(activity, byActor, toFollowersOf, t) | ||
83 | } | ||
84 | |||
67 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
68 | 86 | ||
69 | export { | 87 | export { |
70 | sendDeleteVideo, | 88 | sendDeleteVideo, |
71 | sendDeleteActor, | 89 | sendDeleteActor, |
72 | sendDeleteVideoComment | 90 | sendDeleteVideoComment, |
91 | sendDeleteVideoPlaylist | ||
73 | } | 92 | } |
74 | 93 | ||
75 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 839f66470..3eb2704fd 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -12,8 +12,13 @@ import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' | |||
12 | import { logger } from '../../../helpers/logger' | 12 | import { logger } from '../../../helpers/logger' |
13 | import { VideoCaptionModel } from '../../../models/video/video-caption' | 13 | import { VideoCaptionModel } from '../../../models/video/video-caption' |
14 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 14 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
15 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | ||
16 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
17 | import { getServerActor } from '../../../helpers/utils' | ||
15 | 18 | ||
16 | async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) { | 19 | async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) { |
20 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | ||
21 | |||
17 | logger.info('Creating job to update video %s.', video.url) | 22 | logger.info('Creating job to update video %s.', video.url) |
18 | 23 | ||
19 | const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor | 24 | const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor |
@@ -73,12 +78,35 @@ async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoR | |||
73 | return sendVideoRelatedActivity(activityBuilder, { byActor, video }) | 78 | return sendVideoRelatedActivity(activityBuilder, { byActor, video }) |
74 | } | 79 | } |
75 | 80 | ||
81 | async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) { | ||
82 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined | ||
83 | |||
84 | const byActor = videoPlaylist.OwnerAccount.Actor | ||
85 | |||
86 | logger.info('Creating job to update video playlist %s.', videoPlaylist.url) | ||
87 | |||
88 | const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString()) | ||
89 | |||
90 | const object = await videoPlaylist.toActivityPubObject() | ||
91 | const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC) | ||
92 | |||
93 | const updateActivity = buildUpdateActivity(url, byActor, object, audience) | ||
94 | |||
95 | const serverActor = await getServerActor() | ||
96 | const toFollowersOf = [ byActor, serverActor ] | ||
97 | |||
98 | if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor) | ||
99 | |||
100 | return broadcastToFollowers(updateActivity, byActor, toFollowersOf, t) | ||
101 | } | ||
102 | |||
76 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
77 | 104 | ||
78 | export { | 105 | export { |
79 | sendUpdateActor, | 106 | sendUpdateActor, |
80 | sendUpdateVideo, | 107 | sendUpdateVideo, |
81 | sendUpdateCacheFile | 108 | sendUpdateCacheFile, |
109 | sendUpdateVideoPlaylist | ||
82 | } | 110 | } |
83 | 111 | ||
84 | // --------------------------------------------------------------------------- | 112 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 4229fe094..00bbbba2d 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -7,11 +7,21 @@ import { VideoCommentModel } from '../../models/video/video-comment' | |||
7 | import { VideoFileModel } from '../../models/video/video-file' | 7 | import { VideoFileModel } from '../../models/video/video-file' |
8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | 8 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' |
9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 9 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
10 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
11 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | ||
10 | 12 | ||
11 | function getVideoActivityPubUrl (video: VideoModel) { | 13 | function getVideoActivityPubUrl (video: VideoModel) { |
12 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | 14 | return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid |
13 | } | 15 | } |
14 | 16 | ||
17 | function getVideoPlaylistActivityPubUrl (videoPlaylist: VideoPlaylistModel) { | ||
18 | return CONFIG.WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid | ||
19 | } | ||
20 | |||
21 | function getVideoPlaylistElementActivityPubUrl (videoPlaylist: VideoPlaylistModel, video: VideoModel) { | ||
22 | return CONFIG.WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid | ||
23 | } | ||
24 | |||
15 | function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { | 25 | function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { |
16 | const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : '' | 26 | const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : '' |
17 | 27 | ||
@@ -98,6 +108,8 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
98 | 108 | ||
99 | export { | 109 | export { |
100 | getVideoActivityPubUrl, | 110 | getVideoActivityPubUrl, |
111 | getVideoPlaylistElementActivityPubUrl, | ||
112 | getVideoPlaylistActivityPubUrl, | ||
101 | getVideoCacheStreamingPlaylistActivityPubUrl, | 113 | getVideoCacheStreamingPlaylistActivityPubUrl, |
102 | getVideoChannelActivityPubUrl, | 114 | getVideoChannelActivityPubUrl, |
103 | getAccountActivityPubUrl, | 115 | getAccountActivityPubUrl, |
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index 67ccfa995..52225f64f 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -5,13 +5,16 @@ import { addVideoComments } from '../../activitypub/video-comments' | |||
5 | import { crawlCollectionPage } from '../../activitypub/crawl' | 5 | import { crawlCollectionPage } from '../../activitypub/crawl' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
7 | import { addVideoShares, createRates } from '../../activitypub' | 7 | import { addVideoShares, createRates } from '../../activitypub' |
8 | import { createAccountPlaylists } from '../../activitypub/playlist' | ||
9 | import { AccountModel } from '../../../models/account/account' | ||
8 | 10 | ||
9 | type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 11 | type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists' |
10 | 12 | ||
11 | export type ActivitypubHttpFetcherPayload = { | 13 | export type ActivitypubHttpFetcherPayload = { |
12 | uri: string | 14 | uri: string |
13 | type: FetchType | 15 | type: FetchType |
14 | videoId?: number | 16 | videoId?: number |
17 | accountId?: number | ||
15 | } | 18 | } |
16 | 19 | ||
17 | async function processActivityPubHttpFetcher (job: Bull.Job) { | 20 | async function processActivityPubHttpFetcher (job: Bull.Job) { |
@@ -22,12 +25,16 @@ async function processActivityPubHttpFetcher (job: Bull.Job) { | |||
22 | let video: VideoModel | 25 | let video: VideoModel |
23 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) | 26 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) |
24 | 27 | ||
28 | let account: AccountModel | ||
29 | if (payload.accountId) account = await AccountModel.load(payload.accountId) | ||
30 | |||
25 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { | 31 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { |
26 | 'activity': items => processActivities(items, { outboxUrl: payload.uri }), | 32 | 'activity': items => processActivities(items, { outboxUrl: payload.uri }), |
27 | 'video-likes': items => createRates(items, video, 'like'), | 33 | 'video-likes': items => createRates(items, video, 'like'), |
28 | 'video-dislikes': items => createRates(items, video, 'dislike'), | 34 | 'video-dislikes': items => createRates(items, video, 'dislike'), |
29 | 'video-shares': items => addVideoShares(items, video), | 35 | 'video-shares': items => addVideoShares(items, video), |
30 | 'video-comments': items => addVideoComments(items, video) | 36 | 'video-comments': items => addVideoComments(items, video), |
37 | 'account-playlists': items => createAccountPlaylists(items, account) | ||
31 | } | 38 | } |
32 | 39 | ||
33 | return crawlCollectionPage(payload.uri, fetcherType[payload.type]) | 40 | return crawlCollectionPage(payload.uri, fetcherType[payload.type]) |