aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actor.ts212
-rw-r--r--server/lib/activitypub/crawl.ts25
-rw-r--r--server/lib/activitypub/playlist.ts69
-rw-r--r--server/lib/activitypub/process/process-delete.ts6
-rw-r--r--server/lib/activitypub/process/process-update.ts13
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/share.ts30
-rw-r--r--server/lib/activitypub/video-comments.ts22
-rw-r--r--server/lib/activitypub/video-rates.ts22
-rw-r--r--server/lib/activitypub/videos.ts81
-rw-r--r--server/lib/actor-image.ts97
-rw-r--r--server/lib/auth/external-auth.ts (renamed from server/lib/auth.ts)129
-rw-r--r--server/lib/auth/oauth-model.ts (renamed from server/lib/oauth-model.ts)137
-rw-r--r--server/lib/auth/oauth.ts180
-rw-r--r--server/lib/auth/tokens-cache.ts52
-rw-r--r--server/lib/avatar.ts85
-rw-r--r--server/lib/client-html.ts9
-rw-r--r--server/lib/emailer.ts81
-rw-r--r--server/lib/emails/peertube-version-new/html.pug9
-rw-r--r--server/lib/emails/plugin-version-new/html.pug9
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts2
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/files-cache/videos-torrent-cache.ts15
-rw-r--r--server/lib/hls.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts63
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts5
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts15
-rw-r--r--server/lib/notifier.ts74
-rw-r--r--server/lib/plugins/plugin-index.ts26
-rw-r--r--server/lib/plugins/register-helpers.ts2
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts8
-rw-r--r--server/lib/schedulers/peertube-version-check-scheduler.ts55
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts6
-rw-r--r--server/lib/thumbnail.ts9
-rw-r--r--server/lib/user.ts4
-rw-r--r--server/lib/video-blacklist.ts6
-rw-r--r--server/lib/video-channel.ts16
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
2import { Op, Transaction } from 'sequelize' 3import { Op, Transaction } from 'sequelize'
3import { URL } from 'url' 4import { URL } from 'url'
4import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 13import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 14import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 15import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 17import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest } from '../../helpers/requests' 18import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 19import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 20import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
21import { sequelizeTypescript } from '../../initializers/database'
16import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
23import { ActorImageModel } from '../../models/account/actor-image'
17import { ActorModel } from '../../models/activitypub/actor' 24import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 25import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 26import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue'
22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23import { sequelizeTypescript } from '../../initializers/database'
24import { 27import {
25 MAccount, 28 MAccount,
26 MAccountDefault, 29 MAccountDefault,
@@ -28,15 +31,14 @@ import {
28 MActorAccountChannelId, 31 MActorAccountChannelId,
29 MActorAccountChannelIdActor, 32 MActorAccountChannelIdActor,
30 MActorAccountId, 33 MActorAccountId,
31 MActorDefault,
32 MActorFull, 34 MActorFull,
33 MActorFullActor, 35 MActorFullActor,
34 MActorId, 36 MActorId,
37 MActorImage,
38 MActorImages,
35 MChannel 39 MChannel
36} from '../../types/models' 40} from '../../types/models'
37import { extname } from 'path' 41import { JobQueue } from '../job-queue'
38import { getServerActor } from '@server/models/application/application'
39import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
40 42
41// Set account keys, this could be long so process after the account creation and do not block the client 43// Set account keys, this could be long so process after the account creation and do not block the client
42async function generateAndSaveActorKeys <T extends MActor> (actor: T) { 44async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
168 } 170 }
169} 171}
170 172
171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } 173type ImageInfo = {
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { 174 name: string
173 if (!info.name) return actor 175 fileUrl: string
176 height: number
177 width: number
178 onDisk?: boolean
179}
180async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
181 const oldImageModel = type === ActorImageType.AVATAR
182 ? actor.Avatar
183 : actor.Banner
174 184
175 if (actor.Avatar) { 185 if (oldImageModel) {
176 // Don't update the avatar if the file URL did not change 186 // Don't update the avatar if the file URL did not change
177 if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor 187 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
178 188
179 try { 189 try {
180 await actor.Avatar.destroy({ transaction: t }) 190 await oldImageModel.destroy({ transaction: t })
191
192 setActorImage(actor, type, null)
181 } catch (err) { 193 } catch (err) {
182 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 194 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
183 } 195 }
184 } 196 }
185 197
186 const avatar = await AvatarModel.create({ 198 if (imageInfo) {
187 filename: info.name, 199 const imageModel = await ActorImageModel.create({
188 onDisk: info.onDisk, 200 filename: imageInfo.name,
189 fileUrl: info.fileUrl 201 onDisk: imageInfo.onDisk ?? false,
190 }, { transaction: t }) 202 fileUrl: imageInfo.fileUrl,
191 203 height: imageInfo.height,
192 actor.avatarId = avatar.id 204 width: imageInfo.width,
193 actor.Avatar = avatar 205 type
206 }, { transaction: t })
207
208 setActorImage(actor, type, imageModel)
209 }
194 210
195 return actor 211 return actor
196} 212}
197 213
198async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { 214async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
199 try { 215 try {
200 await actor.Avatar.destroy({ transaction: t }) 216 if (type === ActorImageType.AVATAR) {
217 await actor.Avatar.destroy({ transaction: t })
218
219 actor.avatarId = null
220 actor.Avatar = null
221 } else {
222 await actor.Banner.destroy({ transaction: t })
223
224 actor.bannerId = null
225 actor.Banner = null
226 }
201 } catch (err) { 227 } catch (err) {
202 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 228 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
203 } 229 }
204 230
205 actor.avatarId = null
206 actor.Avatar = null
207
208 return actor 231 return actor
209} 232}
210 233
211async function fetchActorTotalItems (url: string) { 234async function fetchActorTotalItems (url: string) {
212 const options = {
213 uri: url,
214 method: 'GET',
215 json: true,
216 activityPub: true
217 }
218
219 try { 235 try {
220 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) 236 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
221 return body.totalItems ? body.totalItems : 0 237
238 return body.totalItems || 0
222 } catch (err) { 239 } catch (err) {
223 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 240 logger.warn('Cannot fetch remote actor count %s.', url, { err })
224 return 0 241 return 0
225 } 242 }
226} 243}
227 244
228function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 245function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
229 const mimetypes = MIMETYPES.IMAGE 246 const mimetypes = MIMETYPES.IMAGE
230 const icon = actorJSON.icon 247 const icon = type === ActorImageType.AVATAR
248 ? actorJSON.icon
249 : actorJSON.image
231 250
232 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 251 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
233 252
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
245 264
246 return { 265 return {
247 name: uuidv4() + extension, 266 name: uuidv4() + extension,
248 fileUrl: icon.url 267 fileUrl: icon.url,
268 height: icon.height,
269 width: icon.width,
270 type
249 } 271 }
250} 272}
251 273
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
285 actorUrl = actor.url 307 actorUrl = actor.url
286 } 308 }
287 309
288 const { result, statusCode } = await fetchRemoteActor(actorUrl) 310 const { result } = await fetchRemoteActor(actorUrl)
289
290 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
291 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
292 actor.Account
293 ? await actor.Account.destroy()
294 : await actor.VideoChannel.destroy()
295
296 return { actor: undefined, refreshed: false }
297 }
298 311
299 if (result === undefined) { 312 if (result === undefined) {
300 logger.warn('Cannot fetch remote actor in refresh actor.') 313 logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
304 return sequelizeTypescript.transaction(async t => { 317 return sequelizeTypescript.transaction(async t => {
305 updateInstanceWithAnother(actor, result.actor) 318 updateInstanceWithAnother(actor, result.actor)
306 319
307 if (result.avatar !== undefined) { 320 await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t)
308 const avatarInfo = { 321 await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t)
309 name: result.avatar.name,
310 fileUrl: result.avatar.fileUrl,
311 onDisk: false
312 }
313
314 await updateActorAvatarInstance(actor, avatarInfo, t)
315 }
316 322
317 // Force update 323 // Force update
318 actor.setDataValue('updatedAt', new Date()) 324 actor.setDataValue('updatedAt', new Date())
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
334 return { refreshed: true, actor } 340 return { refreshed: true, actor }
335 }) 341 })
336 } catch (err) { 342 } catch (err) {
343 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
344 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
345 actor.Account
346 ? await actor.Account.destroy()
347 : await actor.VideoChannel.destroy()
348
349 return { actor: undefined, refreshed: false }
350 }
351
337 logger.warn('Cannot refresh actor %s.', actor.url, { err }) 352 logger.warn('Cannot refresh actor %s.', actor.url, { err })
338 return { actor, refreshed: false } 353 return { actor, refreshed: false }
339 } 354 }
@@ -344,16 +359,32 @@ export {
344 buildActorInstance, 359 buildActorInstance,
345 generateAndSaveActorKeys, 360 generateAndSaveActorKeys,
346 fetchActorTotalItems, 361 fetchActorTotalItems,
347 getAvatarInfoIfExists, 362 getImageInfoIfExists,
348 updateActorInstance, 363 updateActorInstance,
349 deleteActorAvatarInstance, 364 deleteActorImageInstance,
350 refreshActorIfNeeded, 365 refreshActorIfNeeded,
351 updateActorAvatarInstance, 366 updateActorImageInstance,
352 addFetchOutboxJob 367 addFetchOutboxJob
353} 368}
354 369
355// --------------------------------------------------------------------------- 370// ---------------------------------------------------------------------------
356 371
372function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
373 const id = imageModel
374 ? imageModel.id
375 : null
376
377 if (type === ActorImageType.AVATAR) {
378 actorModel.avatarId = id
379 actorModel.Avatar = imageModel
380 } else {
381 actorModel.bannerId = id
382 actorModel.Banner = imageModel
383 }
384
385 return actorModel
386}
387
357function saveActorAndServerAndModelIfNotExist ( 388function saveActorAndServerAndModelIfNotExist (
358 result: FetchRemoteActorResult, 389 result: FetchRemoteActorResult,
359 ownerActor?: MActorFullActor, 390 ownerActor?: MActorFullActor,
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist (
384 415
385 // Avatar? 416 // Avatar?
386 if (result.avatar) { 417 if (result.avatar) {
387 const avatar = await AvatarModel.create({ 418 const avatar = await ActorImageModel.create({
388 filename: result.avatar.name, 419 filename: result.avatar.name,
389 fileUrl: result.avatar.fileUrl, 420 fileUrl: result.avatar.fileUrl,
390 onDisk: false 421 width: result.avatar.width,
422 height: result.avatar.height,
423 onDisk: false,
424 type: ActorImageType.AVATAR
391 }, { transaction: t }) 425 }, { transaction: t })
392 426
393 actor.avatarId = avatar.id 427 actor.avatarId = avatar.id
394 } 428 }
395 429
430 // Banner?
431 if (result.banner) {
432 const banner = await ActorImageModel.create({
433 filename: result.banner.name,
434 fileUrl: result.banner.fileUrl,
435 width: result.banner.width,
436 height: result.banner.height,
437 onDisk: false,
438 type: ActorImageType.BANNER
439 }, { transaction: t })
440
441 actor.bannerId = banner.id
442 }
443
396 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 444 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
397 // (which could be false in a retried query) 445 // (which could be false in a retried query)
398 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ 446 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist (
436 } 484 }
437} 485}
438 486
487type ImageResult = {
488 name: string
489 fileUrl: string
490 height: number
491 width: number
492}
493
439type FetchRemoteActorResult = { 494type FetchRemoteActorResult = {
440 actor: MActor 495 actor: MActor
441 name: string 496 name: string
442 summary: string 497 summary: string
443 support?: string 498 support?: string
444 playlists?: string 499 playlists?: string
445 avatar?: { 500 avatar?: ImageResult
446 name: string 501 banner?: ImageResult
447 fileUrl: string
448 }
449 attributedTo: ActivityPubAttributedTo[] 502 attributedTo: ActivityPubAttributedTo[]
450} 503}
451async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 504async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
452 const options = {
453 uri: actorUrl,
454 method: 'GET',
455 json: true,
456 activityPub: true
457 }
458
459 logger.info('Fetching remote actor %s.', actorUrl) 505 logger.info('Fetching remote actor %s.', actorUrl)
460 506
461 const requestResult = await doRequest<ActivityPubActor>(options) 507 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
462 const actorJSON = requestResult.body 508 const actorJSON = requestResult.body
463 509
464 if (sanitizeAndCheckActorObject(actorJSON) === false) { 510 if (sanitizeAndCheckActorObject(actorJSON) === false) {
465 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 511 logger.debug('Remote actor JSON is not valid.', { actorJSON })
466 return { result: undefined, statusCode: requestResult.response.statusCode } 512 return { result: undefined, statusCode: requestResult.statusCode }
467 } 513 }
468 514
469 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 515 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
470 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) 516 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
471 return { result: undefined, statusCode: requestResult.response.statusCode } 517 return { result: undefined, statusCode: requestResult.statusCode }
472 } 518 }
473 519
474 const followersCount = await fetchActorTotalItems(actorJSON.followers) 520 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
492 : null 538 : null
493 }) 539 })
494 540
495 const avatarInfo = await getAvatarInfoIfExists(actorJSON) 541 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
542 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
496 543
497 const name = actorJSON.name || actorJSON.preferredUsername 544 const name = actorJSON.name || actorJSON.preferredUsername
498 return { 545 return {
499 statusCode: requestResult.response.statusCode, 546 statusCode: requestResult.statusCode,
500 result: { 547 result: {
501 actor, 548 actor,
502 name, 549 name,
503 avatar: avatarInfo, 550 avatar: avatarInfo,
551 banner: bannerInfo,
504 summary: actorJSON.summary, 552 summary: actorJSON.summary,
505 support: actorJSON.support, 553 support: actorJSON.support,
506 playlists: actorJSON.playlists, 554 playlists: actorJSON.playlists,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1ed105bbe..278abf7de 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,27 +1,26 @@
1import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { URL } from 'url' 2import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
4import { logger } from '../../helpers/logger'
5import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
10 10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { 11async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
12 logger.info('Crawling ActivityPub data on %s.', uri) 12 let url = argUrl
13
14 logger.info('Crawling ActivityPub data on %s.', url)
13 15
14 const options = { 16 const options = {
15 method: 'GET',
16 uri,
17 json: true,
18 activityPub: true, 17 activityPub: true,
19 timeout: REQUEST_TIMEOUT 18 timeout: REQUEST_TIMEOUT
20 } 19 }
21 20
22 const startDate = new Date() 21 const startDate = new Date()
23 22
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 23 const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
25 const firstBody = response.body 24 const firstBody = response.body
26 25
27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 26 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
35 const remoteHost = new URL(nextLink).host 34 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 35 if (remoteHost === WEBSERVER.HOST) continue
37 36
38 options.uri = nextLink 37 url = nextLink
39 38
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options) 39 const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
41 body = res.body 40 body = res.body
42 } else { 41 } else {
43 // nextLink is already the object we want 42 // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
49 48
50 if (Array.isArray(body.orderedItems)) { 49 if (Array.isArray(body.orderedItems)) {
51 const items = body.orderedItems 50 const items = body.orderedItems
52 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) 51 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
53 52
54 await handler(items) 53 await handler(items)
55 } 54 }
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index d5a3ef7c8..7166c68a6 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,24 +1,24 @@
1import * as Bluebird from 'bluebird'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 5import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { checkUrlsSameHost } from '../../helpers/activitypub'
7import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
4import { isArray } from '../../helpers/custom-validators/misc' 8import { isArray } from '../../helpers/custom-validators/misc'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
11import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { sequelizeTypescript } from '../../initializers/database'
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 13import { VideoPlaylistModel } from '../../models/video/video-playlist'
8import { doRequest } from '../../helpers/requests'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import * as Bluebird from 'bluebird'
11import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
12import { getOrCreateVideoAndAccountAndChannel } from './videos'
13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
16import { sequelizeTypescript } from '../../initializers/database'
17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
18import { FilteredModelAttributes } from '../../types/sequelize'
19import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' 15import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' 16import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
21import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 17import { FilteredModelAttributes } from '../../types/sequelize'
18import { createPlaylistMiniatureFromUrl } from '../thumbnail'
19import { getOrCreateActorAndServerAndModel } from './actor'
20import { crawlCollectionPage } from './crawl'
21import { getOrCreateVideoAndAccountAndChannel } from './videos'
22 22
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
56 if (exists === true) return 56 if (exists === true) return
57 57
58 // Fetch url 58 // Fetch url
59 const { body } = await doRequest<PlaylistObject>({ 59 const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
60 uri: playlistUrl,
61 json: true,
62 activityPub: true
63 })
64 60
65 if (!isPlaylistObjectValid(body)) { 61 if (!isPlaylistObjectValid(body)) {
66 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) 62 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
120 if (!videoPlaylist.isOutdated()) return videoPlaylist 116 if (!videoPlaylist.isOutdated()) return videoPlaylist
121 117
122 try { 118 try {
123 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) 119 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
124 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
125 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
126
127 await videoPlaylist.destroy()
128 return undefined
129 }
130 120
131 if (playlistObject === undefined) { 121 if (playlistObject === undefined) {
132 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) 122 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
140 130
141 return videoPlaylist 131 return videoPlaylist
142 } catch (err) { 132 } catch (err) {
133 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
134 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
135
136 await videoPlaylist.destroy()
137 return undefined
138 }
139
143 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) 140 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
144 141
145 await videoPlaylist.setAsRefreshed() 142 await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
164 161
165 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
166 try { 163 try {
167 // Fetch url 164 const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
168 const { body } = await doRequest<PlaylistElementObject>({
169 uri: elementUrl,
170 json: true,
171 activityPub: true
172 })
173 165
174 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) 166 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
175 167
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
199} 191}
200 192
201async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 193async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
202 const options = {
203 uri: playlistUrl,
204 method: 'GET',
205 json: true,
206 activityPub: true
207 }
208
209 logger.info('Fetching remote playlist %s.', playlistUrl) 194 logger.info('Fetching remote playlist %s.', playlistUrl)
210 195
211 const { response, body } = await doRequest<any>(options) 196 const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
212 197
213 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 198 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
214 logger.debug('Remote video playlist JSON is not valid.', { body }) 199 logger.debug('Remote video playlist JSON is not valid.', { body })
215 return { statusCode: response.statusCode, playlistObject: undefined } 200 return { statusCode, playlistObject: undefined }
216 } 201 }
217 202
218 return { statusCode: response.statusCode, playlistObject: body } 203 return { statusCode, playlistObject: body }
219} 204}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index a86def936..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'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model' 9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' 10import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models'
11import { markCommentAsDeleted } from '../../video-comment' 11import { markCommentAsDeleted } from '../../video-comment'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { 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'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model' 17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../types/models' 18import { MActorSignature, MAccountIdActor } from '../../../types/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy' 19import { isRedundancyAccepted } from '@server/lib/redundancy'
20import { ActorImageType } from '@shared/models'
20 21
21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 22async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
22 const { activity, byActor } = options 23 const { activity, byActor } = options
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
119 let accountOrChannelFieldsSave: object 120 let accountOrChannelFieldsSave: object
120 121
121 // Fetch icon? 122 // Fetch icon?
122 const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) 123 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
124 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
123 125
124 try { 126 try {
125 await sequelizeTypescript.transaction(async t => { 127 await sequelizeTypescript.transaction(async t => {
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
132 134
133 await updateActorInstance(actor, actorAttributesToUpdate) 135 await updateActorInstance(actor, actorAttributesToUpdate)
134 136
135 if (avatarInfo !== undefined) { 137 await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t)
136 const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) 138 await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t)
137
138 await updateActorAvatarInstance(actor, avatarOptions, t)
139 }
140 139
141 await actor.save({ transaction: t }) 140 await actor.save({ transaction: t })
142 141
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9fb218224..baded642a 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
4import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { 9import {
10 MActorLight, 10 MActorLight,
@@ -18,10 +18,12 @@ import {
18import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context' 19import { ContextType } from '@shared/models/activitypub/context'
20 20
21const lTags = loggerTagsFactory('ap', 'create')
22
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 23async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 24 if (!video.hasPrivacyForFederation()) return undefined
23 25
24 logger.info('Creating job to send video creation of %s.', video.url) 26 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
25 27
26 const byActor = video.VideoChannel.Account.Actor 28 const byActor = video.VideoChannel.Account.Actor
27 const videoObject = video.toActivityPubObject() 29 const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
37 video: MVideoAccountLight, 39 video: MVideoAccountLight,
38 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 40 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
39) { 41) {
40 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 42 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
41 43
42 return sendVideoRelatedCreateActivity({ 44 return sendVideoRelatedCreateActivity({
43 byActor, 45 byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
51async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { 53async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
52 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 54 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
53 55
54 logger.info('Creating job to send create video playlist of %s.', playlist.url) 56 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
55 57
56 const byActor = playlist.OwnerAccount.Actor 58 const byActor = playlist.OwnerAccount.Actor
57 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 59 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1f8a8f3c4..c22fa0893 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,15 +1,17 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { logger, loggerTagsFactory } from '../../helpers/logger'
6import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
2import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor'
3import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
4import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
5import * as Bluebird from 'bluebird' 13
6import { doRequest } from '../../helpers/requests' 14const lTags = loggerTagsFactory('share')
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { logger } from '../../helpers/logger'
9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
12import { getServerActor } from '@server/models/application/application'
13 15
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 16async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 17 if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
25 oldVideoChannel: MChannelActorLight, 27 oldVideoChannel: MChannelActorLight,
26 t: Transaction 28 t: Transaction
27) { 29) {
28 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 30 logger.info(
31 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
32 lTags(video.uuid)
33 )
29 34
30 await undoShareByVideoChannel(video, oldVideoChannel, t) 35 await undoShareByVideoChannel(video, oldVideoChannel, t)
31 36
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
35async function addVideoShares (shareUrls: string[], video: MVideoId) { 40async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 41 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 42 try {
38 // Fetch url 43 const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
39 const { body } = await doRequest<any>({
40 uri: shareUrl,
41 json: true,
42 activityPub: true
43 })
44 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 44 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
45 45
46 const actorUrl = getAPId(body.actor) 46 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d025ed7f1..e23e0c0e7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird'
2import { checkUrlsSameHost } from '../../helpers/activitypub'
1import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
2import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 5import { doJSONRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
6import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
7import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
8import * as Bluebird from 'bluebird'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string 13 url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, async commentUrl => {
22 return resolveThread({ url: commentUrl, isVideo: false }) 22 try {
23 await resolveThread({ url: commentUrl, isVideo: false })
24 } catch (err) {
25 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
26 }
23 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 27 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
24} 28}
25 29
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
126 throw new Error('Recursion limit reached when resolving a thread') 130 throw new Error('Recursion limit reached when resolving a thread')
127 } 131 }
128 132
129 const { body } = await doRequest<any>({ 133 const { body } = await doJSONRequest<any>(url, { activityPub: true })
130 uri: url,
131 json: true,
132 activityPub: true
133 })
134 134
135 if (sanitizeAndCheckVideoCommentObject(body) === false) { 135 if (sanitizeAndCheckVideoCommentObject(body) === false) {
136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) 136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index e246b1313..f40c07fea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,26 +1,22 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
2import { sendLike, sendUndoDislike, sendUndoLike } from './send' 3import { doJSONRequest } from '@server/helpers/requests'
3import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
4import * as Bluebird from 'bluebird' 5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { AccountVideoRateModel } from '../../models/account/account-video-rate'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
9import { doRequest } from '../../helpers/requests' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 14
15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
16 await Bluebird.map(ratesUrl, async rateUrl => { 16 await Bluebird.map(ratesUrl, async rateUrl => {
17 try { 17 try {
18 // Fetch url 18 // Fetch url
19 const { body } = await doRequest<any>({ 19 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
20 uri: rateUrl,
21 json: true,
22 activityPub: true
23 })
24 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 20 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
25 21
26 const actorUrl = getAPId(body.actor) 22 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c02578aad..9014791c0 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,8 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename } from 'path'
5import * as request from 'request'
6import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -17,7 +16,7 @@ import {
17 ActivityUrlObject, 16 ActivityUrlObject,
18 ActivityVideoUrlObject 17 ActivityVideoUrlObject
19} from '../../../shared/index' 18} from '../../../shared/index'
20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
21import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc'
31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 30import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 31import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
33import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
34import { doRequest } from '../../helpers/requests' 33import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
38 LAZY_STATIC_PATHS,
39 MIMETYPES, 37 MIMETYPES,
40 P2P_MEDIA_LOADER_PEER_VERSION, 38 P2P_MEDIA_LOADER_PEER_VERSION,
41 PREVIEWS_SIZE, 39 PREVIEWS_SIZE,
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
115 } 113 }
116} 114}
117 115
118async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { 116async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
119 const options = {
120 uri: videoUrl,
121 method: 'GET',
122 json: true,
123 activityPub: true
124 }
125
126 logger.info('Fetching remote video %s.', videoUrl) 117 logger.info('Fetching remote video %s.', videoUrl)
127 118
128 const { response, body } = await doRequest<any>(options) 119 const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
129 120
130 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 121 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
131 logger.debug('Remote video JSON is not valid.', { body }) 122 logger.debug('Remote video JSON is not valid.', { body })
132 return { response, videoObject: undefined } 123 return { statusCode, videoObject: undefined }
133 } 124 }
134 125
135 return { response, videoObject: body } 126 return { statusCode, videoObject: body }
136} 127}
137 128
138async function fetchRemoteVideoDescription (video: MVideoAccountLight) { 129async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
139 const host = video.VideoChannel.Account.Actor.Server.host 130 const host = video.VideoChannel.Account.Actor.Server.host
140 const path = video.getDescriptionAPIPath() 131 const path = video.getDescriptionAPIPath()
141 const options = { 132 const url = REMOTE_SCHEME.HTTP + '://' + host + path
142 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
143 json: true
144 }
145 133
146 const { body } = await doRequest<any>(options) 134 const { body } = await doJSONRequest<any>(url)
147 return body.description ? body.description : '' 135 return body.description || ''
148} 136}
149 137
150function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { 138function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: {
378 366
379 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 367 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
380 368
381 if (videoUpdated.getPreview()) { 369 const previewIcon = getPreviewFromIcons(videoObject)
382 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) 370 if (videoUpdated.getPreview() && previewIcon) {
383 const previewModel = createPlaceholderThumbnail({ 371 const previewModel = createPlaceholderThumbnail({
384 fileUrl: previewUrl, 372 fileUrl: previewIcon.url,
385 video, 373 video,
386 type: ThumbnailType.PREVIEW, 374 type: ThumbnailType.PREVIEW,
387 size: PREVIEWS_SIZE 375 size: previewIcon
388 }) 376 })
389 await videoUpdated.addAndSaveThumbnail(previewModel, t) 377 await videoUpdated.addAndSaveThumbnail(previewModel, t)
390 } 378 }
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: {
534 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 522 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
535 523
536 try { 524 try {
537 const { response, videoObject } = await fetchRemoteVideo(video.url) 525 const { videoObject } = await fetchRemoteVideo(video.url)
538 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
539 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
540
541 // Video does not exist anymore
542 await video.destroy()
543 return undefined
544 }
545 526
546 if (videoObject === undefined) { 527 if (videoObject === undefined) {
547 logger.warn('Cannot refresh remote video %s: invalid body.', video.url) 528 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -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
909function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
910 return previewIcon
911 ? previewIcon.url
912 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
913}
914
915function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { 900function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
916 let wsFound = false 901 let wsFound = false
917 902
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 @@
1import 'multer'
2import { queue } from 'async'
3import * as LRUCache from 'lru-cache'
4import { extname, join } from 'path'
5import { v4 as uuidv4 } from 'uuid'
6import { ActorImageType } from '@shared/models'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { processImage } from '../helpers/image-utils'
9import { downloadImage } from '../helpers/requests'
10import { CONFIG } from '../initializers/config'
11import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
12import { sequelizeTypescript } from '../initializers/database'
13import { MAccountDefault, MChannelDefault } from '../types/models'
14import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor'
15import { sendUpdateActor } from './activitypub/send'
16
17async 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
54async 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
67type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
68
69const 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
79function 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
90const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE })
91
92export {
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
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 2import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 4import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' 5import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import { 8import {
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'
14import * as express from 'express' 13import { UserRole } from '@shared/models'
15import * as OAuthServer from 'express-oauth-server'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17
18const 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
28const authBypassTokens = new Map<string, { 16const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
37 npmName: string 25 npmName: string
38}>() 26}>()
39 27
40async 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
53async 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
76async function onExternalUserAuthenticated (options: { 28async 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// --------------------------------------------------------------------------- 73async function getAuthNameFromRefreshGrant (refreshToken?: string) {
122 74 if (!refreshToken) return undefined
123export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
124
125// ---------------------------------------------------------------------------
126
127function 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
143async 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
151async function proxifyPasswordGrant (req: express.Request, res: express.Response) { 81async 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
216function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { 146function 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
214export {
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as LRUCache from 'lru-cache'
3import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
4import { Transaction } from 'sequelize'
5import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
6import { ActorModel } from '@server/models/activitypub/actor' 4import { ActorModel } from '@server/models/activitypub/actor'
5import { MOAuthClient } from '@server/types/models'
7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
8import { MUser } from '@server/types/models/user/user' 7import { MUser } from '@server/types/models/user/user'
9import { UserAdminFlag } from '@shared/models/users/user-flag.model' 8import { UserAdminFlag } from '@shared/models/users/user-flag.model'
10import { UserRole } from '@shared/models/users/user-role' 9import { UserRole } from '@shared/models/users/user-role'
11import { logger } from '../helpers/logger' 10import { logger } from '../../helpers/logger'
12import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../../initializers/config'
13import { LRU_CACHE } from '../initializers/constants' 12import { UserModel } from '../../models/account/user'
14import { UserModel } from '../models/account/user' 13import { OAuthClientModel } from '../../models/oauth/oauth-client'
15import { OAuthClientModel } from '../models/oauth/oauth-client' 14import { OAuthTokenModel } from '../../models/oauth/oauth-token'
16import { OAuthTokenModel } from '../models/oauth/oauth-token' 15import { createUserAccountAndChannelAndPlaylist } from '../user'
17import { createUserAccountAndChannelAndPlaylist } from './user' 16import { TokensCache } from './tokens-cache'
18 17
19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = {
20 19 accessToken: string
21const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 20 refreshToken: string
22const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 21 accessTokenExpiresAt: Date
23 22 refreshTokenExpiresAt: Date
24// ---------------------------------------------------------------------------
25
26function deleteUserToken (userId: number, t?: Transaction) {
27 clearCacheByUserId(userId)
28
29 return OAuthTokenModel.deleteUserToken(userId, t)
30} 23}
31 24
32function clearCacheByUserId (userId: number) { 25export 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
41function 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
102async function getUser (usernameOrEmail?: string, password?: string) { 86async 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
146async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { 127async 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
168async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 156async 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
209export { 203export {
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
221async function createUserFromExternal (pluginAuth: string, options: { 214async 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: {
252function checkUserValidityOrThrow (user: MUser) { 245function 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
249function 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 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { 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
22const 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
32async 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
84async 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
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async 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
126async 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
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function 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
171async 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 @@
1import * as LRUCache from 'lru-cache'
2import { MOAuthTokenUser } from '@server/types/models'
3import { LRU_CACHE } from '../../initializers/constants'
4
5export 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 @@
1import 'multer'
2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { v4 as uuidv4 } from 'uuid'
9import { CONFIG } from '../initializers/config'
10import { sequelizeTypescript } from '../initializers/database'
11import * as LRUCache from 'lru-cache'
12import { queue } from 'async'
13import { downloadImage } from '../helpers/requests'
14import { MAccountDefault, MChannelDefault } from '../types/models'
15
16async 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
44async 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
59type DownloadImageQueueTask = { fileUrl: string, filename: string }
60
61const 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
67function 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
78const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE })
79
80export {
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'
5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' 5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' 7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
8import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' 8import { isTestInstance, sha256 } from '../helpers/core-utils'
9import { escapeHTML } from '@shared/core-utils/renderer'
9import { logger } from '../helpers/logger' 10import { logger } from '../helpers/logger'
10import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
11import { 12import {
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
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' 7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' 8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' 9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
10import { SendEmailOptions } from '../../shared/models/server/emailer.model' 10import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
11import { isTestInstance, root } from '../helpers/core-utils' 11import { isTestInstance, root } from '../helpers/core-utils'
12import { bunyanLogger, logger } from '../helpers/logger' 12import { bunyanLogger, logger } from '../helpers/logger'
13import { CONFIG, isEmailEnabled } from '../initializers/config' 13import { CONFIG, isEmailEnabled } from '../initializers/config'
14import { WEBSERVER } from '../initializers/constants' 14import { WEBSERVER } from '../initializers/constants'
15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue' 17import { 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 @@
1extends ../common/greetings
2
3block title
4 | New PeerTube version available
5
6block 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 @@
1extends ../common/greetings
2
3block title
4 | New plugin version available
5
6block 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'
5import { FILES_CACHE } from '../../initializers/constants' 5import { FILES_CACHE } from '../../initializers/constants'
6import { VideoModel } from '../../models/video/video' 6import { VideoModel } from '../../models/video/video'
7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
8import { MVideo, MVideoFile } from '@server/types/models'
8 9
9class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { 10class 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
52export { 61export {
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Bull from 'bull' 2import * as Bull from 'bull'
3import { checkUrlsSameHost } from '@server/helpers/activitypub' 3import { checkUrlsSameHost } from '@server/helpers/activitypub'
4import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' 4import {
5import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' 5 isAnnounceActivityValid,
6 isDislikeActivityValid,
7 isLikeActivityValid
8} from '@server/helpers/custom-validators/activitypub/activity'
6import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' 9import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
7import { doRequest } from '@server/helpers/requests' 10import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
8import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' 11import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
9import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
10import { VideoCommentModel } from '@server/models/video/video-comment' 13import { 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
121function rateOptionsFactory () { 124function rateOptionsFactory () {
@@ -149,7 +152,7 @@ function rateOptionsFactory () {
149 152
150function shareOptionsFactory () { 153function 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'
6import { buildDigest } from '@server/helpers/peertube-crypto' 6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context' 7import { ContextType } from '@shared/models/activitypub/context'
8 8
9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } 9type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
10 10
11async function computeBody (payload: Payload) { 11async 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
23async function buildSignedRequestOptions (payload: Payload) { 26async 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
44function buildGlobalHeaders (body: any) { 47function 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'
19import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
21import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { 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 @@
1import { doRequest } from '../../helpers/requests' 1import { sanitizeUrl } from '@server/helpers/core-utils'
2import { CONFIG } from '../../initializers/config' 2import { ResultList } from '../../../shared/models'
3import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
4import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
3import { 5import {
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'
7import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
8import { ResultList } from '../../../shared/models'
9import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
10import { PluginModel } from '../../models/server/plugin'
11import { PluginManager } from './plugin-manager'
12import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest } from '../../helpers/requests'
11import { CONFIG } from '../../initializers/config'
13import { PEERTUBE_VERSION } from '../../initializers/constants' 12import { PEERTUBE_VERSION } from '../../initializers/constants'
14import { sanitizeUrl } from '@server/helpers/core-utils' 13import { PluginModel } from '../../models/server/plugin'
14import { PluginManager } from './plugin-manager'
15 15
16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { 16async 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'
10import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
11import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
12import { 12import {
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 @@
1import { chunk } from 'lodash' 1import { chunk } from 'lodash'
2import { doRequest } from '@server/helpers/requests' 2import { doJSONRequest } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 4import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
5import { getServerActor } from '@server/models/application/application' 5import { 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
2import { doJSONRequest } from '@server/helpers/requests'
3import { ApplicationModel } from '@server/models/application/application'
4import { compareSemVer } from '@shared/core-utils'
5import { JoinPeerTubeVersions } from '@shared/models'
6import { logger } from '../../helpers/logger'
7import { CONFIG } from '../../initializers/config'
8import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
9import { Notifier } from '../notifier'
10import { AbstractScheduler } from './abstract-scheduler'
11
12export 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'
6import { chunk } from 'lodash' 6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index' 7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' 8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier'
9 10
10export class PluginsCheckScheduler extends AbstractScheduler { 11export 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 @@
1import { join } from 'path' 1import { join } from 'path'
2
2import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
3import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
4import { processImage } from '../helpers/image-utils' 5import { generateImageFilename, processImage } from '../helpers/image-utils'
5import { downloadImage } from '../helpers/requests' 6import { downloadImage } from '../helpers/requests'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
11import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
12import { getVideoFilePath } from './video-paths' 13import { getVideoFilePath } from './video-paths'
13 14
14type ImageSize = { height: number, width: number } 15type ImageSize = { height?: number, width?: number }
15 16
16function createPlaylistMiniatureFromExisting (options: { 17function 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'
12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' 12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
13import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 13import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
14import { logger } from '../helpers/logger' 14import { logger, loggerTagsFactory } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 15import { CONFIG } from '../initializers/config'
16import { VideoBlacklistModel } from '../models/video/video-blacklist' 16import { VideoBlacklistModel } from '../models/video/video-blacklist'
17import { sendDeleteVideo } from './activitypub/send' 17import { sendDeleteVideo } from './activitypub/send'
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager'
20import { Notifier } from './notifier' 20import { Notifier } from './notifier'
21import { Hooks } from './plugins/hooks' 21import { Hooks } from './plugins/hooks'
22 22
23const lTags = loggerTagsFactory('blacklist')
24
23async function autoBlacklistVideoIfNeeded (parameters: { 25async 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'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoModel } from '../models/video/video' 4import { VideoModel } from '../models/video/video'
5import { VideoChannelModel } from '../models/video/video-channel' 5import { VideoChannelModel } from '../models/video/video-channel'
6import { MAccountId, MChannelDefault, MChannelId } from '../types/models' 6import { MAccountId, MChannelId } from '../types/models'
7import { buildActorInstance } from './activitypub/actor' 7import { buildActorInstance } from './activitypub/actor'
8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' 8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos' 9import { federateVideoIfNeeded } from './activitypub/videos'
10 10
11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } 11async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
12
13async 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