diff options
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r-- | server/lib/activitypub/actor.ts | 212 | ||||
-rw-r--r-- | server/lib/activitypub/crawl.ts | 25 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 69 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-delete.ts | 13 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 13 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-create.ts | 10 | ||||
-rw-r--r-- | server/lib/activitypub/share.ts | 30 | ||||
-rw-r--r-- | server/lib/activitypub/video-comments.ts | 22 | ||||
-rw-r--r-- | server/lib/activitypub/video-rates.ts | 22 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 83 |
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { extname } from 'path' | ||
2 | import { Op, Transaction } from 'sequelize' | 3 | import { Op, Transaction } from 'sequelize' |
3 | import { URL } from 'url' | 4 | import { URL } from 'url' |
4 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid' |
6 | import { getServerActor } from '@server/models/application/application' | ||
7 | import { ActorImageType } from '@shared/models' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 9 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 10 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
12 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
8 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' | 13 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 14 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 15 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
11 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 17 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
13 | import { doRequest } from '../../helpers/requests' | 18 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 19 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
15 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 20 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
21 | import { sequelizeTypescript } from '../../initializers/database' | ||
16 | import { AccountModel } from '../../models/account/account' | 22 | import { AccountModel } from '../../models/account/account' |
23 | import { ActorImageModel } from '../../models/account/actor-image' | ||
17 | import { ActorModel } from '../../models/activitypub/actor' | 24 | import { ActorModel } from '../../models/activitypub/actor' |
18 | import { AvatarModel } from '../../models/avatar/avatar' | ||
19 | import { ServerModel } from '../../models/server/server' | 25 | import { ServerModel } from '../../models/server/server' |
20 | import { VideoChannelModel } from '../../models/video/video-channel' | 26 | import { VideoChannelModel } from '../../models/video/video-channel' |
21 | import { JobQueue } from '../job-queue' | ||
22 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
23 | import { sequelizeTypescript } from '../../initializers/database' | ||
24 | import { | 27 | import { |
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' |
37 | import { extname } from 'path' | 41 | import { JobQueue } from '../job-queue' |
38 | import { getServerActor } from '@server/models/application/application' | ||
39 | import { 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 |
42 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { | 44 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { |
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ | |||
168 | } | 170 | } |
169 | } | 171 | } |
170 | 172 | ||
171 | type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } | 173 | type ImageInfo = { |
172 | async 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 | } | ||
180 | async 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 | ||
198 | async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { | 214 | async 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 | ||
211 | async function fetchActorTotalItems (url: string) { | 234 | async 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 | ||
228 | function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | 245 | function 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 | ||
372 | function 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 | |||
357 | function saveActorAndServerAndModelIfNotExist ( | 388 | function 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 | ||
487 | type ImageResult = { | ||
488 | name: string | ||
489 | fileUrl: string | ||
490 | height: number | ||
491 | width: number | ||
492 | } | ||
493 | |||
439 | type FetchRemoteActorResult = { | 494 | type 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 | } |
451 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { | 504 | async 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 @@ | |||
1 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
2 | import { doRequest } from '../../helpers/requests' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
6 | import { URL } from 'url' | 2 | import { URL } from 'url' |
3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { doJSONRequest } from '../../helpers/requests' | ||
6 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
7 | 7 | ||
8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) | 8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) |
9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) | 9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) |
10 | 10 | ||
11 | async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { | 11 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | 5 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { checkUrlsSameHost } from '../../helpers/activitypub' |
7 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
4 | import { isArray } from '../../helpers/custom-validators/misc' | 8 | import { isArray } from '../../helpers/custom-validators/misc' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
11 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../../initializers/database' | ||
7 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 13 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
8 | import { doRequest } from '../../helpers/requests' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import * as Bluebird from 'bluebird' | ||
11 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
12 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
13 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
15 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
16 | import { sequelizeTypescript } from '../../initializers/database' | ||
17 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
18 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
19 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' | 15 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' |
20 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' | 16 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' |
21 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 17 | import { FilteredModelAttributes } from '../../types/sequelize' |
18 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
19 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
20 | import { crawlCollectionPage } from './crawl' | ||
21 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
22 | 22 | ||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 23 | function 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 | ||
201 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | 193 | async 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' | |||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' | 10 | import { |
11 | MAccountActor, | ||
12 | MActor, | ||
13 | MActorFull, | ||
14 | MActorSignature, | ||
15 | MChannelAccountActor, | ||
16 | MChannelActor, | ||
17 | MCommentOwnerVideo | ||
18 | } from '../../../types/models' | ||
11 | import { markCommentAsDeleted } from '../../video-comment' | 19 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 20 | import { 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' | |||
6 | import { AccountModel } from '../../../models/account/account' | 6 | import { AccountModel } from '../../../models/account/account' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
9 | import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' | 9 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' |
10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' | 10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' |
11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | 11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' |
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist' | |||
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' | 18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' |
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 19 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
20 | import { ActorImageType } from '@shared/models' | ||
20 | 21 | ||
21 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 22 | async 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' | |||
4 | import { VideoCommentModel } from '../../../models/video/video-comment' | 4 | import { VideoCommentModel } from '../../../models/video/video-comment' |
5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { | 9 | import { |
10 | MActorLight, | 10 | MActorLight, |
@@ -18,10 +18,12 @@ import { | |||
18 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
19 | import { ContextType } from '@shared/models/activitypub/context' | 19 | import { ContextType } from '@shared/models/activitypub/context' |
20 | 20 | ||
21 | const lTags = loggerTagsFactory('ap', 'create') | ||
22 | |||
21 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 23 | async 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 ( | |||
51 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { | 53 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
3 | import { getServerActor } from '@server/models/application/application' | ||
4 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
5 | import { logger, loggerTagsFactory } from '../../helpers/logger' | ||
6 | import { doJSONRequest } from '../../helpers/requests' | ||
7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
2 | import { VideoShareModel } from '../../models/video/video-share' | 8 | import { VideoShareModel } from '../../models/video/video-share' |
9 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
3 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 11 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
4 | import { getLocalVideoAnnounceActivityPubUrl } from './url' | 12 | import { getLocalVideoAnnounceActivityPubUrl } from './url' |
5 | import * as Bluebird from 'bluebird' | 13 | |
6 | import { doRequest } from '../../helpers/requests' | 14 | const lTags = loggerTagsFactory('share') |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 15 | ||
14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { | 16 | async 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 ( | |||
35 | async function addVideoShares (shareUrls: string[], video: MVideoId) { | 40 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
1 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' | 3 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' |
2 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
3 | import { doRequest } from '../../helpers/requests' | 5 | import { doJSONRequest } from '../../helpers/requests' |
4 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
5 | import { VideoCommentModel } from '../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../models/video/video-comment' |
8 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
6 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateActorAndServerAndModel } from './actor' |
7 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' |
8 | import * as Bluebird from 'bluebird' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
11 | 11 | ||
12 | type ResolveThreadParams = { | 12 | type ResolveThreadParams = { |
13 | url: string | 13 | url: string |
@@ -18,8 +18,12 @@ type ResolveThreadParams = { | |||
18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> | 18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> |
19 | 19 | ||
20 | async function addVideoComments (commentUrls: string[]) { | 20 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
2 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { VideoRateType } from '../../../shared/models/videos' | 4 | import { VideoRateType } from '../../../shared/models/videos' |
4 | import * as Bluebird from 'bluebird' | 5 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | ||
7 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
8 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
9 | import { doRequest } from '../../helpers/requests' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' | 9 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' |
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
11 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
14 | 14 | ||
15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { | 15 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { basename, join } from 'path' | 4 | import { basename } from 'path' |
5 | import * as request from 'request' | ||
6 | import { Transaction } from 'sequelize/types' | 5 | import { Transaction } from 'sequelize/types' |
7 | import { TrackerModel } from '@server/models/server/tracker' | 6 | import { TrackerModel } from '@server/models/server/tracker' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 7 | import { 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' |
20 | import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | 19 | import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' |
21 | import { VideoPrivacy } from '../../../shared/models/videos' | 20 | import { VideoPrivacy } from '../../../shared/models/videos' |
22 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
23 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 22 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc' | |||
31 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 30 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
32 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 31 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
33 | import { logger } from '../../helpers/logger' | 32 | import { logger } from '../../helpers/logger' |
34 | import { doRequest } from '../../helpers/requests' | 33 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
35 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' | 34 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' |
36 | import { | 35 | import { |
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 | ||
118 | async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { | 116 | async 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 | ||
138 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | 129 | async 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 | ||
150 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { | 138 | function 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 | ||
909 | function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) { | ||
910 | return previewIcon | ||
911 | ? previewIcon.url | ||
912 | : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | ||
913 | } | ||
914 | |||
915 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { | 900 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { |
916 | let wsFound = false | 901 | let wsFound = false |
917 | 902 | ||