aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/actor.ts51
-rw-r--r--server/lib/activitypub/audience.ts8
-rw-r--r--server/lib/activitypub/cache-file.ts4
-rw-r--r--server/lib/activitypub/crawl.ts6
-rw-r--r--server/lib/activitypub/follow.ts1
-rw-r--r--server/lib/activitypub/process/process-view.ts3
-rw-r--r--server/lib/activitypub/send/send-accept.ts2
-rw-r--r--server/lib/activitypub/send/send-announce.ts2
-rw-r--r--server/lib/activitypub/send/send-create.ts13
-rw-r--r--server/lib/activitypub/send/send-dislike.ts2
-rw-r--r--server/lib/activitypub/send/send-flag.ts2
-rw-r--r--server/lib/activitypub/send/send-like.ts2
-rw-r--r--server/lib/activitypub/send/send-reject.ts2
-rw-r--r--server/lib/activitypub/send/send-undo.ts10
-rw-r--r--server/lib/activitypub/send/send-update.ts5
-rw-r--r--server/lib/activitypub/send/send-view.ts6
-rw-r--r--server/lib/activitypub/send/utils.ts40
-rw-r--r--server/lib/activitypub/video-comments.ts14
-rw-r--r--server/lib/activitypub/video-rates.ts2
-rw-r--r--server/lib/activitypub/videos.ts196
20 files changed, 214 insertions, 157 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 0b21de0ca..9eabef4b0 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,6 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import * as url from 'url' 3import { URL } from 'url'
4import * as uuidv4 from 'uuid/v4' 4import * as uuidv4 from 'uuid/v4'
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
@@ -33,9 +33,9 @@ import {
33 MActorFull, 33 MActorFull,
34 MActorFullActor, 34 MActorFullActor,
35 MActorId, 35 MActorId,
36 MChannel, 36 MChannel
37 MChannelAccountDefault
38} from '../../typings/models' 37} from '../../typings/models'
38import { extname } from 'path'
39 39
40// Set account keys, this could be long so process after the account creation and do not block the client 40// Set account keys, this could be long so process after the account creation and do not block the client
41function setAsyncActorKeys <T extends MActor> (actor: T) { 41function setAsyncActorKeys <T extends MActor> (actor: T) {
@@ -121,13 +121,13 @@ async function getOrCreateActorAndServerAndModel (
121 121
122 if ((created === true || refreshed === true) && updateCollections === true) { 122 if ((created === true || refreshed === true) && updateCollections === true) {
123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } 123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
124 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 124 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
125 } 125 }
126 126
127 // We created a new account: fetch the playlists 127 // We created a new account: fetch the playlists
128 if (created === true && actor.Account && accountPlaylistsUrl) { 128 if (created === true && actor.Account && accountPlaylistsUrl) {
129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } 129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
130 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 130 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
131 } 131 }
132 132
133 return actorRefreshed 133 return actorRefreshed
@@ -215,20 +215,28 @@ async function fetchActorTotalItems (url: string) {
215 } 215 }
216} 216}
217 217
218async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 218function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
219 if ( 219 const mimetypes = MIMETYPES.IMAGE
220 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && 220 const icon = actorJSON.icon
221 isActivityPubUrlValid(actorJSON.icon.url)
222 ) {
223 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
224 221
225 return { 222 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
226 name: uuidv4() + extension, 223
227 fileUrl: actorJSON.icon.url 224 let extension: string
228 } 225
226 if (icon.mediaType) {
227 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
228 } else {
229 const tmp = extname(icon.url)
230
231 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
229 } 232 }
230 233
231 return undefined 234 if (!extension) return undefined
235
236 return {
237 name: uuidv4() + extension,
238 fileUrl: icon.url
239 }
232} 240}
233 241
234async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { 242async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
@@ -271,7 +279,10 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
271 279
272 if (statusCode === 404) { 280 if (statusCode === 404) {
273 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) 281 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
274 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() 282 actor.Account
283 ? await actor.Account.destroy()
284 : await actor.VideoChannel.destroy()
285
275 return { actor: undefined, refreshed: false } 286 return { actor: undefined, refreshed: false }
276 } 287 }
277 288
@@ -337,14 +348,14 @@ function saveActorAndServerAndModelIfNotExist (
337 ownerActor?: MActorFullActor, 348 ownerActor?: MActorFullActor,
338 t?: Transaction 349 t?: Transaction
339): Bluebird<MActorFullActor> | Promise<MActorFullActor> { 350): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
340 let actor = result.actor 351 const actor = result.actor
341 352
342 if (t !== undefined) return save(t) 353 if (t !== undefined) return save(t)
343 354
344 return sequelizeTypescript.transaction(t => save(t)) 355 return sequelizeTypescript.transaction(t => save(t))
345 356
346 async function save (t: Transaction) { 357 async function save (t: Transaction) {
347 const actorHost = url.parse(actor.url).host 358 const actorHost = new URL(actor.url).host
348 359
349 const serverOptions = { 360 const serverOptions = {
350 where: { 361 where: {
@@ -402,7 +413,7 @@ type FetchRemoteActorResult = {
402 support?: string 413 support?: string
403 playlists?: string 414 playlists?: string
404 avatar?: { 415 avatar?: {
405 name: string, 416 name: string
406 fileUrl: string 417 fileUrl: string
407 } 418 }
408 attributedTo: ActivityPubAttributedTo[] 419 attributedTo: ActivityPubAttributedTo[]
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index f2ab54cf7..9933ae2b5 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -4,11 +4,11 @@ import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/activitypub/actor' 4import { ActorModel } from '../../models/activitypub/actor'
5import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
6import { VideoShareModel } from '../../models/video/video-share' 6import { VideoShareModel } from '../../models/video/video-share'
7import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models' 7import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../typings/models'
8 8
9function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { 9function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
10 return { 10 return {
11 to: [ video.VideoChannel.Account.Actor.url ], 11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl) 12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 } 13 }
14} 14}
@@ -48,7 +48,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[
48 } 48 }
49} 49}
50 50
51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { 51async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) 52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
53 53
54 const videoAll = video as VideoModel 54 const videoAll = video as VideoModel
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
index 65b2dcb49..8252e95e9 100644
--- a/server/lib/activitypub/cache-file.ts
+++ b/server/lib/activitypub/cache-file.ts
@@ -13,7 +13,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) 13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url)
14 14
15 return { 15 return {
16 expiresOn: new Date(cacheFileObject.expires), 16 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
17 url: cacheFileObject.id, 17 url: cacheFileObject.id,
18 fileUrl: url.href, 18 fileUrl: url.href,
19 strategy: null, 19 strategy: null,
@@ -30,7 +30,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) 30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
31 31
32 return { 32 return {
33 expiresOn: new Date(cacheFileObject.expires), 33 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
34 url: cacheFileObject.id, 34 url: cacheFileObject.id,
35 fileUrl: url.href, 35 fileUrl: url.href,
36 strategy: null, 36 strategy: null,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 9e469e3e6..eeafdf4ba 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -3,7 +3,7 @@ import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { 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'
6import { parse } from 'url' 6import { URL } from 'url'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
@@ -24,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
25 const firstBody = response.body 25 const firstBody = response.body
26 26
27 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
28 let i = 0 28 let i = 0
29 let nextLink = firstBody.first 29 let nextLink = firstBody.first
30 while (nextLink && i < limit) { 30 while (nextLink && i < limit) {
@@ -32,7 +32,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
32 32
33 if (typeof nextLink === 'string') { 33 if (typeof nextLink === 'string') {
34 // Don't crawl ourselves 34 // Don't crawl ourselves
35 const remoteHost = parse(nextLink).host 35 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 36 if (remoteHost === WEBSERVER.HOST) continue
37 37
38 options.uri = nextLink 38 options.uri = nextLink
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
index 1abf43cd4..a1c95504e 100644
--- a/server/lib/activitypub/follow.ts
+++ b/server/lib/activitypub/follow.ts
@@ -27,7 +27,6 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
27 } 27 }
28 28
29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
30 .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
31 } 30 }
32} 31}
33 32
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index df29ee968..b3b6c933d 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -23,7 +23,8 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct
23 23
24 const options = { 24 const options = {
25 videoObject, 25 videoObject,
26 fetchType: 'only-video' as 'only-video' 26 fetchType: 'only-immutable-attributes' as 'only-immutable-attributes',
27 allowRefresh: false as false
27 } 28 }
28 const { video } = await getOrCreateVideoAndAccountAndChannel(options) 29 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
29 30
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index 9f0225b64..c4c6b849b 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor, MActorFollowActors } from '../../../typings/models' 6import { MActor, MActorFollowActors } from '../../../typings/models'
7 7
8async function sendAccept (actorFollow: MActorFollowActors) { 8function sendAccept (actorFollow: MActorFollowActors) {
9 const follower = actorFollow.ActorFollower 9 const follower = actorFollow.ActorFollower
10 const me = actorFollow.ActorFollowing 10 const me = actorFollow.ActorFollowing
11 11
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index a0f33852c..d03b358f1 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -28,7 +28,7 @@ async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
28 logger.info('Creating job to send announce %s.', videoShare.url) 28 logger.info('Creating job to send announce %s.', videoShare.url)
29 29
30 const followersException = [ byActor ] 30 const followersException = [ byActor ]
31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException) 31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException, 'Announce')
32} 32}
33 33
34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { 34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 1709d8348..8bdcf6417 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -16,6 +16,7 @@ import {
16 MVideoRedundancyFileVideo, 16 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo 17 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models' 18} from '../../../typings/models'
19import { ContextType } from '@server/helpers/activitypub'
19 20
20async function sendCreateVideo (video: MVideoAP, t: Transaction) { 21async function sendCreateVideo (video: MVideoAP, t: Transaction) {
21 if (!video.hasPrivacyForFederation()) return undefined 22 if (!video.hasPrivacyForFederation()) return undefined
@@ -42,7 +43,8 @@ async function sendCreateCacheFile (
42 byActor, 43 byActor,
43 video, 44 video,
44 url: fileRedundancy.url, 45 url: fileRedundancy.url,
45 object: fileRedundancy.toActivityPubObject() 46 object: fileRedundancy.toActivityPubObject(),
47 contextType: 'CacheFile'
46 }) 48 })
47} 49}
48 50
@@ -130,11 +132,12 @@ export {
130// --------------------------------------------------------------------------- 132// ---------------------------------------------------------------------------
131 133
132async function sendVideoRelatedCreateActivity (options: { 134async function sendVideoRelatedCreateActivity (options: {
133 byActor: MActorLight, 135 byActor: MActorLight
134 video: MVideoAccountLight, 136 video: MVideoAccountLight
135 url: string, 137 url: string
136 object: any, 138 object: any
137 transaction?: Transaction 139 transaction?: Transaction
140 contextType?: ContextType
138}) { 141}) {
139 const activityBuilder = (audience: ActivityAudience) => { 142 const activityBuilder = (audience: ActivityAudience) => {
140 return buildCreateActivity(options.url, options.byActor, options.object, audience) 143 return buildCreateActivity(options.url, options.byActor, options.object, audience)
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
index 6e41f241f..600469c71 100644
--- a/server/lib/activitypub/send/send-dislike.ts
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -6,7 +6,7 @@ import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to dislike %s.', video.url) 10 logger.info('Creating job to dislike %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index da7638a7b..e4e523631 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -7,7 +7,7 @@ import { Transaction } from 'sequelize'
7import { MActor, MVideoFullLight } from '../../../typings/models' 7import { MActor, MVideoFullLight } from '../../../typings/models'
8import { MVideoAbuseVideo } from '../../../typings/models/video' 8import { MVideoAbuseVideo } from '../../../typings/models/video'
9 9
10async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user
12 12
13 const url = getVideoAbuseActivityPubUrl(videoAbuse) 13 const url = getVideoAbuseActivityPubUrl(videoAbuse)
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index e84a6f98b..5db252325 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to like %s.', video.url) 10 logger.info('Creating job to like %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
index 4258a3c36..643c468a9 100644
--- a/server/lib/activitypub/send/send-reject.ts
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor } from '../../../typings/models' 6import { MActor } from '../../../typings/models'
7 7
8async function sendReject (follower: MActor, following: MActor) { 8function sendReject (follower: MActor, following: MActor) {
9 if (!follower.serverId) { // This should never happen 9 if (!follower.serverId) { // This should never happen
10 logger.warn('Do not sending reject to local follower.') 10 logger.warn('Do not sending reject to local follower.')
11 return 11 return
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index e9ab5b3c5..33f1d4921 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -28,7 +28,7 @@ import {
28 MVideoShare 28 MVideoShare
29} from '../../../typings/models' 29} from '../../../typings/models'
30 30
31async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { 31function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
32 const me = actorFollow.ActorFollower 32 const me = actorFollow.ActorFollower
33 const following = actorFollow.ActorFollowing 33 const following = actorFollow.ActorFollowing
34 34
@@ -118,10 +118,10 @@ function undoActivityData (
118} 118}
119 119
120async function sendUndoVideoRelatedActivity (options: { 120async function sendUndoVideoRelatedActivity (options: {
121 byActor: MActor, 121 byActor: MActor
122 video: MVideoAccountLight, 122 video: MVideoAccountLight
123 url: string, 123 url: string
124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
125 transaction: Transaction 125 transaction: Transaction
126}) { 126}) {
127 const activityBuilder = (audience: ActivityAudience) => { 127 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 9c76671b5..2b01ca5e7 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -8,7 +8,6 @@ import { getUpdateActivityPubUrl } from '../url'
8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' 8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
11import { VideoCaptionModel } from '../../../models/video/video-caption'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 11import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils' 12import { getServerActor } from '../../../helpers/utils'
14import { 13import {
@@ -29,7 +28,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction
29 28
30 logger.info('Creating job to update video %s.', video.url) 29 logger.info('Creating job to update video %s.', video.url)
31 30
32 const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor 31 const byActor = overrodeByActor || video.VideoChannel.Account.Actor
33 32
34 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) 33 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
35 34
@@ -85,7 +84,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
85 return buildUpdateActivity(url, byActor, redundancyObject, audience) 84 return buildUpdateActivity(url, byActor, redundancyObject, audience)
86 } 85 }
87 86
88 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 87 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
89} 88}
90 89
91async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { 90async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index 8809417f9..1f864ea52 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -5,9 +5,9 @@ import { getVideoLikeActivityPubUrl } from '../url'
5import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models' 8import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/typings/models'
9 9
10async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) { 10async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
11 logger.info('Creating job to send view of %s.', video.url) 11 logger.info('Creating job to send view of %s.', video.url)
12 12
13 const activityBuilder = (audience: ActivityAudience) => { 13 const activityBuilder = (audience: ActivityAudience) => {
@@ -16,7 +16,7 @@ async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Tran
16 return buildViewActivity(url, byActor, video, audience) 16 return buildViewActivity(url, byActor, video, audience)
17 } 17 }
18 18
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t, contextType: 'View' })
20} 20}
21 21
22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { 22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView {
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 77b723479..b57bae8fd 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -7,24 +7,28 @@ import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { getServerActor } from '../../../helpers/utils' 8import { getServerActor } from '../../../helpers/utils'
9import { afterCommitIfTransaction } from '../../../helpers/database-utils' 9import { afterCommitIfTransaction } from '../../../helpers/database-utils'
10import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' 10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
11import { ContextType } from '@server/helpers/activitypub'
11 12
12async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
13 byActor: MActorLight, 14 byActor: MActorLight
14 video: MVideoAccountLight, 15 video: MVideoImmutable | MVideoAccountLight
15 transaction?: Transaction 16 transaction?: Transaction
17 contextType?: ContextType
16}) { 18}) {
17 const { byActor, video, transaction } = options 19 const { byActor, video, transaction, contextType } = options
18 20
19 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) 21 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction)
20 22
21 // Send to origin 23 // Send to origin
22 if (video.isOwned() === false) { 24 if (video.isOwned() === false) {
23 const audience = getRemoteVideoAudience(video, actorsInvolvedInVideo) 25 const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id)
26
27 const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
24 const activity = activityBuilder(audience) 28 const activity = activityBuilder(audience)
25 29
26 return afterCommitIfTransaction(transaction, () => { 30 return afterCommitIfTransaction(transaction, () => {
27 return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()) 31 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
28 }) 32 })
29 } 33 }
30 34
@@ -34,14 +38,14 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
34 38
35 const actorsException = [ byActor ] 39 const actorsException = [ byActor ]
36 40
37 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException) 41 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
38} 42}
39 43
40async function forwardVideoRelatedActivity ( 44async function forwardVideoRelatedActivity (
41 activity: Activity, 45 activity: Activity,
42 t: Transaction, 46 t: Transaction,
43 followersException: MActorWithInboxes[] = [], 47 followersException: MActorWithInboxes[] = [],
44 video: MVideo 48 video: MVideoId
45) { 49) {
46 // Mastodon does not add our announces in audience, so we forward to them manually 50 // Mastodon does not add our announces in audience, so we forward to them manually
47 const additionalActors = await getActorsInvolvedInVideo(video, t) 51 const additionalActors = await getActorsInvolvedInVideo(video, t)
@@ -90,11 +94,12 @@ async function broadcastToFollowers (
90 byActor: MActorId, 94 byActor: MActorId,
91 toFollowersOf: MActorId[], 95 toFollowersOf: MActorId[],
92 t: Transaction, 96 t: Transaction,
93 actorsException: MActorWithInboxes[] = [] 97 actorsException: MActorWithInboxes[] = [],
98 contextType?: ContextType
94) { 99) {
95 const uris = await computeFollowerUris(toFollowersOf, actorsException, t) 100 const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
96 101
97 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 102 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
98} 103}
99 104
100async function broadcastToActors ( 105async function broadcastToActors (
@@ -102,13 +107,14 @@ async function broadcastToActors (
102 byActor: MActorId, 107 byActor: MActorId,
103 toActors: MActor[], 108 toActors: MActor[],
104 t?: Transaction, 109 t?: Transaction,
105 actorsException: MActorWithInboxes[] = [] 110 actorsException: MActorWithInboxes[] = [],
111 contextType?: ContextType
106) { 112) {
107 const uris = await computeUris(toActors, actorsException) 113 const uris = await computeUris(toActors, actorsException)
108 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 114 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
109} 115}
110 116
111function broadcastTo (uris: string[], data: any, byActor: MActorId) { 117function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) {
112 if (uris.length === 0) return undefined 118 if (uris.length === 0) return undefined
113 119
114 logger.debug('Creating broadcast job.', { uris }) 120 logger.debug('Creating broadcast job.', { uris })
@@ -116,19 +122,21 @@ function broadcastTo (uris: string[], data: any, byActor: MActorId) {
116 const payload = { 122 const payload = {
117 uris, 123 uris,
118 signatureActorId: byActor.id, 124 signatureActorId: byActor.id,
119 body: data 125 body: data,
126 contextType
120 } 127 }
121 128
122 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) 129 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })
123} 130}
124 131
125function unicastTo (data: any, byActor: MActorId, toActorUrl: string) { 132function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) {
126 logger.debug('Creating unicast job.', { uri: toActorUrl }) 133 logger.debug('Creating unicast job.', { uri: toActorUrl })
127 134
128 const payload = { 135 const payload = {
129 uri: toActorUrl, 136 uri: toActorUrl,
130 signatureActorId: byActor.id, 137 signatureActorId: byActor.id,
131 body: data 138 body: data,
139 contextType
132 } 140 }
133 141
134 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) 142 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload })
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d5c078a29..8642d2432 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -10,9 +10,9 @@ import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video' 10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string, 13 url: string
14 comments?: MCommentOwner[], 14 comments?: MCommentOwner[]
15 isVideo?: boolean, 15 isVideo?: boolean
16 commentCreated?: boolean 16 commentCreated?: boolean
17} 17}
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
@@ -28,7 +28,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult
28 if (params.commentCreated === undefined) params.commentCreated = false 28 if (params.commentCreated === undefined) params.commentCreated = false
29 if (params.comments === undefined) params.comments = [] 29 if (params.comments === undefined) params.comments = []
30 30
31 // Already have this comment? 31 // Already have this comment?
32 if (isVideo !== true) { 32 if (isVideo !== true) {
33 const result = await resolveCommentFromDB(params) 33 const result = await resolveCommentFromDB(params)
34 if (result) return result 34 if (result) return result
@@ -87,7 +87,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
87 87
88 let resultComment: MCommentOwnerVideo 88 let resultComment: MCommentOwnerVideo
89 if (comments.length !== 0) { 89 if (comments.length !== 0) {
90 const firstReply = comments[ comments.length - 1 ] as MCommentOwnerVideo 90 const firstReply = comments[comments.length - 1] as MCommentOwnerVideo
91 firstReply.inReplyToCommentId = null 91 firstReply.inReplyToCommentId = null
92 firstReply.originCommentId = null 92 firstReply.originCommentId = null
93 firstReply.videoId = video.id 93 firstReply.videoId = video.id
@@ -97,9 +97,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
97 comments[comments.length - 1] = await firstReply.save() 97 comments[comments.length - 1] = await firstReply.save()
98 98
99 for (let i = comments.length - 2; i >= 0; i--) { 99 for (let i = comments.length - 2; i >= 0; i--) {
100 const comment = comments[ i ] as MCommentOwnerVideo 100 const comment = comments[i] as MCommentOwnerVideo
101 comment.originCommentId = firstReply.id 101 comment.originCommentId = firstReply.id
102 comment.inReplyToCommentId = comments[ i + 1 ].id 102 comment.inReplyToCommentId = comments[i + 1].id
103 comment.videoId = video.id 103 comment.videoId = video.id
104 comment.changed('updatedAt', true) 104 comment.changed('updatedAt', true)
105 comment.Video = video 105 comment.Video = video
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 6bd46bb58..79ccfbc7e 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -58,8 +58,6 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy
58 const field = rate === 'like' ? 'likes' : 'dislikes' 58 const field = rate === 'like' ? 'likes' : 'dislikes'
59 await video.increment(field, { by: rateCounts }) 59 await video.increment(field, { by: rateCounts })
60 } 60 }
61
62 return
63} 61}
64 62
65async function sendVideoRateChange ( 63async function sendVideoRateChange (
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index ade93150f..d182ca5a2 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -6,7 +6,8 @@ import {
6 ActivityHashTagObject, 6 ActivityHashTagObject,
7 ActivityMagnetUrlObject, 7 ActivityMagnetUrlObject,
8 ActivityPlaylistSegmentHashesObject, 8 ActivityPlaylistSegmentHashesObject,
9 ActivityPlaylistUrlObject, ActivityTagObject, 9 ActivityPlaylistUrlObject,
10 ActivityTagObject,
10 ActivityUrlObject, 11 ActivityUrlObject,
11 ActivityVideoUrlObject, 12 ActivityVideoUrlObject,
12 VideoState 13 VideoState
@@ -17,14 +18,14 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
17import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 18import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
18import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 19import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
19import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
20import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 21import { doRequest } from '../../helpers/requests'
21import { 22import {
22 ACTIVITY_PUB, 23 ACTIVITY_PUB,
23 MIMETYPES, 24 MIMETYPES,
24 P2P_MEDIA_LOADER_PEER_VERSION, 25 P2P_MEDIA_LOADER_PEER_VERSION,
25 PREVIEWS_SIZE, 26 PREVIEWS_SIZE,
26 REMOTE_SCHEME, 27 REMOTE_SCHEME,
27 STATIC_PATHS 28 STATIC_PATHS, THUMBNAILS_SIZE
28} from '../../initializers/constants' 29} from '../../initializers/constants'
29import { TagModel } from '../../models/video/tag' 30import { TagModel } from '../../models/video/tag'
30import { VideoModel } from '../../models/video/video' 31import { VideoModel } from '../../models/video/video'
@@ -40,7 +41,7 @@ import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub
40import { createRates } from './video-rates' 41import { createRates } from './video-rates'
41import { addVideoShares, shareVideoByServerAndChannel } from './share' 42import { addVideoShares, shareVideoByServerAndChannel } from './share'
42import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 43import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
43import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 44import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
44import { Notifier } from '../notifier' 45import { Notifier } from '../notifier'
45import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 46import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
46import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 47import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -67,10 +68,11 @@ import {
67 MVideoAPWithoutCaption, 68 MVideoAPWithoutCaption,
68 MVideoFile, 69 MVideoFile,
69 MVideoFullLight, 70 MVideoFullLight,
70 MVideoId, 71 MVideoId, MVideoImmutable,
71 MVideoThumbnail 72 MVideoThumbnail
72} from '../../typings/models' 73} from '../../typings/models'
73import { MThumbnail } from '../../typings/models/video/thumbnail' 74import { MThumbnail } from '../../typings/models/video/thumbnail'
75import { maxBy, minBy } from 'lodash'
74 76
75async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 77async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
76 const video = videoArg as MVideoAP 78 const video = videoArg as MVideoAP
@@ -131,19 +133,6 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
131 return body.description ? body.description : '' 133 return body.description ? body.description : ''
132} 134}
133 135
134function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
135 const url = buildRemoteBaseUrl(video, path)
136
137 // We need to provide a callback, if no we could have an uncaught exception
138 return doRequestAndSaveToFile({ uri: url }, destPath)
139}
140
141function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
142 const host = video.VideoChannel.Account.Actor.Server.host
143
144 return REMOTE_SCHEME.HTTP + '://' + host + path
145}
146
147function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 136function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
148 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 137 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
149 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 138 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
@@ -173,7 +162,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) 162 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
174 163
175 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) 164 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
176 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) 165 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes }))
177 } else { 166 } else {
178 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) 167 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
179 } 168 }
@@ -183,7 +172,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
183 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) 172 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
184 173
185 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) 174 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
186 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) 175 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes }))
187 } else { 176 } else {
188 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) 177 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
189 } 178 }
@@ -193,7 +182,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
193 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) 182 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
194 183
195 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) 184 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
196 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) 185 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares }))
197 } else { 186 } else {
198 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 187 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
199 } 188 }
@@ -203,32 +192,49 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
203 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) 192 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
204 193
205 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) 194 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
206 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) 195 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments }))
207 } else { 196 } else {
208 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) 197 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
209 } 198 }
210 199
211 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 200 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }))
212} 201}
213 202
214function getOrCreateVideoAndAccountAndChannel (options: { 203type GetVideoResult <T> = Promise<{
215 videoObject: { id: string } | string, 204 video: T
216 syncParam?: SyncParam, 205 created: boolean
217 fetchType?: 'all', 206 autoBlacklisted?: boolean
207}>
208
209type GetVideoParamAll = {
210 videoObject: { id: string } | string
211 syncParam?: SyncParam
212 fetchType?: 'all'
218 allowRefresh?: boolean 213 allowRefresh?: boolean
219}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> 214}
220function getOrCreateVideoAndAccountAndChannel (options: { 215
221 videoObject: { id: string } | string, 216type GetVideoParamImmutable = {
222 syncParam?: SyncParam, 217 videoObject: { id: string } | string
223 fetchType?: VideoFetchByUrlType, 218 syncParam?: SyncParam
219 fetchType: 'only-immutable-attributes'
220 allowRefresh: false
221}
222
223type GetVideoParamOther = {
224 videoObject: { id: string } | string
225 syncParam?: SyncParam
226 fetchType?: 'all' | 'only-video'
224 allowRefresh?: boolean 227 allowRefresh?: boolean
225}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> 228}
226async function getOrCreateVideoAndAccountAndChannel (options: { 229
227 videoObject: { id: string } | string, 230function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
228 syncParam?: SyncParam, 231function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
229 fetchType?: VideoFetchByUrlType, 232function getOrCreateVideoAndAccountAndChannel (
230 allowRefresh?: boolean // true by default 233 options: GetVideoParamOther
231}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { 234): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
235async function getOrCreateVideoAndAccountAndChannel (
236 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
237): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
232 // Default params 238 // Default params
233 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 239 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
234 const fetchType = options.fetchType || 'all' 240 const fetchType = options.fetchType || 'all'
@@ -236,18 +242,25 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
236 242
237 // Get video url 243 // Get video url
238 const videoUrl = getAPId(options.videoObject) 244 const videoUrl = getAPId(options.videoObject)
239
240 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 245 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
246
241 if (videoFromDatabase) { 247 if (videoFromDatabase) {
242 if (videoFromDatabase.isOutdated() && allowRefresh === true) { 248 // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
249 if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
243 const refreshOptions = { 250 const refreshOptions = {
244 video: videoFromDatabase, 251 video: videoFromDatabase as MVideoThumbnail,
245 fetchedType: fetchType, 252 fetchedType: fetchType,
246 syncParam 253 syncParam
247 } 254 }
248 255
249 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 256 if (syncParam.refreshVideo === true) {
250 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) 257 videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
258 } else {
259 await JobQueue.Instance.createJobWithPromise({
260 type: 'activitypub-refresher',
261 payload: { type: 'video', url: videoFromDatabase.url }
262 })
263 }
251 } 264 }
252 265
253 return { video: videoFromDatabase, created: false } 266 return { video: videoFromDatabase, created: false }
@@ -266,10 +279,10 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
266} 279}
267 280
268async function updateVideoFromAP (options: { 281async function updateVideoFromAP (options: {
269 video: MVideoAccountLightBlacklistAllFiles, 282 video: MVideoAccountLightBlacklistAllFiles
270 videoObject: VideoTorrentObject, 283 videoObject: VideoTorrentObject
271 account: MAccountIdActor, 284 account: MAccountIdActor
272 channel: MChannelDefault, 285 channel: MChannelDefault
273 overrideTo?: string[] 286 overrideTo?: string[]
274}) { 287}) {
275 const { video, videoObject, account, channel, overrideTo } = options 288 const { video, videoObject, account, channel, overrideTo } = options
@@ -284,7 +297,7 @@ async function updateVideoFromAP (options: {
284 let thumbnailModel: MThumbnail 297 let thumbnailModel: MThumbnail
285 298
286 try { 299 try {
287 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 300 thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
288 } catch (err) { 301 } catch (err) {
289 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) 302 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
290 } 303 }
@@ -300,7 +313,7 @@ async function updateVideoFromAP (options: {
300 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) 313 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
301 } 314 }
302 315
303 const to = overrideTo ? overrideTo : videoObject.to 316 const to = overrideTo || videoObject.to
304 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) 317 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
305 video.name = videoData.name 318 video.name = videoData.name
306 video.uuid = videoData.uuid 319 video.uuid = videoData.uuid
@@ -327,10 +340,11 @@ async function updateVideoFromAP (options: {
327 340
328 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 341 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
329 342
330 // FIXME: use icon URL instead 343 if (videoUpdated.getPreview()) {
331 const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename)) 344 const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
332 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 345 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
333 await videoUpdated.addAndSaveThumbnail(previewModel, t) 346 await videoUpdated.addAndSaveThumbnail(previewModel, t)
347 }
334 348
335 { 349 {
336 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) 350 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
@@ -391,7 +405,7 @@ async function updateVideoFromAP (options: {
391 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) 405 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
392 406
393 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 407 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
394 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t) 408 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
395 }) 409 })
396 await Promise.all(videoCaptionsPromises) 410 await Promise.all(videoCaptionsPromises)
397 } 411 }
@@ -424,8 +438,8 @@ async function updateVideoFromAP (options: {
424} 438}
425 439
426async function refreshVideoIfNeeded (options: { 440async function refreshVideoIfNeeded (options: {
427 video: MVideoThumbnail, 441 video: MVideoThumbnail
428 fetchedType: VideoFetchByUrlType, 442 fetchedType: VideoFetchByUrlType
429 syncParam: SyncParam 443 syncParam: SyncParam
430}): Promise<MVideoThumbnail> { 444}): Promise<MVideoThumbnail> {
431 if (!options.video.isOutdated()) return options.video 445 if (!options.video.isOutdated()) return options.video
@@ -483,7 +497,6 @@ export {
483 federateVideoIfNeeded, 497 federateVideoIfNeeded,
484 fetchRemoteVideo, 498 fetchRemoteVideo,
485 getOrCreateVideoAndAccountAndChannel, 499 getOrCreateVideoAndAccountAndChannel,
486 fetchRemoteVideoStaticFile,
487 fetchRemoteVideoDescription, 500 fetchRemoteVideoDescription,
488 getOrCreateVideoChannelFromVideoObject 501 getOrCreateVideoChannelFromVideoObject
489} 502}
@@ -519,7 +532,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
519 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) 532 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
520 const video = VideoModel.build(videoData) as MVideoThumbnail 533 const video = VideoModel.build(videoData) as MVideoThumbnail
521 534
522 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 535 const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
536 .catch(err => {
537 logger.error('Cannot create miniature from url.', { err })
538 return undefined
539 })
523 540
524 let thumbnailModel: MThumbnail 541 let thumbnailModel: MThumbnail
525 if (waitThumbnail === true) { 542 if (waitThumbnail === true) {
@@ -534,9 +551,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
534 551
535 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 552 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
536 553
537 // FIXME: use icon URL instead 554 const previewIcon = getPreviewFromIcons(videoObject)
538 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 555 const previewUrl = previewIcon
539 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 556 ? previewIcon.url
557 : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
558 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
559
540 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 560 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
541 561
542 // Process files 562 // Process files
@@ -567,7 +587,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
567 587
568 // Process captions 588 // Process captions
569 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 589 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
570 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) 590 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
571 }) 591 })
572 await Promise.all(videoCaptionsPromises) 592 await Promise.all(videoCaptionsPromises)
573 593
@@ -588,7 +608,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
588 }) 608 })
589 609
590 if (waitThumbnail === false) { 610 if (waitThumbnail === false) {
611 // Error is already caught above
612 // eslint-disable-next-line @typescript-eslint/no-floating-promises
591 promiseThumbnail.then(thumbnailModel => { 613 promiseThumbnail.then(thumbnailModel => {
614 if (!thumbnailModel) return
615
592 thumbnailModel = videoCreated.id 616 thumbnailModel = videoCreated.id
593 617
594 return thumbnailModel.save() 618 return thumbnailModel.save()
@@ -598,24 +622,19 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
598 return { autoBlacklisted, videoCreated } 622 return { autoBlacklisted, videoCreated }
599} 623}
600 624
601async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { 625function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
602 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 626 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
603 const duration = videoObject.duration.replace(/[^\d]+/, '') 627 const duration = videoObject.duration.replace(/[^\d]+/, '')
604 628
605 let language: string | undefined 629 const language = videoObject.language?.identifier
606 if (videoObject.language) {
607 language = videoObject.language.identifier
608 }
609 630
610 let category: number | undefined 631 const category = videoObject.category
611 if (videoObject.category) { 632 ? parseInt(videoObject.category.identifier, 10)
612 category = parseInt(videoObject.category.identifier, 10) 633 : undefined
613 }
614 634
615 let licence: number | undefined 635 const licence = videoObject.licence
616 if (videoObject.licence) { 636 ? parseInt(videoObject.licence.identifier, 10)
617 licence = parseInt(videoObject.licence.identifier, 10) 637 : undefined
618 }
619 638
620 const description = videoObject.content || null 639 const description = videoObject.content || null
621 const support = videoObject.support || null 640 const support = videoObject.support || null
@@ -638,8 +657,11 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
638 duration: parseInt(duration, 10), 657 duration: parseInt(duration, 10),
639 createdAt: new Date(videoObject.published), 658 createdAt: new Date(videoObject.published),
640 publishedAt: new Date(videoObject.published), 659 publishedAt: new Date(videoObject.published),
641 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, 660
642 // FIXME: updatedAt does not seems to be considered by Sequelize 661 originallyPublishedAt: videoObject.originallyPublishedAt
662 ? new Date(videoObject.originallyPublishedAt)
663 : null,
664
643 updatedAt: new Date(videoObject.updated), 665 updatedAt: new Date(videoObject.updated),
644 views: videoObject.views, 666 views: videoObject.views,
645 likes: 0, 667 likes: 0,
@@ -672,7 +694,7 @@ function videoFileActivityUrlToDBAttributes (
672 694
673 const mediaType = fileUrl.mediaType 695 const mediaType = fileUrl.mediaType
674 const attribute = { 696 const attribute = {
675 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], 697 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[mediaType],
676 infoHash: parsed.infoHash, 698 infoHash: parsed.infoHash,
677 resolution: fileUrl.height, 699 resolution: fileUrl.height,
678 size: fileUrl.size, 700 size: fileUrl.size,
@@ -722,3 +744,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
722 744
723 return attributes 745 return attributes
724} 746}
747
748function getThumbnailFromIcons (videoObject: VideoTorrentObject) {
749 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
750 // Fallback if there are not valid icons
751 if (validIcons.length === 0) validIcons = videoObject.icon
752
753 return minBy(validIcons, 'width')
754}
755
756function getPreviewFromIcons (videoObject: VideoTorrentObject) {
757 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
758
759 // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
760
761 return maxBy(validIcons, 'width')
762}