diff options
Diffstat (limited to 'server/lib')
38 files changed, 988 insertions, 607 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..070ee0f1d 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -7,7 +7,7 @@ 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 { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models' |
11 | import { markCommentAsDeleted } from '../../video-comment' | 11 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 12 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | 13 | ||
@@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete | |||
30 | } else if (byActorFull.type === 'Group') { | 30 | } 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.') | 31 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') |
32 | 32 | ||
33 | const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor | 33 | const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull }) |
34 | channelToDelete.Actor = byActorFull | ||
35 | |||
36 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) | 34 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) |
37 | } | 35 | } |
38 | } | 36 | } |
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..9014791c0 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) |
@@ -565,6 +546,14 @@ async function refreshVideoIfNeeded (options: { | |||
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 | ||
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..f271f0b5b --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | ||
3 | import * as LRUCache from 'lru-cache' | ||
4 | import { extname, join } from 'path' | ||
5 | import { v4 as uuidv4 } from 'uuid' | ||
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { processImage } from '../helpers/image-utils' | ||
9 | import { downloadImage } from '../helpers/requests' | ||
10 | import { CONFIG } from '../initializers/config' | ||
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../initializers/database' | ||
13 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
14 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' | ||
15 | import { sendUpdateActor } from './activitypub/send' | ||
16 | |||
17 | async function updateLocalActorImageFile ( | ||
18 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
19 | imagePhysicalFile: Express.Multer.File, | ||
20 | type: ActorImageType | ||
21 | ) { | ||
22 | const imageSize = type === ActorImageType.AVATAR | ||
23 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
24 | : ACTOR_IMAGES_SIZE.BANNERS | ||
25 | |||
26 | const extension = extname(imagePhysicalFile.filename) | ||
27 | |||
28 | const imageName = uuidv4() + extension | ||
29 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | ||
30 | await processImage(imagePhysicalFile.path, destination, imageSize) | ||
31 | |||
32 | return retryTransactionWrapper(() => { | ||
33 | return sequelizeTypescript.transaction(async t => { | ||
34 | const actorImageInfo = { | ||
35 | name: imageName, | ||
36 | fileUrl: null, | ||
37 | height: imageSize.height, | ||
38 | width: imageSize.width, | ||
39 | onDisk: true | ||
40 | } | ||
41 | |||
42 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | ||
43 | await updatedActor.save({ transaction: t }) | ||
44 | |||
45 | await sendUpdateActor(accountOrChannel, t) | ||
46 | |||
47 | return type === ActorImageType.AVATAR | ||
48 | ? updatedActor.Avatar | ||
49 | : updatedActor.Banner | ||
50 | }) | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | ||
55 | return retryTransactionWrapper(() => { | ||
56 | return sequelizeTypescript.transaction(async t => { | ||
57 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | ||
58 | await updatedActor.save({ transaction: t }) | ||
59 | |||
60 | await sendUpdateActor(accountOrChannel, t) | ||
61 | |||
62 | return updatedActor.Avatar | ||
63 | }) | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | ||
68 | |||
69 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
70 | const size = task.type === ActorImageType.AVATAR | ||
71 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
72 | : ACTOR_IMAGES_SIZE.BANNERS | ||
73 | |||
74 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
75 | .then(() => cb()) | ||
76 | .catch(err => cb(err)) | ||
77 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | ||
78 | |||
79 | function pushActorImageProcessInQueue (task: DownloadImageQueueTask) { | ||
80 | return new Promise<void>((res, rej) => { | ||
81 | downloadImageQueue.push(task, err => { | ||
82 | if (err) return rej(err) | ||
83 | |||
84 | return res() | ||
85 | }) | ||
86 | }) | ||
87 | } | ||
88 | |||
89 | // Unsafe so could returns paths that does not exist anymore | ||
90 | const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) | ||
91 | |||
92 | export { | ||
93 | actorImagePathUnsafeCache, | ||
94 | updateLocalActorImageFile, | ||
95 | deleteLocalActorImageFile, | ||
96 | pushActorImageProcessInQueue | ||
97 | } | ||
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts index dbd421a7b..80f5064b6 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,28 +1,16 @@ | |||
1 | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
3 | import { generateRandomString } from '@server/helpers/utils' | 4 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
5 | import { revokeToken } from '@server/lib/oauth-model' | ||
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
8 | import { UserRole } from '@shared/models' | ||
9 | import { | 8 | import { |
10 | RegisterServerAuthenticatedResult, | 9 | RegisterServerAuthenticatedResult, |
11 | RegisterServerAuthPassOptions, | 10 | RegisterServerAuthPassOptions, |
12 | RegisterServerExternalAuthenticatedResult | 11 | RegisterServerExternalAuthenticatedResult |
13 | } from '@server/types/plugins/register-server-auth.model' | 12 | } from '@server/types/plugins/register-server-auth.model' |
14 | import * as express from 'express' | 13 | import { UserRole } from '@shared/models' |
15 | import * as OAuthServer from 'express-oauth-server' | ||
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | |||
18 | const oAuthServer = new OAuthServer({ | ||
19 | useErrorHandler: true, | ||
20 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
21 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
22 | allowExtendedTokenAttributes: true, | ||
23 | continueMiddleware: true, | ||
24 | model: require('./oauth-model') | ||
25 | }) | ||
26 | 14 | ||
27 | // Token is the key, expiration date is the value | 15 | // Token is the key, expiration date is the value |
28 | const authBypassTokens = new Map<string, { | 16 | const authBypassTokens = new Map<string, { |
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, { | |||
37 | npmName: string | 25 | npmName: string |
38 | }>() | 26 | }>() |
39 | 27 | ||
40 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
41 | const grantType = req.body.grant_type | ||
42 | |||
43 | if (grantType === 'password') { | ||
44 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) | ||
45 | else await proxifyPasswordGrant(req, res) | ||
46 | } else if (grantType === 'refresh_token') { | ||
47 | await proxifyRefreshGrant(req, res) | ||
48 | } | ||
49 | |||
50 | return forwardTokenReq(req, res, next) | ||
51 | } | ||
52 | |||
53 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
54 | const token = res.locals.oauth.token | ||
55 | |||
56 | res.locals.explicitLogout = true | ||
57 | const result = await revokeToken(token) | ||
58 | |||
59 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | ||
60 | // oAuthServer.revoke(req, res, err => { | ||
61 | // if (err) { | ||
62 | // logger.warn('Error in revoke token handler.', { err }) | ||
63 | // | ||
64 | // return res.status(err.status) | ||
65 | // .json({ | ||
66 | // error: err.message, | ||
67 | // code: err.name | ||
68 | // }) | ||
69 | // .end() | ||
70 | // } | ||
71 | // }) | ||
72 | |||
73 | return res.json(result) | ||
74 | } | ||
75 | |||
76 | async function onExternalUserAuthenticated (options: { | 28 | async function onExternalUserAuthenticated (options: { |
77 | npmName: string | 29 | npmName: string |
78 | authName: string | 30 | authName: string |
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: { | |||
107 | authName | 59 | authName |
108 | }) | 60 | }) |
109 | 61 | ||
110 | // Cleanup | 62 | // Cleanup expired tokens |
111 | const now = new Date() | 63 | const now = new Date() |
112 | for (const [ key, value ] of authBypassTokens) { | 64 | for (const [ key, value ] of authBypassTokens) { |
113 | if (value.expires.getTime() < now.getTime()) { | 65 | if (value.expires.getTime() < now.getTime()) { |
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: { | |||
118 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | 70 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) |
119 | } | 71 | } |
120 | 72 | ||
121 | // --------------------------------------------------------------------------- | 73 | async function getAuthNameFromRefreshGrant (refreshToken?: string) { |
122 | 74 | if (!refreshToken) return undefined | |
123 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } | ||
124 | |||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
127 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
128 | return oAuthServer.token()(req, res, err => { | ||
129 | if (err) { | ||
130 | logger.warn('Login error.', { err }) | ||
131 | |||
132 | return res.status(err.status) | ||
133 | .json({ | ||
134 | error: err.message, | ||
135 | code: err.name | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | if (next) return next() | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | ||
144 | const refreshToken = req.body.refresh_token | ||
145 | if (!refreshToken) return | ||
146 | 75 | ||
147 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | 76 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) |
148 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | 77 | |
78 | return tokenModel?.authName | ||
149 | } | 79 | } |
150 | 80 | ||
151 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | 81 | async function getBypassFromPasswordGrant (username: string, password: string) { |
152 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 82 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
153 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
154 | 84 | ||
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
174 | }) | 104 | }) |
175 | 105 | ||
176 | const loginOptions = { | 106 | const loginOptions = { |
177 | id: req.body.username, | 107 | id: username, |
178 | password: req.body.password | 108 | password |
179 | } | 109 | } |
180 | 110 | ||
181 | for (const pluginAuth of pluginAuths) { | 111 | for (const pluginAuth of pluginAuths) { |
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
199 | authName, npmName, loginOptions.id | 129 | authName, npmName, loginOptions.id |
200 | ) | 130 | ) |
201 | 131 | ||
202 | res.locals.bypassLogin = { | 132 | return { |
203 | bypass: true, | 133 | bypass: true, |
204 | pluginName: pluginAuth.npmName, | 134 | pluginName: pluginAuth.npmName, |
205 | authName: authOptions.authName, | 135 | authName: authOptions.authName, |
206 | user: buildUserResult(loginResult) | 136 | user: buildUserResult(loginResult) |
207 | } | 137 | } |
208 | |||
209 | return | ||
210 | } catch (err) { | 138 | } catch (err) { |
211 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
212 | } | 140 | } |
213 | } | 141 | } |
142 | |||
143 | return undefined | ||
214 | } | 144 | } |
215 | 145 | ||
216 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | 146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { |
217 | const obj = authBypassTokens.get(req.body.externalAuthToken) | 147 | const obj = authBypassTokens.get(externalAuthToken) |
218 | if (!obj) { | 148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
219 | logger.error('Cannot authenticate user with unknown bypass token') | ||
220 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
221 | } | ||
222 | 149 | ||
223 | const { expires, user, authName, npmName } = obj | 150 | const { expires, user, authName, npmName } = obj |
224 | 151 | ||
225 | const now = new Date() | 152 | const now = new Date() |
226 | if (now.getTime() > expires.getTime()) { | 153 | if (now.getTime() > expires.getTime()) { |
227 | logger.error('Cannot authenticate user with an expired external auth token') | 154 | throw new Error('Cannot authenticate user with an expired external auth token') |
228 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
229 | } | 155 | } |
230 | 156 | ||
231 | if (user.username !== req.body.username) { | 157 | if (user.username !== username) { |
232 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | 158 | throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`) |
233 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
234 | } | 159 | } |
235 | 160 | ||
236 | // Bypass oauth library validation | ||
237 | req.body.password = 'fake' | ||
238 | |||
239 | logger.info( | 161 | logger.info( |
240 | 'Auth success with external auth method %s of plugin %s for %s.', | 162 | 'Auth success with external auth method %s of plugin %s for %s.', |
241 | authName, npmName, user.email | 163 | authName, npmName, user.email |
242 | ) | 164 | ) |
243 | 165 | ||
244 | res.locals.bypassLogin = { | 166 | return { |
245 | bypass: true, | 167 | bypass: true, |
246 | pluginName: npmName, | 168 | pluginName: npmName, |
247 | authName: authName, | 169 | authName: authName, |
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
286 | displayName: pluginResult.displayName || pluginResult.username | 208 | displayName: pluginResult.displayName || pluginResult.username |
287 | } | 209 | } |
288 | } | 210 | } |
211 | |||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
214 | export { | ||
215 | onExternalUserAuthenticated, | ||
216 | getBypassFromExternalAuth, | ||
217 | getAuthNameFromRefreshGrant, | ||
218 | getBypassFromPasswordGrant | ||
219 | } | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts index a2c53a2c9..b9c69eb2d 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,49 +1,36 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as LRUCache from 'lru-cache' | ||
3 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
4 | import { Transaction } from 'sequelize' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
6 | import { ActorModel } from '@server/models/activitypub/actor' | 4 | import { ActorModel } from '@server/models/activitypub/actor' |
5 | import { MOAuthClient } from '@server/types/models' | ||
7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
8 | import { MUser } from '@server/types/models/user/user' | 7 | import { MUser } from '@server/types/models/user/user' |
9 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 8 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
10 | import { UserRole } from '@shared/models/users/user-role' | 9 | import { UserRole } from '@shared/models/users/user-role' |
11 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../../initializers/config' |
13 | import { LRU_CACHE } from '../initializers/constants' | 12 | import { UserModel } from '../../models/account/user' |
14 | import { UserModel } from '../models/account/user' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
15 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
16 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 15 | import { createUserAccountAndChannelAndPlaylist } from '../user' |
17 | import { createUserAccountAndChannelAndPlaylist } from './user' | 16 | import { TokensCache } from './tokens-cache' |
18 | 17 | ||
19 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 18 | type TokenInfo = { |
20 | 19 | accessToken: string | |
21 | const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 20 | refreshToken: string |
22 | const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 21 | accessTokenExpiresAt: Date |
23 | 22 | refreshTokenExpiresAt: Date | |
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function deleteUserToken (userId: number, t?: Transaction) { | ||
27 | clearCacheByUserId(userId) | ||
28 | |||
29 | return OAuthTokenModel.deleteUserToken(userId, t) | ||
30 | } | 23 | } |
31 | 24 | ||
32 | function clearCacheByUserId (userId: number) { | 25 | export type BypassLogin = { |
33 | const token = userHavingToken.get(userId) | 26 | bypass: boolean |
34 | 27 | pluginName: string | |
35 | if (token !== undefined) { | 28 | authName?: string |
36 | accessTokenCache.del(token) | 29 | user: { |
37 | userHavingToken.del(userId) | 30 | username: string |
38 | } | 31 | email: string |
39 | } | 32 | displayName: string |
40 | 33 | role: UserRole | |
41 | function clearCacheByToken (token: string) { | ||
42 | const tokenModel = accessTokenCache.get(token) | ||
43 | |||
44 | if (tokenModel !== undefined) { | ||
45 | userHavingToken.del(tokenModel.userId) | ||
46 | accessTokenCache.del(token) | ||
47 | } | 34 | } |
48 | } | 35 | } |
49 | 36 | ||
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) { | |||
54 | 41 | ||
55 | let tokenModel: MOAuthTokenUser | 42 | let tokenModel: MOAuthTokenUser |
56 | 43 | ||
57 | if (accessTokenCache.has(bearerToken)) { | 44 | if (TokensCache.Instance.hasToken(bearerToken)) { |
58 | tokenModel = accessTokenCache.get(bearerToken) | 45 | tokenModel = TokensCache.Instance.getByToken(bearerToken) |
59 | } else { | 46 | } else { |
60 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 47 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
61 | 48 | ||
62 | if (tokenModel) { | 49 | if (tokenModel) TokensCache.Instance.setToken(tokenModel) |
63 | accessTokenCache.set(bearerToken, tokenModel) | ||
64 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
65 | } | ||
66 | } | 50 | } |
67 | 51 | ||
68 | if (!tokenModel) return undefined | 52 | if (!tokenModel) return undefined |
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) { | |||
99 | return tokenInfo | 83 | return tokenInfo |
100 | } | 84 | } |
101 | 85 | ||
102 | async function getUser (usernameOrEmail?: string, password?: string) { | 86 | async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) { |
103 | const res: express.Response = this.request.res | ||
104 | |||
105 | // Special treatment coming from a plugin | 87 | // Special treatment coming from a plugin |
106 | if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { | 88 | if (bypassLogin && bypassLogin.bypass === true) { |
107 | const obj = res.locals.bypassLogin | 89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
108 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | ||
109 | 90 | ||
110 | let user = await UserModel.loadByEmail(obj.user.email) | 91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
111 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
112 | 93 | ||
113 | // Cannot create a user | 94 | // Cannot create a user |
114 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 95 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
117 | // Then we just go through a regular login process | 98 | // Then we just go through a regular login process |
118 | if (user.pluginAuth !== null) { | 99 | if (user.pluginAuth !== null) { |
119 | // This user does not belong to this plugin, skip it | 100 | // This user does not belong to this plugin, skip it |
120 | if (user.pluginAuth !== obj.pluginName) return null | 101 | if (user.pluginAuth !== bypassLogin.pluginName) return null |
121 | 102 | ||
122 | checkUserValidityOrThrow(user) | 103 | checkUserValidityOrThrow(user) |
123 | 104 | ||
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
143 | return user | 124 | return user |
144 | } | 125 | } |
145 | 126 | ||
146 | async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { | 127 | async function revokeToken ( |
147 | const res: express.Response = this.request.res | 128 | tokenInfo: { refreshToken: string }, |
129 | options: { | ||
130 | req?: express.Request | ||
131 | explicitLogout?: boolean | ||
132 | } = {} | ||
133 | ): Promise<{ success: boolean, redirectUrl?: string }> { | ||
134 | const { req, explicitLogout } = options | ||
135 | |||
148 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 136 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
149 | 137 | ||
150 | if (token) { | 138 | if (token) { |
151 | let redirectUrl: string | 139 | let redirectUrl: string |
152 | 140 | ||
153 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | 141 | if (explicitLogout === true && token.User.pluginAuth && token.authName) { |
154 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) | 142 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req) |
155 | } | 143 | } |
156 | 144 | ||
157 | clearCacheByToken(token.accessToken) | 145 | TokensCache.Instance.clearCacheByToken(token.accessToken) |
158 | 146 | ||
159 | token.destroy() | 147 | token.destroy() |
160 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | 148 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) |
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ | |||
165 | return { success: false } | 153 | return { success: false } |
166 | } | 154 | } |
167 | 155 | ||
168 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 156 | async function saveToken ( |
169 | const res: express.Response = this.request.res | 157 | token: TokenInfo, |
170 | 158 | client: MOAuthClient, | |
159 | user: MUser, | ||
160 | options: { | ||
161 | refreshTokenAuthName?: string | ||
162 | bypassLogin?: BypassLogin | ||
163 | } = {} | ||
164 | ) { | ||
165 | const { refreshTokenAuthName, bypassLogin } = options | ||
171 | let authName: string = null | 166 | let authName: string = null |
172 | if (res.locals.bypassLogin?.bypass === true) { | 167 | |
173 | authName = res.locals.bypassLogin.authName | 168 | if (bypassLogin?.bypass === true) { |
174 | } else if (res.locals.refreshTokenAuthName) { | 169 | authName = bypassLogin.authName |
175 | authName = res.locals.refreshTokenAuthName | 170 | } else if (refreshTokenAuthName) { |
171 | authName = refreshTokenAuthName | ||
176 | } | 172 | } |
177 | 173 | ||
178 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 174 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
199 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, | 195 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, |
200 | client, | 196 | client, |
201 | user, | 197 | user, |
202 | refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) | 198 | accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt), |
199 | refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt) | ||
203 | } | 200 | } |
204 | } | 201 | } |
205 | 202 | ||
206 | // --------------------------------------------------------------------------- | ||
207 | |||
208 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
209 | export { | 203 | export { |
210 | deleteUserToken, | ||
211 | clearCacheByUserId, | ||
212 | clearCacheByToken, | ||
213 | getAccessToken, | 204 | getAccessToken, |
214 | getClient, | 205 | getClient, |
215 | getRefreshToken, | 206 | getRefreshToken, |
@@ -218,6 +209,8 @@ export { | |||
218 | saveToken | 209 | saveToken |
219 | } | 210 | } |
220 | 211 | ||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
221 | async function createUserFromExternal (pluginAuth: string, options: { | 214 | async function createUserFromExternal (pluginAuth: string, options: { |
222 | username: string | 215 | username: string |
223 | email: string | 216 | email: string |
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
252 | function checkUserValidityOrThrow (user: MUser) { | 245 | function checkUserValidityOrThrow (user: MUser) { |
253 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 246 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
254 | } | 247 | } |
248 | |||
249 | function buildExpiresIn (expiresAt: Date) { | ||
250 | return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) | ||
251 | } | ||
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts new file mode 100644 index 000000000..5b6130d56 --- /dev/null +++ b/server/lib/auth/oauth.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | import * as express from 'express' | ||
2 | import { | ||
3 | InvalidClientError, | ||
4 | InvalidGrantError, | ||
5 | InvalidRequestError, | ||
6 | Request, | ||
7 | Response, | ||
8 | UnauthorizedClientError, | ||
9 | UnsupportedGrantTypeError | ||
10 | } from 'oauth2-server' | ||
11 | import { randomBytesPromise, sha1 } from '@server/helpers/core-utils' | ||
12 | import { MOAuthClient } from '@server/types/models' | ||
13 | import { OAUTH_LIFETIME } from '../../initializers/constants' | ||
14 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | ||
15 | |||
16 | /** | ||
17 | * | ||
18 | * Reimplement some functions of OAuth2Server to inject external auth methods | ||
19 | * | ||
20 | */ | ||
21 | |||
22 | const oAuthServer = new (require('oauth2-server'))({ | ||
23 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
24 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
25 | |||
26 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
27 | model: require('./oauth-model') | ||
28 | }) | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) { | ||
33 | const request = new Request(req) | ||
34 | const { refreshTokenAuthName, bypassLogin } = options | ||
35 | |||
36 | if (request.method !== 'POST') { | ||
37 | throw new InvalidRequestError('Invalid request: method must be POST') | ||
38 | } | ||
39 | |||
40 | if (!request.is([ 'application/x-www-form-urlencoded' ])) { | ||
41 | throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded') | ||
42 | } | ||
43 | |||
44 | const clientId = request.body.client_id | ||
45 | const clientSecret = request.body.client_secret | ||
46 | |||
47 | if (!clientId || !clientSecret) { | ||
48 | throw new InvalidClientError('Invalid client: cannot retrieve client credentials') | ||
49 | } | ||
50 | |||
51 | const client = await getClient(clientId, clientSecret) | ||
52 | if (!client) { | ||
53 | throw new InvalidClientError('Invalid client: client is invalid') | ||
54 | } | ||
55 | |||
56 | const grantType = request.body.grant_type | ||
57 | if (!grantType) { | ||
58 | throw new InvalidRequestError('Missing parameter: `grant_type`') | ||
59 | } | ||
60 | |||
61 | if (![ 'password', 'refresh_token' ].includes(grantType)) { | ||
62 | throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid') | ||
63 | } | ||
64 | |||
65 | if (!client.grants.includes(grantType)) { | ||
66 | throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid') | ||
67 | } | ||
68 | |||
69 | if (grantType === 'password') { | ||
70 | return handlePasswordGrant({ | ||
71 | request, | ||
72 | client, | ||
73 | bypassLogin | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | return handleRefreshGrant({ | ||
78 | request, | ||
79 | client, | ||
80 | refreshTokenAuthName | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function handleOAuthAuthenticate ( | ||
85 | req: express.Request, | ||
86 | res: express.Response, | ||
87 | authenticateInQuery = false | ||
88 | ) { | ||
89 | const options = authenticateInQuery | ||
90 | ? { allowBearerTokensInQueryString: true } | ||
91 | : {} | ||
92 | |||
93 | return oAuthServer.authenticate(new Request(req), new Response(res), options) | ||
94 | } | ||
95 | |||
96 | export { | ||
97 | handleOAuthToken, | ||
98 | handleOAuthAuthenticate | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | async function handlePasswordGrant (options: { | ||
104 | request: Request | ||
105 | client: MOAuthClient | ||
106 | bypassLogin?: BypassLogin | ||
107 | }) { | ||
108 | const { request, client, bypassLogin } = options | ||
109 | |||
110 | if (!request.body.username) { | ||
111 | throw new InvalidRequestError('Missing parameter: `username`') | ||
112 | } | ||
113 | |||
114 | if (!bypassLogin && !request.body.password) { | ||
115 | throw new InvalidRequestError('Missing parameter: `password`') | ||
116 | } | ||
117 | |||
118 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | ||
119 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
120 | |||
121 | const token = await buildToken() | ||
122 | |||
123 | return saveToken(token, client, user, { bypassLogin }) | ||
124 | } | ||
125 | |||
126 | async function handleRefreshGrant (options: { | ||
127 | request: Request | ||
128 | client: MOAuthClient | ||
129 | refreshTokenAuthName: string | ||
130 | }) { | ||
131 | const { request, client, refreshTokenAuthName } = options | ||
132 | |||
133 | if (!request.body.refresh_token) { | ||
134 | throw new InvalidRequestError('Missing parameter: `refresh_token`') | ||
135 | } | ||
136 | |||
137 | const refreshToken = await getRefreshToken(request.body.refresh_token) | ||
138 | |||
139 | if (!refreshToken) { | ||
140 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
141 | } | ||
142 | |||
143 | if (refreshToken.client.id !== client.id) { | ||
144 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
145 | } | ||
146 | |||
147 | if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) { | ||
148 | throw new InvalidGrantError('Invalid grant: refresh token has expired') | ||
149 | } | ||
150 | |||
151 | await revokeToken({ refreshToken: refreshToken.refreshToken }) | ||
152 | |||
153 | const token = await buildToken() | ||
154 | |||
155 | return saveToken(token, client, refreshToken.user, { refreshTokenAuthName }) | ||
156 | } | ||
157 | |||
158 | function generateRandomToken () { | ||
159 | return randomBytesPromise(256) | ||
160 | .then(buffer => sha1(buffer)) | ||
161 | } | ||
162 | |||
163 | function getTokenExpiresAt (type: 'access' | 'refresh') { | ||
164 | const lifetime = type === 'access' | ||
165 | ? OAUTH_LIFETIME.ACCESS_TOKEN | ||
166 | : OAUTH_LIFETIME.REFRESH_TOKEN | ||
167 | |||
168 | return new Date(Date.now() + lifetime * 1000) | ||
169 | } | ||
170 | |||
171 | async function buildToken () { | ||
172 | const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ]) | ||
173 | |||
174 | return { | ||
175 | accessToken, | ||
176 | refreshToken, | ||
177 | accessTokenExpiresAt: getTokenExpiresAt('access'), | ||
178 | refreshTokenExpiresAt: getTokenExpiresAt('refresh') | ||
179 | } | ||
180 | } | ||
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts new file mode 100644 index 000000000..b027ce69a --- /dev/null +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as LRUCache from 'lru-cache' | ||
2 | import { MOAuthTokenUser } from '@server/types/models' | ||
3 | import { LRU_CACHE } from '../../initializers/constants' | ||
4 | |||
5 | export class TokensCache { | ||
6 | |||
7 | private static instance: TokensCache | ||
8 | |||
9 | private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
10 | private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
11 | |||
12 | private constructor () { } | ||
13 | |||
14 | static get Instance () { | ||
15 | return this.instance || (this.instance = new this()) | ||
16 | } | ||
17 | |||
18 | hasToken (token: string) { | ||
19 | return this.accessTokenCache.has(token) | ||
20 | } | ||
21 | |||
22 | getByToken (token: string) { | ||
23 | return this.accessTokenCache.get(token) | ||
24 | } | ||
25 | |||
26 | setToken (token: MOAuthTokenUser) { | ||
27 | this.accessTokenCache.set(token.accessToken, token) | ||
28 | this.userHavingToken.set(token.userId, token.accessToken) | ||
29 | } | ||
30 | |||
31 | deleteUserToken (userId: number) { | ||
32 | this.clearCacheByUserId(userId) | ||
33 | } | ||
34 | |||
35 | clearCacheByUserId (userId: number) { | ||
36 | const token = this.userHavingToken.get(userId) | ||
37 | |||
38 | if (token !== undefined) { | ||
39 | this.accessTokenCache.del(token) | ||
40 | this.userHavingToken.del(userId) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | clearCacheByToken (token: string) { | ||
45 | const tokenModel = this.accessTokenCache.get(token) | ||
46 | |||
47 | if (tokenModel !== undefined) { | ||
48 | this.userHavingToken.del(tokenModel.userId) | ||
49 | this.accessTokenCache.del(token) | ||
50 | } | ||
51 | } | ||
52 | } | ||
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts deleted file mode 100644 index 86f1e7bdb..000000000 --- a/server/lib/avatar.ts +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | import 'multer' | ||
2 | import { sendUpdateActor } from './activitypub/send' | ||
3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
4 | import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor' | ||
5 | import { processImage } from '../helpers/image-utils' | ||
6 | import { extname, join } from 'path' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | import { CONFIG } from '../initializers/config' | ||
10 | import { sequelizeTypescript } from '../initializers/database' | ||
11 | import * as LRUCache from 'lru-cache' | ||
12 | import { queue } from 'async' | ||
13 | import { downloadImage } from '../helpers/requests' | ||
14 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
15 | |||
16 | async function updateLocalActorAvatarFile ( | ||
17 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
18 | avatarPhysicalFile: Express.Multer.File | ||
19 | ) { | ||
20 | const extension = extname(avatarPhysicalFile.filename) | ||
21 | |||
22 | const avatarName = uuidv4() + extension | ||
23 | const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | ||
24 | await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE) | ||
25 | |||
26 | return retryTransactionWrapper(() => { | ||
27 | return sequelizeTypescript.transaction(async t => { | ||
28 | const avatarInfo = { | ||
29 | name: avatarName, | ||
30 | fileUrl: null, | ||
31 | onDisk: true | ||
32 | } | ||
33 | |||
34 | const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t) | ||
35 | await updatedActor.save({ transaction: t }) | ||
36 | |||
37 | await sendUpdateActor(accountOrChannel, t) | ||
38 | |||
39 | return updatedActor.Avatar | ||
40 | }) | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | async function deleteLocalActorAvatarFile ( | ||
45 | accountOrChannel: MAccountDefault | MChannelDefault | ||
46 | ) { | ||
47 | return retryTransactionWrapper(() => { | ||
48 | return sequelizeTypescript.transaction(async t => { | ||
49 | const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t) | ||
50 | await updatedActor.save({ transaction: t }) | ||
51 | |||
52 | await sendUpdateActor(accountOrChannel, t) | ||
53 | |||
54 | return updatedActor.Avatar | ||
55 | }) | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | type DownloadImageQueueTask = { fileUrl: string, filename: string } | ||
60 | |||
61 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
62 | downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE) | ||
63 | .then(() => cb()) | ||
64 | .catch(err => cb(err)) | ||
65 | }, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE) | ||
66 | |||
67 | function pushAvatarProcessInQueue (task: DownloadImageQueueTask) { | ||
68 | return new Promise<void>((res, rej) => { | ||
69 | downloadImageQueue.push(task, err => { | ||
70 | if (err) return rej(err) | ||
71 | |||
72 | return res() | ||
73 | }) | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | // Unsafe so could returns paths that does not exist anymore | ||
78 | const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE }) | ||
79 | |||
80 | export { | ||
81 | avatarPathUnsafeCache, | ||
82 | updateLocalActorAvatarFile, | ||
83 | deleteLocalActorAvatarFile, | ||
84 | pushAvatarProcessInQueue | ||
85 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index f19ec7df0..6ddaa82c8 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -5,12 +5,13 @@ import validator from 'validator' | |||
5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
8 | import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' | 8 | import { isTestInstance, sha256 } from '../helpers/core-utils' |
9 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
9 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../helpers/logger' |
10 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
11 | import { | 12 | import { |
12 | ACCEPT_HEADERS, | 13 | ACCEPT_HEADERS, |
13 | AVATARS_SIZE, | 14 | ACTOR_IMAGES_SIZE, |
14 | CUSTOM_HTML_TAG_COMMENTS, | 15 | CUSTOM_HTML_TAG_COMMENTS, |
15 | EMBED_SIZE, | 16 | EMBED_SIZE, |
16 | FILES_CONTENT_HASH, | 17 | FILES_CONTENT_HASH, |
@@ -245,8 +246,8 @@ class ClientHtml { | |||
245 | 246 | ||
246 | const image = { | 247 | const image = { |
247 | url: entity.Actor.getAvatarUrl(), | 248 | url: entity.Actor.getAvatarUrl(), |
248 | width: AVATARS_SIZE.width, | 249 | width: ACTOR_IMAGES_SIZE.AVATARS.width, |
249 | height: AVATARS_SIZE.height | 250 | height: ACTOR_IMAGES_SIZE.AVATARS.height |
250 | } | 251 | } |
251 | 252 | ||
252 | const ogType = 'website' | 253 | const ogType = 'website' |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 969eae77b..9ca0d5d5b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m | |||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | 7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' |
8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | 9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' |
10 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | 10 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
11 | import { isTestInstance, root } from '../helpers/core-utils' | 11 | import { isTestInstance, root } from '../helpers/core-utils' |
12 | import { bunyanLogger, logger } from '../helpers/logger' | 12 | import { bunyanLogger, logger } from '../helpers/logger' |
13 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 13 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
14 | import { WEBSERVER } from '../initializers/constants' | 14 | import { WEBSERVER } from '../initializers/constants' |
15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' |
16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
17 | import { JobQueue } from './job-queue' | 17 | import { JobQueue } from './job-queue' |
18 | 18 | ||
@@ -403,9 +403,9 @@ class Emailer { | |||
403 | } | 403 | } |
404 | 404 | ||
405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | 405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
406 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 406 | const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | 407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
408 | const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | 408 | const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() |
409 | 409 | ||
410 | const emailPayload: EmailPayload = { | 410 | const emailPayload: EmailPayload = { |
411 | template: 'video-auto-blacklist-new', | 411 | template: 'video-auto-blacklist-new', |
@@ -417,7 +417,7 @@ class Emailer { | |||
417 | videoName: videoBlacklist.Video.name, | 417 | videoName: videoBlacklist.Video.name, |
418 | action: { | 418 | action: { |
419 | text: 'Review autoblacklist', | 419 | text: 'Review autoblacklist', |
420 | url: VIDEO_AUTO_BLACKLIST_URL | 420 | url: videoAutoBlacklistUrl |
421 | } | 421 | } |
422 | } | 422 | } |
423 | } | 423 | } |
@@ -472,6 +472,36 @@ class Emailer { | |||
472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
473 | } | 473 | } |
474 | 474 | ||
475 | addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { | ||
476 | const emailPayload: EmailPayload = { | ||
477 | to, | ||
478 | template: 'peertube-version-new', | ||
479 | subject: `A new PeerTube version is available: ${latestVersion}`, | ||
480 | locals: { | ||
481 | latestVersion | ||
482 | } | ||
483 | } | ||
484 | |||
485 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
486 | } | ||
487 | |||
488 | addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { | ||
489 | const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type | ||
490 | |||
491 | const emailPayload: EmailPayload = { | ||
492 | to, | ||
493 | template: 'plugin-version-new', | ||
494 | subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, | ||
495 | locals: { | ||
496 | pluginName: plugin.name, | ||
497 | latestVersion: plugin.latestVersion, | ||
498 | pluginUrl | ||
499 | } | ||
500 | } | ||
501 | |||
502 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
503 | } | ||
504 | |||
475 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { | 505 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { |
476 | const emailPayload: EmailPayload = { | 506 | const emailPayload: EmailPayload = { |
477 | template: 'password-reset', | 507 | template: 'password-reset', |
@@ -569,26 +599,27 @@ class Emailer { | |||
569 | }) | 599 | }) |
570 | 600 | ||
571 | for (const to of options.to) { | 601 | for (const to of options.to) { |
572 | await email | 602 | const baseOptions: SendEmailDefaultOptions = { |
573 | .send(merge( | 603 | template: 'common', |
574 | { | 604 | message: { |
575 | template: 'common', | 605 | to, |
576 | message: { | 606 | from: options.from, |
577 | to, | 607 | subject: options.subject, |
578 | from: options.from, | 608 | replyTo: options.replyTo |
579 | subject: options.subject, | 609 | }, |
580 | replyTo: options.replyTo | 610 | locals: { // default variables available in all templates |
581 | }, | 611 | WEBSERVER, |
582 | locals: { // default variables available in all templates | 612 | EMAIL: CONFIG.EMAIL, |
583 | WEBSERVER, | 613 | instanceName: CONFIG.INSTANCE.NAME, |
584 | EMAIL: CONFIG.EMAIL, | 614 | text: options.text, |
585 | instanceName: CONFIG.INSTANCE.NAME, | 615 | subject: options.subject |
586 | text: options.text, | 616 | } |
587 | subject: options.subject | 617 | } |
588 | } | 618 | |
589 | }, | 619 | // overriden/new variables given for a specific template in the payload |
590 | options // overriden/new variables given for a specific template in the payload | 620 | const sendOptions = merge(baseOptions, options) |
591 | ) as SendEmailOptions) | 621 | |
622 | await email.send(sendOptions) | ||
592 | .then(res => logger.debug('Sent email.', { res })) | 623 | .then(res => logger.debug('Sent email.', { res })) |
593 | .catch(err => logger.error('Error in email sender.', { err })) | 624 | .catch(err => logger.error('Error in email sender.', { err })) |
594 | } | 625 | } |
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug new file mode 100644 index 000000000..2f4d9399d --- /dev/null +++ b/server/lib/emails/peertube-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New PeerTube version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of PeerTube is available: #{latestVersion}. | ||
9 | | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube]. | ||
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug new file mode 100644 index 000000000..86d3d87e8 --- /dev/null +++ b/server/lib/emails/plugin-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New plugin version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}. | ||
9 | | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface]. | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts index ee0447010..58e2260b6 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = videoCaption.getFileUrl(video) | 41 | const remoteUrl = videoCaption.getFileUrl(video) |
42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) | 42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) |
43 | 43 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 44 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 45 | ||
46 | return { isOwned: false, path: destPath } | 46 | return { isOwned: false, path: destPath } |
47 | } | 47 | } |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index ee72cd3f9..dd3a84aca 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) | 39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) |
40 | 40 | ||
41 | const remoteUrl = preview.getFileUrl(video) | 41 | const remoteUrl = preview.getFileUrl(video) |
42 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 42 | await doRequestAndSaveToFile(remoteUrl, destPath) |
43 | 43 | ||
44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) | 44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) |
45 | 45 | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts index ca0e1770d..23217f140 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/videos-torrent-cache.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
8 | import { MVideo, MVideoFile } from '@server/types/models' | ||
8 | 9 | ||
9 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { |
10 | 11 | ||
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
22 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) | 23 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) |
23 | if (!file) return undefined | 24 | if (!file) return undefined |
24 | 25 | ||
25 | if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } | 26 | if (file.getVideo().isOwned()) { |
27 | const downloadName = this.buildDownloadName(file.getVideo(), file) | ||
28 | |||
29 | return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName } | ||
30 | } | ||
26 | 31 | ||
27 | return this.loadRemoteFile(filename) | 32 | return this.loadRemoteFile(filename) |
28 | } | 33 | } |
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = file.getRemoteTorrentUrl(video) | 46 | const remoteUrl = file.getRemoteTorrentUrl(video) |
42 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) | 47 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) |
43 | 48 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 49 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 50 | ||
46 | const downloadName = `${video.name}-${file.resolution}p.torrent` | 51 | const downloadName = this.buildDownloadName(video, file) |
47 | 52 | ||
48 | return { isOwned: false, path: destPath, downloadName } | 53 | return { isOwned: false, path: destPath, downloadName } |
49 | } | 54 | } |
55 | |||
56 | private buildDownloadName (video: MVideo, file: MVideoFile) { | ||
57 | return `${video.name}-${file.resolution}p.torrent` | ||
58 | } | ||
50 | } | 59 | } |
51 | 60 | ||
52 | export { | 61 | export { |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 04187668c..84539e2c1 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
135 | const destPath = join(tmpDirectory, basename(fileUrl)) | 135 | const destPath = join(tmpDirectory, basename(fileUrl)) |
136 | 136 | ||
137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB | 137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB |
138 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) | 138 | await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit }) |
139 | } | 139 | } |
140 | 140 | ||
141 | clearTimeout(timer) | 141 | clearTimeout(timer) |
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
156 | } | 156 | } |
157 | 157 | ||
158 | async function fetchUniqUrls (playlistUrl: string) { | 158 | async function fetchUniqUrls (playlistUrl: string) { |
159 | const { body } = await doRequest<string>({ uri: playlistUrl }) | 159 | const { body } = await doRequest(playlistUrl) |
160 | 160 | ||
161 | if (!body) return [] | 161 | if (!body) return [] |
162 | 162 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts index b58bbc983..1caca1dcc 100644 --- a/server/lib/job-queue/handlers/activitypub-cleaner.ts +++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts | |||
@@ -1,10 +1,13 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Bull from 'bull' | 2 | import * as Bull from 'bull' |
3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | 3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' |
4 | import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' | 4 | import { |
5 | import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' | 5 | isAnnounceActivityValid, |
6 | isDislikeActivityValid, | ||
7 | isLikeActivityValid | ||
8 | } from '@server/helpers/custom-validators/activitypub/activity' | ||
6 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' | 9 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' |
7 | import { doRequest } from '@server/helpers/requests' | 10 | import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests' |
8 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' | 11 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' |
9 | import { VideoModel } from '@server/models/video/video' | 12 | import { VideoModel } from '@server/models/video/video' |
10 | import { VideoCommentModel } from '@server/models/video/video-comment' | 13 | import { VideoCommentModel } from '@server/models/video/video-comment' |
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> ( | |||
78 | updater: (url: string, newUrl: string) => Promise<T>, | 81 | updater: (url: string, newUrl: string) => Promise<T>, |
79 | deleter: (url: string) => Promise<T> | 82 | deleter: (url: string) => Promise<T> |
80 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { | 83 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { |
81 | // Fetch url | 84 | const on404OrTombstone = async () => { |
82 | const { response, body } = await doRequest<any>({ | ||
83 | uri: url, | ||
84 | json: true, | ||
85 | activityPub: true | ||
86 | }) | ||
87 | |||
88 | // Does not exist anymore, remove entry | ||
89 | if (response.statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
90 | logger.info('Removing remote AP object %s.', url) | 85 | logger.info('Removing remote AP object %s.', url) |
91 | const data = await deleter(url) | 86 | const data = await deleter(url) |
92 | 87 | ||
93 | return { status: 'deleted', data } | 88 | return { status: 'deleted' as 'deleted', data } |
94 | } | 89 | } |
95 | 90 | ||
96 | // If not same id, check same host and update | 91 | try { |
97 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) | 92 | const { body } = await doJSONRequest<any>(url, { activityPub: true }) |
98 | 93 | ||
99 | if (body.type === 'Tombstone') { | 94 | // If not same id, check same host and update |
100 | logger.info('Removing remote AP object %s.', url) | 95 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) |
101 | const data = await deleter(url) | ||
102 | 96 | ||
103 | return { status: 'deleted', data } | 97 | if (body.type === 'Tombstone') { |
104 | } | 98 | return on404OrTombstone() |
99 | } | ||
105 | 100 | ||
106 | const newUrl = body.id | 101 | const newUrl = body.id |
107 | if (newUrl !== url) { | 102 | if (newUrl !== url) { |
108 | if (checkUrlsSameHost(newUrl, url) !== true) { | 103 | if (checkUrlsSameHost(newUrl, url) !== true) { |
109 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) | 104 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) |
105 | } | ||
106 | |||
107 | logger.info('Updating remote AP object %s.', url) | ||
108 | const data = await updater(url, newUrl) | ||
109 | |||
110 | return { status: 'updated', data } | ||
110 | } | 111 | } |
111 | 112 | ||
112 | logger.info('Updating remote AP object %s.', url) | 113 | return null |
113 | const data = await updater(url, newUrl) | 114 | } catch (err) { |
115 | // Does not exist anymore, remove entry | ||
116 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
117 | return on404OrTombstone() | ||
118 | } | ||
114 | 119 | ||
115 | return { status: 'updated', data } | 120 | throw err |
116 | } | 121 | } |
117 | |||
118 | return null | ||
119 | } | 122 | } |
120 | 123 | ||
121 | function rateOptionsFactory () { | 124 | function rateOptionsFactory () { |
@@ -149,7 +152,7 @@ function rateOptionsFactory () { | |||
149 | 152 | ||
150 | function shareOptionsFactory () { | 153 | function shareOptionsFactory () { |
151 | return { | 154 | return { |
152 | bodyValidator: (body: any) => isShareActivityValid(body), | 155 | bodyValidator: (body: any) => isAnnounceActivityValid(body), |
153 | 156 | ||
154 | updater: async (url: string, newUrl: string) => { | 157 | updater: async (url: string, newUrl: string) => { |
155 | const share = await VideoShareModel.loadByUrl(url, undefined) | 158 | const share = await VideoShareModel.loadByUrl(url, undefined) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index 7174786d6..c69ff9e83 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri: '', | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
28 | const goodUrls: string[] = [] | 27 | const goodUrls: string[] = [] |
29 | 28 | ||
30 | await Bluebird.map(payload.uris, uri => { | 29 | await Bluebird.map(payload.uris, uri => { |
31 | return doRequest(Object.assign({}, options, { uri })) | 30 | return doRequest(uri, options) |
32 | .then(() => goodUrls.push(uri)) | 31 | .then(() => goodUrls.push(uri)) |
33 | .catch(() => badUrls.push(uri)) | 32 | .catch(() => badUrls.push(uri)) |
34 | }, { concurrency: BROADCAST_CONCURRENCY }) | 33 | }, { concurrency: BROADCAST_CONCURRENCY }) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index 74989d62e..585dad671 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri, | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
25 | } | 24 | } |
26 | 25 | ||
27 | try { | 26 | try { |
28 | await doRequest(options) | 27 | await doRequest(uri, options) |
29 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) | 28 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) |
30 | } catch (err) { | 29 | } catch (err) { |
31 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) | 30 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) |
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index c030d31ef..e8a91450d 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application' | |||
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | 6 | import { buildDigest } from '@server/helpers/peertube-crypto' |
7 | import { ContextType } from '@shared/models/activitypub/context' | 7 | import { ContextType } from '@shared/models/activitypub/context' |
8 | 8 | ||
9 | type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } | 9 | type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number } |
10 | 10 | ||
11 | async function computeBody (payload: Payload) { | 11 | async function computeBody <T> ( |
12 | payload: Payload<T> | ||
13 | ): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> { | ||
12 | let body = payload.body | 14 | let body = payload.body |
13 | 15 | ||
14 | if (payload.signatureActorId) { | 16 | if (payload.signatureActorId) { |
15 | const actorSignature = await ActorModel.load(payload.signatureActorId) | 17 | const actorSignature = await ActorModel.load(payload.signatureActorId) |
16 | if (!actorSignature) throw new Error('Unknown signature actor id.') | 18 | if (!actorSignature) throw new Error('Unknown signature actor id.') |
19 | |||
17 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) | 20 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) |
18 | } | 21 | } |
19 | 22 | ||
20 | return body | 23 | return body |
21 | } | 24 | } |
22 | 25 | ||
23 | async function buildSignedRequestOptions (payload: Payload) { | 26 | async function buildSignedRequestOptions (payload: Payload<any>) { |
24 | let actor: MActor | null | 27 | let actor: MActor | null |
25 | 28 | ||
26 | if (payload.signatureActorId) { | 29 | if (payload.signatureActorId) { |
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) { | |||
43 | 46 | ||
44 | function buildGlobalHeaders (body: any) { | 47 | function buildGlobalHeaders (body: any) { |
45 | return { | 48 | return { |
46 | 'Digest': buildDigest(body), | 49 | 'digest': buildDigest(body), |
47 | 'Content-Type': 'application/activity+json', | 50 | 'content-type': 'application/activity+json', |
48 | 'Accept': ACTIVITY_PUB.ACCEPT_HEADER | 51 | 'accept': ACTIVITY_PUB.ACCEPT_HEADER |
49 | } | 52 | } |
50 | } | 53 | } |
51 | 54 | ||
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 740c274d7..da7f7cc05 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config' | |||
19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
20 | import { UserModel } from '../models/account/user' | 20 | import { UserModel } from '../models/account/user' |
21 | import { UserNotificationModel } from '../models/account/user-notification' | 21 | import { UserNotificationModel } from '../models/account/user-notification' |
22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' | 22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' |
23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | 23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
24 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
25 | import { Emailer } from './emailer' | 25 | import { Emailer } from './emailer' |
@@ -144,6 +144,20 @@ class Notifier { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
148 | this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion) | ||
149 | .catch(err => { | ||
150 | logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err }) | ||
151 | }) | ||
152 | } | ||
153 | |||
154 | notifyOfNewPluginVersion (plugin: MPlugin) { | ||
155 | this.notifyAdminsOfNewPluginVersion(plugin) | ||
156 | .catch(err => { | ||
157 | logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }) | ||
158 | }) | ||
159 | } | ||
160 | |||
147 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { | 161 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { |
148 | // List all followers that are users | 162 | // List all followers that are users |
149 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) | 163 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) |
@@ -667,6 +681,64 @@ class Notifier { | |||
667 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 681 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
668 | } | 682 | } |
669 | 683 | ||
684 | private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
685 | // Use the debug right to know who is an administrator | ||
686 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
687 | if (admins.length === 0) return | ||
688 | |||
689 | logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion) | ||
690 | |||
691 | function settingGetter (user: MUserWithNotificationSetting) { | ||
692 | return user.NotificationSetting.newPeerTubeVersion | ||
693 | } | ||
694 | |||
695 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
696 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
697 | type: UserNotificationType.NEW_PEERTUBE_VERSION, | ||
698 | userId: user.id, | ||
699 | applicationId: application.id | ||
700 | }) | ||
701 | notification.Application = application | ||
702 | |||
703 | return notification | ||
704 | } | ||
705 | |||
706 | function emailSender (emails: string[]) { | ||
707 | return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion) | ||
708 | } | ||
709 | |||
710 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
711 | } | ||
712 | |||
713 | private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) { | ||
714 | // Use the debug right to know who is an administrator | ||
715 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
716 | if (admins.length === 0) return | ||
717 | |||
718 | logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion) | ||
719 | |||
720 | function settingGetter (user: MUserWithNotificationSetting) { | ||
721 | return user.NotificationSetting.newPluginVersion | ||
722 | } | ||
723 | |||
724 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
725 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
726 | type: UserNotificationType.NEW_PLUGIN_VERSION, | ||
727 | userId: user.id, | ||
728 | pluginId: plugin.id | ||
729 | }) | ||
730 | notification.Plugin = plugin | ||
731 | |||
732 | return notification | ||
733 | } | ||
734 | |||
735 | function emailSender (emails: string[]) { | ||
736 | return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin) | ||
737 | } | ||
738 | |||
739 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
740 | } | ||
741 | |||
670 | private async notify<T extends MUserWithNotificationSetting> (options: { | 742 | private async notify<T extends MUserWithNotificationSetting> (options: { |
671 | users: T[] | 743 | users: T[] |
672 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> | 744 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> |
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 7bcb6ed4c..624f5da1d 100644 --- a/server/lib/plugins/plugin-index.ts +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -1,22 +1,22 @@ | |||
1 | import { doRequest } from '../../helpers/requests' | 1 | import { sanitizeUrl } from '@server/helpers/core-utils' |
2 | import { CONFIG } from '../../initializers/config' | 2 | import { ResultList } from '../../../shared/models' |
3 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
4 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
3 | import { | 5 | import { |
4 | PeertubePluginLatestVersionRequest, | 6 | PeertubePluginLatestVersionRequest, |
5 | PeertubePluginLatestVersionResponse | 7 | PeertubePluginLatestVersionResponse |
6 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' | 8 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' |
7 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
8 | import { ResultList } from '../../../shared/models' | ||
9 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
10 | import { PluginModel } from '../../models/server/plugin' | ||
11 | import { PluginManager } from './plugin-manager' | ||
12 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest } from '../../helpers/requests' | ||
11 | import { CONFIG } from '../../initializers/config' | ||
13 | import { PEERTUBE_VERSION } from '../../initializers/constants' | 12 | import { PEERTUBE_VERSION } from '../../initializers/constants' |
14 | import { sanitizeUrl } from '@server/helpers/core-utils' | 13 | import { PluginModel } from '../../models/server/plugin' |
14 | import { PluginManager } from './plugin-manager' | ||
15 | 15 | ||
16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { | 16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { |
17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options | 17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options |
18 | 18 | ||
19 | const qs: PeertubePluginIndexList = { | 19 | const searchParams: PeertubePluginIndexList & Record<string, string | number> = { |
20 | start, | 20 | start, |
21 | count, | 21 | count, |
22 | sort, | 22 | sort, |
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) | |||
28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' | 28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' |
29 | 29 | ||
30 | try { | 30 | try { |
31 | const { body } = await doRequest<any>({ uri, qs, json: true }) | 31 | const { body } = await doJSONRequest<any>(uri, { searchParams }) |
32 | 32 | ||
33 | logger.debug('Got result from PeerTube index.', { body }) | 33 | logger.debug('Got result from PeerTube index.', { body }) |
34 | 34 | ||
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu | |||
58 | 58 | ||
59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' | 59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' |
60 | 60 | ||
61 | const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) | 61 | const options = { |
62 | json: bodyRequest, | ||
63 | method: 'POST' as 'POST' | ||
64 | } | ||
65 | const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options) | ||
62 | 66 | ||
63 | return body | 67 | return body |
64 | } | 68 | } |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index 1f2a88c27..9b5e1a546 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | VIDEO_PLAYLIST_PRIVACIES, | 7 | VIDEO_PLAYLIST_PRIVACIES, |
8 | VIDEO_PRIVACIES | 8 | VIDEO_PRIVACIES |
9 | } from '@server/initializers/constants' | 9 | } from '@server/initializers/constants' |
10 | import { onExternalUserAuthenticated } from '@server/lib/auth' | 10 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' |
11 | import { PluginModel } from '@server/models/server/plugin' | 11 | import { PluginModel } from '@server/models/server/plugin' |
12 | import { | 12 | import { |
13 | RegisterServerAuthExternalOptions, | 13 | RegisterServerAuthExternalOptions, |
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index f62f52f9c..0b8cd1389 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { chunk } from 'lodash' | 1 | import { chunk } from 'lodash' |
2 | import { doRequest } from '@server/helpers/requests' | 2 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler { | |||
34 | try { | 34 | try { |
35 | const serverActor = await getServerActor() | 35 | const serverActor = await getServerActor() |
36 | 36 | ||
37 | const qs = { count: 1000 } | 37 | const searchParams = { count: 1000 } |
38 | if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) | 38 | if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() }) |
39 | 39 | ||
40 | this.lastCheck = new Date() | 40 | this.lastCheck = new Date() |
41 | 41 | ||
42 | const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) | 42 | const { body } = await doJSONRequest<any>(indexUrl, { searchParams }) |
43 | if (!body.data || Array.isArray(body.data) === false) { | 43 | if (!body.data || Array.isArray(body.data) === false) { |
44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) | 44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) |
45 | return | 45 | return |
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts new file mode 100644 index 000000000..c8960465c --- /dev/null +++ b/server/lib/schedulers/peertube-version-check-scheduler.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | |||
2 | import { doJSONRequest } from '@server/helpers/requests' | ||
3 | import { ApplicationModel } from '@server/models/application/application' | ||
4 | import { compareSemVer } from '@shared/core-utils' | ||
5 | import { JoinPeerTubeVersions } from '@shared/models' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
9 | import { Notifier } from '../notifier' | ||
10 | import { AbstractScheduler } from './abstract-scheduler' | ||
11 | |||
12 | export class PeerTubeVersionCheckScheduler extends AbstractScheduler { | ||
13 | |||
14 | private static instance: AbstractScheduler | ||
15 | |||
16 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion | ||
17 | |||
18 | private constructor () { | ||
19 | super() | ||
20 | } | ||
21 | |||
22 | protected async internalExecute () { | ||
23 | return this.checkLatestVersion() | ||
24 | } | ||
25 | |||
26 | private async checkLatestVersion () { | ||
27 | if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return | ||
28 | |||
29 | logger.info('Checking latest PeerTube version.') | ||
30 | |||
31 | const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL) | ||
32 | |||
33 | if (!body?.peertube?.latestVersion) { | ||
34 | logger.warn('Cannot check latest PeerTube version: body is invalid.', { body }) | ||
35 | return | ||
36 | } | ||
37 | |||
38 | const latestVersion = body.peertube.latestVersion | ||
39 | const application = await ApplicationModel.load() | ||
40 | |||
41 | // Already checked this version | ||
42 | if (application.latestPeerTubeVersion === latestVersion) return | ||
43 | |||
44 | if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) { | ||
45 | application.latestPeerTubeVersion = latestVersion | ||
46 | await application.save() | ||
47 | |||
48 | Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | static get Instance () { | ||
53 | return this.instance || (this.instance = new this()) | ||
54 | } | ||
55 | } | ||
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts index 014993e94..9a1ae3ec5 100644 --- a/server/lib/schedulers/plugins-check-scheduler.ts +++ b/server/lib/schedulers/plugins-check-scheduler.ts | |||
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin' | |||
6 | import { chunk } from 'lodash' | 6 | import { chunk } from 'lodash' |
7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | 7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' |
8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' | 8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' |
9 | import { Notifier } from '../notifier' | ||
9 | 10 | ||
10 | export class PluginsCheckScheduler extends AbstractScheduler { | 11 | export class PluginsCheckScheduler extends AbstractScheduler { |
11 | 12 | ||
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler { | |||
53 | plugin.latestVersion = result.latestVersion | 54 | plugin.latestVersion = result.latestVersion |
54 | await plugin.save() | 55 | await plugin.save() |
55 | 56 | ||
57 | // Notify if there is an higher plugin version available | ||
58 | if (compareSemVer(plugin.version, result.latestVersion) < 0) { | ||
59 | Notifier.Instance.notifyOfNewPluginVersion(plugin) | ||
60 | } | ||
61 | |||
56 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) | 62 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) |
57 | } | 63 | } |
58 | } | 64 | } |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 106f5fdaa..cfee69cfc 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | |||
2 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 3 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
3 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 4 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
4 | import { processImage } from '../helpers/image-utils' | 5 | import { generateImageFilename, processImage } from '../helpers/image-utils' |
5 | import { downloadImage } from '../helpers/requests' | 6 | import { downloadImage } from '../helpers/requests' |
6 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
7 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 8 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail' | |||
11 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 12 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
12 | import { getVideoFilePath } from './video-paths' | 13 | import { getVideoFilePath } from './video-paths' |
13 | 14 | ||
14 | type ImageSize = { height: number, width: number } | 15 | type ImageSize = { height?: number, width?: number } |
15 | 16 | ||
16 | function createPlaylistMiniatureFromExisting (options: { | 17 | function createPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string | 18 | inputPath: string |
@@ -200,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
200 | : undefined | 201 | : undefined |
201 | 202 | ||
202 | if (type === ThumbnailType.MINIATURE) { | 203 | if (type === ThumbnailType.MINIATURE) { |
203 | const filename = video.generateThumbnailName() | 204 | const filename = generateImageFilename() |
204 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR | 205 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR |
205 | 206 | ||
206 | return { | 207 | return { |
@@ -214,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
214 | } | 215 | } |
215 | 216 | ||
216 | if (type === ThumbnailType.PREVIEW) { | 217 | if (type === ThumbnailType.PREVIEW) { |
217 | const filename = video.generatePreviewName() | 218 | const filename = generateImageFilename() |
218 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR | 219 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR |
219 | 220 | ||
220 | return { | 221 | return { |
diff --git a/server/lib/user.ts b/server/lib/user.ts index e1892f22c..9b0a0a2f1 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
193 | newInstanceFollower: UserNotificationSettingValue.WEB, | 193 | newInstanceFollower: UserNotificationSettingValue.WEB, |
194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
196 | autoInstanceFollowing: UserNotificationSettingValue.WEB | 196 | autoInstanceFollowing: UserNotificationSettingValue.WEB, |
197 | newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
198 | newPluginVersion: UserNotificationSettingValue.WEB | ||
197 | } | 199 | } |
198 | 200 | ||
199 | return UserNotificationSettingModel.create(values, { transaction: t }) | 201 | return UserNotificationSettingModel.create(values, { transaction: t }) |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index dbb37e0b2..37c43c3b0 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | } from '@server/types/models' | 11 | } from '@server/types/models' |
12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' | 12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' |
13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | 13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' |
14 | import { logger } from '../helpers/logger' | 14 | import { logger, loggerTagsFactory } from '../helpers/logger' |
15 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
17 | import { sendDeleteVideo } from './activitypub/send' | 17 | import { sendDeleteVideo } from './activitypub/send' |
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager' | |||
20 | import { Notifier } from './notifier' | 20 | import { Notifier } from './notifier' |
21 | import { Hooks } from './plugins/hooks' | 21 | import { Hooks } from './plugins/hooks' |
22 | 22 | ||
23 | const lTags = loggerTagsFactory('blacklist') | ||
24 | |||
23 | async function autoBlacklistVideoIfNeeded (parameters: { | 25 | async function autoBlacklistVideoIfNeeded (parameters: { |
24 | video: MVideoWithBlacklistLight | 26 | video: MVideoWithBlacklistLight |
25 | user?: MUser | 27 | user?: MUser |
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
60 | }) | 62 | }) |
61 | } | 63 | } |
62 | 64 | ||
63 | logger.info('Video %s auto-blacklisted.', video.uuid) | 65 | logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid)) |
64 | 66 | ||
65 | return true | 67 | return true |
66 | } | 68 | } |
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 49bdf4869..0476cb2d5 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts | |||
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid' | |||
3 | import { VideoChannelCreate } from '../../shared/models' | 3 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoModel } from '../models/video/video' | 4 | import { VideoModel } from '../models/video/video' |
5 | import { VideoChannelModel } from '../models/video/video-channel' | 5 | import { VideoChannelModel } from '../models/video/video-channel' |
6 | import { MAccountId, MChannelDefault, MChannelId } from '../types/models' | 6 | import { MAccountId, MChannelId } from '../types/models' |
7 | import { buildActorInstance } from './activitypub/actor' | 7 | import { buildActorInstance } from './activitypub/actor' |
8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' | 8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' |
9 | import { federateVideoIfNeeded } from './activitypub/videos' | 9 | import { federateVideoIfNeeded } from './activitypub/videos' |
10 | 10 | ||
11 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } | 11 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { |
12 | |||
13 | async function createLocalVideoChannel <T extends MAccountId> ( | ||
14 | videoChannelInfo: VideoChannelCreate, | ||
15 | account: T, | ||
16 | t: Sequelize.Transaction | ||
17 | ): Promise<CustomVideoChannelModelAccount<T>> { | ||
18 | const uuid = uuidv4() | 12 | const uuid = uuidv4() |
19 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) | 13 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) |
20 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) | 14 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) |
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> ( | |||
32 | const videoChannel = new VideoChannelModel(videoChannelData) | 26 | const videoChannel = new VideoChannelModel(videoChannelData) |
33 | 27 | ||
34 | const options = { transaction: t } | 28 | const options = { transaction: t } |
35 | const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault | 29 | const videoChannelCreated = await videoChannel.save(options) |
36 | 30 | ||
37 | // Do not forget to add Account/Actor information to the created video channel | ||
38 | videoChannelCreated.Account = account | ||
39 | videoChannelCreated.Actor = actorInstanceCreated | 31 | videoChannelCreated.Actor = actorInstanceCreated |
40 | 32 | ||
41 | // No need to seed this empty video channel to followers | 33 | // No need to send this empty video channel to followers |
42 | return videoChannelCreated | 34 | return videoChannelCreated |
43 | } | 35 | } |
44 | 36 | ||