aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/actor.ts212
-rw-r--r--server/lib/activitypub/crawl.ts25
-rw-r--r--server/lib/activitypub/playlist.ts69
-rw-r--r--server/lib/activitypub/process/process-delete.ts13
-rw-r--r--server/lib/activitypub/process/process-update.ts13
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/share.ts30
-rw-r--r--server/lib/activitypub/video-comments.ts22
-rw-r--r--server/lib/activitypub/video-rates.ts22
-rw-r--r--server/lib/activitypub/videos.ts83
10 files changed, 260 insertions, 239 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index a726f9e20..eec951d4e 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,26 +1,29 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
2import { Op, Transaction } from 'sequelize' 3import { Op, Transaction } from 'sequelize'
3import { URL } from 'url' 4import { URL } from 'url'
4import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 13import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 14import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 15import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 17import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest } from '../../helpers/requests' 18import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 19import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 20import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
21import { sequelizeTypescript } from '../../initializers/database'
16import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
23import { ActorImageModel } from '../../models/account/actor-image'
17import { ActorModel } from '../../models/activitypub/actor' 24import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 25import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 26import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue'
22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23import { sequelizeTypescript } from '../../initializers/database'
24import { 27import {
25 MAccount, 28 MAccount,
26 MAccountDefault, 29 MAccountDefault,
@@ -28,15 +31,14 @@ import {
28 MActorAccountChannelId, 31 MActorAccountChannelId,
29 MActorAccountChannelIdActor, 32 MActorAccountChannelIdActor,
30 MActorAccountId, 33 MActorAccountId,
31 MActorDefault,
32 MActorFull, 34 MActorFull,
33 MActorFullActor, 35 MActorFullActor,
34 MActorId, 36 MActorId,
37 MActorImage,
38 MActorImages,
35 MChannel 39 MChannel
36} from '../../types/models' 40} from '../../types/models'
37import { extname } from 'path' 41import { JobQueue } from '../job-queue'
38import { getServerActor } from '@server/models/application/application'
39import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
40 42
41// Set account keys, this could be long so process after the account creation and do not block the client 43// Set account keys, this could be long so process after the account creation and do not block the client
42async function generateAndSaveActorKeys <T extends MActor> (actor: T) { 44async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
168 } 170 }
169} 171}
170 172
171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } 173type ImageInfo = {
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { 174 name: string
173 if (!info.name) return actor 175 fileUrl: string
176 height: number
177 width: number
178 onDisk?: boolean
179}
180async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
181 const oldImageModel = type === ActorImageType.AVATAR
182 ? actor.Avatar
183 : actor.Banner
174 184
175 if (actor.Avatar) { 185 if (oldImageModel) {
176 // Don't update the avatar if the file URL did not change 186 // Don't update the avatar if the file URL did not change
177 if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor 187 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
178 188
179 try { 189 try {
180 await actor.Avatar.destroy({ transaction: t }) 190 await oldImageModel.destroy({ transaction: t })
191
192 setActorImage(actor, type, null)
181 } catch (err) { 193 } catch (err) {
182 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 194 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
183 } 195 }
184 } 196 }
185 197
186 const avatar = await AvatarModel.create({ 198 if (imageInfo) {
187 filename: info.name, 199 const imageModel = await ActorImageModel.create({
188 onDisk: info.onDisk, 200 filename: imageInfo.name,
189 fileUrl: info.fileUrl 201 onDisk: imageInfo.onDisk ?? false,
190 }, { transaction: t }) 202 fileUrl: imageInfo.fileUrl,
191 203 height: imageInfo.height,
192 actor.avatarId = avatar.id 204 width: imageInfo.width,
193 actor.Avatar = avatar 205 type
206 }, { transaction: t })
207
208 setActorImage(actor, type, imageModel)
209 }
194 210
195 return actor 211 return actor
196} 212}
197 213
198async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { 214async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
199 try { 215 try {
200 await actor.Avatar.destroy({ transaction: t }) 216 if (type === ActorImageType.AVATAR) {
217 await actor.Avatar.destroy({ transaction: t })
218
219 actor.avatarId = null
220 actor.Avatar = null
221 } else {
222 await actor.Banner.destroy({ transaction: t })
223
224 actor.bannerId = null
225 actor.Banner = null
226 }
201 } catch (err) { 227 } catch (err) {
202 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 228 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
203 } 229 }
204 230
205 actor.avatarId = null
206 actor.Avatar = null
207
208 return actor 231 return actor
209} 232}
210 233
211async function fetchActorTotalItems (url: string) { 234async function fetchActorTotalItems (url: string) {
212 const options = {
213 uri: url,
214 method: 'GET',
215 json: true,
216 activityPub: true
217 }
218
219 try { 235 try {
220 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) 236 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
221 return body.totalItems ? body.totalItems : 0 237
238 return body.totalItems || 0
222 } catch (err) { 239 } catch (err) {
223 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 240 logger.warn('Cannot fetch remote actor count %s.', url, { err })
224 return 0 241 return 0
225 } 242 }
226} 243}
227 244
228function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 245function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
229 const mimetypes = MIMETYPES.IMAGE 246 const mimetypes = MIMETYPES.IMAGE
230 const icon = actorJSON.icon 247 const icon = type === ActorImageType.AVATAR
248 ? actorJSON.icon
249 : actorJSON.image
231 250
232 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 251 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
233 252
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
245 264
246 return { 265 return {
247 name: uuidv4() + extension, 266 name: uuidv4() + extension,
248 fileUrl: icon.url 267 fileUrl: icon.url,
268 height: icon.height,
269 width: icon.width,
270 type
249 } 271 }
250} 272}
251 273
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
285 actorUrl = actor.url 307 actorUrl = actor.url
286 } 308 }
287 309
288 const { result, statusCode } = await fetchRemoteActor(actorUrl) 310 const { result } = await fetchRemoteActor(actorUrl)
289
290 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
291 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
292 actor.Account
293 ? await actor.Account.destroy()
294 : await actor.VideoChannel.destroy()
295
296 return { actor: undefined, refreshed: false }
297 }
298 311
299 if (result === undefined) { 312 if (result === undefined) {
300 logger.warn('Cannot fetch remote actor in refresh actor.') 313 logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
304 return sequelizeTypescript.transaction(async t => { 317 return sequelizeTypescript.transaction(async t => {
305 updateInstanceWithAnother(actor, result.actor) 318 updateInstanceWithAnother(actor, result.actor)
306 319
307 if (result.avatar !== undefined) { 320 await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t)
308 const avatarInfo = { 321 await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t)
309 name: result.avatar.name,
310 fileUrl: result.avatar.fileUrl,
311 onDisk: false
312 }
313
314 await updateActorAvatarInstance(actor, avatarInfo, t)
315 }
316 322
317 // Force update 323 // Force update
318 actor.setDataValue('updatedAt', new Date()) 324 actor.setDataValue('updatedAt', new Date())
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
334 return { refreshed: true, actor } 340 return { refreshed: true, actor }
335 }) 341 })
336 } catch (err) { 342 } catch (err) {
343 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
344 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
345 actor.Account
346 ? await actor.Account.destroy()
347 : await actor.VideoChannel.destroy()
348
349 return { actor: undefined, refreshed: false }
350 }
351
337 logger.warn('Cannot refresh actor %s.', actor.url, { err }) 352 logger.warn('Cannot refresh actor %s.', actor.url, { err })
338 return { actor, refreshed: false } 353 return { actor, refreshed: false }
339 } 354 }
@@ -344,16 +359,32 @@ export {
344 buildActorInstance, 359 buildActorInstance,
345 generateAndSaveActorKeys, 360 generateAndSaveActorKeys,
346 fetchActorTotalItems, 361 fetchActorTotalItems,
347 getAvatarInfoIfExists, 362 getImageInfoIfExists,
348 updateActorInstance, 363 updateActorInstance,
349 deleteActorAvatarInstance, 364 deleteActorImageInstance,
350 refreshActorIfNeeded, 365 refreshActorIfNeeded,
351 updateActorAvatarInstance, 366 updateActorImageInstance,
352 addFetchOutboxJob 367 addFetchOutboxJob
353} 368}
354 369
355// --------------------------------------------------------------------------- 370// ---------------------------------------------------------------------------
356 371
372function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
373 const id = imageModel
374 ? imageModel.id
375 : null
376
377 if (type === ActorImageType.AVATAR) {
378 actorModel.avatarId = id
379 actorModel.Avatar = imageModel
380 } else {
381 actorModel.bannerId = id
382 actorModel.Banner = imageModel
383 }
384
385 return actorModel
386}
387
357function saveActorAndServerAndModelIfNotExist ( 388function saveActorAndServerAndModelIfNotExist (
358 result: FetchRemoteActorResult, 389 result: FetchRemoteActorResult,
359 ownerActor?: MActorFullActor, 390 ownerActor?: MActorFullActor,
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist (
384 415
385 // Avatar? 416 // Avatar?
386 if (result.avatar) { 417 if (result.avatar) {
387 const avatar = await AvatarModel.create({ 418 const avatar = await ActorImageModel.create({
388 filename: result.avatar.name, 419 filename: result.avatar.name,
389 fileUrl: result.avatar.fileUrl, 420 fileUrl: result.avatar.fileUrl,
390 onDisk: false 421 width: result.avatar.width,
422 height: result.avatar.height,
423 onDisk: false,
424 type: ActorImageType.AVATAR
391 }, { transaction: t }) 425 }, { transaction: t })
392 426
393 actor.avatarId = avatar.id 427 actor.avatarId = avatar.id
394 } 428 }
395 429
430 // Banner?
431 if (result.banner) {
432 const banner = await ActorImageModel.create({
433 filename: result.banner.name,
434 fileUrl: result.banner.fileUrl,
435 width: result.banner.width,
436 height: result.banner.height,
437 onDisk: false,
438 type: ActorImageType.BANNER
439 }, { transaction: t })
440
441 actor.bannerId = banner.id
442 }
443
396 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 444 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
397 // (which could be false in a retried query) 445 // (which could be false in a retried query)
398 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ 446 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist (
436 } 484 }
437} 485}
438 486
487type ImageResult = {
488 name: string
489 fileUrl: string
490 height: number
491 width: number
492}
493
439type FetchRemoteActorResult = { 494type FetchRemoteActorResult = {
440 actor: MActor 495 actor: MActor
441 name: string 496 name: string
442 summary: string 497 summary: string
443 support?: string 498 support?: string
444 playlists?: string 499 playlists?: string
445 avatar?: { 500 avatar?: ImageResult
446 name: string 501 banner?: ImageResult
447 fileUrl: string
448 }
449 attributedTo: ActivityPubAttributedTo[] 502 attributedTo: ActivityPubAttributedTo[]
450} 503}
451async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 504async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
452 const options = {
453 uri: actorUrl,
454 method: 'GET',
455 json: true,
456 activityPub: true
457 }
458
459 logger.info('Fetching remote actor %s.', actorUrl) 505 logger.info('Fetching remote actor %s.', actorUrl)
460 506
461 const requestResult = await doRequest<ActivityPubActor>(options) 507 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
462 const actorJSON = requestResult.body 508 const actorJSON = requestResult.body
463 509
464 if (sanitizeAndCheckActorObject(actorJSON) === false) { 510 if (sanitizeAndCheckActorObject(actorJSON) === false) {
465 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 511 logger.debug('Remote actor JSON is not valid.', { actorJSON })
466 return { result: undefined, statusCode: requestResult.response.statusCode } 512 return { result: undefined, statusCode: requestResult.statusCode }
467 } 513 }
468 514
469 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 515 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
470 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) 516 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
471 return { result: undefined, statusCode: requestResult.response.statusCode } 517 return { result: undefined, statusCode: requestResult.statusCode }
472 } 518 }
473 519
474 const followersCount = await fetchActorTotalItems(actorJSON.followers) 520 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
492 : null 538 : null
493 }) 539 })
494 540
495 const avatarInfo = await getAvatarInfoIfExists(actorJSON) 541 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
542 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
496 543
497 const name = actorJSON.name || actorJSON.preferredUsername 544 const name = actorJSON.name || actorJSON.preferredUsername
498 return { 545 return {
499 statusCode: requestResult.response.statusCode, 546 statusCode: requestResult.statusCode,
500 result: { 547 result: {
501 actor, 548 actor,
502 name, 549 name,
503 avatar: avatarInfo, 550 avatar: avatarInfo,
551 banner: bannerInfo,
504 summary: actorJSON.summary, 552 summary: actorJSON.summary,
505 support: actorJSON.support, 553 support: actorJSON.support,
506 playlists: actorJSON.playlists, 554 playlists: actorJSON.playlists,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1ed105bbe..278abf7de 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,27 +1,26 @@
1import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { URL } from 'url' 2import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
4import { logger } from '../../helpers/logger'
5import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
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>)
10 10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { 11async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
12 logger.info('Crawling ActivityPub data on %s.', uri) 12 let url = argUrl
13
14 logger.info('Crawling ActivityPub data on %s.', url)
13 15
14 const options = { 16 const options = {
15 method: 'GET',
16 uri,
17 json: true,
18 activityPub: true, 17 activityPub: true,
19 timeout: REQUEST_TIMEOUT 18 timeout: REQUEST_TIMEOUT
20 } 19 }
21 20
22 const startDate = new Date() 21 const startDate = new Date()
23 22
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 23 const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
25 const firstBody = response.body 24 const firstBody = response.body
26 25
27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 26 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
35 const remoteHost = new URL(nextLink).host 34 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 35 if (remoteHost === WEBSERVER.HOST) continue
37 36
38 options.uri = nextLink 37 url = nextLink
39 38
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options) 39 const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
41 body = res.body 40 body = res.body
42 } else { 41 } else {
43 // nextLink is already the object we want 42 // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
49 48
50 if (Array.isArray(body.orderedItems)) { 49 if (Array.isArray(body.orderedItems)) {
51 const items = body.orderedItems 50 const items = body.orderedItems
52 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) 51 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
53 52
54 await handler(items) 53 await handler(items)
55 } 54 }
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index d5a3ef7c8..7166c68a6 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,24 +1,24 @@
1import * as Bluebird from 'bluebird'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 5import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { checkUrlsSameHost } from '../../helpers/activitypub'
7import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
4import { isArray } from '../../helpers/custom-validators/misc' 8import { isArray } from '../../helpers/custom-validators/misc'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
11import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { sequelizeTypescript } from '../../initializers/database'
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 13import { VideoPlaylistModel } from '../../models/video/video-playlist'
8import { doRequest } from '../../helpers/requests'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import * as Bluebird from 'bluebird'
11import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
12import { getOrCreateVideoAndAccountAndChannel } from './videos'
13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
16import { sequelizeTypescript } from '../../initializers/database'
17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
18import { FilteredModelAttributes } from '../../types/sequelize'
19import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' 15import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' 16import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
21import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 17import { FilteredModelAttributes } from '../../types/sequelize'
18import { createPlaylistMiniatureFromUrl } from '../thumbnail'
19import { getOrCreateActorAndServerAndModel } from './actor'
20import { crawlCollectionPage } from './crawl'
21import { getOrCreateVideoAndAccountAndChannel } from './videos'
22 22
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
56 if (exists === true) return 56 if (exists === true) return
57 57
58 // Fetch url 58 // Fetch url
59 const { body } = await doRequest<PlaylistObject>({ 59 const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
60 uri: playlistUrl,
61 json: true,
62 activityPub: true
63 })
64 60
65 if (!isPlaylistObjectValid(body)) { 61 if (!isPlaylistObjectValid(body)) {
66 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) 62 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
120 if (!videoPlaylist.isOutdated()) return videoPlaylist 116 if (!videoPlaylist.isOutdated()) return videoPlaylist
121 117
122 try { 118 try {
123 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) 119 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
124 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
125 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
126
127 await videoPlaylist.destroy()
128 return undefined
129 }
130 120
131 if (playlistObject === undefined) { 121 if (playlistObject === undefined) {
132 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) 122 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
140 130
141 return videoPlaylist 131 return videoPlaylist
142 } catch (err) { 132 } catch (err) {
133 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
134 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
135
136 await videoPlaylist.destroy()
137 return undefined
138 }
139
143 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) 140 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
144 141
145 await videoPlaylist.setAsRefreshed() 142 await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
164 161
165 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
166 try { 163 try {
167 // Fetch url 164 const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
168 const { body } = await doRequest<PlaylistElementObject>({
169 uri: elementUrl,
170 json: true,
171 activityPub: true
172 })
173 165
174 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) 166 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
175 167
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
199} 191}
200 192
201async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 193async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
202 const options = {
203 uri: playlistUrl,
204 method: 'GET',
205 json: true,
206 activityPub: true
207 }
208
209 logger.info('Fetching remote playlist %s.', playlistUrl) 194 logger.info('Fetching remote playlist %s.', playlistUrl)
210 195
211 const { response, body } = await doRequest<any>(options) 196 const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
212 197
213 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 198 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
214 logger.debug('Remote video playlist JSON is not valid.', { body }) 199 logger.debug('Remote video playlist JSON is not valid.', { body })
215 return { statusCode: response.statusCode, playlistObject: undefined } 200 return { statusCode, playlistObject: undefined }
216 } 201 }
217 202
218 return { statusCode: response.statusCode, playlistObject: body } 203 return { statusCode, playlistObject: body }
219} 204}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index a86def936..88a968318 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -7,7 +7,15 @@ import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model' 9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' 10import {
11 MAccountActor,
12 MActor,
13 MActorFull,
14 MActorSignature,
15 MChannelAccountActor,
16 MChannelActor,
17 MCommentOwnerVideo
18} from '../../../types/models'
11import { markCommentAsDeleted } from '../../video-comment' 19import { markCommentAsDeleted } from '../../video-comment'
12import { forwardVideoRelatedActivity } from '../send/utils' 20import { forwardVideoRelatedActivity } from '../send/utils'
13 21
@@ -30,9 +38,8 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
30 } else if (byActorFull.type === 'Group') { 38 } else if (byActorFull.type === 'Group') {
31 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') 39 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
32 40
33 const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor 41 const channelToDelete = byActorFull.VideoChannel as MChannelAccountActor & { Actor: MActorFull }
34 channelToDelete.Actor = byActorFull 42 channelToDelete.Actor = byActorFull
35
36 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) 43 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
37 } 44 }
38 } 45 }
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 849f70b94..6df9b93b2 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model' 17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../types/models' 18import { MActorSignature, MAccountIdActor } from '../../../types/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy' 19import { isRedundancyAccepted } from '@server/lib/redundancy'
20import { ActorImageType } from '@shared/models'
20 21
21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 22async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
22 const { activity, byActor } = options 23 const { activity, byActor } = options
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
119 let accountOrChannelFieldsSave: object 120 let accountOrChannelFieldsSave: object
120 121
121 // Fetch icon? 122 // Fetch icon?
122 const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) 123 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
124 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
123 125
124 try { 126 try {
125 await sequelizeTypescript.transaction(async t => { 127 await sequelizeTypescript.transaction(async t => {
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
132 134
133 await updateActorInstance(actor, actorAttributesToUpdate) 135 await updateActorInstance(actor, actorAttributesToUpdate)
134 136
135 if (avatarInfo !== undefined) { 137 await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t)
136 const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) 138 await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t)
137
138 await updateActorAvatarInstance(actor, avatarOptions, t)
139 }
140 139
141 await actor.save({ transaction: t }) 140 await actor.save({ transaction: t })
142 141
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9fb218224..baded642a 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
4import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { 9import {
10 MActorLight, 10 MActorLight,
@@ -18,10 +18,12 @@ import {
18import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context' 19import { ContextType } from '@shared/models/activitypub/context'
20 20
21const lTags = loggerTagsFactory('ap', 'create')
22
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 23async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 24 if (!video.hasPrivacyForFederation()) return undefined
23 25
24 logger.info('Creating job to send video creation of %s.', video.url) 26 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
25 27
26 const byActor = video.VideoChannel.Account.Actor 28 const byActor = video.VideoChannel.Account.Actor
27 const videoObject = video.toActivityPubObject() 29 const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
37 video: MVideoAccountLight, 39 video: MVideoAccountLight,
38 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 40 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
39) { 41) {
40 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 42 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
41 43
42 return sendVideoRelatedCreateActivity({ 44 return sendVideoRelatedCreateActivity({
43 byActor, 45 byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
51async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { 53async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
52 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 54 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
53 55
54 logger.info('Creating job to send create video playlist of %s.', playlist.url) 56 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
55 57
56 const byActor = playlist.OwnerAccount.Actor 58 const byActor = playlist.OwnerAccount.Actor
57 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 59 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1f8a8f3c4..c22fa0893 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,15 +1,17 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { logger, loggerTagsFactory } from '../../helpers/logger'
6import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
2import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor'
3import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
4import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
5import * as Bluebird from 'bluebird' 13
6import { doRequest } from '../../helpers/requests' 14const lTags = loggerTagsFactory('share')
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { logger } from '../../helpers/logger'
9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
12import { getServerActor } from '@server/models/application/application'
13 15
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 16async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 17 if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
25 oldVideoChannel: MChannelActorLight, 27 oldVideoChannel: MChannelActorLight,
26 t: Transaction 28 t: Transaction
27) { 29) {
28 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 30 logger.info(
31 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
32 lTags(video.uuid)
33 )
29 34
30 await undoShareByVideoChannel(video, oldVideoChannel, t) 35 await undoShareByVideoChannel(video, oldVideoChannel, t)
31 36
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
35async function addVideoShares (shareUrls: string[], video: MVideoId) { 40async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 41 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 42 try {
38 // Fetch url 43 const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
39 const { body } = await doRequest<any>({
40 uri: shareUrl,
41 json: true,
42 activityPub: true
43 })
44 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 44 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
45 45
46 const actorUrl = getAPId(body.actor) 46 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d025ed7f1..e23e0c0e7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird'
2import { checkUrlsSameHost } from '../../helpers/activitypub'
1import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
2import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 5import { doJSONRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
6import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
7import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
8import * as Bluebird from 'bluebird'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string 13 url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, async commentUrl => {
22 return resolveThread({ url: commentUrl, isVideo: false }) 22 try {
23 await resolveThread({ url: commentUrl, isVideo: false })
24 } catch (err) {
25 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
26 }
23 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 27 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
24} 28}
25 29
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
126 throw new Error('Recursion limit reached when resolving a thread') 130 throw new Error('Recursion limit reached when resolving a thread')
127 } 131 }
128 132
129 const { body } = await doRequest<any>({ 133 const { body } = await doJSONRequest<any>(url, { activityPub: true })
130 uri: url,
131 json: true,
132 activityPub: true
133 })
134 134
135 if (sanitizeAndCheckVideoCommentObject(body) === false) { 135 if (sanitizeAndCheckVideoCommentObject(body) === false) {
136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) 136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index e246b1313..f40c07fea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,26 +1,22 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
2import { sendLike, sendUndoDislike, sendUndoLike } from './send' 3import { doJSONRequest } from '@server/helpers/requests'
3import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
4import * as Bluebird from 'bluebird' 5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { AccountVideoRateModel } from '../../models/account/account-video-rate'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
9import { doRequest } from '../../helpers/requests' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 14
15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
16 await Bluebird.map(ratesUrl, async rateUrl => { 16 await Bluebird.map(ratesUrl, async rateUrl => {
17 try { 17 try {
18 // Fetch url 18 // Fetch url
19 const { body } = await doRequest<any>({ 19 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
20 uri: rateUrl,
21 json: true,
22 activityPub: true
23 })
24 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 20 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
25 21
26 const actorUrl = getAPId(body.actor) 22 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c02578aad..506204674 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,8 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename } from 'path'
5import * as request from 'request'
6import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -17,7 +16,7 @@ import {
17 ActivityUrlObject, 16 ActivityUrlObject,
18 ActivityVideoUrlObject 17 ActivityVideoUrlObject
19} from '../../../shared/index' 18} from '../../../shared/index'
20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
21import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc'
31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 30import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 31import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
33import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
34import { doRequest } from '../../helpers/requests' 33import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
38 LAZY_STATIC_PATHS,
39 MIMETYPES, 37 MIMETYPES,
40 P2P_MEDIA_LOADER_PEER_VERSION, 38 P2P_MEDIA_LOADER_PEER_VERSION,
41 PREVIEWS_SIZE, 39 PREVIEWS_SIZE,
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
115 } 113 }
116} 114}
117 115
118async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { 116async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
119 const options = {
120 uri: videoUrl,
121 method: 'GET',
122 json: true,
123 activityPub: true
124 }
125
126 logger.info('Fetching remote video %s.', videoUrl) 117 logger.info('Fetching remote video %s.', videoUrl)
127 118
128 const { response, body } = await doRequest<any>(options) 119 const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
129 120
130 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 121 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
131 logger.debug('Remote video JSON is not valid.', { body }) 122 logger.debug('Remote video JSON is not valid.', { body })
132 return { response, videoObject: undefined } 123 return { statusCode, videoObject: undefined }
133 } 124 }
134 125
135 return { response, videoObject: body } 126 return { statusCode, videoObject: body }
136} 127}
137 128
138async function fetchRemoteVideoDescription (video: MVideoAccountLight) { 129async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
139 const host = video.VideoChannel.Account.Actor.Server.host 130 const host = video.VideoChannel.Account.Actor.Server.host
140 const path = video.getDescriptionAPIPath() 131 const path = video.getDescriptionAPIPath()
141 const options = { 132 const url = REMOTE_SCHEME.HTTP + '://' + host + path
142 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
143 json: true
144 }
145 133
146 const { body } = await doRequest<any>(options) 134 const { body } = await doJSONRequest<any>(url)
147 return body.description ? body.description : '' 135 return body.description || ''
148} 136}
149 137
150function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { 138function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: {
378 366
379 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 367 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
380 368
381 if (videoUpdated.getPreview()) { 369 const previewIcon = getPreviewFromIcons(videoObject)
382 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) 370 if (videoUpdated.getPreview() && previewIcon) {
383 const previewModel = createPlaceholderThumbnail({ 371 const previewModel = createPlaceholderThumbnail({
384 fileUrl: previewUrl, 372 fileUrl: previewIcon.url,
385 video, 373 video,
386 type: ThumbnailType.PREVIEW, 374 type: ThumbnailType.PREVIEW,
387 size: PREVIEWS_SIZE 375 size: previewIcon
388 }) 376 })
389 await videoUpdated.addAndSaveThumbnail(previewModel, t) 377 await videoUpdated.addAndSaveThumbnail(previewModel, t)
390 } 378 }
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: {
534 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 522 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
535 523
536 try { 524 try {
537 const { response, videoObject } = await fetchRemoteVideo(video.url) 525 const { videoObject } = await fetchRemoteVideo(video.url)
538 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
539 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
540
541 // Video does not exist anymore
542 await video.destroy()
543 return undefined
544 }
545 526
546 if (videoObject === undefined) { 527 if (videoObject === undefined) {
547 logger.warn('Cannot refresh remote video %s: invalid body.', video.url) 528 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -558,13 +539,21 @@ async function refreshVideoIfNeeded (options: {
558 account: channelActor.VideoChannel.Account, 539 account: channelActor.VideoChannel.Account,
559 channel: channelActor.VideoChannel 540 channel: channelActor.VideoChannel
560 } 541 }
561 await retryTransactionWrapper(updateVideoFromAP, updateOptions) 542 await updateVideoFromAP(updateOptions)
562 await syncVideoExternalAttributes(video, videoObject, options.syncParam) 543 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
563 544
564 ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId) 545 ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId)
565 546
566 return video 547 return video
567 } catch (err) { 548 } catch (err) {
549 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
550 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
551
552 // Video does not exist anymore
553 await video.destroy()
554 return undefined
555 }
556
568 logger.warn('Cannot refresh video %s.', options.video.url, { err }) 557 logger.warn('Cannot refresh video %s.', options.video.url, { err })
569 558
570 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) 559 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
@@ -638,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
638 627
639 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 628 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
640 629
641 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) 630 const previewIcon = getPreviewFromIcons(videoObject)
642 const previewModel = createPlaceholderThumbnail({ 631 if (previewIcon) {
643 fileUrl: previewUrl, 632 const previewModel = createPlaceholderThumbnail({
644 video: videoCreated, 633 fileUrl: previewIcon.url,
645 type: ThumbnailType.PREVIEW, 634 video: videoCreated,
646 size: PREVIEWS_SIZE 635 type: ThumbnailType.PREVIEW,
647 }) 636 size: previewIcon
637 })
648 638
649 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 639 await videoCreated.addAndSaveThumbnail(previewModel, t)
640 }
650 641
651 // Process files 642 // Process files
652 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) 643 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
@@ -906,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) {
906 return maxBy(validIcons, 'width') 897 return maxBy(validIcons, 'width')
907} 898}
908 899
909function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
910 return previewIcon
911 ? previewIcon.url
912 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
913}
914
915function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { 900function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
916 let wsFound = false 901 let wsFound = false
917 902