aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-26 10:55:40 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-03-18 11:17:59 +0100
commit418d092afa81e2c8fe8ac6838fc4b5eb0af6a782 (patch)
tree5e9bc5604fd5d66a006cfebb7acdbdd5486e5d1e /server/lib
parentb427febb4d5cebf03b815bca2c59af6e82491569 (diff)
downloadPeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.tar.gz
PeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.tar.zst
PeerTube-418d092afa81e2c8fe8ac6838fc4b5eb0af6a782.zip
Playlist server API
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actor.ts13
-rw-r--r--server/lib/activitypub/cache-file.ts2
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/activitypub/playlist.ts162
-rw-r--r--server/lib/activitypub/process/process-create.ts19
-rw-r--r--server/lib/activitypub/process/process-update.ts15
-rw-r--r--server/lib/activitypub/send/send-create.ts23
-rw-r--r--server/lib/activitypub/send/send-delete.ts21
-rw-r--r--server/lib/activitypub/send/send-update.ts30
-rw-r--r--server/lib/activitypub/url.ts12
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts11
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 @@
1import { ActivityPlaylistUrlObject, ActivityVideoUrlObject, CacheFileObject } from '../../../shared/index' 1import { CacheFileObject } from '../../../shared/index'
2import { VideoModel } from '../../models/video/video' 2import { VideoModel } from '../../models/video/video'
3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
4import { Transaction } from 'sequelize' 4import { 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'
4import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6 6
7async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 7async 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 @@
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl'
3import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
4import { AccountModel } from '../../models/account/account'
5import { isArray } from '../../helpers/custom-validators/misc'
6import { getOrCreateActorAndServerAndModel } from './actor'
7import { logger } from '../../helpers/logger'
8import { VideoPlaylistModel } from '../../models/video/video-playlist'
9import { doRequest, downloadImage } from '../../helpers/requests'
10import { checkUrlsSameHost } from '../../helpers/activitypub'
11import * as Bluebird from 'bluebird'
12import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
13import { getOrCreateVideoAndAccountAndChannel } from './videos'
14import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
15import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
16import { VideoModel } from '../../models/video/video'
17import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
18import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
19import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
20
21function 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
35function 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
46async 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
74async 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
110export {
111 createAccountPlaylists,
112 playlistObjectToDBAttributes,
113 playlistElementObjectToDBAttributes,
114 createOrUpdateVideoPlaylist
115}
116
117// ---------------------------------------------------------------------------
118
119async 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
158function 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'
12import { processViewActivity } from './process-view' 12import { processViewActivity } from './process-view'
13import { processDislikeActivity } from './process-dislike' 13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag' 14import { processFlagActivity } from './process-flag'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist'
15 17
16async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 18async 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
66async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { 72async 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
108async 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
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
13import { createOrUpdateCacheFile } from '../cache-file' 13import { createOrUpdateCacheFile } from '../cache-file'
14import { forwardVideoRelatedActivity } from '../send/utils' 14import { forwardVideoRelatedActivity } from '../send/utils'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist'
15 17
16async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorModel) { 18async 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
145async 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
8import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 8import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 10import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils'
11 14
12async function sendCreateVideo (video: VideoModel, t: Transaction) { 15async 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
40async 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
37async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) { 59async 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'
8import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 8import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { getServerActor } from '../../../helpers/utils'
11 13
12async function sendDeleteVideo (video: VideoModel, transaction: Transaction) { 14async 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
69async 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
69export { 87export {
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'
12import { logger } from '../../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { VideoCaptionModel } from '../../../models/video/video-caption' 13import { VideoCaptionModel } from '../../../models/video/video-caption'
14import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 14import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
15import { VideoPlaylistModel } from '../../../models/video/video-playlist'
16import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
17import { getServerActor } from '../../../helpers/utils'
15 18
16async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) { 19async 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
81async 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
78export { 105export {
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'
7import { VideoFileModel } from '../../models/video/video-file' 7import { VideoFileModel } from '../../models/video/video-file'
8import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' 8import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
9import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 9import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
10import { VideoPlaylistModel } from '../../models/video/video-playlist'
11import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
10 12
11function getVideoActivityPubUrl (video: VideoModel) { 13function 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
17function getVideoPlaylistActivityPubUrl (videoPlaylist: VideoPlaylistModel) {
18 return CONFIG.WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid
19}
20
21function getVideoPlaylistElementActivityPubUrl (videoPlaylist: VideoPlaylistModel, video: VideoModel) {
22 return CONFIG.WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid
23}
24
15function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { 25function 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
99export { 109export {
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'
5import { crawlCollectionPage } from '../../activitypub/crawl' 5import { crawlCollectionPage } from '../../activitypub/crawl'
6import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
7import { addVideoShares, createRates } from '../../activitypub' 7import { addVideoShares, createRates } from '../../activitypub'
8import { createAccountPlaylists } from '../../activitypub/playlist'
9import { AccountModel } from '../../../models/account/account'
8 10
9type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' 11type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
10 12
11export type ActivitypubHttpFetcherPayload = { 13export 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
17async function processActivityPubHttpFetcher (job: Bull.Job) { 20async 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])