diff options
Diffstat (limited to 'server/lib')
92 files changed, 2408 insertions, 997 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 0b21de0ca..c743dcf3f 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
3 | import * as url from 'url' | 3 | import { URL } from 'url' |
4 | import * as uuidv4 from 'uuid/v4' | 4 | import { v4 as uuidv4 } from 'uuid' |
5 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 5 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
8 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' |
@@ -19,7 +19,6 @@ import { AvatarModel } from '../../models/avatar/avatar' | |||
19 | import { ServerModel } from '../../models/server/server' | 19 | import { ServerModel } from '../../models/server/server' |
20 | import { VideoChannelModel } from '../../models/video/video-channel' | 20 | import { VideoChannelModel } from '../../models/video/video-channel' |
21 | import { JobQueue } from '../job-queue' | 21 | import { JobQueue } from '../job-queue' |
22 | import { getServerActor } from '../../helpers/utils' | ||
23 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | 22 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' |
24 | import { sequelizeTypescript } from '../../initializers/database' | 23 | import { sequelizeTypescript } from '../../initializers/database' |
25 | import { | 24 | import { |
@@ -33,9 +32,10 @@ import { | |||
33 | MActorFull, | 32 | MActorFull, |
34 | MActorFullActor, | 33 | MActorFullActor, |
35 | MActorId, | 34 | MActorId, |
36 | MChannel, | 35 | MChannel |
37 | MChannelAccountDefault | ||
38 | } from '../../typings/models' | 36 | } from '../../typings/models' |
37 | import { extname } from 'path' | ||
38 | import { getServerActor } from '@server/models/application/application' | ||
39 | 39 | ||
40 | // Set account keys, this could be long so process after the account creation and do not block the client | 40 | // Set account keys, this could be long so process after the account creation and do not block the client |
41 | function setAsyncActorKeys <T extends MActor> (actor: T) { | 41 | function setAsyncActorKeys <T extends MActor> (actor: T) { |
@@ -117,17 +117,17 @@ async function getOrCreateActorAndServerAndModel ( | |||
117 | if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor | 117 | if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor |
118 | 118 | ||
119 | const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) | 119 | const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) |
120 | if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.') | 120 | if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.') |
121 | 121 | ||
122 | if ((created === true || refreshed === true) && updateCollections === true) { | 122 | if ((created === true || refreshed === true) && updateCollections === true) { |
123 | const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } | 123 | const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } |
124 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 124 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) |
125 | } | 125 | } |
126 | 126 | ||
127 | // We created a new account: fetch the playlists | 127 | // We created a new account: fetch the playlists |
128 | if (created === true && actor.Account && accountPlaylistsUrl) { | 128 | if (created === true && actor.Account && accountPlaylistsUrl) { |
129 | const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } | 129 | const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } |
130 | await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | 130 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) |
131 | } | 131 | } |
132 | 132 | ||
133 | return actorRefreshed | 133 | return actorRefreshed |
@@ -207,7 +207,7 @@ async function fetchActorTotalItems (url: string) { | |||
207 | } | 207 | } |
208 | 208 | ||
209 | try { | 209 | try { |
210 | const { body } = await doRequest(options) | 210 | const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) |
211 | return body.totalItems ? body.totalItems : 0 | 211 | return body.totalItems ? body.totalItems : 0 |
212 | } catch (err) { | 212 | } catch (err) { |
213 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) | 213 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) |
@@ -215,20 +215,28 @@ async function fetchActorTotalItems (url: string) { | |||
215 | } | 215 | } |
216 | } | 216 | } |
217 | 217 | ||
218 | async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | 218 | function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { |
219 | if ( | 219 | const mimetypes = MIMETYPES.IMAGE |
220 | actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && | 220 | const icon = actorJSON.icon |
221 | isActivityPubUrlValid(actorJSON.icon.url) | ||
222 | ) { | ||
223 | const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] | ||
224 | 221 | ||
225 | return { | 222 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined |
226 | name: uuidv4() + extension, | 223 | |
227 | fileUrl: actorJSON.icon.url | 224 | let extension: string |
228 | } | 225 | |
226 | if (icon.mediaType) { | ||
227 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
228 | } else { | ||
229 | const tmp = extname(icon.url) | ||
230 | |||
231 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | ||
229 | } | 232 | } |
230 | 233 | ||
231 | return undefined | 234 | if (!extension) return undefined |
235 | |||
236 | return { | ||
237 | name: uuidv4() + extension, | ||
238 | fileUrl: icon.url | ||
239 | } | ||
232 | } | 240 | } |
233 | 241 | ||
234 | async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { | 242 | async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { |
@@ -271,7 +279,10 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
271 | 279 | ||
272 | if (statusCode === 404) { | 280 | if (statusCode === 404) { |
273 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | 281 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) |
274 | actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() | 282 | actor.Account |
283 | ? await actor.Account.destroy() | ||
284 | : await actor.VideoChannel.destroy() | ||
285 | |||
275 | return { actor: undefined, refreshed: false } | 286 | return { actor: undefined, refreshed: false } |
276 | } | 287 | } |
277 | 288 | ||
@@ -337,14 +348,14 @@ function saveActorAndServerAndModelIfNotExist ( | |||
337 | ownerActor?: MActorFullActor, | 348 | ownerActor?: MActorFullActor, |
338 | t?: Transaction | 349 | t?: Transaction |
339 | ): Bluebird<MActorFullActor> | Promise<MActorFullActor> { | 350 | ): Bluebird<MActorFullActor> | Promise<MActorFullActor> { |
340 | let actor = result.actor | 351 | const actor = result.actor |
341 | 352 | ||
342 | if (t !== undefined) return save(t) | 353 | if (t !== undefined) return save(t) |
343 | 354 | ||
344 | return sequelizeTypescript.transaction(t => save(t)) | 355 | return sequelizeTypescript.transaction(t => save(t)) |
345 | 356 | ||
346 | async function save (t: Transaction) { | 357 | async function save (t: Transaction) { |
347 | const actorHost = url.parse(actor.url).host | 358 | const actorHost = new URL(actor.url).host |
348 | 359 | ||
349 | const serverOptions = { | 360 | const serverOptions = { |
350 | where: { | 361 | where: { |
@@ -402,7 +413,7 @@ type FetchRemoteActorResult = { | |||
402 | support?: string | 413 | support?: string |
403 | playlists?: string | 414 | playlists?: string |
404 | avatar?: { | 415 | avatar?: { |
405 | name: string, | 416 | name: string |
406 | fileUrl: string | 417 | fileUrl: string |
407 | } | 418 | } |
408 | attributedTo: ActivityPubAttributedTo[] | 419 | attributedTo: ActivityPubAttributedTo[] |
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index f2ab54cf7..551d04ae3 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts | |||
@@ -4,11 +4,11 @@ import { ACTIVITY_PUB } from '../../initializers/constants' | |||
4 | import { ActorModel } from '../../models/activitypub/actor' | 4 | import { ActorModel } from '../../models/activitypub/actor' |
5 | import { VideoModel } from '../../models/video/video' | 5 | import { VideoModel } from '../../models/video/video' |
6 | import { VideoShareModel } from '../../models/video/video-share' | 6 | import { VideoShareModel } from '../../models/video/video-share' |
7 | import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models' | 7 | import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../typings/models' |
8 | 8 | ||
9 | function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { | 9 | function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { |
10 | return { | 10 | return { |
11 | to: [ video.VideoChannel.Account.Actor.url ], | 11 | to: [ accountActor.url ], |
12 | cc: actorsInvolvedInVideo.map(a => a.followersUrl) | 12 | cc: actorsInvolvedInVideo.map(a => a.followersUrl) |
13 | } | 13 | } |
14 | } | 14 | } |
@@ -32,6 +32,8 @@ function getVideoCommentAudience ( | |||
32 | 32 | ||
33 | // Send to actors we reply to | 33 | // Send to actors we reply to |
34 | for (const parentComment of threadParentComments) { | 34 | for (const parentComment of threadParentComments) { |
35 | if (parentComment.isDeleted()) continue | ||
36 | |||
35 | cc.push(parentComment.Account.Actor.url) | 37 | cc.push(parentComment.Account.Actor.url) |
36 | } | 38 | } |
37 | 39 | ||
@@ -48,7 +50,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[ | |||
48 | } | 50 | } |
49 | } | 51 | } |
50 | 52 | ||
51 | async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { | 53 | async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) { |
52 | const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) | 54 | const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) |
53 | 55 | ||
54 | const videoAll = video as VideoModel | 56 | const videoAll = video as VideoModel |
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index 65b2dcb49..8252e95e9 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -13,7 +13,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | 13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) |
14 | 14 | ||
15 | return { | 15 | return { |
16 | expiresOn: new Date(cacheFileObject.expires), | 16 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, |
17 | url: cacheFileObject.id, | 17 | url: cacheFileObject.id, |
18 | fileUrl: url.href, | 18 | fileUrl: url.href, |
19 | strategy: null, | 19 | strategy: null, |
@@ -30,7 +30,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject | |||
30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | 30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) |
31 | 31 | ||
32 | return { | 32 | return { |
33 | expiresOn: new Date(cacheFileObject.expires), | 33 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, |
34 | url: cacheFileObject.id, | 34 | url: cacheFileObject.id, |
35 | fileUrl: url.href, | 35 | fileUrl: url.href, |
36 | strategy: null, | 36 | strategy: null, |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 9e469e3e6..eeafdf4ba 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -3,7 +3,7 @@ import { doRequest } from '../../helpers/requests' | |||
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import * as Bluebird from 'bluebird' | 4 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | import { parse } from 'url' | 6 | import { URL } from 'url' |
7 | 7 | ||
8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) | 8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) |
9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) | 9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) |
@@ -24,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
24 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) | 24 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) |
25 | const firstBody = response.body | 25 | const firstBody = response.body |
26 | 26 | ||
27 | let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT | 27 | const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT |
28 | let i = 0 | 28 | let i = 0 |
29 | let nextLink = firstBody.first | 29 | let nextLink = firstBody.first |
30 | while (nextLink && i < limit) { | 30 | while (nextLink && i < limit) { |
@@ -32,7 +32,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
32 | 32 | ||
33 | if (typeof nextLink === 'string') { | 33 | if (typeof nextLink === 'string') { |
34 | // Don't crawl ourselves | 34 | // Don't crawl ourselves |
35 | const remoteHost = parse(nextLink).host | 35 | const remoteHost = new URL(nextLink).host |
36 | if (remoteHost === WEBSERVER.HOST) continue | 36 | if (remoteHost === WEBSERVER.HOST) continue |
37 | 37 | ||
38 | options.uri = nextLink | 38 | options.uri = nextLink |
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts index 1abf43cd4..3b5ad47c9 100644 --- a/server/lib/activitypub/follow.ts +++ b/server/lib/activitypub/follow.ts | |||
@@ -3,8 +3,8 @@ import { CONFIG } from '../../initializers/config' | |||
3 | import { SERVER_ACTOR_NAME } from '../../initializers/constants' | 3 | import { SERVER_ACTOR_NAME } from '../../initializers/constants' |
4 | import { JobQueue } from '../job-queue' | 4 | import { JobQueue } from '../job-queue' |
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../helpers/logger' |
6 | import { getServerActor } from '../../helpers/utils' | ||
7 | import { ServerModel } from '../../models/server/server' | 6 | import { ServerModel } from '../../models/server/server' |
7 | import { getServerActor } from '@server/models/application/application' | ||
8 | 8 | ||
9 | async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { | 9 | async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { |
10 | if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return | 10 | if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return |
@@ -27,7 +27,6 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { | |||
27 | } | 27 | } |
28 | 28 | ||
29 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | 29 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) |
30 | .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err)) | ||
31 | } | 30 | } |
32 | } | 31 | } |
33 | 32 | ||
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts deleted file mode 100644 index d8c7d83b7..000000000 --- a/server/lib/activitypub/index.ts +++ /dev/null | |||
@@ -1,9 +0,0 @@ | |||
1 | export * from './process' | ||
2 | export * from './send' | ||
3 | export * from './actor' | ||
4 | export * from './share' | ||
5 | export * from './playlist' | ||
6 | export * from './videos' | ||
7 | export * from './video-comments' | ||
8 | export * from './video-rates' | ||
9 | export * from './url' | ||
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index c52b715ef..c1d932a68 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -20,7 +20,9 @@ import { MAccountDefault, MAccountId, MVideoId } from '../../typings/models' | |||
20 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist' | 20 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist' |
21 | 21 | ||
22 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 22 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { |
23 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED | 23 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
24 | ? VideoPlaylistPrivacy.PUBLIC | ||
25 | : VideoPlaylistPrivacy.UNLISTED | ||
24 | 26 | ||
25 | return { | 27 | return { |
26 | name: playlistObject.name, | 28 | name: playlistObject.name, |
@@ -205,7 +207,7 @@ async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusC | |||
205 | 207 | ||
206 | logger.info('Fetching remote playlist %s.', playlistUrl) | 208 | logger.info('Fetching remote playlist %s.', playlistUrl) |
207 | 209 | ||
208 | const { response, body } = await doRequest(options) | 210 | const { response, body } = await doRequest<any>(options) |
209 | 211 | ||
210 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | 212 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { |
211 | logger.debug('Remote video playlist JSON is not valid.', { body }) | 213 | logger.debug('Remote video playlist JSON is not valid.', { body }) |
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index 7e22125d5..26427aaa1 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ActivityAnnounce } from '../../../../shared/models/activitypub' | 1 | import { ActivityAnnounce } from '../../../../shared/models/activitypub' |
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { sequelizeTypescript } from '../../../initializers' | 3 | import { sequelizeTypescript } from '../../../initializers/database' |
4 | import { VideoShareModel } from '../../../models/video/video-share' | 4 | import { VideoShareModel } from '../../../models/video/video-share' |
5 | import { forwardVideoRelatedActivity } from '../send/utils' | 5 | import { forwardVideoRelatedActivity } from '../send/utils' |
6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index bee853721..566bf6992 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -2,7 +2,7 @@ import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../.. | |||
2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' | 2 | import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { resolveThread } from '../video-comments' | 6 | import { resolveThread } from '../video-comments' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { forwardVideoRelatedActivity } from '../send/utils' | 8 | import { forwardVideoRelatedActivity } from '../send/utils' |
@@ -12,6 +12,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl | |||
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | 12 | import { createOrUpdateVideoPlaylist } from '../playlist' |
13 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 13 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
14 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' | 14 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' |
15 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
15 | 16 | ||
16 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 17 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { |
17 | const { activity, byActor } = options | 18 | const { activity, byActor } = options |
@@ -60,6 +61,8 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | |||
60 | } | 61 | } |
61 | 62 | ||
62 | async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { | 63 | async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { |
64 | if (await isRedundancyAccepted(activity, byActor) !== true) return | ||
65 | |||
63 | const cacheFile = activity.object as CacheFileObject | 66 | const cacheFile = activity.object as CacheFileObject |
64 | 67 | ||
65 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 68 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) |
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index e76132f91..7c8dc83e8 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { ActivityDelete } from '../../../../shared/models/activitypub' | 1 | import { ActivityDelete } from '../../../../shared/models/activitypub' |
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { sequelizeTypescript } from '../../../initializers' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts index debd8a67c..fcdd0b86e 100644 --- a/server/lib/activitypub/process/process-dislike.ts +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { ActivityCreate, ActivityDislike } from '../../../../shared' | 1 | import { ActivityCreate, ActivityDislike } from '../../../../shared' |
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | 2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { sequelizeTypescript } from '../../../initializers' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
7 | import { forwardVideoRelatedActivity } from '../send/utils' | 7 | import { forwardVideoRelatedActivity } from '../send/utils' |
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index e6e9084de..7337f337c 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts | |||
@@ -2,13 +2,14 @@ import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../share | |||
2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' | 2 | import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 6 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { Notifier } from '../../notifier' | 8 | import { Notifier } from '../../notifier' |
9 | import { getAPId } from '../../../helpers/activitypub' | 9 | import { getAPId } from '../../../helpers/activitypub' |
10 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 10 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
11 | import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models' | 11 | import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models' |
12 | import { AccountModel } from '@server/models/account/account' | ||
12 | 13 | ||
13 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { | 14 | async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { |
14 | const { activity, byActor } = options | 15 | const { activity, byActor } = options |
@@ -36,8 +37,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, | |||
36 | logger.debug('Reporting remote abuse for video %s.', getAPId(object)) | 37 | logger.debug('Reporting remote abuse for video %s.', getAPId(object)) |
37 | 38 | ||
38 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) | 39 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) |
40 | const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t)) | ||
39 | 41 | ||
40 | const videoAbuse = await sequelizeTypescript.transaction(async t => { | 42 | const videoAbuseInstance = await sequelizeTypescript.transaction(async t => { |
41 | const videoAbuseData = { | 43 | const videoAbuseData = { |
42 | reporterAccountId: account.id, | 44 | reporterAccountId: account.id, |
43 | reason: flag.content, | 45 | reason: flag.content, |
@@ -45,15 +47,22 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, | |||
45 | state: VideoAbuseState.PENDING | 47 | state: VideoAbuseState.PENDING |
46 | } | 48 | } |
47 | 49 | ||
48 | const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo | 50 | const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) |
49 | videoAbuseInstance.Video = video | 51 | videoAbuseInstance.Video = video |
52 | videoAbuseInstance.Account = reporterAccount | ||
50 | 53 | ||
51 | logger.info('Remote abuse for video uuid %s created', flag.object) | 54 | logger.info('Remote abuse for video uuid %s created', flag.object) |
52 | 55 | ||
53 | return videoAbuseInstance | 56 | return videoAbuseInstance |
54 | }) | 57 | }) |
55 | 58 | ||
56 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) | 59 | const videoAbuseJSON = videoAbuseInstance.toFormattedJSON() |
60 | |||
61 | Notifier.Instance.notifyOnNewVideoAbuse({ | ||
62 | videoAbuse: videoAbuseJSON, | ||
63 | videoAbuseInstance, | ||
64 | reporter: reporterAccount.Actor.getIdentifier() | ||
65 | }) | ||
57 | } catch (err) { | 66 | } catch (err) { |
58 | logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) | 67 | logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) |
59 | } | 68 | } |
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index db7fb8568..950d421dd 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts | |||
@@ -1,17 +1,17 @@ | |||
1 | import { ActivityFollow } from '../../../../shared/models/activitypub' | 1 | import { ActivityFollow } from '../../../../shared/models/activitypub' |
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { sequelizeTypescript } from '../../../initializers' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
7 | import { sendAccept, sendReject } from '../send' | 7 | import { sendAccept, sendReject } from '../send' |
8 | import { Notifier } from '../../notifier' | 8 | import { Notifier } from '../../notifier' |
9 | import { getAPId } from '../../../helpers/activitypub' | 9 | import { getAPId } from '../../../helpers/activitypub' |
10 | import { getServerActor } from '../../../helpers/utils' | ||
11 | import { CONFIG } from '../../../initializers/config' | 10 | import { CONFIG } from '../../../initializers/config' |
12 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
13 | import { MActorFollowActors, MActorSignature } from '../../../typings/models' | 12 | import { MActorFollowActors, MActorSignature } from '../../../typings/models' |
14 | import { autoFollowBackIfNeeded } from '../follow' | 13 | import { autoFollowBackIfNeeded } from '../follow' |
14 | import { getServerActor } from '@server/models/application/application' | ||
15 | 15 | ||
16 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { | 16 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { |
17 | const { activity, byActor } = options | 17 | const { activity, byActor } = options |
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index 62be0de42..fba3c76a4 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ActivityLike } from '../../../../shared/models/activitypub' | 1 | import { ActivityLike } from '../../../../shared/models/activitypub' |
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { sequelizeTypescript } from '../../../initializers' | 3 | import { sequelizeTypescript } from '../../../initializers/database' |
4 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 4 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
5 | import { forwardVideoRelatedActivity } from '../send/utils' | 5 | import { forwardVideoRelatedActivity } from '../send/utils' |
6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts index 00e9afa10..9804436a2 100644 --- a/server/lib/activitypub/process/process-reject.ts +++ b/server/lib/activitypub/process/process-reject.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { ActivityReject } from '../../../../shared/models/activitypub/activity' | 1 | import { ActivityReject } from '../../../../shared/models/activitypub/activity' |
2 | import { sequelizeTypescript } from '../../../initializers' | 2 | import { sequelizeTypescript } from '../../../initializers/database' |
3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
4 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 4 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
5 | import { MActor } from '../../../typings/models' | 5 | import { MActor } from '../../../typings/models' |
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 10643b2e9..9ef6a8a97 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -2,7 +2,7 @@ import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFile | |||
2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' | 2 | import { DislikeObject } from '../../../../shared/models/activitypub/objects' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 6 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index a47d605d8..98ab0f83d 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -2,7 +2,7 @@ import { ActivityUpdate, CacheFileObject, VideoTorrentObject } from '../../../.. | |||
2 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 2 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
3 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 3 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers' | 5 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { AccountModel } from '../../../models/account/account' | 6 | import { AccountModel } from '../../../models/account/account' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
@@ -16,6 +16,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl | |||
16 | import { createOrUpdateVideoPlaylist } from '../playlist' | 16 | import { createOrUpdateVideoPlaylist } from '../playlist' |
17 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 17 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
18 | import { MActorSignature, MAccountIdActor } from '../../../typings/models' | 18 | import { MActorSignature, MAccountIdActor } from '../../../typings/models' |
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
19 | 20 | ||
20 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 21 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { |
21 | const { activity, byActor } = options | 22 | const { activity, byActor } = options |
@@ -78,6 +79,8 @@ async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpd | |||
78 | } | 79 | } |
79 | 80 | ||
80 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { | 81 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { |
82 | if (await isRedundancyAccepted(activity, byActor) !== true) return | ||
83 | |||
81 | const cacheFileObject = activity.object as CacheFileObject | 84 | const cacheFileObject = activity.object as CacheFileObject |
82 | 85 | ||
83 | if (!isCacheFileObjectValid(cacheFileObject)) { | 86 | if (!isCacheFileObjectValid(cacheFileObject)) { |
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts index df29ee968..b3b6c933d 100644 --- a/server/lib/activitypub/process/process-view.ts +++ b/server/lib/activitypub/process/process-view.ts | |||
@@ -23,7 +23,8 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct | |||
23 | 23 | ||
24 | const options = { | 24 | const options = { |
25 | videoObject, | 25 | videoObject, |
26 | fetchType: 'only-video' as 'only-video' | 26 | fetchType: 'only-immutable-attributes' as 'only-immutable-attributes', |
27 | allowRefresh: false as false | ||
27 | } | 28 | } |
28 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | 29 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) |
29 | 30 | ||
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts index 9f0225b64..c4c6b849b 100644 --- a/server/lib/activitypub/send/send-accept.ts +++ b/server/lib/activitypub/send/send-accept.ts | |||
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow' | |||
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { MActor, MActorFollowActors } from '../../../typings/models' | 6 | import { MActor, MActorFollowActors } from '../../../typings/models' |
7 | 7 | ||
8 | async function sendAccept (actorFollow: MActorFollowActors) { | 8 | function sendAccept (actorFollow: MActorFollowActors) { |
9 | const follower = actorFollow.ActorFollower | 9 | const follower = actorFollow.ActorFollower |
10 | const me = actorFollow.ActorFollowing | 10 | const me = actorFollow.ActorFollowing |
11 | 11 | ||
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index a0f33852c..d03b358f1 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts | |||
@@ -28,7 +28,7 @@ async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, | |||
28 | logger.info('Creating job to send announce %s.', videoShare.url) | 28 | logger.info('Creating job to send announce %s.', videoShare.url) |
29 | 29 | ||
30 | const followersException = [ byActor ] | 30 | const followersException = [ byActor ] |
31 | return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException) | 31 | return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException, 'Announce') |
32 | } | 32 | } |
33 | 33 | ||
34 | function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { | 34 | function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 1709d8348..e521cabbc 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -6,7 +6,6 @@ import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unic | |||
6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { getServerActor } from '../../../helpers/utils' | ||
10 | import { | 9 | import { |
11 | MActorLight, | 10 | MActorLight, |
12 | MCommentOwnerVideo, | 11 | MCommentOwnerVideo, |
@@ -16,6 +15,8 @@ import { | |||
16 | MVideoRedundancyFileVideo, | 15 | MVideoRedundancyFileVideo, |
17 | MVideoRedundancyStreamingPlaylistVideo | 16 | MVideoRedundancyStreamingPlaylistVideo |
18 | } from '../../../typings/models' | 17 | } from '../../../typings/models' |
18 | import { getServerActor } from '@server/models/application/application' | ||
19 | import { ContextType } from '@shared/models/activitypub/context' | ||
19 | 20 | ||
20 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 21 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { |
21 | if (!video.hasPrivacyForFederation()) return undefined | 22 | if (!video.hasPrivacyForFederation()) return undefined |
@@ -42,7 +43,8 @@ async function sendCreateCacheFile ( | |||
42 | byActor, | 43 | byActor, |
43 | video, | 44 | video, |
44 | url: fileRedundancy.url, | 45 | url: fileRedundancy.url, |
45 | object: fileRedundancy.toActivityPubObject() | 46 | object: fileRedundancy.toActivityPubObject(), |
47 | contextType: 'CacheFile' | ||
46 | }) | 48 | }) |
47 | } | 49 | } |
48 | 50 | ||
@@ -78,7 +80,8 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transacti | |||
78 | // Add the actor that commented too | 80 | // Add the actor that commented too |
79 | actorsInvolvedInComment.push(byActor) | 81 | actorsInvolvedInComment.push(byActor) |
80 | 82 | ||
81 | const parentsCommentActors = threadParentComments.map(c => c.Account.Actor) | 83 | const parentsCommentActors = threadParentComments.filter(c => !c.isDeleted()) |
84 | .map(c => c.Account.Actor) | ||
82 | 85 | ||
83 | let audience: ActivityAudience | 86 | let audience: ActivityAudience |
84 | if (isOrigin) { | 87 | if (isOrigin) { |
@@ -130,11 +133,12 @@ export { | |||
130 | // --------------------------------------------------------------------------- | 133 | // --------------------------------------------------------------------------- |
131 | 134 | ||
132 | async function sendVideoRelatedCreateActivity (options: { | 135 | async function sendVideoRelatedCreateActivity (options: { |
133 | byActor: MActorLight, | 136 | byActor: MActorLight |
134 | video: MVideoAccountLight, | 137 | video: MVideoAccountLight |
135 | url: string, | 138 | url: string |
136 | object: any, | 139 | object: any |
137 | transaction?: Transaction | 140 | transaction?: Transaction |
141 | contextType?: ContextType | ||
138 | }) { | 142 | }) { |
139 | const activityBuilder = (audience: ActivityAudience) => { | 143 | const activityBuilder = (audience: ActivityAudience) => { |
140 | return buildCreateActivity(options.url, options.byActor, options.object, audience) | 144 | return buildCreateActivity(options.url, options.byActor, options.object, audience) |
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index 3225ebf32..fd3f06dec 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts | |||
@@ -7,9 +7,9 @@ import { getDeleteActivityPubUrl } from '../url' | |||
7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 7 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
8 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' | 8 | import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' |
9 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
10 | import { getServerActor } from '../../../helpers/utils' | ||
11 | import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' | 10 | import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' |
12 | import { MActorUrl } from '../../../typings/models' | 11 | import { MActorUrl } from '../../../typings/models' |
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 13 | ||
14 | async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { | 14 | async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { |
15 | logger.info('Creating job to broadcast delete of video %s.', video.url) | 15 | logger.info('Creating job to broadcast delete of video %s.', video.url) |
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts index 6e41f241f..600469c71 100644 --- a/server/lib/activitypub/send/send-dislike.ts +++ b/server/lib/activitypub/send/send-dislike.ts | |||
@@ -6,7 +6,7 @@ import { sendVideoRelatedActivity } from './utils' | |||
6 | import { audiencify, getAudience } from '../audience' | 6 | import { audiencify, getAudience } from '../audience' |
7 | import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' | 7 | import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' |
8 | 8 | ||
9 | async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { | 9 | function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { |
10 | logger.info('Creating job to dislike %s.', video.url) | 10 | logger.info('Creating job to dislike %s.', video.url) |
11 | 11 | ||
12 | const activityBuilder = (audience: ActivityAudience) => { | 12 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts index da7638a7b..e4e523631 100644 --- a/server/lib/activitypub/send/send-flag.ts +++ b/server/lib/activitypub/send/send-flag.ts | |||
@@ -7,7 +7,7 @@ import { Transaction } from 'sequelize' | |||
7 | import { MActor, MVideoFullLight } from '../../../typings/models' | 7 | import { MActor, MVideoFullLight } from '../../../typings/models' |
8 | import { MVideoAbuseVideo } from '../../../typings/models/video' | 8 | import { MVideoAbuseVideo } from '../../../typings/models/video' |
9 | 9 | ||
10 | async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { | 10 | function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { |
11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user | 11 | if (!video.VideoChannel.Account.Actor.serverId) return // Local user |
12 | 12 | ||
13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) | 13 | const url = getVideoAbuseActivityPubUrl(videoAbuse) |
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index e84a6f98b..5db252325 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts | |||
@@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience' | |||
6 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' | 7 | import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' |
8 | 8 | ||
9 | async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { | 9 | function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { |
10 | logger.info('Creating job to like %s.', video.url) | 10 | logger.info('Creating job to like %s.', video.url) |
11 | 11 | ||
12 | const activityBuilder = (audience: ActivityAudience) => { | 12 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts index 4258a3c36..643c468a9 100644 --- a/server/lib/activitypub/send/send-reject.ts +++ b/server/lib/activitypub/send/send-reject.ts | |||
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow' | |||
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { MActor } from '../../../typings/models' | 6 | import { MActor } from '../../../typings/models' |
7 | 7 | ||
8 | async function sendReject (follower: MActor, following: MActor) { | 8 | function sendReject (follower: MActor, following: MActor) { |
9 | if (!follower.serverId) { // This should never happen | 9 | if (!follower.serverId) { // This should never happen |
10 | logger.warn('Do not sending reject to local follower.') | 10 | logger.warn('Do not sending reject to local follower.') |
11 | return | 11 | return |
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index e9ab5b3c5..33f1d4921 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts | |||
@@ -28,7 +28,7 @@ import { | |||
28 | MVideoShare | 28 | MVideoShare |
29 | } from '../../../typings/models' | 29 | } from '../../../typings/models' |
30 | 30 | ||
31 | async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { | 31 | function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { |
32 | const me = actorFollow.ActorFollower | 32 | const me = actorFollow.ActorFollower |
33 | const following = actorFollow.ActorFollowing | 33 | const following = actorFollow.ActorFollowing |
34 | 34 | ||
@@ -118,10 +118,10 @@ function undoActivityData ( | |||
118 | } | 118 | } |
119 | 119 | ||
120 | async function sendUndoVideoRelatedActivity (options: { | 120 | async function sendUndoVideoRelatedActivity (options: { |
121 | byActor: MActor, | 121 | byActor: MActor |
122 | video: MVideoAccountLight, | 122 | video: MVideoAccountLight |
123 | url: string, | 123 | url: string |
124 | activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, | 124 | activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce |
125 | transaction: Transaction | 125 | transaction: Transaction |
126 | }) { | 126 | }) { |
127 | const activityBuilder = (audience: ActivityAudience) => { | 127 | const activityBuilder = (audience: ActivityAudience) => { |
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 9c76671b5..7a4cf3f56 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts | |||
@@ -8,9 +8,7 @@ import { getUpdateActivityPubUrl } from '../url' | |||
8 | import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' | 8 | import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' |
9 | import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' | 9 | import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' |
10 | import { logger } from '../../../helpers/logger' | 10 | import { logger } from '../../../helpers/logger' |
11 | import { VideoCaptionModel } from '../../../models/video/video-caption' | ||
12 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 11 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
13 | import { getServerActor } from '../../../helpers/utils' | ||
14 | import { | 12 | import { |
15 | MAccountDefault, | 13 | MAccountDefault, |
16 | MActor, | 14 | MActor, |
@@ -21,6 +19,7 @@ import { | |||
21 | MVideoPlaylistFull, | 19 | MVideoPlaylistFull, |
22 | MVideoRedundancyVideo | 20 | MVideoRedundancyVideo |
23 | } from '../../../typings/models' | 21 | } from '../../../typings/models' |
22 | import { getServerActor } from '@server/models/application/application' | ||
24 | 23 | ||
25 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { | 24 | async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { |
26 | const video = videoArg as MVideoAP | 25 | const video = videoArg as MVideoAP |
@@ -29,7 +28,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction | |||
29 | 28 | ||
30 | logger.info('Creating job to update video %s.', video.url) | 29 | logger.info('Creating job to update video %s.', video.url) |
31 | 30 | ||
32 | const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor | 31 | const byActor = overrodeByActor || video.VideoChannel.Account.Actor |
33 | 32 | ||
34 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) | 33 | const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) |
35 | 34 | ||
@@ -85,7 +84,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide | |||
85 | return buildUpdateActivity(url, byActor, redundancyObject, audience) | 84 | return buildUpdateActivity(url, byActor, redundancyObject, audience) |
86 | } | 85 | } |
87 | 86 | ||
88 | return sendVideoRelatedActivity(activityBuilder, { byActor, video }) | 87 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' }) |
89 | } | 88 | } |
90 | 89 | ||
91 | async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { | 90 | async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts index 8809417f9..1f864ea52 100644 --- a/server/lib/activitypub/send/send-view.ts +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -5,9 +5,9 @@ import { getVideoLikeActivityPubUrl } from '../url' | |||
5 | import { sendVideoRelatedActivity } from './utils' | 5 | import { sendVideoRelatedActivity } from './utils' |
6 | import { audiencify, getAudience } from '../audience' | 6 | import { audiencify, getAudience } from '../audience' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger } from '../../../helpers/logger' |
8 | import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models' | 8 | import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/typings/models' |
9 | 9 | ||
10 | async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) { | 10 | async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) { |
11 | logger.info('Creating job to send view of %s.', video.url) | 11 | logger.info('Creating job to send view of %s.', video.url) |
12 | 12 | ||
13 | const activityBuilder = (audience: ActivityAudience) => { | 13 | const activityBuilder = (audience: ActivityAudience) => { |
@@ -16,7 +16,7 @@ async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Tran | |||
16 | return buildViewActivity(url, byActor, video, audience) | 16 | return buildViewActivity(url, byActor, video, audience) |
17 | } | 17 | } |
18 | 18 | ||
19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) | 19 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t, contextType: 'View' }) |
20 | } | 20 | } |
21 | 21 | ||
22 | function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { | 22 | function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { |
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts index 77b723479..44a8926e5 100644 --- a/server/lib/activitypub/send/utils.ts +++ b/server/lib/activitypub/send/utils.ts | |||
@@ -5,26 +5,30 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
6 | import { JobQueue } from '../../job-queue' | 6 | import { JobQueue } from '../../job-queue' |
7 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' | 7 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' |
8 | import { getServerActor } from '../../../helpers/utils' | ||
9 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' | 8 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' |
10 | import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' | 9 | import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models' |
10 | import { getServerActor } from '@server/models/application/application' | ||
11 | import { ContextType } from '@shared/models/activitypub/context' | ||
11 | 12 | ||
12 | async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { | 13 | async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { |
13 | byActor: MActorLight, | 14 | byActor: MActorLight |
14 | video: MVideoAccountLight, | 15 | video: MVideoImmutable | MVideoAccountLight |
15 | transaction?: Transaction | 16 | transaction?: Transaction |
17 | contextType?: ContextType | ||
16 | }) { | 18 | }) { |
17 | const { byActor, video, transaction } = options | 19 | const { byActor, video, transaction, contextType } = options |
18 | 20 | ||
19 | const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) | 21 | const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) |
20 | 22 | ||
21 | // Send to origin | 23 | // Send to origin |
22 | if (video.isOwned() === false) { | 24 | if (video.isOwned() === false) { |
23 | const audience = getRemoteVideoAudience(video, actorsInvolvedInVideo) | 25 | const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id) |
26 | |||
27 | const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo) | ||
24 | const activity = activityBuilder(audience) | 28 | const activity = activityBuilder(audience) |
25 | 29 | ||
26 | return afterCommitIfTransaction(transaction, () => { | 30 | return afterCommitIfTransaction(transaction, () => { |
27 | return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()) | 31 | return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType) |
28 | }) | 32 | }) |
29 | } | 33 | } |
30 | 34 | ||
@@ -34,14 +38,14 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud | |||
34 | 38 | ||
35 | const actorsException = [ byActor ] | 39 | const actorsException = [ byActor ] |
36 | 40 | ||
37 | return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException) | 41 | return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType) |
38 | } | 42 | } |
39 | 43 | ||
40 | async function forwardVideoRelatedActivity ( | 44 | async function forwardVideoRelatedActivity ( |
41 | activity: Activity, | 45 | activity: Activity, |
42 | t: Transaction, | 46 | t: Transaction, |
43 | followersException: MActorWithInboxes[] = [], | 47 | followersException: MActorWithInboxes[], |
44 | video: MVideo | 48 | video: MVideoId |
45 | ) { | 49 | ) { |
46 | // Mastodon does not add our announces in audience, so we forward to them manually | 50 | // Mastodon does not add our announces in audience, so we forward to them manually |
47 | const additionalActors = await getActorsInvolvedInVideo(video, t) | 51 | const additionalActors = await getActorsInvolvedInVideo(video, t) |
@@ -90,11 +94,12 @@ async function broadcastToFollowers ( | |||
90 | byActor: MActorId, | 94 | byActor: MActorId, |
91 | toFollowersOf: MActorId[], | 95 | toFollowersOf: MActorId[], |
92 | t: Transaction, | 96 | t: Transaction, |
93 | actorsException: MActorWithInboxes[] = [] | 97 | actorsException: MActorWithInboxes[] = [], |
98 | contextType?: ContextType | ||
94 | ) { | 99 | ) { |
95 | const uris = await computeFollowerUris(toFollowersOf, actorsException, t) | 100 | const uris = await computeFollowerUris(toFollowersOf, actorsException, t) |
96 | 101 | ||
97 | return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) | 102 | return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) |
98 | } | 103 | } |
99 | 104 | ||
100 | async function broadcastToActors ( | 105 | async function broadcastToActors ( |
@@ -102,13 +107,14 @@ async function broadcastToActors ( | |||
102 | byActor: MActorId, | 107 | byActor: MActorId, |
103 | toActors: MActor[], | 108 | toActors: MActor[], |
104 | t?: Transaction, | 109 | t?: Transaction, |
105 | actorsException: MActorWithInboxes[] = [] | 110 | actorsException: MActorWithInboxes[] = [], |
111 | contextType?: ContextType | ||
106 | ) { | 112 | ) { |
107 | const uris = await computeUris(toActors, actorsException) | 113 | const uris = await computeUris(toActors, actorsException) |
108 | return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) | 114 | return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) |
109 | } | 115 | } |
110 | 116 | ||
111 | function broadcastTo (uris: string[], data: any, byActor: MActorId) { | 117 | function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) { |
112 | if (uris.length === 0) return undefined | 118 | if (uris.length === 0) return undefined |
113 | 119 | ||
114 | logger.debug('Creating broadcast job.', { uris }) | 120 | logger.debug('Creating broadcast job.', { uris }) |
@@ -116,19 +122,21 @@ function broadcastTo (uris: string[], data: any, byActor: MActorId) { | |||
116 | const payload = { | 122 | const payload = { |
117 | uris, | 123 | uris, |
118 | signatureActorId: byActor.id, | 124 | signatureActorId: byActor.id, |
119 | body: data | 125 | body: data, |
126 | contextType | ||
120 | } | 127 | } |
121 | 128 | ||
122 | return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) | 129 | return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) |
123 | } | 130 | } |
124 | 131 | ||
125 | function unicastTo (data: any, byActor: MActorId, toActorUrl: string) { | 132 | function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) { |
126 | logger.debug('Creating unicast job.', { uri: toActorUrl }) | 133 | logger.debug('Creating unicast job.', { uri: toActorUrl }) |
127 | 134 | ||
128 | const payload = { | 135 | const payload = { |
129 | uri: toActorUrl, | 136 | uri: toActorUrl, |
130 | signatureActorId: byActor.id, | 137 | signatureActorId: byActor.id, |
131 | body: data | 138 | body: data, |
139 | contextType | ||
132 | } | 140 | } |
133 | 141 | ||
134 | JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) | 142 | JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) |
@@ -153,7 +161,7 @@ async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: | |||
153 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) | 161 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) |
154 | const sharedInboxesException = await buildSharedInboxesException(actorsException) | 162 | const sharedInboxesException = await buildSharedInboxesException(actorsException) |
155 | 163 | ||
156 | return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) | 164 | return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) |
157 | } | 165 | } |
158 | 166 | ||
159 | async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { | 167 | async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { |
@@ -166,7 +174,7 @@ async function computeUris (toActors: MActor[], actorsException: MActorWithInbox | |||
166 | 174 | ||
167 | const sharedInboxesException = await buildSharedInboxesException(actorsException) | 175 | const sharedInboxesException = await buildSharedInboxesException(actorsException) |
168 | return Array.from(toActorSharedInboxesSet) | 176 | return Array.from(toActorSharedInboxesSet) |
169 | .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) | 177 | .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false) |
170 | } | 178 | } |
171 | 179 | ||
172 | async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { | 180 | async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index e847c4b7d..d2cbc59a8 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '../../helpers/utils' | ||
3 | import { VideoShareModel } from '../../models/video/video-share' | 2 | import { VideoShareModel } from '../../models/video/video-share' |
4 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 3 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
5 | import { getVideoAnnounceActivityPubUrl } from './url' | 4 | import { getVideoAnnounceActivityPubUrl } from './url' |
@@ -10,6 +9,7 @@ import { logger } from '../../helpers/logger' | |||
10 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 9 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
12 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' | 11 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' |
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 13 | ||
14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { | 14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { |
15 | if (!video.hasPrivacyForFederation()) return undefined | 15 | if (!video.hasPrivacyForFederation()) return undefined |
@@ -36,7 +36,7 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) { | |||
36 | await Bluebird.map(shareUrls, async shareUrl => { | 36 | await Bluebird.map(shareUrls, async shareUrl => { |
37 | try { | 37 | try { |
38 | // Fetch url | 38 | // Fetch url |
39 | const { body } = await doRequest({ | 39 | const { body } = await doRequest<any>({ |
40 | uri: shareUrl, | 40 | uri: shareUrl, |
41 | json: true, | 41 | json: true, |
42 | activityPub: true | 42 | activityPub: true |
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index d5c078a29..3aee6799e 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -10,9 +10,9 @@ import { checkUrlsSameHost } from '../../helpers/activitypub' | |||
10 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video' | 10 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video' |
11 | 11 | ||
12 | type ResolveThreadParams = { | 12 | type ResolveThreadParams = { |
13 | url: string, | 13 | url: string |
14 | comments?: MCommentOwner[], | 14 | comments?: MCommentOwner[] |
15 | isVideo?: boolean, | 15 | isVideo?: boolean |
16 | commentCreated?: boolean | 16 | commentCreated?: boolean |
17 | } | 17 | } |
18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> | 18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> |
@@ -28,7 +28,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult | |||
28 | if (params.commentCreated === undefined) params.commentCreated = false | 28 | if (params.commentCreated === undefined) params.commentCreated = false |
29 | if (params.comments === undefined) params.comments = [] | 29 | if (params.comments === undefined) params.comments = [] |
30 | 30 | ||
31 | // Already have this comment? | 31 | // Already have this comment? |
32 | if (isVideo !== true) { | 32 | if (isVideo !== true) { |
33 | const result = await resolveCommentFromDB(params) | 33 | const result = await resolveCommentFromDB(params) |
34 | if (result) return result | 34 | if (result) return result |
@@ -87,7 +87,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) { | |||
87 | 87 | ||
88 | let resultComment: MCommentOwnerVideo | 88 | let resultComment: MCommentOwnerVideo |
89 | if (comments.length !== 0) { | 89 | if (comments.length !== 0) { |
90 | const firstReply = comments[ comments.length - 1 ] as MCommentOwnerVideo | 90 | const firstReply = comments[comments.length - 1] as MCommentOwnerVideo |
91 | firstReply.inReplyToCommentId = null | 91 | firstReply.inReplyToCommentId = null |
92 | firstReply.originCommentId = null | 92 | firstReply.originCommentId = null |
93 | firstReply.videoId = video.id | 93 | firstReply.videoId = video.id |
@@ -97,9 +97,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) { | |||
97 | comments[comments.length - 1] = await firstReply.save() | 97 | comments[comments.length - 1] = await firstReply.save() |
98 | 98 | ||
99 | for (let i = comments.length - 2; i >= 0; i--) { | 99 | for (let i = comments.length - 2; i >= 0; i--) { |
100 | const comment = comments[ i ] as MCommentOwnerVideo | 100 | const comment = comments[i] as MCommentOwnerVideo |
101 | comment.originCommentId = firstReply.id | 101 | comment.originCommentId = firstReply.id |
102 | comment.inReplyToCommentId = comments[ i + 1 ].id | 102 | comment.inReplyToCommentId = comments[i + 1].id |
103 | comment.videoId = video.id | 103 | comment.videoId = video.id |
104 | comment.changed('updatedAt', true) | 104 | comment.changed('updatedAt', true) |
105 | comment.Video = video | 105 | comment.Video = video |
@@ -120,7 +120,7 @@ async function resolveParentComment (params: ResolveThreadParams) { | |||
120 | throw new Error('Recursion limit reached when resolving a thread') | 120 | throw new Error('Recursion limit reached when resolving a thread') |
121 | } | 121 | } |
122 | 122 | ||
123 | const { body } = await doRequest({ | 123 | const { body } = await doRequest<any>({ |
124 | uri: url, | 124 | uri: url, |
125 | json: true, | 125 | json: true, |
126 | activityPub: true | 126 | activityPub: true |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 6bd46bb58..202368c8f 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -18,7 +18,7 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy | |||
18 | await Bluebird.map(ratesUrl, async rateUrl => { | 18 | await Bluebird.map(ratesUrl, async rateUrl => { |
19 | try { | 19 | try { |
20 | // Fetch url | 20 | // Fetch url |
21 | const { body } = await doRequest({ | 21 | const { body } = await doRequest<any>({ |
22 | uri: rateUrl, | 22 | uri: rateUrl, |
23 | json: true, | 23 | json: true, |
24 | activityPub: true | 24 | activityPub: true |
@@ -58,8 +58,6 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy | |||
58 | const field = rate === 'like' ? 'likes' : 'dislikes' | 58 | const field = rate === 'like' ? 'likes' : 'dislikes' |
59 | await video.increment(field, { by: rateCounts }) | 59 | await video.increment(field, { by: rateCounts }) |
60 | } | 60 | } |
61 | |||
62 | return | ||
63 | } | 61 | } |
64 | 62 | ||
65 | async function sendVideoRateChange ( | 63 | async function sendVideoRateChange ( |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index ade93150f..7d16bd390 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -6,25 +6,27 @@ import { | |||
6 | ActivityHashTagObject, | 6 | ActivityHashTagObject, |
7 | ActivityMagnetUrlObject, | 7 | ActivityMagnetUrlObject, |
8 | ActivityPlaylistSegmentHashesObject, | 8 | ActivityPlaylistSegmentHashesObject, |
9 | ActivityPlaylistUrlObject, ActivityTagObject, | 9 | ActivityPlaylistUrlObject, ActivitypubHttpFetcherPayload, |
10 | ActivityTagObject, | ||
10 | ActivityUrlObject, | 11 | ActivityUrlObject, |
11 | ActivityVideoUrlObject, | 12 | ActivityVideoUrlObject, |
12 | VideoState | 13 | VideoState |
13 | } from '../../../shared/index' | 14 | } from '../../../shared/index' |
14 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 15 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
15 | import { VideoPrivacy } from '../../../shared/models/videos' | 16 | import { VideoPrivacy } from '../../../shared/models/videos' |
16 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 17 | import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
17 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 18 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
18 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 19 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
19 | import { logger } from '../../helpers/logger' | 20 | import { logger } from '../../helpers/logger' |
20 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 21 | import { doRequest } from '../../helpers/requests' |
21 | import { | 22 | import { |
22 | ACTIVITY_PUB, | 23 | ACTIVITY_PUB, |
23 | MIMETYPES, | 24 | MIMETYPES, |
24 | P2P_MEDIA_LOADER_PEER_VERSION, | 25 | P2P_MEDIA_LOADER_PEER_VERSION, |
25 | PREVIEWS_SIZE, | 26 | PREVIEWS_SIZE, |
26 | REMOTE_SCHEME, | 27 | REMOTE_SCHEME, |
27 | STATIC_PATHS | 28 | STATIC_PATHS, |
29 | THUMBNAILS_SIZE | ||
28 | } from '../../initializers/constants' | 30 | } from '../../initializers/constants' |
29 | import { TagModel } from '../../models/video/tag' | 31 | import { TagModel } from '../../models/video/tag' |
30 | import { VideoModel } from '../../models/video/video' | 32 | import { VideoModel } from '../../models/video/video' |
@@ -36,11 +38,10 @@ import { sendCreateVideo, sendUpdateVideo } from './send' | |||
36 | import { isArray } from '../../helpers/custom-validators/misc' | 38 | import { isArray } from '../../helpers/custom-validators/misc' |
37 | import { VideoCaptionModel } from '../../models/video/video-caption' | 39 | import { VideoCaptionModel } from '../../models/video/video-caption' |
38 | import { JobQueue } from '../job-queue' | 40 | import { JobQueue } from '../job-queue' |
39 | import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher' | ||
40 | import { createRates } from './video-rates' | 41 | import { createRates } from './video-rates' |
41 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | 42 | import { addVideoShares, shareVideoByServerAndChannel } from './share' |
42 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 43 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
43 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 44 | import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
44 | import { Notifier } from '../notifier' | 45 | import { Notifier } from '../notifier' |
45 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 46 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
46 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 47 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
@@ -68,9 +69,11 @@ import { | |||
68 | MVideoFile, | 69 | MVideoFile, |
69 | MVideoFullLight, | 70 | MVideoFullLight, |
70 | MVideoId, | 71 | MVideoId, |
72 | MVideoImmutable, | ||
71 | MVideoThumbnail | 73 | MVideoThumbnail |
72 | } from '../../typings/models' | 74 | } from '../../typings/models' |
73 | import { MThumbnail } from '../../typings/models/video/thumbnail' | 75 | import { MThumbnail } from '../../typings/models/video/thumbnail' |
76 | import { maxBy, minBy } from 'lodash' | ||
74 | 77 | ||
75 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 78 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
76 | const video = videoArg as MVideoAP | 79 | const video = videoArg as MVideoAP |
@@ -109,7 +112,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request. | |||
109 | 112 | ||
110 | logger.info('Fetching remote video %s.', videoUrl) | 113 | logger.info('Fetching remote video %s.', videoUrl) |
111 | 114 | ||
112 | const { response, body } = await doRequest(options) | 115 | const { response, body } = await doRequest<any>(options) |
113 | 116 | ||
114 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { | 117 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
115 | logger.debug('Remote video JSON is not valid.', { body }) | 118 | logger.debug('Remote video JSON is not valid.', { body }) |
@@ -127,23 +130,10 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | |||
127 | json: true | 130 | json: true |
128 | } | 131 | } |
129 | 132 | ||
130 | const { body } = await doRequest(options) | 133 | const { body } = await doRequest<any>(options) |
131 | return body.description ? body.description : '' | 134 | return body.description ? body.description : '' |
132 | } | 135 | } |
133 | 136 | ||
134 | function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) { | ||
135 | const url = buildRemoteBaseUrl(video, path) | ||
136 | |||
137 | // We need to provide a callback, if no we could have an uncaught exception | ||
138 | return doRequestAndSaveToFile({ uri: url }, destPath) | ||
139 | } | ||
140 | |||
141 | function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) { | ||
142 | const host = video.VideoChannel.Account.Actor.Server.host | ||
143 | |||
144 | return REMOTE_SCHEME.HTTP + '://' + host + path | ||
145 | } | ||
146 | |||
147 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 137 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
148 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | 138 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') |
149 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | 139 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) |
@@ -173,7 +163,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo | |||
173 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) | 163 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) |
174 | 164 | ||
175 | await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) | 165 | await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) |
176 | .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) | 166 | .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes })) |
177 | } else { | 167 | } else { |
178 | jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) | 168 | jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) |
179 | } | 169 | } |
@@ -183,7 +173,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo | |||
183 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) | 173 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) |
184 | 174 | ||
185 | await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) | 175 | await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) |
186 | .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) | 176 | .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes })) |
187 | } else { | 177 | } else { |
188 | jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) | 178 | jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) |
189 | } | 179 | } |
@@ -193,7 +183,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo | |||
193 | const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) | 183 | const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) |
194 | 184 | ||
195 | await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) | 185 | await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) |
196 | .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) | 186 | .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares })) |
197 | } else { | 187 | } else { |
198 | jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) | 188 | jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) |
199 | } | 189 | } |
@@ -203,32 +193,49 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo | |||
203 | const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) | 193 | const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) |
204 | 194 | ||
205 | await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) | 195 | await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) |
206 | .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) | 196 | .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments })) |
207 | } else { | 197 | } else { |
208 | jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) | 198 | jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) |
209 | } | 199 | } |
210 | 200 | ||
211 | await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) | 201 | await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })) |
212 | } | 202 | } |
213 | 203 | ||
214 | function getOrCreateVideoAndAccountAndChannel (options: { | 204 | type GetVideoResult <T> = Promise<{ |
215 | videoObject: { id: string } | string, | 205 | video: T |
216 | syncParam?: SyncParam, | 206 | created: boolean |
217 | fetchType?: 'all', | 207 | autoBlacklisted?: boolean |
208 | }> | ||
209 | |||
210 | type GetVideoParamAll = { | ||
211 | videoObject: { id: string } | string | ||
212 | syncParam?: SyncParam | ||
213 | fetchType?: 'all' | ||
218 | allowRefresh?: boolean | 214 | allowRefresh?: boolean |
219 | }): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> | 215 | } |
220 | function getOrCreateVideoAndAccountAndChannel (options: { | 216 | |
221 | videoObject: { id: string } | string, | 217 | type GetVideoParamImmutable = { |
222 | syncParam?: SyncParam, | 218 | videoObject: { id: string } | string |
223 | fetchType?: VideoFetchByUrlType, | 219 | syncParam?: SyncParam |
220 | fetchType: 'only-immutable-attributes' | ||
221 | allowRefresh: false | ||
222 | } | ||
223 | |||
224 | type GetVideoParamOther = { | ||
225 | videoObject: { id: string } | string | ||
226 | syncParam?: SyncParam | ||
227 | fetchType?: 'all' | 'only-video' | ||
224 | allowRefresh?: boolean | 228 | allowRefresh?: boolean |
225 | }): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> | 229 | } |
226 | async function getOrCreateVideoAndAccountAndChannel (options: { | 230 | |
227 | videoObject: { id: string } | string, | 231 | function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles> |
228 | syncParam?: SyncParam, | 232 | function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable> |
229 | fetchType?: VideoFetchByUrlType, | 233 | function getOrCreateVideoAndAccountAndChannel ( |
230 | allowRefresh?: boolean // true by default | 234 | options: GetVideoParamOther |
231 | }): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { | 235 | ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> |
236 | async function getOrCreateVideoAndAccountAndChannel ( | ||
237 | options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther | ||
238 | ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { | ||
232 | // Default params | 239 | // Default params |
233 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 240 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
234 | const fetchType = options.fetchType || 'all' | 241 | const fetchType = options.fetchType || 'all' |
@@ -236,18 +243,25 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
236 | 243 | ||
237 | // Get video url | 244 | // Get video url |
238 | const videoUrl = getAPId(options.videoObject) | 245 | const videoUrl = getAPId(options.videoObject) |
239 | |||
240 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 246 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
247 | |||
241 | if (videoFromDatabase) { | 248 | if (videoFromDatabase) { |
242 | if (videoFromDatabase.isOutdated() && allowRefresh === true) { | 249 | // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type |
250 | if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) { | ||
243 | const refreshOptions = { | 251 | const refreshOptions = { |
244 | video: videoFromDatabase, | 252 | video: videoFromDatabase as MVideoThumbnail, |
245 | fetchedType: fetchType, | 253 | fetchedType: fetchType, |
246 | syncParam | 254 | syncParam |
247 | } | 255 | } |
248 | 256 | ||
249 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | 257 | if (syncParam.refreshVideo === true) { |
250 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) | 258 | videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) |
259 | } else { | ||
260 | await JobQueue.Instance.createJobWithPromise({ | ||
261 | type: 'activitypub-refresher', | ||
262 | payload: { type: 'video', url: videoFromDatabase.url } | ||
263 | }) | ||
264 | } | ||
251 | } | 265 | } |
252 | 266 | ||
253 | return { video: videoFromDatabase, created: false } | 267 | return { video: videoFromDatabase, created: false } |
@@ -266,10 +280,10 @@ async function getOrCreateVideoAndAccountAndChannel (options: { | |||
266 | } | 280 | } |
267 | 281 | ||
268 | async function updateVideoFromAP (options: { | 282 | async function updateVideoFromAP (options: { |
269 | video: MVideoAccountLightBlacklistAllFiles, | 283 | video: MVideoAccountLightBlacklistAllFiles |
270 | videoObject: VideoTorrentObject, | 284 | videoObject: VideoTorrentObject |
271 | account: MAccountIdActor, | 285 | account: MAccountIdActor |
272 | channel: MChannelDefault, | 286 | channel: MChannelDefault |
273 | overrideTo?: string[] | 287 | overrideTo?: string[] |
274 | }) { | 288 | }) { |
275 | const { video, videoObject, account, channel, overrideTo } = options | 289 | const { video, videoObject, account, channel, overrideTo } = options |
@@ -284,7 +298,7 @@ async function updateVideoFromAP (options: { | |||
284 | let thumbnailModel: MThumbnail | 298 | let thumbnailModel: MThumbnail |
285 | 299 | ||
286 | try { | 300 | try { |
287 | thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) | 301 | thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) |
288 | } catch (err) { | 302 | } catch (err) { |
289 | logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) | 303 | logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) |
290 | } | 304 | } |
@@ -300,7 +314,7 @@ async function updateVideoFromAP (options: { | |||
300 | throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) | 314 | throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) |
301 | } | 315 | } |
302 | 316 | ||
303 | const to = overrideTo ? overrideTo : videoObject.to | 317 | const to = overrideTo || videoObject.to |
304 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) | 318 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) |
305 | video.name = videoData.name | 319 | video.name = videoData.name |
306 | video.uuid = videoData.uuid | 320 | video.uuid = videoData.uuid |
@@ -327,10 +341,11 @@ async function updateVideoFromAP (options: { | |||
327 | 341 | ||
328 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) | 342 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) |
329 | 343 | ||
330 | // FIXME: use icon URL instead | 344 | if (videoUpdated.getPreview()) { |
331 | const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename)) | 345 | const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated) |
332 | const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | 346 | const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) |
333 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | 347 | await videoUpdated.addAndSaveThumbnail(previewModel, t) |
348 | } | ||
334 | 349 | ||
335 | { | 350 | { |
336 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) | 351 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) |
@@ -391,7 +406,7 @@ async function updateVideoFromAP (options: { | |||
391 | await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) | 406 | await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) |
392 | 407 | ||
393 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | 408 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { |
394 | return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t) | 409 | return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t) |
395 | }) | 410 | }) |
396 | await Promise.all(videoCaptionsPromises) | 411 | await Promise.all(videoCaptionsPromises) |
397 | } | 412 | } |
@@ -424,8 +439,8 @@ async function updateVideoFromAP (options: { | |||
424 | } | 439 | } |
425 | 440 | ||
426 | async function refreshVideoIfNeeded (options: { | 441 | async function refreshVideoIfNeeded (options: { |
427 | video: MVideoThumbnail, | 442 | video: MVideoThumbnail |
428 | fetchedType: VideoFetchByUrlType, | 443 | fetchedType: VideoFetchByUrlType |
429 | syncParam: SyncParam | 444 | syncParam: SyncParam |
430 | }): Promise<MVideoThumbnail> { | 445 | }): Promise<MVideoThumbnail> { |
431 | if (!options.video.isOutdated()) return options.video | 446 | if (!options.video.isOutdated()) return options.video |
@@ -483,7 +498,6 @@ export { | |||
483 | federateVideoIfNeeded, | 498 | federateVideoIfNeeded, |
484 | fetchRemoteVideo, | 499 | fetchRemoteVideo, |
485 | getOrCreateVideoAndAccountAndChannel, | 500 | getOrCreateVideoAndAccountAndChannel, |
486 | fetchRemoteVideoStaticFile, | ||
487 | fetchRemoteVideoDescription, | 501 | fetchRemoteVideoDescription, |
488 | getOrCreateVideoChannelFromVideoObject | 502 | getOrCreateVideoChannelFromVideoObject |
489 | } | 503 | } |
@@ -494,7 +508,7 @@ function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject { | |||
494 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | 508 | const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) |
495 | 509 | ||
496 | const urlMediaType = url.mediaType | 510 | const urlMediaType = url.mediaType |
497 | return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') | 511 | return mimeTypes.includes(urlMediaType) && urlMediaType.startsWith('video/') |
498 | } | 512 | } |
499 | 513 | ||
500 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | 514 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { |
@@ -519,7 +533,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
519 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) | 533 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) |
520 | const video = VideoModel.build(videoData) as MVideoThumbnail | 534 | const video = VideoModel.build(videoData) as MVideoThumbnail |
521 | 535 | ||
522 | const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) | 536 | const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE) |
537 | .catch(err => { | ||
538 | logger.error('Cannot create miniature from url.', { err }) | ||
539 | return undefined | ||
540 | }) | ||
523 | 541 | ||
524 | let thumbnailModel: MThumbnail | 542 | let thumbnailModel: MThumbnail |
525 | if (waitThumbnail === true) { | 543 | if (waitThumbnail === true) { |
@@ -534,9 +552,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
534 | 552 | ||
535 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 553 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
536 | 554 | ||
537 | // FIXME: use icon URL instead | 555 | const previewIcon = getPreviewFromIcons(videoObject) |
538 | const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | 556 | const previewUrl = previewIcon |
539 | const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | 557 | ? previewIcon.url |
558 | : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | ||
559 | const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | ||
560 | |||
540 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 561 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) |
541 | 562 | ||
542 | // Process files | 563 | // Process files |
@@ -567,7 +588,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
567 | 588 | ||
568 | // Process captions | 589 | // Process captions |
569 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | 590 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { |
570 | return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) | 591 | return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t) |
571 | }) | 592 | }) |
572 | await Promise.all(videoCaptionsPromises) | 593 | await Promise.all(videoCaptionsPromises) |
573 | 594 | ||
@@ -588,7 +609,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
588 | }) | 609 | }) |
589 | 610 | ||
590 | if (waitThumbnail === false) { | 611 | if (waitThumbnail === false) { |
612 | // Error is already caught above | ||
613 | // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
591 | promiseThumbnail.then(thumbnailModel => { | 614 | promiseThumbnail.then(thumbnailModel => { |
615 | if (!thumbnailModel) return | ||
616 | |||
592 | thumbnailModel = videoCreated.id | 617 | thumbnailModel = videoCreated.id |
593 | 618 | ||
594 | return thumbnailModel.save() | 619 | return thumbnailModel.save() |
@@ -598,24 +623,21 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc | |||
598 | return { autoBlacklisted, videoCreated } | 623 | return { autoBlacklisted, videoCreated } |
599 | } | 624 | } |
600 | 625 | ||
601 | async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { | 626 | function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { |
602 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED | 627 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
603 | const duration = videoObject.duration.replace(/[^\d]+/, '') | 628 | ? VideoPrivacy.PUBLIC |
629 | : VideoPrivacy.UNLISTED | ||
604 | 630 | ||
605 | let language: string | undefined | 631 | const duration = videoObject.duration.replace(/[^\d]+/, '') |
606 | if (videoObject.language) { | 632 | const language = videoObject.language?.identifier |
607 | language = videoObject.language.identifier | ||
608 | } | ||
609 | 633 | ||
610 | let category: number | undefined | 634 | const category = videoObject.category |
611 | if (videoObject.category) { | 635 | ? parseInt(videoObject.category.identifier, 10) |
612 | category = parseInt(videoObject.category.identifier, 10) | 636 | : undefined |
613 | } | ||
614 | 637 | ||
615 | let licence: number | undefined | 638 | const licence = videoObject.licence |
616 | if (videoObject.licence) { | 639 | ? parseInt(videoObject.licence.identifier, 10) |
617 | licence = parseInt(videoObject.licence.identifier, 10) | 640 | : undefined |
618 | } | ||
619 | 641 | ||
620 | const description = videoObject.content || null | 642 | const description = videoObject.content || null |
621 | const support = videoObject.support || null | 643 | const support = videoObject.support || null |
@@ -638,8 +660,11 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide | |||
638 | duration: parseInt(duration, 10), | 660 | duration: parseInt(duration, 10), |
639 | createdAt: new Date(videoObject.published), | 661 | createdAt: new Date(videoObject.published), |
640 | publishedAt: new Date(videoObject.published), | 662 | publishedAt: new Date(videoObject.published), |
641 | originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, | 663 | |
642 | // FIXME: updatedAt does not seems to be considered by Sequelize | 664 | originallyPublishedAt: videoObject.originallyPublishedAt |
665 | ? new Date(videoObject.originallyPublishedAt) | ||
666 | : null, | ||
667 | |||
643 | updatedAt: new Date(videoObject.updated), | 668 | updatedAt: new Date(videoObject.updated), |
644 | views: videoObject.views, | 669 | views: videoObject.views, |
645 | likes: 0, | 670 | likes: 0, |
@@ -670,13 +695,22 @@ function videoFileActivityUrlToDBAttributes ( | |||
670 | throw new Error('Cannot parse magnet URI ' + magnet.href) | 695 | throw new Error('Cannot parse magnet URI ' + magnet.href) |
671 | } | 696 | } |
672 | 697 | ||
698 | // Fetch associated metadata url, if any | ||
699 | const metadata = urls.filter(isAPVideoFileMetadataObject) | ||
700 | .find(u => { | ||
701 | return u.height === fileUrl.height && | ||
702 | u.fps === fileUrl.fps && | ||
703 | u.rel.includes(fileUrl.mediaType) | ||
704 | }) | ||
705 | |||
673 | const mediaType = fileUrl.mediaType | 706 | const mediaType = fileUrl.mediaType |
674 | const attribute = { | 707 | const attribute = { |
675 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], | 708 | extname: MIMETYPES.VIDEO.MIMETYPE_EXT[mediaType], |
676 | infoHash: parsed.infoHash, | 709 | infoHash: parsed.infoHash, |
677 | resolution: fileUrl.height, | 710 | resolution: fileUrl.height, |
678 | size: fileUrl.size, | 711 | size: fileUrl.size, |
679 | fps: fileUrl.fps || -1, | 712 | fps: fileUrl.fps || -1, |
713 | metadataUrl: metadata?.href, | ||
680 | 714 | ||
681 | // This is a video file owned by a video or by a streaming playlist | 715 | // This is a video file owned by a video or by a streaming playlist |
682 | videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, | 716 | videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, |
@@ -722,3 +756,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec | |||
722 | 756 | ||
723 | return attributes | 757 | return attributes |
724 | } | 758 | } |
759 | |||
760 | function getThumbnailFromIcons (videoObject: VideoTorrentObject) { | ||
761 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | ||
762 | // Fallback if there are not valid icons | ||
763 | if (validIcons.length === 0) validIcons = videoObject.icon | ||
764 | |||
765 | return minBy(validIcons, 'width') | ||
766 | } | ||
767 | |||
768 | function getPreviewFromIcons (videoObject: VideoTorrentObject) { | ||
769 | const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth) | ||
770 | |||
771 | // FIXME: don't put a fallback here for compatibility with PeerTube <2.2 | ||
772 | |||
773 | return maxBy(validIcons, 'width') | ||
774 | } | ||
diff --git a/server/lib/auth.ts b/server/lib/auth.ts new file mode 100644 index 000000000..8579bdbb4 --- /dev/null +++ b/server/lib/auth.ts | |||
@@ -0,0 +1,286 @@ | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { generateRandomString } from '@server/helpers/utils' | ||
4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | ||
5 | import { revokeToken } from '@server/lib/oauth-model' | ||
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
8 | import { UserRole } from '@shared/models' | ||
9 | import { | ||
10 | RegisterServerAuthenticatedResult, | ||
11 | RegisterServerAuthPassOptions, | ||
12 | RegisterServerExternalAuthenticatedResult | ||
13 | } from '@shared/models/plugins/register-server-auth.model' | ||
14 | import * as express from 'express' | ||
15 | import * as OAuthServer from 'express-oauth-server' | ||
16 | |||
17 | const oAuthServer = new OAuthServer({ | ||
18 | useErrorHandler: true, | ||
19 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
20 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
21 | continueMiddleware: true, | ||
22 | model: require('./oauth-model') | ||
23 | }) | ||
24 | |||
25 | // Token is the key, expiration date is the value | ||
26 | const authBypassTokens = new Map<string, { | ||
27 | expires: Date | ||
28 | user: { | ||
29 | username: string | ||
30 | email: string | ||
31 | displayName: string | ||
32 | role: UserRole | ||
33 | } | ||
34 | authName: string | ||
35 | npmName: string | ||
36 | }>() | ||
37 | |||
38 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
39 | const grantType = req.body.grant_type | ||
40 | |||
41 | if (grantType === 'password') { | ||
42 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) | ||
43 | else await proxifyPasswordGrant(req, res) | ||
44 | } else if (grantType === 'refresh_token') { | ||
45 | await proxifyRefreshGrant(req, res) | ||
46 | } | ||
47 | |||
48 | return forwardTokenReq(req, res, next) | ||
49 | } | ||
50 | |||
51 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
52 | const token = res.locals.oauth.token | ||
53 | |||
54 | res.locals.explicitLogout = true | ||
55 | await revokeToken(token) | ||
56 | |||
57 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | ||
58 | // oAuthServer.revoke(req, res, err => { | ||
59 | // if (err) { | ||
60 | // logger.warn('Error in revoke token handler.', { err }) | ||
61 | // | ||
62 | // return res.status(err.status) | ||
63 | // .json({ | ||
64 | // error: err.message, | ||
65 | // code: err.name | ||
66 | // }) | ||
67 | // .end() | ||
68 | // } | ||
69 | // }) | ||
70 | |||
71 | return res.json() | ||
72 | } | ||
73 | |||
74 | async function onExternalUserAuthenticated (options: { | ||
75 | npmName: string | ||
76 | authName: string | ||
77 | authResult: RegisterServerExternalAuthenticatedResult | ||
78 | }) { | ||
79 | const { npmName, authName, authResult } = options | ||
80 | |||
81 | if (!authResult.req || !authResult.res) { | ||
82 | logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName) | ||
83 | return | ||
84 | } | ||
85 | |||
86 | const { res } = authResult | ||
87 | |||
88 | if (!isAuthResultValid(npmName, authName, authResult)) { | ||
89 | res.redirect('/login?externalAuthError=true') | ||
90 | return | ||
91 | } | ||
92 | |||
93 | logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName) | ||
94 | |||
95 | const bypassToken = await generateRandomString(32) | ||
96 | |||
97 | const expires = new Date() | ||
98 | expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME) | ||
99 | |||
100 | const user = buildUserResult(authResult) | ||
101 | authBypassTokens.set(bypassToken, { | ||
102 | expires, | ||
103 | user, | ||
104 | npmName, | ||
105 | authName | ||
106 | }) | ||
107 | |||
108 | // Cleanup | ||
109 | const now = new Date() | ||
110 | for (const [ key, value ] of authBypassTokens) { | ||
111 | if (value.expires.getTime() < now.getTime()) { | ||
112 | authBypassTokens.delete(key) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | ||
117 | } | ||
118 | |||
119 | // --------------------------------------------------------------------------- | ||
120 | |||
121 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } | ||
122 | |||
123 | // --------------------------------------------------------------------------- | ||
124 | |||
125 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
126 | return oAuthServer.token()(req, res, err => { | ||
127 | if (err) { | ||
128 | logger.warn('Login error.', { err }) | ||
129 | |||
130 | return res.status(err.status) | ||
131 | .json({ | ||
132 | error: err.message, | ||
133 | code: err.name | ||
134 | }) | ||
135 | } | ||
136 | |||
137 | if (next) return next() | ||
138 | }) | ||
139 | } | ||
140 | |||
141 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | ||
142 | const refreshToken = req.body.refresh_token | ||
143 | if (!refreshToken) return | ||
144 | |||
145 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | ||
146 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | ||
147 | } | ||
148 | |||
149 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | ||
150 | const plugins = PluginManager.Instance.getIdAndPassAuths() | ||
151 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | ||
152 | |||
153 | for (const plugin of plugins) { | ||
154 | const auths = plugin.idAndPassAuths | ||
155 | |||
156 | for (const auth of auths) { | ||
157 | pluginAuths.push({ | ||
158 | npmName: plugin.npmName, | ||
159 | registerAuthOptions: auth | ||
160 | }) | ||
161 | } | ||
162 | } | ||
163 | |||
164 | pluginAuths.sort((a, b) => { | ||
165 | const aWeight = a.registerAuthOptions.getWeight() | ||
166 | const bWeight = b.registerAuthOptions.getWeight() | ||
167 | |||
168 | // DESC weight order | ||
169 | if (aWeight === bWeight) return 0 | ||
170 | if (aWeight < bWeight) return 1 | ||
171 | return -1 | ||
172 | }) | ||
173 | |||
174 | const loginOptions = { | ||
175 | id: req.body.username, | ||
176 | password: req.body.password | ||
177 | } | ||
178 | |||
179 | for (const pluginAuth of pluginAuths) { | ||
180 | const authOptions = pluginAuth.registerAuthOptions | ||
181 | const authName = authOptions.authName | ||
182 | const npmName = pluginAuth.npmName | ||
183 | |||
184 | logger.debug( | ||
185 | 'Using auth method %s of plugin %s to login %s with weight %d.', | ||
186 | authName, npmName, loginOptions.id, authOptions.getWeight() | ||
187 | ) | ||
188 | |||
189 | try { | ||
190 | const loginResult = await authOptions.login(loginOptions) | ||
191 | |||
192 | if (!loginResult) continue | ||
193 | if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue | ||
194 | |||
195 | logger.info( | ||
196 | 'Login success with auth method %s of plugin %s for %s.', | ||
197 | authName, npmName, loginOptions.id | ||
198 | ) | ||
199 | |||
200 | res.locals.bypassLogin = { | ||
201 | bypass: true, | ||
202 | pluginName: pluginAuth.npmName, | ||
203 | authName: authOptions.authName, | ||
204 | user: buildUserResult(loginResult) | ||
205 | } | ||
206 | |||
207 | return | ||
208 | } catch (err) { | ||
209 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | ||
210 | } | ||
211 | } | ||
212 | } | ||
213 | |||
214 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | ||
215 | const obj = authBypassTokens.get(req.body.externalAuthToken) | ||
216 | if (!obj) { | ||
217 | logger.error('Cannot authenticate user with unknown bypass token') | ||
218 | return res.sendStatus(400) | ||
219 | } | ||
220 | |||
221 | const { expires, user, authName, npmName } = obj | ||
222 | |||
223 | const now = new Date() | ||
224 | if (now.getTime() > expires.getTime()) { | ||
225 | logger.error('Cannot authenticate user with an expired external auth token') | ||
226 | return res.sendStatus(400) | ||
227 | } | ||
228 | |||
229 | if (user.username !== req.body.username) { | ||
230 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | ||
231 | return res.sendStatus(400) | ||
232 | } | ||
233 | |||
234 | // Bypass oauth library validation | ||
235 | req.body.password = 'fake' | ||
236 | |||
237 | logger.info( | ||
238 | 'Auth success with external auth method %s of plugin %s for %s.', | ||
239 | authName, npmName, user.email | ||
240 | ) | ||
241 | |||
242 | res.locals.bypassLogin = { | ||
243 | bypass: true, | ||
244 | pluginName: npmName, | ||
245 | authName: authName, | ||
246 | user | ||
247 | } | ||
248 | } | ||
249 | |||
250 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | ||
251 | if (!isUserUsernameValid(result.username)) { | ||
252 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) | ||
253 | return false | ||
254 | } | ||
255 | |||
256 | if (!result.email) { | ||
257 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) | ||
258 | return false | ||
259 | } | ||
260 | |||
261 | // role is optional | ||
262 | if (result.role && !isUserRoleValid(result.role)) { | ||
263 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) | ||
264 | return false | ||
265 | } | ||
266 | |||
267 | // display name is optional | ||
268 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | ||
269 | logger.error( | ||
270 | 'Auth method %s of plugin %s did not provide a valid display name.', | ||
271 | authName, npmName, { displayName: result.displayName } | ||
272 | ) | ||
273 | return false | ||
274 | } | ||
275 | |||
276 | return true | ||
277 | } | ||
278 | |||
279 | function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | ||
280 | return { | ||
281 | username: pluginResult.username, | ||
282 | email: pluginResult.email, | ||
283 | role: pluginResult.role ?? UserRole.USER, | ||
284 | displayName: pluginResult.displayName || pluginResult.username | ||
285 | } | ||
286 | } | ||
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts index ad4cdd3ab..282d834a2 100644 --- a/server/lib/avatar.ts +++ b/server/lib/avatar.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import { sendUpdateActor } from './activitypub/send' | 2 | import { sendUpdateActor } from './activitypub/send' |
3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | 3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' |
4 | import { updateActorAvatarInstance } from './activitypub' | 4 | import { updateActorAvatarInstance } from './activitypub/actor' |
5 | import { processImage } from '../helpers/image-utils' | 5 | import { processImage } from '../helpers/image-utils' |
6 | import { extname, join } from 'path' | 6 | import { extname, join } from 'path' |
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../helpers/database-utils' |
8 | import * as uuidv4 from 'uuid/v4' | 8 | import { v4 as uuidv4 } from 'uuid' |
9 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
10 | import { sequelizeTypescript } from '../initializers/database' | 10 | import { sequelizeTypescript } from '../initializers/database' |
11 | import * as LRUCache from 'lru-cache' | 11 | import * as LRUCache from 'lru-cache' |
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts index 28c69b46e..842eecb5b 100644 --- a/server/lib/blocklist.ts +++ b/server/lib/blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { sequelizeTypescript } from '../initializers' | 1 | import { sequelizeTypescript } from '@server/initializers/database' |
2 | import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models' | ||
2 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 3 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
3 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 4 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
4 | import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models' | ||
5 | 5 | ||
6 | function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { | 6 | function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { |
7 | return sequelizeTypescript.transaction(async t => { | 7 | return sequelizeTypescript.transaction(async t => { |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 1d8a08ed0..4a4b0d12f 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -17,7 +17,7 @@ import { MAccountActor, MChannelActor, MVideo } from '../typings/models' | |||
17 | 17 | ||
18 | export class ClientHtml { | 18 | export class ClientHtml { |
19 | 19 | ||
20 | private static htmlCache: { [ path: string ]: string } = {} | 20 | private static htmlCache: { [path: string]: string } = {} |
21 | 21 | ||
22 | static invalidCache () { | 22 | static invalidCache () { |
23 | logger.info('Cleaning HTML cache.') | 23 | logger.info('Cleaning HTML cache.') |
@@ -94,7 +94,7 @@ export class ClientHtml { | |||
94 | 94 | ||
95 | private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { | 95 | private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { |
96 | const path = ClientHtml.getIndexPath(req, res, paramLang) | 96 | const path = ClientHtml.getIndexPath(req, res, paramLang) |
97 | if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ] | 97 | if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] |
98 | 98 | ||
99 | const buffer = await readFile(path) | 99 | const buffer = await readFile(path) |
100 | 100 | ||
@@ -104,7 +104,7 @@ export class ClientHtml { | |||
104 | html = ClientHtml.addCustomCSS(html) | 104 | html = ClientHtml.addCustomCSS(html) |
105 | html = await ClientHtml.addAsyncPluginCSS(html) | 105 | html = await ClientHtml.addAsyncPluginCSS(html) |
106 | 106 | ||
107 | ClientHtml.htmlCache[ path ] = html | 107 | ClientHtml.htmlCache[path] = html |
108 | 108 | ||
109 | return html | 109 | return html |
110 | } | 110 | } |
@@ -119,7 +119,7 @@ export class ClientHtml { | |||
119 | // Save locale in cookies | 119 | // Save locale in cookies |
120 | res.cookie('clientLanguage', lang, { | 120 | res.cookie('clientLanguage', lang, { |
121 | secure: WEBSERVER.SCHEME === 'https', | 121 | secure: WEBSERVER.SCHEME === 'https', |
122 | sameSite: true, | 122 | sameSite: 'none', |
123 | maxAge: 1000 * 3600 * 24 * 90 // 3 months | 123 | maxAge: 1000 * 3600 * 24 * 90 // 3 months |
124 | }) | 124 | }) |
125 | 125 | ||
@@ -214,21 +214,21 @@ export class ClientHtml { | |||
214 | const schemaTags = { | 214 | const schemaTags = { |
215 | '@context': 'http://schema.org', | 215 | '@context': 'http://schema.org', |
216 | '@type': 'VideoObject', | 216 | '@type': 'VideoObject', |
217 | name: videoNameEscaped, | 217 | 'name': videoNameEscaped, |
218 | description: videoDescriptionEscaped, | 218 | 'description': videoDescriptionEscaped, |
219 | thumbnailUrl: previewUrl, | 219 | 'thumbnailUrl': previewUrl, |
220 | uploadDate: video.createdAt.toISOString(), | 220 | 'uploadDate': video.createdAt.toISOString(), |
221 | duration: getActivityStreamDuration(video.duration), | 221 | 'duration': getActivityStreamDuration(video.duration), |
222 | contentUrl: videoUrl, | 222 | 'contentUrl': videoUrl, |
223 | embedUrl: embedUrl, | 223 | 'embedUrl': embedUrl, |
224 | interactionCount: video.views | 224 | 'interactionCount': video.views |
225 | } | 225 | } |
226 | 226 | ||
227 | let tagsString = '' | 227 | let tagsString = '' |
228 | 228 | ||
229 | // Opengraph | 229 | // Opengraph |
230 | Object.keys(openGraphMetaTags).forEach(tagName => { | 230 | Object.keys(openGraphMetaTags).forEach(tagName => { |
231 | const tagValue = openGraphMetaTags[ tagName ] | 231 | const tagValue = openGraphMetaTags[tagName] |
232 | 232 | ||
233 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` | 233 | tagsString += `<meta property="${tagName}" content="${tagValue}" />` |
234 | }) | 234 | }) |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 7484524a4..935c9e882 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | 1 | import { createTransport, Transporter } from 'nodemailer' |
2 | import { isTestInstance } from '../helpers/core-utils' | 2 | import { isTestInstance, root } from '../helpers/core-utils' |
3 | import { bunyanLogger, logger } from '../helpers/logger' | 3 | import { bunyanLogger, logger } from '../helpers/logger' |
4 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
5 | import { JobQueue } from './job-queue' | 5 | import { JobQueue } from './job-queue' |
6 | import { EmailPayload } from './job-queue/handlers/email' | ||
7 | import { readFileSync } from 'fs-extra' | 6 | import { readFileSync } from 'fs-extra' |
8 | import { WEBSERVER } from '../initializers/constants' | 7 | import { WEBSERVER } from '../initializers/constants' |
9 | import { | 8 | import { |
@@ -16,15 +15,13 @@ import { | |||
16 | } from '../typings/models/video' | 15 | } from '../typings/models/video' |
17 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' | 16 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' |
18 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' | 17 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' |
19 | 18 | import { EmailPayload } from '@shared/models' | |
20 | type SendEmailOptions = { | 19 | import { join } from 'path' |
21 | to: string[] | 20 | import { VideoAbuse } from '../../shared/models/videos' |
22 | subject: string | 21 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' |
23 | text: string | 22 | import { merge } from 'lodash' |
24 | 23 | import { VideoChannelModel } from '@server/models/video/video-channel' | |
25 | fromDisplayName?: string | 24 | const Email = require('email-templates') |
26 | replyTo?: string | ||
27 | } | ||
28 | 25 | ||
29 | class Emailer { | 26 | class Emailer { |
30 | 27 | ||
@@ -32,41 +29,52 @@ class Emailer { | |||
32 | private initialized = false | 29 | private initialized = false |
33 | private transporter: Transporter | 30 | private transporter: Transporter |
34 | 31 | ||
35 | private constructor () {} | 32 | private constructor () { |
33 | } | ||
36 | 34 | ||
37 | init () { | 35 | init () { |
38 | // Already initialized | 36 | // Already initialized |
39 | if (this.initialized === true) return | 37 | if (this.initialized === true) return |
40 | this.initialized = true | 38 | this.initialized = true |
41 | 39 | ||
42 | if (Emailer.isEnabled()) { | 40 | if (isEmailEnabled()) { |
43 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) | 41 | if (CONFIG.SMTP.TRANSPORT === 'smtp') { |
42 | logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) | ||
44 | 43 | ||
45 | let tls | 44 | let tls |
46 | if (CONFIG.SMTP.CA_FILE) { | 45 | if (CONFIG.SMTP.CA_FILE) { |
47 | tls = { | 46 | tls = { |
48 | ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] | 47 | ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] |
48 | } | ||
49 | } | 49 | } |
50 | } | ||
51 | 50 | ||
52 | let auth | 51 | let auth |
53 | if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) { | 52 | if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) { |
54 | auth = { | 53 | auth = { |
55 | user: CONFIG.SMTP.USERNAME, | 54 | user: CONFIG.SMTP.USERNAME, |
56 | pass: CONFIG.SMTP.PASSWORD | 55 | pass: CONFIG.SMTP.PASSWORD |
56 | } | ||
57 | } | 57 | } |
58 | } | ||
59 | 58 | ||
60 | this.transporter = createTransport({ | 59 | this.transporter = createTransport({ |
61 | host: CONFIG.SMTP.HOSTNAME, | 60 | host: CONFIG.SMTP.HOSTNAME, |
62 | port: CONFIG.SMTP.PORT, | 61 | port: CONFIG.SMTP.PORT, |
63 | secure: CONFIG.SMTP.TLS, | 62 | secure: CONFIG.SMTP.TLS, |
64 | debug: CONFIG.LOG.LEVEL === 'debug', | 63 | debug: CONFIG.LOG.LEVEL === 'debug', |
65 | logger: bunyanLogger as any, | 64 | logger: bunyanLogger as any, |
66 | ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS, | 65 | ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS, |
67 | tls, | 66 | tls, |
68 | auth | 67 | auth |
69 | }) | 68 | }) |
69 | } else { // sendmail | ||
70 | logger.info('Using sendmail to send emails') | ||
71 | |||
72 | this.transporter = createTransport({ | ||
73 | sendmail: true, | ||
74 | newline: 'unix', | ||
75 | path: CONFIG.SMTP.SENDMAIL | ||
76 | }) | ||
77 | } | ||
70 | } else { | 78 | } else { |
71 | if (!isTestInstance()) { | 79 | if (!isTestInstance()) { |
72 | logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') | 80 | logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') |
@@ -75,11 +83,17 @@ class Emailer { | |||
75 | } | 83 | } |
76 | 84 | ||
77 | static isEnabled () { | 85 | static isEnabled () { |
78 | return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT | 86 | if (CONFIG.SMTP.TRANSPORT === 'sendmail') { |
87 | return !!CONFIG.SMTP.SENDMAIL | ||
88 | } else if (CONFIG.SMTP.TRANSPORT === 'smtp') { | ||
89 | return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT | ||
90 | } else { | ||
91 | return false | ||
92 | } | ||
79 | } | 93 | } |
80 | 94 | ||
81 | async checkConnectionOrDie () { | 95 | async checkConnectionOrDie () { |
82 | if (!this.transporter) return | 96 | if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return |
83 | 97 | ||
84 | logger.info('Testing SMTP server...') | 98 | logger.info('Testing SMTP server...') |
85 | 99 | ||
@@ -97,37 +111,36 @@ class Emailer { | |||
97 | const channelName = video.VideoChannel.getDisplayName() | 111 | const channelName = video.VideoChannel.getDisplayName() |
98 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 112 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
99 | 113 | ||
100 | const text = `Hi dear user,\n\n` + | ||
101 | `Your subscription ${channelName} just published a new video: ${video.name}` + | ||
102 | `\n\n` + | ||
103 | `You can view it on ${videoUrl} ` + | ||
104 | `\n\n` + | ||
105 | `Cheers,\n` + | ||
106 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
107 | |||
108 | const emailPayload: EmailPayload = { | 114 | const emailPayload: EmailPayload = { |
109 | to, | 115 | to, |
110 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video', | 116 | subject: channelName + ' just published a new video', |
111 | text | 117 | text: `Your subscription ${channelName} just published a new video: "${video.name}".`, |
118 | locals: { | ||
119 | title: 'New content ', | ||
120 | action: { | ||
121 | text: 'View video', | ||
122 | url: videoUrl | ||
123 | } | ||
124 | } | ||
112 | } | 125 | } |
113 | 126 | ||
114 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 127 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
115 | } | 128 | } |
116 | 129 | ||
117 | addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { | 130 | addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { |
118 | const followerName = actorFollow.ActorFollower.Account.getDisplayName() | ||
119 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() | 131 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() |
120 | 132 | ||
121 | const text = `Hi dear user,\n\n` + | ||
122 | `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + | ||
123 | `\n\n` + | ||
124 | `Cheers,\n` + | ||
125 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
126 | |||
127 | const emailPayload: EmailPayload = { | 133 | const emailPayload: EmailPayload = { |
134 | template: 'follower-on-channel', | ||
128 | to, | 135 | to, |
129 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName, | 136 | subject: `New follower on your channel ${followingName}`, |
130 | text | 137 | locals: { |
138 | followerName: actorFollow.ActorFollower.Account.getDisplayName(), | ||
139 | followerUrl: actorFollow.ActorFollower.url, | ||
140 | followingName, | ||
141 | followingUrl: actorFollow.ActorFollowing.url, | ||
142 | followType | ||
143 | } | ||
131 | } | 144 | } |
132 | 145 | ||
133 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 146 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -136,32 +149,28 @@ class Emailer { | |||
136 | addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { | 149 | addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { |
137 | const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' | 150 | const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' |
138 | 151 | ||
139 | const text = `Hi dear admin,\n\n` + | ||
140 | `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` + | ||
141 | `\n\n` + | ||
142 | `Cheers,\n` + | ||
143 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
144 | |||
145 | const emailPayload: EmailPayload = { | 152 | const emailPayload: EmailPayload = { |
146 | to, | 153 | to, |
147 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower', | 154 | subject: 'New instance follower', |
148 | text | 155 | text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`, |
156 | locals: { | ||
157 | title: 'New instance follower', | ||
158 | action: { | ||
159 | text: 'Review followers', | ||
160 | url: WEBSERVER.URL + '/admin/follows/followers-list' | ||
161 | } | ||
162 | } | ||
149 | } | 163 | } |
150 | 164 | ||
151 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 165 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
152 | } | 166 | } |
153 | 167 | ||
154 | addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { | 168 | addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { |
155 | const text = `Hi dear admin,\n\n` + | 169 | const instanceUrl = actorFollow.ActorFollowing.url |
156 | `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` + | ||
157 | `\n\n` + | ||
158 | `Cheers,\n` + | ||
159 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
160 | |||
161 | const emailPayload: EmailPayload = { | 170 | const emailPayload: EmailPayload = { |
162 | to, | 171 | to, |
163 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', | 172 | subject: 'Auto instance following', |
164 | text | 173 | text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.` |
165 | } | 174 | } |
166 | 175 | ||
167 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 176 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -170,18 +179,17 @@ class Emailer { | |||
170 | myVideoPublishedNotification (to: string[], video: MVideo) { | 179 | myVideoPublishedNotification (to: string[], video: MVideo) { |
171 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 180 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
172 | 181 | ||
173 | const text = `Hi dear user,\n\n` + | ||
174 | `Your video ${video.name} has been published.` + | ||
175 | `\n\n` + | ||
176 | `You can view it on ${videoUrl} ` + | ||
177 | `\n\n` + | ||
178 | `Cheers,\n` + | ||
179 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
180 | |||
181 | const emailPayload: EmailPayload = { | 182 | const emailPayload: EmailPayload = { |
182 | to, | 183 | to, |
183 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`, | 184 | subject: `Your video ${video.name} has been published`, |
184 | text | 185 | text: `Your video "${video.name}" has been published.`, |
186 | locals: { | ||
187 | title: 'You video is live', | ||
188 | action: { | ||
189 | text: 'View video', | ||
190 | url: videoUrl | ||
191 | } | ||
192 | } | ||
185 | } | 193 | } |
186 | 194 | ||
187 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 195 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -190,18 +198,17 @@ class Emailer { | |||
190 | myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { | 198 | myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { |
191 | const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() | 199 | const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() |
192 | 200 | ||
193 | const text = `Hi dear user,\n\n` + | ||
194 | `Your video import ${videoImport.getTargetIdentifier()} is finished.` + | ||
195 | `\n\n` + | ||
196 | `You can view the imported video on ${videoUrl} ` + | ||
197 | `\n\n` + | ||
198 | `Cheers,\n` + | ||
199 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
200 | |||
201 | const emailPayload: EmailPayload = { | 201 | const emailPayload: EmailPayload = { |
202 | to, | 202 | to, |
203 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`, | 203 | subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`, |
204 | text | 204 | text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`, |
205 | locals: { | ||
206 | title: 'Import complete', | ||
207 | action: { | ||
208 | text: 'View video', | ||
209 | url: videoUrl | ||
210 | } | ||
211 | } | ||
205 | } | 212 | } |
206 | 213 | ||
207 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 214 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -210,40 +217,47 @@ class Emailer { | |||
210 | myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { | 217 | myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { |
211 | const importUrl = WEBSERVER.URL + '/my-account/video-imports' | 218 | const importUrl = WEBSERVER.URL + '/my-account/video-imports' |
212 | 219 | ||
213 | const text = `Hi dear user,\n\n` + | 220 | const text = |
214 | `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + | 221 | `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` + |
215 | `\n\n` + | 222 | '\n\n' + |
216 | `See your videos import dashboard for more information: ${importUrl}` + | 223 | `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.` |
217 | `\n\n` + | ||
218 | `Cheers,\n` + | ||
219 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
220 | 224 | ||
221 | const emailPayload: EmailPayload = { | 225 | const emailPayload: EmailPayload = { |
222 | to, | 226 | to, |
223 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, | 227 | subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`, |
224 | text | 228 | text, |
229 | locals: { | ||
230 | title: 'Import failed', | ||
231 | action: { | ||
232 | text: 'Review imports', | ||
233 | url: importUrl | ||
234 | } | ||
235 | } | ||
225 | } | 236 | } |
226 | 237 | ||
227 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 238 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
228 | } | 239 | } |
229 | 240 | ||
230 | addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { | 241 | addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { |
231 | const accountName = comment.Account.getDisplayName() | ||
232 | const video = comment.Video | 242 | const video = comment.Video |
243 | const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() | ||
233 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() | 244 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() |
234 | 245 | ||
235 | const text = `Hi dear user,\n\n` + | ||
236 | `A new comment has been posted by ${accountName} on your video ${video.name}` + | ||
237 | `\n\n` + | ||
238 | `You can view it on ${commentUrl} ` + | ||
239 | `\n\n` + | ||
240 | `Cheers,\n` + | ||
241 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
242 | |||
243 | const emailPayload: EmailPayload = { | 246 | const emailPayload: EmailPayload = { |
247 | template: 'video-comment-new', | ||
244 | to, | 248 | to, |
245 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name, | 249 | subject: 'New comment on your video ' + video.name, |
246 | text | 250 | locals: { |
251 | accountName: comment.Account.getDisplayName(), | ||
252 | accountUrl: comment.Account.Actor.url, | ||
253 | comment, | ||
254 | video, | ||
255 | videoUrl, | ||
256 | action: { | ||
257 | text: 'View comment', | ||
258 | url: commentUrl | ||
259 | } | ||
260 | } | ||
247 | } | 261 | } |
248 | 262 | ||
249 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 263 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -252,75 +266,88 @@ class Emailer { | |||
252 | addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { | 266 | addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { |
253 | const accountName = comment.Account.getDisplayName() | 267 | const accountName = comment.Account.getDisplayName() |
254 | const video = comment.Video | 268 | const video = comment.Video |
269 | const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() | ||
255 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() | 270 | const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() |
256 | 271 | ||
257 | const text = `Hi dear user,\n\n` + | ||
258 | `${accountName} mentioned you on video ${video.name}` + | ||
259 | `\n\n` + | ||
260 | `You can view the comment on ${commentUrl} ` + | ||
261 | `\n\n` + | ||
262 | `Cheers,\n` + | ||
263 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
264 | |||
265 | const emailPayload: EmailPayload = { | 272 | const emailPayload: EmailPayload = { |
273 | template: 'video-comment-mention', | ||
266 | to, | 274 | to, |
267 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name, | 275 | subject: 'Mention on video ' + video.name, |
268 | text | 276 | locals: { |
277 | comment, | ||
278 | video, | ||
279 | videoUrl, | ||
280 | accountName, | ||
281 | action: { | ||
282 | text: 'View comment', | ||
283 | url: commentUrl | ||
284 | } | ||
285 | } | ||
269 | } | 286 | } |
270 | 287 | ||
271 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 288 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
272 | } | 289 | } |
273 | 290 | ||
274 | addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { | 291 | addVideoAbuseModeratorsNotification (to: string[], parameters: { |
275 | const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() | 292 | videoAbuse: VideoAbuse |
276 | 293 | videoAbuseInstance: MVideoAbuseVideo | |
277 | const text = `Hi,\n\n` + | 294 | reporter: string |
278 | `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + | 295 | }) { |
279 | `Cheers,\n` + | 296 | const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id |
280 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | 297 | const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath() |
281 | 298 | ||
282 | const emailPayload: EmailPayload = { | 299 | const emailPayload: EmailPayload = { |
300 | template: 'video-abuse-new', | ||
283 | to, | 301 | to, |
284 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse', | 302 | subject: `New video abuse report from ${parameters.reporter}`, |
285 | text | 303 | locals: { |
304 | videoUrl, | ||
305 | videoAbuseUrl, | ||
306 | videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(), | ||
307 | videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(), | ||
308 | videoAbuse: parameters.videoAbuse, | ||
309 | reporter: parameters.reporter, | ||
310 | action: { | ||
311 | text: 'View report #' + parameters.videoAbuse.id, | ||
312 | url: videoAbuseUrl | ||
313 | } | ||
314 | } | ||
286 | } | 315 | } |
287 | 316 | ||
288 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 317 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
289 | } | 318 | } |
290 | 319 | ||
291 | addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | 320 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
292 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 321 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
293 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | 322 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
294 | 323 | const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | |
295 | const text = `Hi,\n\n` + | ||
296 | `A recently added video was auto-blacklisted and requires moderator review before publishing.` + | ||
297 | `\n\n` + | ||
298 | `You can view it and take appropriate action on ${videoUrl}` + | ||
299 | `\n\n` + | ||
300 | `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` + | ||
301 | `\n\n` + | ||
302 | `Cheers,\n` + | ||
303 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
304 | 324 | ||
305 | const emailPayload: EmailPayload = { | 325 | const emailPayload: EmailPayload = { |
326 | template: 'video-auto-blacklist-new', | ||
306 | to, | 327 | to, |
307 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review', | 328 | subject: 'A new video is pending moderation', |
308 | text | 329 | locals: { |
330 | channel, | ||
331 | videoUrl, | ||
332 | videoName: videoBlacklist.Video.name, | ||
333 | action: { | ||
334 | text: 'Review autoblacklist', | ||
335 | url: VIDEO_AUTO_BLACKLIST_URL | ||
336 | } | ||
337 | } | ||
309 | } | 338 | } |
310 | 339 | ||
311 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 340 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
312 | } | 341 | } |
313 | 342 | ||
314 | addNewUserRegistrationNotification (to: string[], user: MUser) { | 343 | addNewUserRegistrationNotification (to: string[], user: MUser) { |
315 | const text = `Hi,\n\n` + | ||
316 | `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` + | ||
317 | `Cheers,\n` + | ||
318 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
319 | |||
320 | const emailPayload: EmailPayload = { | 344 | const emailPayload: EmailPayload = { |
345 | template: 'user-registered', | ||
321 | to, | 346 | to, |
322 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST, | 347 | subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`, |
323 | text | 348 | locals: { |
349 | user | ||
350 | } | ||
324 | } | 351 | } |
325 | 352 | ||
326 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 353 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -333,16 +360,13 @@ class Emailer { | |||
333 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' | 360 | const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' |
334 | const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.` | 361 | const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.` |
335 | 362 | ||
336 | const text = 'Hi,\n\n' + | ||
337 | blockedString + | ||
338 | '\n\n' + | ||
339 | 'Cheers,\n' + | ||
340 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
341 | |||
342 | const emailPayload: EmailPayload = { | 363 | const emailPayload: EmailPayload = { |
343 | to, | 364 | to, |
344 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`, | 365 | subject: `Video ${videoName} blacklisted`, |
345 | text | 366 | text: blockedString, |
367 | locals: { | ||
368 | title: 'Your video was blacklisted' | ||
369 | } | ||
346 | } | 370 | } |
347 | 371 | ||
348 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 372 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -351,50 +375,53 @@ class Emailer { | |||
351 | addVideoUnblacklistNotification (to: string[], video: MVideo) { | 375 | addVideoUnblacklistNotification (to: string[], video: MVideo) { |
352 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 376 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
353 | 377 | ||
354 | const text = 'Hi,\n\n' + | ||
355 | `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` + | ||
356 | '\n\n' + | ||
357 | 'Cheers,\n' + | ||
358 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
359 | |||
360 | const emailPayload: EmailPayload = { | 378 | const emailPayload: EmailPayload = { |
361 | to, | 379 | to, |
362 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`, | 380 | subject: `Video ${video.name} unblacklisted`, |
363 | text | 381 | text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`, |
382 | locals: { | ||
383 | title: 'Your video was unblacklisted' | ||
384 | } | ||
364 | } | 385 | } |
365 | 386 | ||
366 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 387 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
367 | } | 388 | } |
368 | 389 | ||
369 | addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { | 390 | addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { |
370 | const text = `Hi dear user,\n\n` + | 391 | const emailPayload: EmailPayload = { |
371 | `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` + | 392 | template: 'password-reset', |
372 | `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` + | 393 | to: [ to ], |
373 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | 394 | subject: 'Reset your account password', |
374 | `Cheers,\n` + | 395 | locals: { |
375 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | 396 | resetPasswordUrl |
397 | } | ||
398 | } | ||
376 | 399 | ||
400 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
401 | } | ||
402 | |||
403 | addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) { | ||
377 | const emailPayload: EmailPayload = { | 404 | const emailPayload: EmailPayload = { |
405 | template: 'password-create', | ||
378 | to: [ to ], | 406 | to: [ to ], |
379 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password', | 407 | subject: 'Create your account password', |
380 | text | 408 | locals: { |
409 | username, | ||
410 | createPasswordUrl | ||
411 | } | ||
381 | } | 412 | } |
382 | 413 | ||
383 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 414 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
384 | } | 415 | } |
385 | 416 | ||
386 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | 417 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { |
387 | const text = `Welcome to PeerTube,\n\n` + | ||
388 | `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` + | ||
389 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | ||
390 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
391 | `Cheers,\n` + | ||
392 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
393 | |||
394 | const emailPayload: EmailPayload = { | 418 | const emailPayload: EmailPayload = { |
419 | template: 'verify-email', | ||
395 | to: [ to ], | 420 | to: [ to ], |
396 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email', | 421 | subject: `Verify your email on ${WEBSERVER.HOST}`, |
397 | text | 422 | locals: { |
423 | verifyEmailUrl | ||
424 | } | ||
398 | } | 425 | } |
399 | 426 | ||
400 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 427 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
@@ -403,61 +430,76 @@ class Emailer { | |||
403 | addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { | 430 | addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { |
404 | const reasonString = reason ? ` for the following reason: ${reason}` : '' | 431 | const reasonString = reason ? ` for the following reason: ${reason}` : '' |
405 | const blockedWord = blocked ? 'blocked' : 'unblocked' | 432 | const blockedWord = blocked ? 'blocked' : 'unblocked' |
406 | const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.` | ||
407 | |||
408 | const text = 'Hi,\n\n' + | ||
409 | blockedString + | ||
410 | '\n\n' + | ||
411 | 'Cheers,\n' + | ||
412 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
413 | 433 | ||
414 | const to = user.email | 434 | const to = user.email |
415 | const emailPayload: EmailPayload = { | 435 | const emailPayload: EmailPayload = { |
416 | to: [ to ], | 436 | to: [ to ], |
417 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord, | 437 | subject: 'Account ' + blockedWord, |
418 | text | 438 | text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.` |
419 | } | 439 | } |
420 | 440 | ||
421 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 441 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
422 | } | 442 | } |
423 | 443 | ||
424 | addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { | 444 | addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { |
425 | const text = 'Hello dear admin,\n\n' + | ||
426 | fromName + ' sent you a message' + | ||
427 | '\n\n---------------------------------------\n\n' + | ||
428 | body + | ||
429 | '\n\n---------------------------------------\n\n' + | ||
430 | 'Cheers,\n' + | ||
431 | 'PeerTube.' | ||
432 | |||
433 | const emailPayload: EmailPayload = { | 445 | const emailPayload: EmailPayload = { |
434 | fromDisplayName: fromEmail, | 446 | template: 'contact-form', |
435 | replyTo: fromEmail, | ||
436 | to: [ CONFIG.ADMIN.EMAIL ], | 447 | to: [ CONFIG.ADMIN.EMAIL ], |
437 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject, | 448 | replyTo: `"${fromName}" <${fromEmail}>`, |
438 | text | 449 | subject: `(contact form) ${subject}`, |
450 | locals: { | ||
451 | fromName, | ||
452 | fromEmail, | ||
453 | body | ||
454 | } | ||
439 | } | 455 | } |
440 | 456 | ||
441 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 457 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
442 | } | 458 | } |
443 | 459 | ||
444 | async sendMail (options: EmailPayload) { | 460 | async sendMail (options: EmailPayload) { |
445 | if (!Emailer.isEnabled()) { | 461 | if (!isEmailEnabled()) { |
446 | throw new Error('Cannot send mail because SMTP is not configured.') | 462 | throw new Error('Cannot send mail because SMTP is not configured.') |
447 | } | 463 | } |
448 | 464 | ||
449 | const fromDisplayName = options.fromDisplayName | 465 | const fromDisplayName = options.from |
450 | ? options.fromDisplayName | 466 | ? options.from |
451 | : WEBSERVER.HOST | 467 | : WEBSERVER.HOST |
452 | 468 | ||
469 | const email = new Email({ | ||
470 | send: true, | ||
471 | message: { | ||
472 | from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>` | ||
473 | }, | ||
474 | transport: this.transporter, | ||
475 | views: { | ||
476 | root: join(root(), 'server', 'lib', 'emails') | ||
477 | }, | ||
478 | subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX | ||
479 | }) | ||
480 | |||
453 | for (const to of options.to) { | 481 | for (const to of options.to) { |
454 | await this.transporter.sendMail({ | 482 | await email |
455 | from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, | 483 | .send(merge( |
456 | replyTo: options.replyTo, | 484 | { |
457 | to, | 485 | template: 'common', |
458 | subject: options.subject, | 486 | message: { |
459 | text: options.text | 487 | to, |
460 | }) | 488 | from: options.from, |
489 | subject: options.subject, | ||
490 | replyTo: options.replyTo | ||
491 | }, | ||
492 | locals: { // default variables available in all templates | ||
493 | WEBSERVER, | ||
494 | EMAIL: CONFIG.EMAIL, | ||
495 | text: options.text, | ||
496 | subject: options.subject | ||
497 | } | ||
498 | }, | ||
499 | options // overriden/new variables given for a specific template in the payload | ||
500 | ) as SendEmailOptions) | ||
501 | .then(logger.info) | ||
502 | .catch(logger.error) | ||
461 | } | 503 | } |
462 | } | 504 | } |
463 | 505 | ||
@@ -474,6 +516,5 @@ class Emailer { | |||
474 | // --------------------------------------------------------------------------- | 516 | // --------------------------------------------------------------------------- |
475 | 517 | ||
476 | export { | 518 | export { |
477 | Emailer, | 519 | Emailer |
478 | SendEmailOptions | ||
479 | } | 520 | } |
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug new file mode 100644 index 000000000..9a1894cab --- /dev/null +++ b/server/lib/emails/common/base.pug | |||
@@ -0,0 +1,267 @@ | |||
1 | //- | ||
2 | The email background color is defined in three places: | ||
3 | 1. body tag: for most email clients | ||
4 | 2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr | ||
5 | 3. mso conditional: For Windows 10 Mail | ||
6 | - var backgroundColor = "#fff"; | ||
7 | - var mainColor = "#f2690d"; | ||
8 | doctype html | ||
9 | head | ||
10 | // This template is heavily adapted from the Cerberus Fluid template. Kudos to them! | ||
11 | meta(charset='utf-8') | ||
12 | //- utf-8 works for most cases | ||
13 | meta(name='viewport' content='width=device-width') | ||
14 | //- Forcing initial-scale shouldn't be necessary | ||
15 | meta(http-equiv='X-UA-Compatible' content='IE=edge') | ||
16 | //- Use the latest (edge) version of IE rendering engine | ||
17 | meta(name='x-apple-disable-message-reformatting') | ||
18 | //- Disable auto-scale in iOS 10 Mail entirely | ||
19 | meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no') | ||
20 | //- Tell iOS not to automatically link certain text strings. | ||
21 | meta(name='color-scheme' content='light') | ||
22 | meta(name='supported-color-schemes' content='light') | ||
23 | //- The title tag shows in email notifications, like Android 4.4. | ||
24 | title #{subject} | ||
25 | //- What it does: Makes background images in 72ppi Outlook render at correct size. | ||
26 | //if gte mso 9 | ||
27 | xml | ||
28 | o:officedocumentsettings | ||
29 | o:allowpng | ||
30 | o:pixelsperinch 96 | ||
31 | //- CSS Reset : BEGIN | ||
32 | style. | ||
33 | /* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */ | ||
34 | :root { | ||
35 | color-scheme: light; | ||
36 | supported-color-schemes: light; | ||
37 | } | ||
38 | /* What it does: Remove spaces around the email design added by some email clients. */ | ||
39 | /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */ | ||
40 | html, | ||
41 | body { | ||
42 | margin: 0 auto !important; | ||
43 | padding: 0 !important; | ||
44 | height: 100% !important; | ||
45 | width: 100% !important; | ||
46 | } | ||
47 | /* What it does: Stops email clients resizing small text. */ | ||
48 | * { | ||
49 | -ms-text-size-adjust: 100%; | ||
50 | -webkit-text-size-adjust: 100%; | ||
51 | } | ||
52 | /* What it does: Centers email on Android 4.4 */ | ||
53 | div[style*="margin: 16px 0"] { | ||
54 | margin: 0 !important; | ||
55 | } | ||
56 | /* What it does: forces Samsung Android mail clients to use the entire viewport */ | ||
57 | #MessageViewBody, #MessageWebViewDiv{ | ||
58 | width: 100% !important; | ||
59 | } | ||
60 | /* What it does: Stops Outlook from adding extra spacing to tables. */ | ||
61 | table, | ||
62 | td { | ||
63 | mso-table-lspace: 0pt !important; | ||
64 | mso-table-rspace: 0pt !important; | ||
65 | } | ||
66 | /* What it does: Fixes webkit padding issue. */ | ||
67 | table { | ||
68 | border-spacing: 0 !important; | ||
69 | border-collapse: collapse !important; | ||
70 | table-layout: fixed !important; | ||
71 | margin: 0 auto !important; | ||
72 | } | ||
73 | /* What it does: Uses a better rendering method when resizing images in IE. */ | ||
74 | img { | ||
75 | -ms-interpolation-mode:bicubic; | ||
76 | } | ||
77 | /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */ | ||
78 | a { | ||
79 | text-decoration: none; | ||
80 | } | ||
81 | a:not(.nocolor) { | ||
82 | color: #{mainColor}; | ||
83 | } | ||
84 | a.nocolor { | ||
85 | color: inherit !important; | ||
86 | } | ||
87 | /* What it does: A work-around for email clients meddling in triggered links. */ | ||
88 | a[x-apple-data-detectors], /* iOS */ | ||
89 | .unstyle-auto-detected-links a, | ||
90 | .aBn { | ||
91 | border-bottom: 0 !important; | ||
92 | cursor: default !important; | ||
93 | color: inherit !important; | ||
94 | text-decoration: none !important; | ||
95 | font-size: inherit !important; | ||
96 | font-family: inherit !important; | ||
97 | font-weight: inherit !important; | ||
98 | line-height: inherit !important; | ||
99 | } | ||
100 | /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */ | ||
101 | .a6S { | ||
102 | display: none !important; | ||
103 | opacity: 0.01 !important; | ||
104 | } | ||
105 | /* What it does: Prevents Gmail from changing the text color in conversation threads. */ | ||
106 | .im { | ||
107 | color: inherit !important; | ||
108 | } | ||
109 | /* If the above doesn't work, add a .g-img class to any image in question. */ | ||
110 | img.g-img + div { | ||
111 | display: none !important; | ||
112 | } | ||
113 | /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */ | ||
114 | /* Create one of these media queries for each additional viewport size you'd like to fix */ | ||
115 | /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */ | ||
116 | @media only screen and (min-device-width: 320px) and (max-device-width: 374px) { | ||
117 | u ~ div .email-container { | ||
118 | min-width: 320px !important; | ||
119 | } | ||
120 | } | ||
121 | /* iPhone 6, 6S, 7, 8, and X */ | ||
122 | @media only screen and (min-device-width: 375px) and (max-device-width: 413px) { | ||
123 | u ~ div .email-container { | ||
124 | min-width: 375px !important; | ||
125 | } | ||
126 | } | ||
127 | /* iPhone 6+, 7+, and 8+ */ | ||
128 | @media only screen and (min-device-width: 414px) { | ||
129 | u ~ div .email-container { | ||
130 | min-width: 414px !important; | ||
131 | } | ||
132 | } | ||
133 | //- CSS Reset : END | ||
134 | //- CSS for PeerTube : START | ||
135 | style. | ||
136 | blockquote { | ||
137 | margin-left: 0; | ||
138 | padding-left: 20px; | ||
139 | border-left: 2px solid #f2690d; | ||
140 | } | ||
141 | //- CSS for PeerTube : END | ||
142 | //- Progressive Enhancements : BEGIN | ||
143 | style. | ||
144 | /* What it does: Hover styles for buttons */ | ||
145 | .button-td, | ||
146 | .button-a { | ||
147 | transition: all 100ms ease-in; | ||
148 | } | ||
149 | .button-td-primary:hover, | ||
150 | .button-a-primary:hover { | ||
151 | background: #555555 !important; | ||
152 | border-color: #555555 !important; | ||
153 | } | ||
154 | /* Media Queries */ | ||
155 | @media screen and (max-width: 600px) { | ||
156 | /* What it does: Adjust typography on small screens to improve readability */ | ||
157 | .email-container p { | ||
158 | font-size: 17px !important; | ||
159 | } | ||
160 | } | ||
161 | //- Progressive Enhancements : END | ||
162 | |||
163 | body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};") | ||
164 | center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};') | ||
165 | //if mso | IE | ||
166 | table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;') | ||
167 | tr | ||
168 | td | ||
169 | //- Visually Hidden Preheader Text : BEGIN | ||
170 | div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true') | ||
171 | block preheader | ||
172 | //- Visually Hidden Preheader Text : END | ||
173 | |||
174 | //- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary. | ||
175 | //- Preview Text Spacing Hack : BEGIN | ||
176 | div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;') | ||
177 | | ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ | ||
178 | //- Preview Text Spacing Hack : END | ||
179 | |||
180 | //- | ||
181 | Set the email width. Defined in two places: | ||
182 | 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px. | ||
183 | 2. MSO tags for Desktop Windows Outlook enforce a 600px width. | ||
184 | .email-container(style='max-width: 600px; margin: 0 auto;') | ||
185 | //if mso | ||
186 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600') | ||
187 | tr | ||
188 | td | ||
189 | //- Email Body : BEGIN | ||
190 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') | ||
191 | //- 1 Column Text + Button : BEGIN | ||
192 | tr | ||
193 | td(style='background-color: #ffffff;') | ||
194 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%') | ||
195 | tr | ||
196 | td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') | ||
197 | table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%") | ||
198 | tr | ||
199 | td(width="40px") | ||
200 | img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="icon" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;") | ||
201 | td | ||
202 | h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;') | ||
203 | block title | ||
204 | if title | ||
205 | | #{title} | ||
206 | else | ||
207 | | Something requires your attention | ||
208 | p(style='margin: 0;') | ||
209 | block body | ||
210 | if action | ||
211 | tr | ||
212 | td(style='padding: 0 20px;') | ||
213 | //- Button : BEGIN | ||
214 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;') | ||
215 | tr | ||
216 | td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;') | ||
217 | a.button-a.button-a-primary(href=action.url style='background: #222222; border: 1px solid #000000; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;') #{action.text} | ||
218 | //- Button : END | ||
219 | //- 1 Column Text + Button : END | ||
220 | //- Clear Spacer : BEGIN | ||
221 | tr | ||
222 | td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') | ||
223 | br | ||
224 | //- Clear Spacer : END | ||
225 | //- 1 Column Text : BEGIN | ||
226 | if username | ||
227 | tr | ||
228 | td(style='background-color: #cccccc;') | ||
229 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%') | ||
230 | tr | ||
231 | td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') | ||
232 | p(style='margin: 0;') | ||
233 | | You are receiving this email as part of your notification settings on #{WEBSERVER.HOST} for your account #{username}. | ||
234 | //- 1 Column Text : END | ||
235 | //- Email Body : END | ||
236 | //- Email Footer : BEGIN | ||
237 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') | ||
238 | tr | ||
239 | td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') | ||
240 | webversion | ||
241 | a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications | ||
242 | br | ||
243 | tr | ||
244 | td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') | ||
245 | unsubscribe | ||
246 | a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile | ||
247 | br | ||
248 | //- Email Footer : END | ||
249 | //if mso | ||
250 | //- Full Bleed Background Section : BEGIN | ||
251 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`) | ||
252 | tr | ||
253 | td | ||
254 | .email-container(align='center' style='max-width: 600px; margin: auto;') | ||
255 | //if mso | ||
256 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center') | ||
257 | tr | ||
258 | td | ||
259 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%') | ||
260 | tr | ||
261 | td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;') | ||
262 | table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%") | ||
263 | tr | ||
264 | td(valign="top") #[a(href="https://github.com/Chocobozzz/PeerTube" style="color: white !important") PeerTube © 2015-#{new Date().getFullYear()}] #[a(href="https://github.com/Chocobozzz/PeerTube/blob/master/CREDITS.md" style="color: white !important") PeerTube Contributors] | ||
265 | //if mso | ||
266 | //- Full Bleed Background Section : END | ||
267 | //if mso | IE | ||
diff --git a/server/lib/emails/common/greetings.pug b/server/lib/emails/common/greetings.pug new file mode 100644 index 000000000..5efe29dfb --- /dev/null +++ b/server/lib/emails/common/greetings.pug | |||
@@ -0,0 +1,11 @@ | |||
1 | extends base | ||
2 | |||
3 | block body | ||
4 | if username | ||
5 | p Hi #{username}, | ||
6 | else | ||
7 | p Hi, | ||
8 | block content | ||
9 | p | ||
10 | | Cheers,#[br] | ||
11 | | #{EMAIL.BODY.SIGNATURE} \ No newline at end of file | ||
diff --git a/server/lib/emails/common/html.pug b/server/lib/emails/common/html.pug new file mode 100644 index 000000000..d76168b85 --- /dev/null +++ b/server/lib/emails/common/html.pug | |||
@@ -0,0 +1,4 @@ | |||
1 | extends greetings | ||
2 | |||
3 | block content | ||
4 | p !{text} \ No newline at end of file | ||
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug new file mode 100644 index 000000000..76b805a24 --- /dev/null +++ b/server/lib/emails/common/mixins.pug | |||
@@ -0,0 +1,3 @@ | |||
1 | mixin channel(channel) | ||
2 | - var handle = `${channel.name}@${channel.host}` | ||
3 | | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file | ||
diff --git a/server/lib/emails/contact-form/html.pug b/server/lib/emails/contact-form/html.pug new file mode 100644 index 000000000..0073ff78e --- /dev/null +++ b/server/lib/emails/contact-form/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone just used the contact form | ||
5 | |||
6 | block content | ||
7 | p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]: | ||
8 | blockquote(style='white-space: pre-wrap') #{body} | ||
9 | p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch. \ No newline at end of file | ||
diff --git a/server/lib/emails/follower-on-channel/html.pug b/server/lib/emails/follower-on-channel/html.pug new file mode 100644 index 000000000..8a352e90f --- /dev/null +++ b/server/lib/emails/follower-on-channel/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New follower on your channel | ||
5 | |||
6 | block content | ||
7 | p. | ||
8 | Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber: | ||
9 | #[a(href=followerUrl) #{followerName}]. \ No newline at end of file | ||
diff --git a/server/lib/emails/password-create/html.pug b/server/lib/emails/password-create/html.pug new file mode 100644 index 000000000..45ff3078a --- /dev/null +++ b/server/lib/emails/password-create/html.pug | |||
@@ -0,0 +1,10 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Password creation for your account | ||
5 | |||
6 | block content | ||
7 | p. | ||
8 | Welcome to #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your PeerTube instance. Your username is: #{username}. | ||
9 | Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}] | ||
10 | (this link will expire within seven days). \ No newline at end of file | ||
diff --git a/server/lib/emails/password-reset/html.pug b/server/lib/emails/password-reset/html.pug new file mode 100644 index 000000000..bb6a9d16b --- /dev/null +++ b/server/lib/emails/password-reset/html.pug | |||
@@ -0,0 +1,12 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Password reset for your account | ||
5 | |||
6 | block content | ||
7 | p. | ||
8 | A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]. | ||
9 | Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}] | ||
10 | (the link will expire within 1 hour) | ||
11 | p. | ||
12 | If you are not the person who initiated this request, please ignore this email. \ No newline at end of file | ||
diff --git a/server/lib/emails/user-registered/html.pug b/server/lib/emails/user-registered/html.pug new file mode 100644 index 000000000..20f62125e --- /dev/null +++ b/server/lib/emails/user-registered/html.pug | |||
@@ -0,0 +1,10 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | A new user registered | ||
5 | |||
6 | block content | ||
7 | - var mail = user.email || user.pendingEmail; | ||
8 | p | ||
9 | | User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered. | ||
10 | | You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}]. \ No newline at end of file | ||
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug new file mode 100644 index 000000000..8a4a77703 --- /dev/null +++ b/server/lib/emails/verify-email/html.pug | |||
@@ -0,0 +1,14 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Account verification | ||
5 | |||
6 | block content | ||
7 | p Welcome to PeerTube! | ||
8 | p. | ||
9 | You just created an account #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your new PeerTube instance. | ||
10 | Your username there is: #{username}. | ||
11 | p. | ||
12 | To start using PeerTube on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] you must verify your email first! | ||
13 | Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you: #[a(href=verifyEmailUrl) #{verifyEmailUrl}] | ||
14 | If you are not the person who initiated this request, please ignore this email. \ No newline at end of file | ||
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug new file mode 100644 index 000000000..999c89d26 --- /dev/null +++ b/server/lib/emails/video-abuse-new/html.pug | |||
@@ -0,0 +1,18 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins.pug | ||
3 | |||
4 | block title | ||
5 | | A video is pending moderation | ||
6 | |||
7 | block content | ||
8 | p | ||
9 | | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video " | ||
10 | a(href=videoUrl) #{videoAbuse.video.name} | ||
11 | | " by #[+channel(videoAbuse.video.channel)] | ||
12 | if videoPublishedAt | ||
13 | | , published the #{videoPublishedAt}. | ||
14 | else | ||
15 | | , uploaded the #{videoCreatedAt} but not yet published. | ||
16 | p The reporter, #{reporter}, cited the following reason(s): | ||
17 | blockquote #{videoAbuse.reason} | ||
18 | br(style="display: none;") | ||
diff --git a/server/lib/emails/video-auto-blacklist-new/html.pug b/server/lib/emails/video-auto-blacklist-new/html.pug new file mode 100644 index 000000000..07c8dfd16 --- /dev/null +++ b/server/lib/emails/video-auto-blacklist-new/html.pug | |||
@@ -0,0 +1,17 @@ | |||
1 | extends ../common/greetings | ||
2 | include ../common/mixins | ||
3 | |||
4 | block title | ||
5 | | A video is pending moderation | ||
6 | |||
7 | block content | ||
8 | p | ||
9 | | A recently added video was auto-blacklisted and requires moderator review before going public: | ||
10 | | | ||
11 | a(href=videoUrl) #{videoName} | ||
12 | | | ||
13 | | by #[+channel(channel)]. | ||
14 | p. | ||
15 | Apart from the publisher and the moderation team, no one will be able to see the video until you | ||
16 | unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so | ||
17 | that they don't require approval before going public. | ||
diff --git a/server/lib/emails/video-comment-mention/html.pug b/server/lib/emails/video-comment-mention/html.pug new file mode 100644 index 000000000..9e9ced62d --- /dev/null +++ b/server/lib/emails/video-comment-mention/html.pug | |||
@@ -0,0 +1,11 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone mentioned you | ||
5 | |||
6 | block content | ||
7 | p. | ||
8 | #[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video | ||
9 | "#[a(href=videoUrl) #{video.name}]": | ||
10 | blockquote #{comment.text} | ||
11 | br(style="display: none;") \ No newline at end of file | ||
diff --git a/server/lib/emails/video-comment-new/html.pug b/server/lib/emails/video-comment-new/html.pug new file mode 100644 index 000000000..075af5717 --- /dev/null +++ b/server/lib/emails/video-comment-new/html.pug | |||
@@ -0,0 +1,11 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Someone commented your video | ||
5 | |||
6 | block content | ||
7 | p. | ||
8 | #[a(href=accountUrl title=handle) #{accountName}] added a comment on your video | ||
9 | "#[a(href=videoUrl) #{video.name}]": | ||
10 | blockquote #{comment.text} | ||
11 | br(style="display: none;") \ No newline at end of file | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts index 440c3fde8..26ab3bd0d 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -5,7 +5,7 @@ import { VideoCaptionModel } from '../../models/video/video-caption' | |||
5 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 5 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
6 | import { CONFIG } from '../../initializers/config' | 6 | import { CONFIG } from '../../initializers/config' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
8 | import { fetchRemoteVideoStaticFile } from '../activitypub' | 8 | import { doRequestAndSaveToFile } from '@server/helpers/requests' |
9 | 9 | ||
10 | type GetPathParam = { videoId: string, language: string } | 10 | type GetPathParam = { videoId: string, language: string } |
11 | 11 | ||
@@ -46,11 +46,10 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> { | |||
46 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 46 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) |
47 | if (!video) return undefined | 47 | if (!video) return undefined |
48 | 48 | ||
49 | // FIXME: use URL | 49 | const remoteUrl = videoCaption.getFileUrl(video) |
50 | const remoteStaticPath = videoCaption.getCaptionStaticPath() | ||
51 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) | 50 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) |
52 | 51 | ||
53 | await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) | 52 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) |
54 | 53 | ||
55 | return { isOwned: false, path: destPath } | 54 | return { isOwned: false, path: destPath } |
56 | } | 55 | } |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index 3da6bb138..d0d4fc5b5 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FILES_CACHE, STATIC_PATHS } from '../../initializers/constants' | 2 | import { FILES_CACHE } from '../../initializers/constants' |
3 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
5 | import { CONFIG } from '../../initializers/config' | 5 | import { doRequestAndSaveToFile } from '@server/helpers/requests' |
6 | import { fetchRemoteVideoStaticFile } from '../activitypub' | ||
7 | 6 | ||
8 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | 7 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { |
9 | 8 | ||
@@ -32,11 +31,11 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
32 | 31 | ||
33 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') | 32 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') |
34 | 33 | ||
35 | // FIXME: use URL | 34 | const preview = video.getPreview() |
36 | const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) | 35 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) |
37 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename) | ||
38 | 36 | ||
39 | await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) | 37 | const remoteUrl = preview.getFileUrl(video) |
38 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | ||
40 | 39 | ||
41 | return { isOwned: false, path: destPath } | 40 | return { isOwned: false, path: destPath } |
42 | } | 41 | } |
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 4a7cda0a2..7034c10d0 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -11,13 +11,7 @@ import { ActorModel } from '../../../models/activitypub/actor' | |||
11 | import { Notifier } from '../../notifier' | 11 | import { Notifier } from '../../notifier' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models' | 13 | import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models' |
14 | 14 | import { ActivitypubFollowPayload } from '@shared/models' | |
15 | export type ActivitypubFollowPayload = { | ||
16 | followerActorId: number | ||
17 | name: string | ||
18 | host: string | ||
19 | isAutoFollow?: boolean | ||
20 | } | ||
21 | 15 | ||
22 | async function processActivityPubFollow (job: Bull.Job) { | 16 | async function processActivityPubFollow (job: Bull.Job) { |
23 | const payload = job.data as ActivitypubFollowPayload | 17 | const payload = job.data as ActivitypubFollowPayload |
@@ -34,6 +28,11 @@ async function processActivityPubFollow (job: Bull.Job) { | |||
34 | targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') | 28 | targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') |
35 | } | 29 | } |
36 | 30 | ||
31 | if (payload.assertIsChannel && !targetActor.VideoChannel) { | ||
32 | logger.warn('Do not follow %s@%s because it is not a channel.', payload.name, host) | ||
33 | return | ||
34 | } | ||
35 | |||
37 | const fromActor = await ActorModel.load(payload.followerActorId) | 36 | const fromActor = await ActorModel.load(payload.followerActorId) |
38 | 37 | ||
39 | return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow) | 38 | return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index 0ff7b44a0..e4d3dbbff 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -5,12 +5,7 @@ import { doRequest } from '../../../helpers/requests' | |||
5 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 5 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
6 | import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' | 6 | import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' |
7 | import { ActorFollowScoreCache } from '../../files-cache' | 7 | import { ActorFollowScoreCache } from '../../files-cache' |
8 | 8 | import { ActivitypubHttpBroadcastPayload } from '@shared/models' | |
9 | export type ActivitypubHttpBroadcastPayload = { | ||
10 | uris: string[] | ||
11 | signatureActorId?: number | ||
12 | body: any | ||
13 | } | ||
14 | 9 | ||
15 | async function processActivityPubHttpBroadcast (job: Bull.Job) { | 10 | async function processActivityPubHttpBroadcast (job: Bull.Job) { |
16 | logger.info('Processing ActivityPub broadcast in job %d.', job.id) | 11 | logger.info('Processing ActivityPub broadcast in job %d.', job.id) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index 0182c5169..524aadc27 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -5,22 +5,15 @@ import { processActivities } from '../../activitypub/process' | |||
5 | import { addVideoComments } from '../../activitypub/video-comments' | 5 | import { addVideoComments } from '../../activitypub/video-comments' |
6 | import { crawlCollectionPage } from '../../activitypub/crawl' | 6 | import { crawlCollectionPage } from '../../activitypub/crawl' |
7 | import { VideoModel } from '../../../models/video/video' | 7 | import { VideoModel } from '../../../models/video/video' |
8 | import { addVideoShares, createRates } from '../../activitypub' | 8 | import { addVideoShares } from '../../activitypub/share' |
9 | import { createRates } from '../../activitypub/video-rates' | ||
9 | import { createAccountPlaylists } from '../../activitypub/playlist' | 10 | import { createAccountPlaylists } from '../../activitypub/playlist' |
10 | import { AccountModel } from '../../../models/account/account' | 11 | import { AccountModel } from '../../../models/account/account' |
11 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 12 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
12 | import { VideoShareModel } from '../../../models/video/video-share' | 13 | import { VideoShareModel } from '../../../models/video/video-share' |
13 | import { VideoCommentModel } from '../../../models/video/video-comment' | 14 | import { VideoCommentModel } from '../../../models/video/video-comment' |
14 | import { MAccountDefault, MVideoFullLight } from '../../../typings/models' | 15 | import { MAccountDefault, MVideoFullLight } from '../../../typings/models' |
15 | 16 | import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' | |
16 | type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists' | ||
17 | |||
18 | export type ActivitypubHttpFetcherPayload = { | ||
19 | uri: string | ||
20 | type: FetchType | ||
21 | videoId?: number | ||
22 | accountId?: number | ||
23 | } | ||
24 | 17 | ||
25 | async function processActivityPubHttpFetcher (job: Bull.Job) { | 18 | async function processActivityPubHttpFetcher (job: Bull.Job) { |
26 | logger.info('Processing ActivityPub fetcher in job %d.', job.id) | 19 | logger.info('Processing ActivityPub fetcher in job %d.', job.id) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index c70ce3be9..b65eeb677 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -4,12 +4,7 @@ import { doRequest } from '../../../helpers/requests' | |||
4 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 4 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
5 | import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' | 5 | import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' |
6 | import { ActorFollowScoreCache } from '../../files-cache' | 6 | import { ActorFollowScoreCache } from '../../files-cache' |
7 | 7 | import { ActivitypubHttpUnicastPayload } from '@shared/models' | |
8 | export type ActivitypubHttpUnicastPayload = { | ||
9 | uri: string | ||
10 | signatureActorId?: number | ||
11 | body: any | ||
12 | } | ||
13 | 8 | ||
14 | async function processActivityPubHttpUnicast (job: Bull.Job) { | 9 | async function processActivityPubHttpUnicast (job: Bull.Job) { |
15 | logger.info('Processing ActivityPub unicast in job %d.', job.id) | 10 | logger.info('Processing ActivityPub unicast in job %d.', job.id) |
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 4d6c38cfa..666e56868 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -1,14 +1,12 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { fetchVideoByUrl } from '../../../helpers/video' | 3 | import { fetchVideoByUrl } from '../../../helpers/video' |
4 | import { refreshActorIfNeeded, refreshVideoIfNeeded, refreshVideoPlaylistIfNeeded } from '../../activitypub' | 4 | import { refreshActorIfNeeded } from '../../activitypub/actor' |
5 | import { refreshVideoIfNeeded } from '../../activitypub/videos' | ||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 6 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 7 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
7 | 8 | import { RefreshPayload } from '@shared/models' | |
8 | export type RefreshPayload = { | 9 | import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist' |
9 | type: 'video' | 'video-playlist' | 'actor' | ||
10 | url: string | ||
11 | } | ||
12 | 10 | ||
13 | async function refreshAPObject (job: Bull.Job) { | 11 | async function refreshAPObject (job: Bull.Job) { |
14 | const payload = job.data as RefreshPayload | 12 | const payload = job.data as RefreshPayload |
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts index 62701222c..3157731e2 100644 --- a/server/lib/job-queue/handlers/email.ts +++ b/server/lib/job-queue/handlers/email.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { logger } from '../../../helpers/logger' |
3 | import { Emailer, SendEmailOptions } from '../../emailer' | 3 | import { Emailer } from '../../emailer' |
4 | 4 | import { EmailPayload } from '@shared/models' | |
5 | export type EmailPayload = SendEmailOptions | ||
6 | 5 | ||
7 | async function processEmail (job: Bull.Job) { | 6 | async function processEmail (job: Bull.Job) { |
8 | const payload = job.data as EmailPayload | 7 | const payload = job.data as EmailPayload |
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 d3bde6e6a..bcb49a731 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { buildSignedActivity } from '../../../../helpers/activitypub' | 1 | import { buildSignedActivity } from '../../../../helpers/activitypub' |
2 | import { getServerActor } from '../../../../helpers/utils' | ||
3 | import { ActorModel } from '../../../../models/activitypub/actor' | 2 | import { ActorModel } from '../../../../models/activitypub/actor' |
4 | import { sha256 } from '../../../../helpers/core-utils' | 3 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' |
5 | import { HTTP_SIGNATURE } from '../../../../initializers/constants' | ||
6 | import { MActor } from '../../../../typings/models' | 4 | import { MActor } from '../../../../typings/models' |
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | ||
7 | import { ContextType } from '@shared/models/activitypub/context' | ||
7 | 8 | ||
8 | type Payload = { body: any, signatureActorId?: number } | 9 | type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } |
9 | 10 | ||
10 | async function computeBody (payload: Payload) { | 11 | async function computeBody (payload: Payload) { |
11 | let body = payload.body | 12 | let body = payload.body |
@@ -13,7 +14,7 @@ async function computeBody (payload: Payload) { | |||
13 | if (payload.signatureActorId) { | 14 | if (payload.signatureActorId) { |
14 | const actorSignature = await ActorModel.load(payload.signatureActorId) | 15 | const actorSignature = await ActorModel.load(payload.signatureActorId) |
15 | if (!actorSignature) throw new Error('Unknown signature actor id.') | 16 | if (!actorSignature) throw new Error('Unknown signature actor id.') |
16 | body = await buildSignedActivity(actorSignature, payload.body) | 17 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) |
17 | } | 18 | } |
18 | 19 | ||
19 | return body | 20 | return body |
@@ -42,18 +43,13 @@ async function buildSignedRequestOptions (payload: Payload) { | |||
42 | 43 | ||
43 | function buildGlobalHeaders (body: any) { | 44 | function buildGlobalHeaders (body: any) { |
44 | return { | 45 | return { |
45 | 'Digest': buildDigest(body) | 46 | 'Digest': buildDigest(body), |
47 | 'Content-Type': 'application/activity+json', | ||
48 | 'Accept': ACTIVITY_PUB.ACCEPT_HEADER | ||
46 | } | 49 | } |
47 | } | 50 | } |
48 | 51 | ||
49 | function buildDigest (body: any) { | ||
50 | const rawBody = typeof body === 'string' ? body : JSON.stringify(body) | ||
51 | |||
52 | return 'SHA-256=' + sha256(rawBody, 'base64') | ||
53 | } | ||
54 | |||
55 | export { | 52 | export { |
56 | buildDigest, | ||
57 | buildGlobalHeaders, | 53 | buildGlobalHeaders, |
58 | computeBody, | 54 | computeBody, |
59 | buildSignedRequestOptions | 55 | buildSignedRequestOptions |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 99c991e72..ae11f1de3 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -9,11 +9,7 @@ import { extname } from 'path' | |||
9 | import { MVideoFile, MVideoWithFile } from '@server/typings/models' | 9 | import { MVideoFile, MVideoWithFile } from '@server/typings/models' |
10 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 10 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
11 | import { getVideoFilePath } from '@server/lib/video-paths' | 11 | import { getVideoFilePath } from '@server/lib/video-paths' |
12 | 12 | import { VideoFileImportPayload } from '@shared/models' | |
13 | export type VideoFileImportPayload = { | ||
14 | videoUUID: string, | ||
15 | filePath: string | ||
16 | } | ||
17 | 13 | ||
18 | async function processVideoFileImport (job: Bull.Job) { | 14 | async function processVideoFileImport (job: Bull.Job) { |
19 | const payload = job.data as VideoFileImportPayload | 15 | const payload = job.data as VideoFileImportPayload |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 1fca17584..ad549c6fc 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -7,9 +7,8 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro | |||
7 | import { extname } from 'path' | 7 | import { extname } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' | 9 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' |
10 | import { VideoState } from '../../../../shared' | 10 | import { VideoImportPayload, VideoImportTorrentPayload, VideoImportYoutubeDLPayload, VideoState } from '../../../../shared' |
11 | import { JobQueue } from '../index' | 11 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
12 | import { federateVideoIfNeeded } from '../../activitypub' | ||
13 | import { VideoModel } from '../../../models/video/video' | 12 | import { VideoModel } from '../../../models/video/video' |
14 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 13 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
15 | import { getSecureTorrentName } from '../../../helpers/utils' | 14 | import { getSecureTorrentName } from '../../../helpers/utils' |
@@ -17,27 +16,12 @@ import { move, remove, stat } from 'fs-extra' | |||
17 | import { Notifier } from '../../notifier' | 16 | import { Notifier } from '../../notifier' |
18 | import { CONFIG } from '../../../initializers/config' | 17 | import { CONFIG } from '../../../initializers/config' |
19 | import { sequelizeTypescript } from '../../../initializers/database' | 18 | import { sequelizeTypescript } from '../../../initializers/database' |
20 | import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' | 19 | import { generateVideoMiniature } from '../../thumbnail' |
21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 20 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
22 | import { MThumbnail } from '../../../typings/models/video/thumbnail' | 21 | import { MThumbnail } from '../../../typings/models/video/thumbnail' |
23 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' | 22 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' |
24 | import { getVideoFilePath } from '@server/lib/video-paths' | 23 | import { getVideoFilePath } from '@server/lib/video-paths' |
25 | 24 | import { addOptimizeOrMergeAudioJob } from '@server/helpers/video' | |
26 | type VideoImportYoutubeDLPayload = { | ||
27 | type: 'youtube-dl' | ||
28 | videoImportId: number | ||
29 | |||
30 | thumbnailUrl: string | ||
31 | downloadThumbnail: boolean | ||
32 | downloadPreview: boolean | ||
33 | } | ||
34 | |||
35 | type VideoImportTorrentPayload = { | ||
36 | type: 'magnet-uri' | 'torrent-file' | ||
37 | videoImportId: number | ||
38 | } | ||
39 | |||
40 | export type VideoImportPayload = VideoImportYoutubeDLPayload | VideoImportTorrentPayload | ||
41 | 25 | ||
42 | async function processVideoImport (job: Bull.Job) { | 26 | async function processVideoImport (job: Bull.Job) { |
43 | const payload = job.data as VideoImportPayload | 27 | const payload = job.data as VideoImportPayload |
@@ -62,9 +46,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP | |||
62 | const options = { | 46 | const options = { |
63 | videoImportId: payload.videoImportId, | 47 | videoImportId: payload.videoImportId, |
64 | 48 | ||
65 | downloadThumbnail: false, | ||
66 | downloadPreview: false, | ||
67 | |||
68 | generateThumbnail: true, | 49 | generateThumbnail: true, |
69 | generatePreview: true | 50 | generatePreview: true |
70 | } | 51 | } |
@@ -82,15 +63,11 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub | |||
82 | const options = { | 63 | const options = { |
83 | videoImportId: videoImport.id, | 64 | videoImportId: videoImport.id, |
84 | 65 | ||
85 | downloadThumbnail: payload.downloadThumbnail, | 66 | generateThumbnail: payload.generateThumbnail, |
86 | downloadPreview: payload.downloadPreview, | 67 | generatePreview: payload.generatePreview |
87 | thumbnailUrl: payload.thumbnailUrl, | ||
88 | |||
89 | generateThumbnail: false, | ||
90 | generatePreview: false | ||
91 | } | 68 | } |
92 | 69 | ||
93 | return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) | 70 | return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options) |
94 | } | 71 | } |
95 | 72 | ||
96 | async function getVideoImportOrDie (videoImportId: number) { | 73 | async function getVideoImportOrDie (videoImportId: number) { |
@@ -105,10 +82,6 @@ async function getVideoImportOrDie (videoImportId: number) { | |||
105 | type ProcessFileOptions = { | 82 | type ProcessFileOptions = { |
106 | videoImportId: number | 83 | videoImportId: number |
107 | 84 | ||
108 | downloadThumbnail: boolean | ||
109 | downloadPreview: boolean | ||
110 | thumbnailUrl?: string | ||
111 | |||
112 | generateThumbnail: boolean | 85 | generateThumbnail: boolean |
113 | generatePreview: boolean | 86 | generatePreview: boolean |
114 | } | 87 | } |
@@ -153,17 +126,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
153 | 126 | ||
154 | // Process thumbnail | 127 | // Process thumbnail |
155 | let thumbnailModel: MThumbnail | 128 | let thumbnailModel: MThumbnail |
156 | if (options.downloadThumbnail && options.thumbnailUrl) { | 129 | if (options.generateThumbnail) { |
157 | thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE) | ||
158 | } else if (options.generateThumbnail || options.downloadThumbnail) { | ||
159 | thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) | 130 | thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) |
160 | } | 131 | } |
161 | 132 | ||
162 | // Process preview | 133 | // Process preview |
163 | let previewModel: MThumbnail | 134 | let previewModel: MThumbnail |
164 | if (options.downloadPreview && options.thumbnailUrl) { | 135 | if (options.generatePreview) { |
165 | previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW) | ||
166 | } else if (options.generatePreview || options.downloadPreview) { | ||
167 | previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) | 136 | previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) |
168 | } | 137 | } |
169 | 138 | ||
@@ -214,14 +183,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
214 | 183 | ||
215 | // Create transcoding jobs? | 184 | // Create transcoding jobs? |
216 | if (video.state === VideoState.TO_TRANSCODE) { | 185 | if (video.state === VideoState.TO_TRANSCODE) { |
217 | // Put uuid because we don't have id auto incremented for now | 186 | await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile) |
218 | const dataInput = { | ||
219 | type: 'optimize' as 'optimize', | ||
220 | videoUUID: videoImportUpdated.Video.uuid, | ||
221 | isNewVideo: true | ||
222 | } | ||
223 | |||
224 | await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | ||
225 | } | 187 | } |
226 | 188 | ||
227 | } catch (err) { | 189 | } catch (err) { |
diff --git a/server/lib/job-queue/handlers/video-redundancy.ts b/server/lib/job-queue/handlers/video-redundancy.ts new file mode 100644 index 000000000..6296dab05 --- /dev/null +++ b/server/lib/job-queue/handlers/video-redundancy.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import * as Bull from 'bull' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { VideosRedundancyScheduler } from '@server/lib/schedulers/videos-redundancy-scheduler' | ||
4 | import { VideoRedundancyPayload } from '@shared/models' | ||
5 | |||
6 | async function processVideoRedundancy (job: Bull.Job) { | ||
7 | const payload = job.data as VideoRedundancyPayload | ||
8 | logger.info('Processing video redundancy in job %d.', job.id) | ||
9 | |||
10 | return VideosRedundancyScheduler.Instance.createManualRedundancy(payload.videoId) | ||
11 | } | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | processVideoRedundancy | ||
17 | } | ||
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 39b9fac98..46d52e1cf 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -1,48 +1,22 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { VideoResolution } from '../../../../shared' | 2 | import { |
3 | MergeAudioTranscodingPayload, | ||
4 | NewResolutionTranscodingPayload, | ||
5 | OptimizeTranscodingPayload, | ||
6 | VideoTranscodingPayload | ||
7 | } from '../../../../shared' | ||
3 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
4 | import { VideoModel } from '../../../models/video/video' | 9 | import { VideoModel } from '../../../models/video/video' |
5 | import { JobQueue } from '../job-queue' | 10 | import { JobQueue } from '../job-queue' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | 11 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 12 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
8 | import { sequelizeTypescript } from '../../../initializers' | 13 | import { sequelizeTypescript } from '../../../initializers/database' |
9 | import * as Bluebird from 'bluebird' | ||
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 14 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' | 15 | import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | 16 | import { Notifier } from '../../notifier' |
13 | import { CONFIG } from '../../../initializers/config' | 17 | import { CONFIG } from '../../../initializers/config' |
14 | import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models' | 18 | import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models' |
15 | 19 | ||
16 | interface BaseTranscodingPayload { | ||
17 | videoUUID: string | ||
18 | isNewVideo?: boolean | ||
19 | } | ||
20 | |||
21 | interface HLSTranscodingPayload extends BaseTranscodingPayload { | ||
22 | type: 'hls' | ||
23 | isPortraitMode?: boolean | ||
24 | resolution: VideoResolution | ||
25 | copyCodecs: boolean | ||
26 | } | ||
27 | |||
28 | interface NewResolutionTranscodingPayload extends BaseTranscodingPayload { | ||
29 | type: 'new-resolution' | ||
30 | isPortraitMode?: boolean | ||
31 | resolution: VideoResolution | ||
32 | } | ||
33 | |||
34 | interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { | ||
35 | type: 'merge-audio' | ||
36 | resolution: VideoResolution | ||
37 | } | ||
38 | |||
39 | interface OptimizeTranscodingPayload extends BaseTranscodingPayload { | ||
40 | type: 'optimize' | ||
41 | } | ||
42 | |||
43 | export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload | ||
44 | | OptimizeTranscodingPayload | MergeAudioTranscodingPayload | ||
45 | |||
46 | async function processVideoTranscoding (job: Bull.Job) { | 20 | async function processVideoTranscoding (job: Bull.Job) { |
47 | const payload = job.data as VideoTranscodingPayload | 21 | const payload = job.data as VideoTranscodingPayload |
48 | logger.info('Processing video file in job %d.', job.id) | 22 | logger.info('Processing video file in job %d.', job.id) |
@@ -105,7 +79,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
105 | 79 | ||
106 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 80 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { |
107 | // Maybe the video changed in database, refresh it | 81 | // Maybe the video changed in database, refresh it |
108 | let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) | 82 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) |
109 | // Video does not exist anymore | 83 | // Video does not exist anymore |
110 | if (!videoDatabase) return undefined | 84 | if (!videoDatabase) return undefined |
111 | 85 | ||
@@ -118,12 +92,11 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
118 | 92 | ||
119 | let videoPublished = false | 93 | let videoPublished = false |
120 | 94 | ||
95 | // Generate HLS version of the max quality file | ||
121 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution }) | 96 | const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution }) |
122 | await createHlsJobIfEnabled(hlsPayload) | 97 | await createHlsJobIfEnabled(hlsPayload) |
123 | 98 | ||
124 | if (resolutionsEnabled.length !== 0) { | 99 | if (resolutionsEnabled.length !== 0) { |
125 | const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = [] | ||
126 | |||
127 | for (const resolution of resolutionsEnabled) { | 100 | for (const resolution of resolutionsEnabled) { |
128 | let dataInput: VideoTranscodingPayload | 101 | let dataInput: VideoTranscodingPayload |
129 | 102 | ||
@@ -143,12 +116,9 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O | |||
143 | } | 116 | } |
144 | } | 117 | } |
145 | 118 | ||
146 | const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) | 119 | JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) |
147 | tasks.push(p) | ||
148 | } | 120 | } |
149 | 121 | ||
150 | await Promise.all(tasks) | ||
151 | |||
152 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) | 122 | logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) |
153 | } else { | 123 | } else { |
154 | // No transcoding to do, it's now published | 124 | // No transcoding to do, it's now published |
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts index 73fa5ed04..7211df237 100644 --- a/server/lib/job-queue/handlers/video-views.ts +++ b/server/lib/job-queue/handlers/video-views.ts | |||
@@ -3,7 +3,7 @@ import { logger } from '../../../helpers/logger' | |||
3 | import { VideoModel } from '../../../models/video/video' | 3 | import { VideoModel } from '../../../models/video/video' |
4 | import { VideoViewModel } from '../../../models/video/video-views' | 4 | import { VideoViewModel } from '../../../models/video/video-views' |
5 | import { isTestInstance } from '../../../helpers/core-utils' | 5 | import { isTestInstance } from '../../../helpers/core-utils' |
6 | import { federateVideoIfNeeded } from '../../activitypub' | 6 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
7 | 7 | ||
8 | async function processVideosViews () { | 8 | async function processVideosViews () { |
9 | const lastHour = new Date() | 9 | const lastHour = new Date() |
@@ -23,6 +23,8 @@ async function processVideosViews () { | |||
23 | for (const videoId of videoIds) { | 23 | for (const videoId of videoIds) { |
24 | try { | 24 | try { |
25 | const views = await Redis.Instance.getVideoViews(videoId, hour) | 25 | const views = await Redis.Instance.getVideoViews(videoId, hour) |
26 | await Redis.Instance.deleteVideoViews(videoId, hour) | ||
27 | |||
26 | if (views) { | 28 | if (views) { |
27 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) | 29 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) |
28 | 30 | ||
@@ -52,8 +54,6 @@ async function processVideosViews () { | |||
52 | logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err }) | 54 | logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err }) |
53 | } | 55 | } |
54 | } | 56 | } |
55 | |||
56 | await Redis.Instance.deleteVideoViews(videoId, hour) | ||
57 | } catch (err) { | 57 | } catch (err) { |
58 | logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err }) | 58 | logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err }) |
59 | } | 59 | } |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index ec601e9ea..14e181835 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -1,18 +1,32 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { JobState, JobType } from '../../../shared/models' | 2 | import { |
3 | ActivitypubFollowPayload, | ||
4 | ActivitypubHttpBroadcastPayload, | ||
5 | ActivitypubHttpFetcherPayload, | ||
6 | ActivitypubHttpUnicastPayload, | ||
7 | EmailPayload, | ||
8 | JobState, | ||
9 | JobType, | ||
10 | RefreshPayload, | ||
11 | VideoFileImportPayload, | ||
12 | VideoImportPayload, | ||
13 | VideoRedundancyPayload, | ||
14 | VideoTranscodingPayload | ||
15 | } from '../../../shared/models' | ||
3 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
4 | import { Redis } from '../redis' | 17 | import { Redis } from '../redis' |
5 | import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants' | 18 | import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants' |
6 | import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' | 19 | import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' |
7 | import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' | 20 | import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' |
8 | import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' | 21 | import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' |
9 | import { EmailPayload, processEmail } from './handlers/email' | 22 | import { processEmail } from './handlers/email' |
10 | import { processVideoTranscoding, VideoTranscodingPayload } from './handlers/video-transcoding' | 23 | import { processVideoTranscoding } from './handlers/video-transcoding' |
11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' | 24 | import { processActivityPubFollow } from './handlers/activitypub-follow' |
12 | import { processVideoImport, VideoImportPayload } from './handlers/video-import' | 25 | import { processVideoImport } from './handlers/video-import' |
13 | import { processVideosViews } from './handlers/video-views' | 26 | import { processVideosViews } from './handlers/video-views' |
14 | import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' | 27 | import { refreshAPObject } from './handlers/activitypub-refresher' |
15 | import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' | 28 | import { processVideoFileImport } from './handlers/video-file-import' |
29 | import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' | ||
16 | 30 | ||
17 | type CreateJobArgument = | 31 | type CreateJobArgument = |
18 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 32 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -24,20 +38,21 @@ type CreateJobArgument = | |||
24 | { type: 'email', payload: EmailPayload } | | 38 | { type: 'email', payload: EmailPayload } | |
25 | { type: 'video-import', payload: VideoImportPayload } | | 39 | { type: 'video-import', payload: VideoImportPayload } | |
26 | { type: 'activitypub-refresher', payload: RefreshPayload } | | 40 | { type: 'activitypub-refresher', payload: RefreshPayload } | |
27 | { type: 'videos-views', payload: {} } | 41 | { type: 'videos-views', payload: {} } | |
42 | { type: 'video-redundancy', payload: VideoRedundancyPayload } | ||
28 | 43 | ||
29 | const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = { | 44 | const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = { |
30 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, | 45 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, |
31 | 'activitypub-http-unicast': processActivityPubHttpUnicast, | 46 | 'activitypub-http-unicast': processActivityPubHttpUnicast, |
32 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, | 47 | 'activitypub-http-fetcher': processActivityPubHttpFetcher, |
33 | 'activitypub-follow': processActivityPubFollow, | 48 | 'activitypub-follow': processActivityPubFollow, |
34 | 'video-file-import': processVideoFileImport, | 49 | 'video-file-import': processVideoFileImport, |
35 | 'video-transcoding': processVideoTranscoding, | 50 | 'video-transcoding': processVideoTranscoding, |
36 | 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3) | ||
37 | 'email': processEmail, | 51 | 'email': processEmail, |
38 | 'video-import': processVideoImport, | 52 | 'video-import': processVideoImport, |
39 | 'videos-views': processVideosViews, | 53 | 'videos-views': processVideosViews, |
40 | 'activitypub-refresher': refreshAPObject | 54 | 'activitypub-refresher': refreshAPObject, |
55 | 'video-redundancy': processVideoRedundancy | ||
41 | } | 56 | } |
42 | 57 | ||
43 | const jobTypes: JobType[] = [ | 58 | const jobTypes: JobType[] = [ |
@@ -50,20 +65,22 @@ const jobTypes: JobType[] = [ | |||
50 | 'video-file-import', | 65 | 'video-file-import', |
51 | 'video-import', | 66 | 'video-import', |
52 | 'videos-views', | 67 | 'videos-views', |
53 | 'activitypub-refresher' | 68 | 'activitypub-refresher', |
69 | 'video-redundancy' | ||
54 | ] | 70 | ] |
55 | 71 | ||
56 | class JobQueue { | 72 | class JobQueue { |
57 | 73 | ||
58 | private static instance: JobQueue | 74 | private static instance: JobQueue |
59 | 75 | ||
60 | private queues: { [ id in JobType ]?: Bull.Queue } = {} | 76 | private queues: { [id in JobType]?: Bull.Queue } = {} |
61 | private initialized = false | 77 | private initialized = false |
62 | private jobRedisPrefix: string | 78 | private jobRedisPrefix: string |
63 | 79 | ||
64 | private constructor () {} | 80 | private constructor () { |
81 | } | ||
65 | 82 | ||
66 | async init () { | 83 | init () { |
67 | // Already initialized | 84 | // Already initialized |
68 | if (this.initialized === true) return | 85 | if (this.initialized === true) return |
69 | this.initialized = true | 86 | this.initialized = true |
@@ -105,11 +122,16 @@ class JobQueue { | |||
105 | } | 122 | } |
106 | } | 123 | } |
107 | 124 | ||
108 | createJob (obj: CreateJobArgument) { | 125 | createJob (obj: CreateJobArgument): void { |
126 | this.createJobWithPromise(obj) | ||
127 | .catch(err => logger.error('Cannot create job.', { err, obj })) | ||
128 | } | ||
129 | |||
130 | createJobWithPromise (obj: CreateJobArgument) { | ||
109 | const queue = this.queues[obj.type] | 131 | const queue = this.queues[obj.type] |
110 | if (queue === undefined) { | 132 | if (queue === undefined) { |
111 | logger.error('Unknown queue %s: cannot create job.', obj.type) | 133 | logger.error('Unknown queue %s: cannot create job.', obj.type) |
112 | throw Error('Unknown queue, cannot create job') | 134 | return |
113 | } | 135 | } |
114 | 136 | ||
115 | const jobArgs: Bull.JobOptions = { | 137 | const jobArgs: Bull.JobOptions = { |
@@ -122,10 +144,10 @@ class JobQueue { | |||
122 | } | 144 | } |
123 | 145 | ||
124 | async listForApi (options: { | 146 | async listForApi (options: { |
125 | state: JobState, | 147 | state: JobState |
126 | start: number, | 148 | start: number |
127 | count: number, | 149 | count: number |
128 | asc?: boolean, | 150 | asc?: boolean |
129 | jobType: JobType | 151 | jobType: JobType |
130 | }): Promise<Bull.Job[]> { | 152 | }): Promise<Bull.Job[]> { |
131 | const { state, start, count, asc, jobType } = options | 153 | const { state, start, count, asc, jobType } = options |
@@ -133,16 +155,14 @@ class JobQueue { | |||
133 | 155 | ||
134 | const filteredJobTypes = this.filterJobTypes(jobType) | 156 | const filteredJobTypes = this.filterJobTypes(jobType) |
135 | 157 | ||
136 | // TODO: optimize | ||
137 | for (const jobType of filteredJobTypes) { | 158 | for (const jobType of filteredJobTypes) { |
138 | const queue = this.queues[ jobType ] | 159 | const queue = this.queues[jobType] |
139 | if (queue === undefined) { | 160 | if (queue === undefined) { |
140 | logger.error('Unknown queue %s to list jobs.', jobType) | 161 | logger.error('Unknown queue %s to list jobs.', jobType) |
141 | continue | 162 | continue |
142 | } | 163 | } |
143 | 164 | ||
144 | // FIXME: Bull queue typings does not have getJobs method | 165 | const jobs = await queue.getJobs([ state ], 0, start + count, asc) |
145 | const jobs = await (queue as any).getJobs(state, 0, start + count, asc) | ||
146 | results = results.concat(jobs) | 166 | results = results.concat(jobs) |
147 | } | 167 | } |
148 | 168 | ||
@@ -164,7 +184,7 @@ class JobQueue { | |||
164 | const filteredJobTypes = this.filterJobTypes(jobType) | 184 | const filteredJobTypes = this.filterJobTypes(jobType) |
165 | 185 | ||
166 | for (const type of filteredJobTypes) { | 186 | for (const type of filteredJobTypes) { |
167 | const queue = this.queues[ type ] | 187 | const queue = this.queues[type] |
168 | if (queue === undefined) { | 188 | if (queue === undefined) { |
169 | logger.error('Unknown queue %s to count jobs.', type) | 189 | logger.error('Unknown queue %s to count jobs.', type) |
170 | continue | 190 | continue |
@@ -172,7 +192,7 @@ class JobQueue { | |||
172 | 192 | ||
173 | const counts = await queue.getJobCounts() | 193 | const counts = await queue.getJobCounts() |
174 | 194 | ||
175 | total += counts[ state ] | 195 | total += counts[state] |
176 | } | 196 | } |
177 | 197 | ||
178 | return total | 198 | return total |
@@ -188,7 +208,7 @@ class JobQueue { | |||
188 | private addRepeatableJobs () { | 208 | private addRepeatableJobs () { |
189 | this.queues['videos-views'].add({}, { | 209 | this.queues['videos-views'].add({}, { |
190 | repeat: REPEAT_JOBS['videos-views'] | 210 | repeat: REPEAT_JOBS['videos-views'] |
191 | }) | 211 | }).catch(err => logger.error('Cannot add repeatable job.', { err })) |
192 | } | 212 | } |
193 | 213 | ||
194 | private filterJobTypes (jobType?: JobType) { | 214 | private filterJobTypes (jobType?: JobType) { |
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index b609f4585..55f7a985d 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts | |||
@@ -15,41 +15,41 @@ export type AcceptResult = { | |||
15 | 15 | ||
16 | // Can be filtered by plugins | 16 | // Can be filtered by plugins |
17 | function isLocalVideoAccepted (object: { | 17 | function isLocalVideoAccepted (object: { |
18 | videoBody: VideoCreate, | 18 | videoBody: VideoCreate |
19 | videoFile: Express.Multer.File & { duration?: number }, | 19 | videoFile: Express.Multer.File & { duration?: number } |
20 | user: UserModel | 20 | user: UserModel |
21 | }): AcceptResult { | 21 | }): AcceptResult { |
22 | return { accepted: true } | 22 | return { accepted: true } |
23 | } | 23 | } |
24 | 24 | ||
25 | function isLocalVideoThreadAccepted (_object: { | 25 | function isLocalVideoThreadAccepted (_object: { |
26 | commentBody: VideoCommentCreate, | 26 | commentBody: VideoCommentCreate |
27 | video: VideoModel, | 27 | video: VideoModel |
28 | user: UserModel | 28 | user: UserModel |
29 | }): AcceptResult { | 29 | }): AcceptResult { |
30 | return { accepted: true } | 30 | return { accepted: true } |
31 | } | 31 | } |
32 | 32 | ||
33 | function isLocalVideoCommentReplyAccepted (_object: { | 33 | function isLocalVideoCommentReplyAccepted (_object: { |
34 | commentBody: VideoCommentCreate, | 34 | commentBody: VideoCommentCreate |
35 | parentComment: VideoCommentModel, | 35 | parentComment: VideoCommentModel |
36 | video: VideoModel, | 36 | video: VideoModel |
37 | user: UserModel | 37 | user: UserModel |
38 | }): AcceptResult { | 38 | }): AcceptResult { |
39 | return { accepted: true } | 39 | return { accepted: true } |
40 | } | 40 | } |
41 | 41 | ||
42 | function isRemoteVideoAccepted (_object: { | 42 | function isRemoteVideoAccepted (_object: { |
43 | activity: ActivityCreate, | 43 | activity: ActivityCreate |
44 | videoAP: VideoTorrentObject, | 44 | videoAP: VideoTorrentObject |
45 | byActor: ActorModel | 45 | byActor: ActorModel |
46 | }): AcceptResult { | 46 | }): AcceptResult { |
47 | return { accepted: true } | 47 | return { accepted: true } |
48 | } | 48 | } |
49 | 49 | ||
50 | function isRemoteVideoCommentAccepted (_object: { | 50 | function isRemoteVideoCommentAccepted (_object: { |
51 | activity: ActivityCreate, | 51 | activity: ActivityCreate |
52 | commentAP: VideoCommentObject, | 52 | commentAP: VideoCommentObject |
53 | byActor: ActorModel | 53 | byActor: ActorModel |
54 | }): AcceptResult { | 54 | }): AcceptResult { |
55 | return { accepted: true } | 55 | return { accepted: true } |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 679b9bcf6..017739523 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -5,8 +5,7 @@ import { UserNotificationModel } from '../models/account/user-notification' | |||
5 | import { UserModel } from '../models/account/user' | 5 | import { UserModel } from '../models/account/user' |
6 | import { PeerTubeSocket } from './peertube-socket' | 6 | import { PeerTubeSocket } from './peertube-socket' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | 8 | import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos' |
9 | import * as Bluebird from 'bluebird' | ||
10 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 9 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
11 | import { | 10 | import { |
12 | MCommentOwnerVideo, | 11 | MCommentOwnerVideo, |
@@ -17,7 +16,8 @@ import { | |||
17 | MVideoFullLight | 16 | MVideoFullLight |
18 | } from '../typings/models/video' | 17 | } from '../typings/models/video' |
19 | import { | 18 | import { |
20 | MUser, MUserAccount, | 19 | MUser, |
20 | MUserAccount, | ||
21 | MUserDefault, | 21 | MUserDefault, |
22 | MUserNotifSettingAccount, | 22 | MUserNotifSettingAccount, |
23 | MUserWithNotificationSetting, | 23 | MUserWithNotificationSetting, |
@@ -26,20 +26,21 @@ import { | |||
26 | import { MAccountDefault, MActorFollowFull } from '../typings/models' | 26 | import { MAccountDefault, MActorFollowFull } from '../typings/models' |
27 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | 27 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' |
28 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | 28 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' |
29 | import { getServerActor } from '@server/helpers/utils' | 29 | import { getServerActor } from '@server/models/application/application' |
30 | 30 | ||
31 | class Notifier { | 31 | class Notifier { |
32 | 32 | ||
33 | private static instance: Notifier | 33 | private static instance: Notifier |
34 | 34 | ||
35 | private constructor () {} | 35 | private constructor () { |
36 | } | ||
36 | 37 | ||
37 | notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { | 38 | notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { |
38 | // Only notify on public and published videos which are not blacklisted | 39 | // Only notify on public and published videos which are not blacklisted |
39 | if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return | 40 | if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return |
40 | 41 | ||
41 | this.notifySubscribersOfNewVideo(video) | 42 | this.notifySubscribersOfNewVideo(video) |
42 | .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) | 43 | .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) |
43 | } | 44 | } |
44 | 45 | ||
45 | notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { | 46 | notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { |
@@ -63,7 +64,9 @@ class Notifier { | |||
63 | if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return | 64 | if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return |
64 | 65 | ||
65 | this.notifyOwnedVideoHasBeenPublished(video) | 66 | this.notifyOwnedVideoHasBeenPublished(video) |
66 | .catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length | 67 | .catch(err => { |
68 | logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err }) | ||
69 | }) | ||
67 | } | 70 | } |
68 | 71 | ||
69 | notifyOnNewComment (comment: MCommentOwnerVideo): void { | 72 | notifyOnNewComment (comment: MCommentOwnerVideo): void { |
@@ -74,19 +77,19 @@ class Notifier { | |||
74 | .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) | 77 | .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) |
75 | } | 78 | } |
76 | 79 | ||
77 | notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void { | 80 | notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void { |
78 | this.notifyModeratorsOfNewVideoAbuse(videoAbuse) | 81 | this.notifyModeratorsOfNewVideoAbuse(parameters) |
79 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) | 82 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err })) |
80 | } | 83 | } |
81 | 84 | ||
82 | notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { | 85 | notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { |
83 | this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) | 86 | this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) |
84 | .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) | 87 | .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) |
85 | } | 88 | } |
86 | 89 | ||
87 | notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { | 90 | notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { |
88 | this.notifyVideoOwnerOfBlacklist(videoBlacklist) | 91 | this.notifyVideoOwnerOfBlacklist(videoBlacklist) |
89 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) | 92 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) |
90 | } | 93 | } |
91 | 94 | ||
92 | notifyOnVideoUnblacklist (video: MVideoFullLight): void { | 95 | notifyOnVideoUnblacklist (video: MVideoFullLight): void { |
@@ -96,7 +99,7 @@ class Notifier { | |||
96 | 99 | ||
97 | notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void { | 100 | notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void { |
98 | this.notifyOwnerVideoImportIsFinished(videoImport, success) | 101 | this.notifyOwnerVideoImportIsFinished(videoImport, success) |
99 | .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) | 102 | .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) |
100 | } | 103 | } |
101 | 104 | ||
102 | notifyOnNewUserRegistration (user: MUserDefault): void { | 105 | notifyOnNewUserRegistration (user: MUserDefault): void { |
@@ -106,14 +109,14 @@ class Notifier { | |||
106 | 109 | ||
107 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { | 110 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { |
108 | this.notifyUserOfNewActorFollow(actorFollow) | 111 | this.notifyUserOfNewActorFollow(actorFollow) |
109 | .catch(err => { | 112 | .catch(err => { |
110 | logger.error( | 113 | logger.error( |
111 | 'Cannot notify owner of channel %s of a new follow by %s.', | 114 | 'Cannot notify owner of channel %s of a new follow by %s.', |
112 | actorFollow.ActorFollowing.VideoChannel.getDisplayName(), | 115 | actorFollow.ActorFollowing.VideoChannel.getDisplayName(), |
113 | actorFollow.ActorFollower.Account.getDisplayName(), | 116 | actorFollow.ActorFollower.Account.getDisplayName(), |
114 | { err } | 117 | { err } |
115 | ) | 118 | ) |
116 | }) | 119 | }) |
117 | } | 120 | } |
118 | 121 | ||
119 | notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { | 122 | notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { |
@@ -347,11 +350,15 @@ class Notifier { | |||
347 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | 350 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) |
348 | } | 351 | } |
349 | 352 | ||
350 | private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { | 353 | private async notifyModeratorsOfNewVideoAbuse (parameters: { |
354 | videoAbuse: VideoAbuse | ||
355 | videoAbuseInstance: MVideoAbuseVideo | ||
356 | reporter: string | ||
357 | }) { | ||
351 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) | 358 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) |
352 | if (moderators.length === 0) return | 359 | if (moderators.length === 0) return |
353 | 360 | ||
354 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) | 361 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url) |
355 | 362 | ||
356 | function settingGetter (user: MUserWithNotificationSetting) { | 363 | function settingGetter (user: MUserWithNotificationSetting) { |
357 | return user.NotificationSetting.videoAbuseAsModerator | 364 | return user.NotificationSetting.videoAbuseAsModerator |
@@ -361,15 +368,15 @@ class Notifier { | |||
361 | const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ | 368 | const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ |
362 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, | 369 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, |
363 | userId: user.id, | 370 | userId: user.id, |
364 | videoAbuseId: videoAbuse.id | 371 | videoAbuseId: parameters.videoAbuse.id |
365 | }) | 372 | }) |
366 | notification.VideoAbuse = videoAbuse | 373 | notification.VideoAbuse = parameters.videoAbuseInstance |
367 | 374 | ||
368 | return notification | 375 | return notification |
369 | } | 376 | } |
370 | 377 | ||
371 | function emailSender (emails: string[]) { | 378 | function emailSender (emails: string[]) { |
372 | return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) | 379 | return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters) |
373 | } | 380 | } |
374 | 381 | ||
375 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 382 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
@@ -548,10 +555,10 @@ class Notifier { | |||
548 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 555 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
549 | } | 556 | } |
550 | 557 | ||
551 | private async notify <T extends MUserWithNotificationSetting> (options: { | 558 | private async notify<T extends MUserWithNotificationSetting> (options: { |
552 | users: T[], | 559 | users: T[] |
553 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi>, | 560 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> |
554 | emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, | 561 | emailSender: (emails: string[]) => void |
555 | settingGetter: (user: T) => UserNotificationSettingValue | 562 | settingGetter: (user: T) => UserNotificationSettingValue |
556 | }) { | 563 | }) { |
557 | const emails: string[] = [] | 564 | const emails: string[] = [] |
@@ -569,7 +576,7 @@ class Notifier { | |||
569 | } | 576 | } |
570 | 577 | ||
571 | if (emails.length !== 0) { | 578 | if (emails.length !== 0) { |
572 | await options.emailSender(emails) | 579 | options.emailSender(emails) |
573 | } | 580 | } |
574 | } | 581 | } |
575 | 582 | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 086856f41..dbcba897a 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as express from 'express' |
2 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
3 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
4 | import { UserModel } from '../models/account/user' | 4 | import { UserModel } from '../models/account/user' |
@@ -9,6 +9,11 @@ import { Transaction } from 'sequelize' | |||
9 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
10 | import * as LRUCache from 'lru-cache' | 10 | import * as LRUCache from 'lru-cache' |
11 | import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' | 11 | import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' |
12 | import { MUser } from '@server/typings/models/user/user' | ||
13 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | ||
14 | import { createUserAccountAndChannelAndPlaylist } from './user' | ||
15 | import { UserRole } from '@shared/models/users/user-role' | ||
16 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
12 | 17 | ||
13 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 18 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } |
14 | 19 | ||
@@ -41,22 +46,33 @@ function clearCacheByToken (token: string) { | |||
41 | } | 46 | } |
42 | } | 47 | } |
43 | 48 | ||
44 | function getAccessToken (bearerToken: string) { | 49 | async function getAccessToken (bearerToken: string) { |
45 | logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') | 50 | logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') |
46 | 51 | ||
47 | if (!bearerToken) return Bluebird.resolve(undefined) | 52 | if (!bearerToken) return undefined |
48 | 53 | ||
49 | if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) | 54 | let tokenModel: MOAuthTokenUser |
50 | 55 | ||
51 | return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 56 | if (accessTokenCache.has(bearerToken)) { |
52 | .then(tokenModel => { | 57 | tokenModel = accessTokenCache.get(bearerToken) |
53 | if (tokenModel) { | 58 | } else { |
54 | accessTokenCache.set(bearerToken, tokenModel) | 59 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
55 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
56 | } | ||
57 | 60 | ||
58 | return tokenModel | 61 | if (tokenModel) { |
59 | }) | 62 | accessTokenCache.set(bearerToken, tokenModel) |
63 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
64 | } | ||
65 | } | ||
66 | |||
67 | if (!tokenModel) return undefined | ||
68 | |||
69 | if (tokenModel.User.pluginAuth) { | ||
70 | const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access') | ||
71 | |||
72 | if (valid !== true) return undefined | ||
73 | } | ||
74 | |||
75 | return tokenModel | ||
60 | } | 76 | } |
61 | 77 | ||
62 | function getClient (clientId: string, clientSecret: string) { | 78 | function getClient (clientId: string, clientSecret: string) { |
@@ -65,20 +81,52 @@ function getClient (clientId: string, clientSecret: string) { | |||
65 | return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) | 81 | return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) |
66 | } | 82 | } |
67 | 83 | ||
68 | function getRefreshToken (refreshToken: string) { | 84 | async function getRefreshToken (refreshToken: string) { |
69 | logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') | 85 | logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') |
70 | 86 | ||
71 | return OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) | 87 | const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) |
88 | if (!tokenInfo) return undefined | ||
89 | |||
90 | const tokenModel = tokenInfo.token | ||
91 | |||
92 | if (tokenModel.User.pluginAuth) { | ||
93 | const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh') | ||
94 | |||
95 | if (valid !== true) return undefined | ||
96 | } | ||
97 | |||
98 | return tokenInfo | ||
72 | } | 99 | } |
73 | 100 | ||
74 | async function getUser (usernameOrEmail: string, password: string) { | 101 | async function getUser (usernameOrEmail?: string, password?: string) { |
102 | const res: express.Response = this.request.res | ||
103 | |||
104 | // Special treatment coming from a plugin | ||
105 | if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { | ||
106 | const obj = res.locals.bypassLogin | ||
107 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | ||
108 | |||
109 | let user = await UserModel.loadByEmail(obj.user.email) | ||
110 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | ||
111 | |||
112 | // If the user does not belongs to a plugin, it was created before its installation | ||
113 | // Then we just go through a regular login process | ||
114 | if (user.pluginAuth !== null) { | ||
115 | // This user does not belong to this plugin, skip it | ||
116 | if (user.pluginAuth !== obj.pluginName) return null | ||
117 | |||
118 | return user | ||
119 | } | ||
120 | } | ||
121 | |||
75 | logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') | 122 | logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') |
76 | 123 | ||
77 | const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) | 124 | const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) |
78 | if (!user) return null | 125 | // If we don't find the user, or if the user belongs to a plugin |
126 | if (!user || user.pluginAuth !== null) return null | ||
79 | 127 | ||
80 | const passwordMatch = await user.isPasswordMatch(password) | 128 | const passwordMatch = await user.isPasswordMatch(password) |
81 | if (passwordMatch === false) return null | 129 | if (passwordMatch !== true) return null |
82 | 130 | ||
83 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 131 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
84 | 132 | ||
@@ -89,29 +137,36 @@ async function getUser (usernameOrEmail: string, password: string) { | |||
89 | return user | 137 | return user |
90 | } | 138 | } |
91 | 139 | ||
92 | async function revokeToken (tokenInfo: TokenInfo) { | 140 | async function revokeToken (tokenInfo: { refreshToken: string }) { |
141 | const res: express.Response = this.request.res | ||
93 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 142 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
143 | |||
94 | if (token) { | 144 | if (token) { |
145 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | ||
146 | PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User) | ||
147 | } | ||
148 | |||
95 | clearCacheByToken(token.accessToken) | 149 | clearCacheByToken(token.accessToken) |
96 | 150 | ||
97 | token.destroy() | 151 | token.destroy() |
98 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | 152 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) |
153 | |||
154 | return true | ||
99 | } | 155 | } |
100 | 156 | ||
101 | /* | 157 | return false |
102 | * Thanks to https://github.com/manjeshpv/node-oauth2-server-implementation/blob/master/components/oauth/mongo-models.js | ||
103 | * "As per the discussion we need set older date | ||
104 | * revokeToken will expected return a boolean in future version | ||
105 | * https://github.com/oauthjs/node-oauth2-server/pull/274 | ||
106 | * https://github.com/oauthjs/node-oauth2-server/issues/290" | ||
107 | */ | ||
108 | const expiredToken = token | ||
109 | expiredToken.refreshTokenExpiresAt = new Date('2015-05-28T06:59:53.000Z') | ||
110 | |||
111 | return expiredToken | ||
112 | } | 158 | } |
113 | 159 | ||
114 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 160 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { |
161 | const res: express.Response = this.request.res | ||
162 | |||
163 | let authName: string = null | ||
164 | if (res.locals.bypassLogin?.bypass === true) { | ||
165 | authName = res.locals.bypassLogin.authName | ||
166 | } else if (res.locals.refreshTokenAuthName) { | ||
167 | authName = res.locals.refreshTokenAuthName | ||
168 | } | ||
169 | |||
115 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 170 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
116 | 171 | ||
117 | const tokenToCreate = { | 172 | const tokenToCreate = { |
@@ -119,11 +174,16 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
119 | accessTokenExpiresAt: token.accessTokenExpiresAt, | 174 | accessTokenExpiresAt: token.accessTokenExpiresAt, |
120 | refreshToken: token.refreshToken, | 175 | refreshToken: token.refreshToken, |
121 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, | 176 | refreshTokenExpiresAt: token.refreshTokenExpiresAt, |
177 | authName, | ||
122 | oAuthClientId: client.id, | 178 | oAuthClientId: client.id, |
123 | userId: user.id | 179 | userId: user.id |
124 | } | 180 | } |
125 | 181 | ||
126 | const tokenCreated = await OAuthTokenModel.create(tokenToCreate) | 182 | const tokenCreated = await OAuthTokenModel.create(tokenToCreate) |
183 | |||
184 | user.lastLoginDate = new Date() | ||
185 | await user.save() | ||
186 | |||
127 | return Object.assign(tokenCreated, { client, user }) | 187 | return Object.assign(tokenCreated, { client, user }) |
128 | } | 188 | } |
129 | 189 | ||
@@ -141,3 +201,30 @@ export { | |||
141 | revokeToken, | 201 | revokeToken, |
142 | saveToken | 202 | saveToken |
143 | } | 203 | } |
204 | |||
205 | async function createUserFromExternal (pluginAuth: string, options: { | ||
206 | username: string | ||
207 | email: string | ||
208 | role: UserRole | ||
209 | displayName: string | ||
210 | }) { | ||
211 | const userToCreate = new UserModel({ | ||
212 | username: options.username, | ||
213 | password: null, | ||
214 | email: options.email, | ||
215 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
216 | autoPlayVideo: true, | ||
217 | role: options.role, | ||
218 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
219 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY, | ||
220 | adminFlags: UserAdminFlag.NONE, | ||
221 | pluginAuth | ||
222 | }) as MUser | ||
223 | |||
224 | const { user } = await createUserAccountAndChannelAndPlaylist({ | ||
225 | userToCreate, | ||
226 | userDisplayName: options.displayName | ||
227 | }) | ||
228 | |||
229 | return user | ||
230 | } | ||
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts index bcc8c674e..aa92f03cc 100644 --- a/server/lib/plugins/hooks.ts +++ b/server/lib/plugins/hooks.ts | |||
@@ -25,7 +25,7 @@ const Hooks = { | |||
25 | }, | 25 | }, |
26 | 26 | ||
27 | runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => { | 27 | runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => { |
28 | PluginManager.Instance.runHook(hookName, params) | 28 | PluginManager.Instance.runHook(hookName, undefined, params) |
29 | .catch(err => logger.error('Fatal hook error.', { err })) | 29 | .catch(err => logger.error('Fatal hook error.', { err })) |
30 | } | 30 | } |
31 | } | 31 | } |
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers.ts new file mode 100644 index 000000000..de82b4918 --- /dev/null +++ b/server/lib/plugins/plugin-helpers.ts | |||
@@ -0,0 +1,133 @@ | |||
1 | import { PeerTubeHelpers } from '@server/typings/plugins' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | import { buildLogger } from '@server/helpers/logger' | ||
4 | import { VideoModel } from '@server/models/video/video' | ||
5 | import { WEBSERVER } from '@server/initializers/constants' | ||
6 | import { ServerModel } from '@server/models/server/server' | ||
7 | import { getServerActor } from '@server/models/application/application' | ||
8 | import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist' | ||
9 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
10 | import { AccountModel } from '@server/models/account/account' | ||
11 | import { VideoBlacklistCreate } from '@shared/models' | ||
12 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' | ||
13 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' | ||
14 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
15 | |||
16 | function buildPluginHelpers (npmName: string): PeerTubeHelpers { | ||
17 | const logger = buildPluginLogger(npmName) | ||
18 | |||
19 | const database = buildDatabaseHelpers() | ||
20 | const videos = buildVideosHelpers() | ||
21 | |||
22 | const config = buildConfigHelpers() | ||
23 | |||
24 | const server = buildServerHelpers() | ||
25 | |||
26 | const moderation = buildModerationHelpers() | ||
27 | |||
28 | return { | ||
29 | logger, | ||
30 | database, | ||
31 | videos, | ||
32 | config, | ||
33 | moderation, | ||
34 | server | ||
35 | } | ||
36 | } | ||
37 | |||
38 | export { | ||
39 | buildPluginHelpers | ||
40 | } | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | function buildPluginLogger (npmName: string) { | ||
45 | return buildLogger(npmName) | ||
46 | } | ||
47 | |||
48 | function buildDatabaseHelpers () { | ||
49 | return { | ||
50 | query: sequelizeTypescript.query.bind(sequelizeTypescript) | ||
51 | } | ||
52 | } | ||
53 | |||
54 | function buildServerHelpers () { | ||
55 | return { | ||
56 | getServerActor: () => getServerActor() | ||
57 | } | ||
58 | } | ||
59 | |||
60 | function buildVideosHelpers () { | ||
61 | return { | ||
62 | loadByUrl: (url: string) => { | ||
63 | return VideoModel.loadByUrl(url) | ||
64 | }, | ||
65 | |||
66 | removeVideo: (id: number) => { | ||
67 | return sequelizeTypescript.transaction(async t => { | ||
68 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t) | ||
69 | |||
70 | await video.destroy({ transaction: t }) | ||
71 | }) | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | |||
76 | function buildModerationHelpers () { | ||
77 | return { | ||
78 | blockServer: async (options: { byAccountId: number, hostToBlock: string }) => { | ||
79 | const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock) | ||
80 | |||
81 | await addServerInBlocklist(options.byAccountId, serverToBlock.id) | ||
82 | }, | ||
83 | |||
84 | unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => { | ||
85 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock) | ||
86 | if (!serverBlock) return | ||
87 | |||
88 | await removeServerFromBlocklist(serverBlock) | ||
89 | }, | ||
90 | |||
91 | blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => { | ||
92 | const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock) | ||
93 | if (!accountToBlock) return | ||
94 | |||
95 | await addAccountInBlocklist(options.byAccountId, accountToBlock.id) | ||
96 | }, | ||
97 | |||
98 | unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => { | ||
99 | const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock) | ||
100 | if (!targetAccount) return | ||
101 | |||
102 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id) | ||
103 | if (!accountBlock) return | ||
104 | |||
105 | await removeAccountFromBlocklist(accountBlock) | ||
106 | }, | ||
107 | |||
108 | blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => { | ||
109 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) | ||
110 | if (!video) return | ||
111 | |||
112 | await blacklistVideo(video, options.createOptions) | ||
113 | }, | ||
114 | |||
115 | unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => { | ||
116 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID) | ||
117 | if (!video) return | ||
118 | |||
119 | const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id) | ||
120 | if (!videoBlacklist) return | ||
121 | |||
122 | await unblacklistVideo(videoBlacklist, video) | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | function buildConfigHelpers () { | ||
128 | return { | ||
129 | getWebserverUrl () { | ||
130 | return WEBSERVER.URL | ||
131 | } | ||
132 | } | ||
133 | } | ||
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 25b4f3c61..170f0c7e2 100644 --- a/server/lib/plugins/plugin-index.ts +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -27,11 +27,11 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) | |||
27 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' | 27 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' |
28 | 28 | ||
29 | try { | 29 | try { |
30 | const { body } = await doRequest({ uri, qs, json: true }) | 30 | const { body } = await doRequest<any>({ uri, qs, json: true }) |
31 | 31 | ||
32 | logger.debug('Got result from PeerTube index.', { body }) | 32 | logger.debug('Got result from PeerTube index.', { body }) |
33 | 33 | ||
34 | await addInstanceInformation(body) | 34 | addInstanceInformation(body) |
35 | 35 | ||
36 | return body as ResultList<PeerTubePluginIndex> | 36 | return body as ResultList<PeerTubePluginIndex> |
37 | } catch (err) { | 37 | } catch (err) { |
@@ -40,7 +40,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | async function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { | 43 | function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { |
44 | for (const d of result.data) { | 44 | for (const d of result.data) { |
45 | d.installed = PluginManager.Instance.isRegistered(d.npmName) | 45 | d.installed = PluginManager.Instance.isRegistered(d.npmName) |
46 | d.name = PluginModel.normalizePluginName(d.npmName) | 46 | d.name = PluginModel.normalizePluginName(d.npmName) |
@@ -57,7 +57,7 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu | |||
57 | 57 | ||
58 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' | 58 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' |
59 | 59 | ||
60 | const { body } = await doRequest({ uri, body: bodyRequest, json: true, method: 'POST' }) | 60 | const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) |
61 | 61 | ||
62 | return body | 62 | return body |
63 | } | 63 | } |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 7ebdabd34..950acf7ad 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -9,23 +9,20 @@ import { | |||
9 | PluginTranslationPaths as PackagePluginTranslations | 9 | PluginTranslationPaths as PackagePluginTranslations |
10 | } from '../../../shared/models/plugins/plugin-package-json.model' | 10 | } from '../../../shared/models/plugins/plugin-package-json.model' |
11 | import { createReadStream, createWriteStream } from 'fs' | 11 | import { createReadStream, createWriteStream } from 'fs' |
12 | import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' | 12 | import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' |
13 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 13 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
14 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' | 14 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' |
15 | import { outputFile, readJSON } from 'fs-extra' | 15 | import { outputFile, readJSON } from 'fs-extra' |
16 | import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' | 16 | import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model' |
17 | import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model' | ||
18 | import { ServerHook, ServerHookName, serverHookObject } from '../../../shared/models/plugins/server-hook.model' | ||
19 | import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' | 17 | import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' |
20 | import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model' | 18 | import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model' |
21 | import { PluginLibrary } from '../../typings/plugins' | 19 | import { PluginLibrary } from '../../typings/plugins' |
22 | import { ClientHtml } from '../client-html' | 20 | import { ClientHtml } from '../client-html' |
23 | import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model' | ||
24 | import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model' | ||
25 | import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' | ||
26 | import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' | ||
27 | import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' | ||
28 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' | 21 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' |
22 | import { RegisterHelpersStore } from './register-helpers-store' | ||
23 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | ||
24 | import { MOAuthTokenUser, MUser } from '@server/typings/models' | ||
25 | import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model' | ||
29 | 26 | ||
30 | export interface RegisteredPlugin { | 27 | export interface RegisteredPlugin { |
31 | npmName: string | 28 | npmName: string |
@@ -44,6 +41,7 @@ export interface RegisteredPlugin { | |||
44 | css: string[] | 41 | css: string[] |
45 | 42 | ||
46 | // Only if this is a plugin | 43 | // Only if this is a plugin |
44 | registerHelpersStore?: RegisterHelpersStore | ||
47 | unregister?: Function | 45 | unregister?: Function |
48 | } | 46 | } |
49 | 47 | ||
@@ -54,35 +52,18 @@ export interface HookInformationValue { | |||
54 | priority: number | 52 | priority: number |
55 | } | 53 | } |
56 | 54 | ||
57 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | ||
58 | type VideoConstant = { [ key in number | string ]: string } | ||
59 | type UpdatedVideoConstant = { | ||
60 | [ name in AlterableVideoConstant ]: { | ||
61 | [ npmName: string ]: { | ||
62 | added: { key: number | string, label: string }[], | ||
63 | deleted: { key: number | string, label: string }[] | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | type PluginLocalesTranslations = { | 55 | type PluginLocalesTranslations = { |
69 | [ locale: string ]: PluginTranslation | 56 | [locale: string]: PluginTranslation |
70 | } | 57 | } |
71 | 58 | ||
72 | export class PluginManager implements ServerHook { | 59 | export class PluginManager implements ServerHook { |
73 | 60 | ||
74 | private static instance: PluginManager | 61 | private static instance: PluginManager |
75 | 62 | ||
76 | private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} | 63 | private registeredPlugins: { [name: string]: RegisteredPlugin } = {} |
77 | private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {} | ||
78 | private hooks: { [ name: string ]: HookInformationValue[] } = {} | ||
79 | private translations: PluginLocalesTranslations = {} | ||
80 | 64 | ||
81 | private updatedVideoConstants: UpdatedVideoConstant = { | 65 | private hooks: { [name: string]: HookInformationValue[] } = {} |
82 | language: {}, | 66 | private translations: PluginLocalesTranslations = {} |
83 | licence: {}, | ||
84 | category: {} | ||
85 | } | ||
86 | 67 | ||
87 | private constructor () { | 68 | private constructor () { |
88 | } | 69 | } |
@@ -97,7 +78,7 @@ export class PluginManager implements ServerHook { | |||
97 | return this.registeredPlugins[npmName] | 78 | return this.registeredPlugins[npmName] |
98 | } | 79 | } |
99 | 80 | ||
100 | getRegisteredPlugin (name: string) { | 81 | getRegisteredPluginByShortName (name: string) { |
101 | const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) | 82 | const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) |
102 | const registered = this.getRegisteredPluginOrTheme(npmName) | 83 | const registered = this.getRegisteredPluginOrTheme(npmName) |
103 | 84 | ||
@@ -106,7 +87,7 @@ export class PluginManager implements ServerHook { | |||
106 | return registered | 87 | return registered |
107 | } | 88 | } |
108 | 89 | ||
109 | getRegisteredTheme (name: string) { | 90 | getRegisteredThemeByShortName (name: string) { |
110 | const npmName = PluginModel.buildNpmName(name, PluginType.THEME) | 91 | const npmName = PluginModel.buildNpmName(name, PluginType.THEME) |
111 | const registered = this.getRegisteredPluginOrTheme(npmName) | 92 | const registered = this.getRegisteredPluginOrTheme(npmName) |
112 | 93 | ||
@@ -123,17 +104,102 @@ export class PluginManager implements ServerHook { | |||
123 | return this.getRegisteredPluginsOrThemes(PluginType.THEME) | 104 | return this.getRegisteredPluginsOrThemes(PluginType.THEME) |
124 | } | 105 | } |
125 | 106 | ||
107 | getIdAndPassAuths () { | ||
108 | return this.getRegisteredPlugins() | ||
109 | .map(p => ({ | ||
110 | npmName: p.npmName, | ||
111 | name: p.name, | ||
112 | version: p.version, | ||
113 | idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths() | ||
114 | })) | ||
115 | .filter(v => v.idAndPassAuths.length !== 0) | ||
116 | } | ||
117 | |||
118 | getExternalAuths () { | ||
119 | return this.getRegisteredPlugins() | ||
120 | .map(p => ({ | ||
121 | npmName: p.npmName, | ||
122 | name: p.name, | ||
123 | version: p.version, | ||
124 | externalAuths: p.registerHelpersStore.getExternalAuths() | ||
125 | })) | ||
126 | .filter(v => v.externalAuths.length !== 0) | ||
127 | } | ||
128 | |||
126 | getRegisteredSettings (npmName: string) { | 129 | getRegisteredSettings (npmName: string) { |
127 | return this.settings[npmName] || [] | 130 | const result = this.getRegisteredPluginOrTheme(npmName) |
131 | if (!result || result.type !== PluginType.PLUGIN) return [] | ||
132 | |||
133 | return result.registerHelpersStore.getSettings() | ||
134 | } | ||
135 | |||
136 | getRouter (npmName: string) { | ||
137 | const result = this.getRegisteredPluginOrTheme(npmName) | ||
138 | if (!result || result.type !== PluginType.PLUGIN) return null | ||
139 | |||
140 | return result.registerHelpersStore.getRouter() | ||
128 | } | 141 | } |
129 | 142 | ||
130 | getTranslations (locale: string) { | 143 | getTranslations (locale: string) { |
131 | return this.translations[locale] || {} | 144 | return this.translations[locale] || {} |
132 | } | 145 | } |
133 | 146 | ||
147 | async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') { | ||
148 | const auth = this.getAuth(token.User.pluginAuth, token.authName) | ||
149 | if (!auth) return true | ||
150 | |||
151 | if (auth.hookTokenValidity) { | ||
152 | try { | ||
153 | const { valid } = await auth.hookTokenValidity({ token, type }) | ||
154 | |||
155 | if (valid === false) { | ||
156 | logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth) | ||
157 | } | ||
158 | |||
159 | return valid | ||
160 | } catch (err) { | ||
161 | logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err }) | ||
162 | return true | ||
163 | } | ||
164 | } | ||
165 | |||
166 | return true | ||
167 | } | ||
168 | |||
169 | // ###################### External events ###################### | ||
170 | |||
171 | onLogout (npmName: string, authName: string, user: MUser) { | ||
172 | const auth = this.getAuth(npmName, authName) | ||
173 | |||
174 | if (auth?.onLogout) { | ||
175 | logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName) | ||
176 | |||
177 | try { | ||
178 | auth.onLogout(user) | ||
179 | } catch (err) { | ||
180 | logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err }) | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | |||
185 | onSettingsChanged (name: string, settings: any) { | ||
186 | const registered = this.getRegisteredPluginByShortName(name) | ||
187 | if (!registered) { | ||
188 | logger.error('Cannot find plugin %s to call on settings changed.', name) | ||
189 | } | ||
190 | |||
191 | for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) { | ||
192 | try { | ||
193 | cb(settings) | ||
194 | } catch (err) { | ||
195 | logger.error('Cannot run on settings changed callback for %s.', registered.npmName, { err }) | ||
196 | } | ||
197 | } | ||
198 | } | ||
199 | |||
134 | // ###################### Hooks ###################### | 200 | // ###################### Hooks ###################### |
135 | 201 | ||
136 | async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { | 202 | async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { |
137 | if (!this.hooks[hookName]) return Promise.resolve(result) | 203 | if (!this.hooks[hookName]) return Promise.resolve(result) |
138 | 204 | ||
139 | const hookType = getHookType(hookName) | 205 | const hookType = getHookType(hookName) |
@@ -185,7 +251,6 @@ export class PluginManager implements ServerHook { | |||
185 | } | 251 | } |
186 | 252 | ||
187 | delete this.registeredPlugins[plugin.npmName] | 253 | delete this.registeredPlugins[plugin.npmName] |
188 | delete this.settings[plugin.npmName] | ||
189 | 254 | ||
190 | this.deleteTranslations(plugin.npmName) | 255 | this.deleteTranslations(plugin.npmName) |
191 | 256 | ||
@@ -197,7 +262,8 @@ export class PluginManager implements ServerHook { | |||
197 | this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) | 262 | this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) |
198 | } | 263 | } |
199 | 264 | ||
200 | this.reinitVideoConstants(plugin.npmName) | 265 | const store = plugin.registerHelpersStore |
266 | store.reinitVideoConstants(plugin.npmName) | ||
201 | 267 | ||
202 | logger.info('Regenerating registered plugin CSS to global file.') | 268 | logger.info('Regenerating registered plugin CSS to global file.') |
203 | await this.regeneratePluginGlobalCSS() | 269 | await this.regeneratePluginGlobalCSS() |
@@ -303,8 +369,11 @@ export class PluginManager implements ServerHook { | |||
303 | this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) | 369 | this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) |
304 | 370 | ||
305 | let library: PluginLibrary | 371 | let library: PluginLibrary |
372 | let registerHelpersStore: RegisterHelpersStore | ||
306 | if (plugin.type === PluginType.PLUGIN) { | 373 | if (plugin.type === PluginType.PLUGIN) { |
307 | library = await this.registerPlugin(plugin, pluginPath, packageJSON) | 374 | const result = await this.registerPlugin(plugin, pluginPath, packageJSON) |
375 | library = result.library | ||
376 | registerHelpersStore = result.registerStore | ||
308 | } | 377 | } |
309 | 378 | ||
310 | const clientScripts: { [id: string]: ClientScript } = {} | 379 | const clientScripts: { [id: string]: ClientScript } = {} |
@@ -312,7 +381,7 @@ export class PluginManager implements ServerHook { | |||
312 | clientScripts[c.script] = c | 381 | clientScripts[c.script] = c |
313 | } | 382 | } |
314 | 383 | ||
315 | this.registeredPlugins[ npmName ] = { | 384 | this.registeredPlugins[npmName] = { |
316 | npmName, | 385 | npmName, |
317 | name: plugin.name, | 386 | name: plugin.name, |
318 | type: plugin.type, | 387 | type: plugin.type, |
@@ -323,6 +392,7 @@ export class PluginManager implements ServerHook { | |||
323 | staticDirs: packageJSON.staticDirs, | 392 | staticDirs: packageJSON.staticDirs, |
324 | clientScripts, | 393 | clientScripts, |
325 | css: packageJSON.css, | 394 | css: packageJSON.css, |
395 | registerHelpersStore: registerHelpersStore || undefined, | ||
326 | unregister: library ? library.unregister : undefined | 396 | unregister: library ? library.unregister : undefined |
327 | } | 397 | } |
328 | 398 | ||
@@ -341,15 +411,15 @@ export class PluginManager implements ServerHook { | |||
341 | throw new Error('Library code is not valid (miss register or unregister function)') | 411 | throw new Error('Library code is not valid (miss register or unregister function)') |
342 | } | 412 | } |
343 | 413 | ||
344 | const registerHelpers = this.getRegisterHelpers(npmName, plugin) | 414 | const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin) |
345 | library.register(registerHelpers) | 415 | library.register(registerOptions) |
346 | .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) | 416 | .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) |
347 | 417 | ||
348 | logger.info('Add plugin %s CSS to global file.', npmName) | 418 | logger.info('Add plugin %s CSS to global file.', npmName) |
349 | 419 | ||
350 | await this.addCSSToGlobalFile(pluginPath, packageJSON.css) | 420 | await this.addCSSToGlobalFile(pluginPath, packageJSON.css) |
351 | 421 | ||
352 | return library | 422 | return { library, registerStore } |
353 | } | 423 | } |
354 | 424 | ||
355 | // ###################### Translations ###################### | 425 | // ###################### Translations ###################### |
@@ -432,13 +502,23 @@ export class PluginManager implements ServerHook { | |||
432 | return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) | 502 | return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) |
433 | } | 503 | } |
434 | 504 | ||
505 | private getAuth (npmName: string, authName: string) { | ||
506 | const plugin = this.getRegisteredPluginOrTheme(npmName) | ||
507 | if (!plugin || plugin.type !== PluginType.PLUGIN) return null | ||
508 | |||
509 | let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths() | ||
510 | auths = auths.concat(plugin.registerHelpersStore.getExternalAuths()) | ||
511 | |||
512 | return auths.find(a => a.authName === authName) | ||
513 | } | ||
514 | |||
435 | // ###################### Private getters ###################### | 515 | // ###################### Private getters ###################### |
436 | 516 | ||
437 | private getRegisteredPluginsOrThemes (type: PluginType) { | 517 | private getRegisteredPluginsOrThemes (type: PluginType) { |
438 | const plugins: RegisteredPlugin[] = [] | 518 | const plugins: RegisteredPlugin[] = [] |
439 | 519 | ||
440 | for (const npmName of Object.keys(this.registeredPlugins)) { | 520 | for (const npmName of Object.keys(this.registeredPlugins)) { |
441 | const plugin = this.registeredPlugins[ npmName ] | 521 | const plugin = this.registeredPlugins[npmName] |
442 | if (plugin.type !== type) continue | 522 | if (plugin.type !== type) continue |
443 | 523 | ||
444 | plugins.push(plugin) | 524 | plugins.push(plugin) |
@@ -449,149 +529,26 @@ export class PluginManager implements ServerHook { | |||
449 | 529 | ||
450 | // ###################### Generate register helpers ###################### | 530 | // ###################### Generate register helpers ###################### |
451 | 531 | ||
452 | private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions { | 532 | private getRegisterHelpers ( |
453 | const registerHook = (options: RegisterServerHookOptions) => { | 533 | npmName: string, |
454 | if (serverHookObject[options.target] !== true) { | 534 | plugin: PluginModel |
455 | logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, npmName) | 535 | ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } { |
456 | return | 536 | const onHookAdded = (options: RegisterServerHookOptions) => { |
457 | } | ||
458 | |||
459 | if (!this.hooks[options.target]) this.hooks[options.target] = [] | 537 | if (!this.hooks[options.target]) this.hooks[options.target] = [] |
460 | 538 | ||
461 | this.hooks[options.target].push({ | 539 | this.hooks[options.target].push({ |
462 | npmName, | 540 | npmName: npmName, |
463 | pluginName: plugin.name, | 541 | pluginName: plugin.name, |
464 | handler: options.handler, | 542 | handler: options.handler, |
465 | priority: options.priority || 0 | 543 | priority: options.priority || 0 |
466 | }) | 544 | }) |
467 | } | 545 | } |
468 | 546 | ||
469 | const registerSetting = (options: RegisterServerSettingOptions) => { | 547 | const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this)) |
470 | if (!this.settings[npmName]) this.settings[npmName] = [] | ||
471 | |||
472 | this.settings[npmName].push(options) | ||
473 | } | ||
474 | |||
475 | const settingsManager: PluginSettingsManager = { | ||
476 | getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name), | ||
477 | |||
478 | setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value) | ||
479 | } | ||
480 | |||
481 | const storageManager: PluginStorageManager = { | ||
482 | getData: (key: string) => PluginModel.getData(plugin.name, plugin.type, key), | ||
483 | |||
484 | storeData: (key: string, data: any) => PluginModel.storeData(plugin.name, plugin.type, key, data) | ||
485 | } | ||
486 | |||
487 | const videoLanguageManager: PluginVideoLanguageManager = { | ||
488 | addLanguage: (key: string, label: string) => this.addConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }), | ||
489 | |||
490 | deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) | ||
491 | } | ||
492 | |||
493 | const videoCategoryManager: PluginVideoCategoryManager = { | ||
494 | addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }), | ||
495 | |||
496 | deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) | ||
497 | } | ||
498 | |||
499 | const videoLicenceManager: PluginVideoLicenceManager = { | ||
500 | addLicence: (key: number, label: string) => this.addConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }), | ||
501 | |||
502 | deleteLicence: (key: number) => this.deleteConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key }) | ||
503 | } | ||
504 | |||
505 | const peertubeHelpers = { | ||
506 | logger | ||
507 | } | ||
508 | 548 | ||
509 | return { | 549 | return { |
510 | registerHook, | 550 | registerStore: registerHelpersStore, |
511 | registerSetting, | 551 | registerOptions: registerHelpersStore.buildRegisterHelpers() |
512 | settingsManager, | ||
513 | storageManager, | ||
514 | videoLanguageManager, | ||
515 | videoCategoryManager, | ||
516 | videoLicenceManager, | ||
517 | peertubeHelpers | ||
518 | } | ||
519 | } | ||
520 | |||
521 | private addConstant <T extends string | number> (parameters: { | ||
522 | npmName: string, | ||
523 | type: AlterableVideoConstant, | ||
524 | obj: VideoConstant, | ||
525 | key: T, | ||
526 | label: string | ||
527 | }) { | ||
528 | const { npmName, type, obj, key, label } = parameters | ||
529 | |||
530 | if (obj[key]) { | ||
531 | logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key) | ||
532 | return false | ||
533 | } | ||
534 | |||
535 | if (!this.updatedVideoConstants[type][npmName]) { | ||
536 | this.updatedVideoConstants[type][npmName] = { | ||
537 | added: [], | ||
538 | deleted: [] | ||
539 | } | ||
540 | } | ||
541 | |||
542 | this.updatedVideoConstants[type][npmName].added.push({ key, label }) | ||
543 | obj[key] = label | ||
544 | |||
545 | return true | ||
546 | } | ||
547 | |||
548 | private deleteConstant <T extends string | number> (parameters: { | ||
549 | npmName: string, | ||
550 | type: AlterableVideoConstant, | ||
551 | obj: VideoConstant, | ||
552 | key: T | ||
553 | }) { | ||
554 | const { npmName, type, obj, key } = parameters | ||
555 | |||
556 | if (!obj[key]) { | ||
557 | logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key) | ||
558 | return false | ||
559 | } | ||
560 | |||
561 | if (!this.updatedVideoConstants[type][npmName]) { | ||
562 | this.updatedVideoConstants[type][npmName] = { | ||
563 | added: [], | ||
564 | deleted: [] | ||
565 | } | ||
566 | } | ||
567 | |||
568 | this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] }) | ||
569 | delete obj[key] | ||
570 | |||
571 | return true | ||
572 | } | ||
573 | |||
574 | private reinitVideoConstants (npmName: string) { | ||
575 | const hash = { | ||
576 | language: VIDEO_LANGUAGES, | ||
577 | licence: VIDEO_LICENCES, | ||
578 | category: VIDEO_CATEGORIES | ||
579 | } | ||
580 | const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ] | ||
581 | |||
582 | for (const type of types) { | ||
583 | const updatedConstants = this.updatedVideoConstants[type][npmName] | ||
584 | if (!updatedConstants) continue | ||
585 | |||
586 | for (const added of updatedConstants.added) { | ||
587 | delete hash[type][added.key] | ||
588 | } | ||
589 | |||
590 | for (const deleted of updatedConstants.deleted) { | ||
591 | hash[type][deleted.key] = deleted.label | ||
592 | } | ||
593 | |||
594 | delete this.updatedVideoConstants[type][npmName] | ||
595 | } | 552 | } |
596 | } | 553 | } |
597 | 554 | ||
@@ -604,7 +561,7 @@ export class PluginManager implements ServerHook { | |||
604 | const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) | 561 | const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) |
605 | if (!packageJSONValid) { | 562 | if (!packageJSONValid) { |
606 | const formattedFields = badFields.map(f => `"${f}"`) | 563 | const formattedFields = badFields.map(f => `"${f}"`) |
607 | .join(', ') | 564 | .join(', ') |
608 | 565 | ||
609 | throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) | 566 | throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) |
610 | } | 567 | } |
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts new file mode 100644 index 000000000..e337b1cb0 --- /dev/null +++ b/server/lib/plugins/register-helpers-store.ts | |||
@@ -0,0 +1,355 @@ | |||
1 | import * as express from 'express' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { | ||
4 | VIDEO_CATEGORIES, | ||
5 | VIDEO_LANGUAGES, | ||
6 | VIDEO_LICENCES, | ||
7 | VIDEO_PLAYLIST_PRIVACIES, | ||
8 | VIDEO_PRIVACIES | ||
9 | } from '@server/initializers/constants' | ||
10 | import { onExternalUserAuthenticated } from '@server/lib/auth' | ||
11 | import { PluginModel } from '@server/models/server/plugin' | ||
12 | import { RegisterServerOptions } from '@server/typings/plugins' | ||
13 | import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model' | ||
14 | import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model' | ||
15 | import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model' | ||
16 | import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model' | ||
17 | import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' | ||
18 | import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' | ||
19 | import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' | ||
20 | import { | ||
21 | RegisterServerAuthExternalOptions, | ||
22 | RegisterServerAuthExternalResult, | ||
23 | RegisterServerAuthPassOptions, | ||
24 | RegisterServerExternalAuthenticatedResult | ||
25 | } from '@shared/models/plugins/register-server-auth.model' | ||
26 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | ||
27 | import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' | ||
28 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' | ||
29 | import { buildPluginHelpers } from './plugin-helpers' | ||
30 | |||
31 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | ||
32 | type VideoConstant = { [key in number | string]: string } | ||
33 | |||
34 | type UpdatedVideoConstant = { | ||
35 | [name in AlterableVideoConstant]: { | ||
36 | added: { key: number | string, label: string }[] | ||
37 | deleted: { key: number | string, label: string }[] | ||
38 | } | ||
39 | } | ||
40 | |||
41 | export class RegisterHelpersStore { | ||
42 | private readonly updatedVideoConstants: UpdatedVideoConstant = { | ||
43 | playlistPrivacy: { added: [], deleted: [] }, | ||
44 | privacy: { added: [], deleted: [] }, | ||
45 | language: { added: [], deleted: [] }, | ||
46 | licence: { added: [], deleted: [] }, | ||
47 | category: { added: [], deleted: [] } | ||
48 | } | ||
49 | |||
50 | private readonly settings: RegisterServerSettingOptions[] = [] | ||
51 | |||
52 | private idAndPassAuths: RegisterServerAuthPassOptions[] = [] | ||
53 | private externalAuths: RegisterServerAuthExternalOptions[] = [] | ||
54 | |||
55 | private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = [] | ||
56 | |||
57 | private readonly router: express.Router | ||
58 | |||
59 | constructor ( | ||
60 | private readonly npmName: string, | ||
61 | private readonly plugin: PluginModel, | ||
62 | private readonly onHookAdded: (options: RegisterServerHookOptions) => void | ||
63 | ) { | ||
64 | this.router = express.Router() | ||
65 | } | ||
66 | |||
67 | buildRegisterHelpers (): RegisterServerOptions { | ||
68 | const registerHook = this.buildRegisterHook() | ||
69 | const registerSetting = this.buildRegisterSetting() | ||
70 | |||
71 | const getRouter = this.buildGetRouter() | ||
72 | |||
73 | const settingsManager = this.buildSettingsManager() | ||
74 | const storageManager = this.buildStorageManager() | ||
75 | |||
76 | const videoLanguageManager = this.buildVideoLanguageManager() | ||
77 | |||
78 | const videoLicenceManager = this.buildVideoLicenceManager() | ||
79 | const videoCategoryManager = this.buildVideoCategoryManager() | ||
80 | |||
81 | const videoPrivacyManager = this.buildVideoPrivacyManager() | ||
82 | const playlistPrivacyManager = this.buildPlaylistPrivacyManager() | ||
83 | |||
84 | const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth() | ||
85 | const registerExternalAuth = this.buildRegisterExternalAuth() | ||
86 | const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() | ||
87 | const unregisterExternalAuth = this.buildUnregisterExternalAuth() | ||
88 | |||
89 | const peertubeHelpers = buildPluginHelpers(this.npmName) | ||
90 | |||
91 | return { | ||
92 | registerHook, | ||
93 | registerSetting, | ||
94 | |||
95 | getRouter, | ||
96 | |||
97 | settingsManager, | ||
98 | storageManager, | ||
99 | |||
100 | videoLanguageManager, | ||
101 | videoCategoryManager, | ||
102 | videoLicenceManager, | ||
103 | |||
104 | videoPrivacyManager, | ||
105 | playlistPrivacyManager, | ||
106 | |||
107 | registerIdAndPassAuth, | ||
108 | registerExternalAuth, | ||
109 | unregisterIdAndPassAuth, | ||
110 | unregisterExternalAuth, | ||
111 | |||
112 | peertubeHelpers | ||
113 | } | ||
114 | } | ||
115 | |||
116 | reinitVideoConstants (npmName: string) { | ||
117 | const hash = { | ||
118 | language: VIDEO_LANGUAGES, | ||
119 | licence: VIDEO_LICENCES, | ||
120 | category: VIDEO_CATEGORIES, | ||
121 | privacy: VIDEO_PRIVACIES, | ||
122 | playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES | ||
123 | } | ||
124 | const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ] | ||
125 | |||
126 | for (const type of types) { | ||
127 | const updatedConstants = this.updatedVideoConstants[type][npmName] | ||
128 | if (!updatedConstants) continue | ||
129 | |||
130 | for (const added of updatedConstants.added) { | ||
131 | delete hash[type][added.key] | ||
132 | } | ||
133 | |||
134 | for (const deleted of updatedConstants.deleted) { | ||
135 | hash[type][deleted.key] = deleted.label | ||
136 | } | ||
137 | |||
138 | delete this.updatedVideoConstants[type][npmName] | ||
139 | } | ||
140 | } | ||
141 | |||
142 | getSettings () { | ||
143 | return this.settings | ||
144 | } | ||
145 | |||
146 | getRouter () { | ||
147 | return this.router | ||
148 | } | ||
149 | |||
150 | getIdAndPassAuths () { | ||
151 | return this.idAndPassAuths | ||
152 | } | ||
153 | |||
154 | getExternalAuths () { | ||
155 | return this.externalAuths | ||
156 | } | ||
157 | |||
158 | getOnSettingsChangedCallbacks () { | ||
159 | return this.onSettingsChangeCallbacks | ||
160 | } | ||
161 | |||
162 | private buildGetRouter () { | ||
163 | return () => this.router | ||
164 | } | ||
165 | |||
166 | private buildRegisterSetting () { | ||
167 | return (options: RegisterServerSettingOptions) => { | ||
168 | this.settings.push(options) | ||
169 | } | ||
170 | } | ||
171 | |||
172 | private buildRegisterHook () { | ||
173 | return (options: RegisterServerHookOptions) => { | ||
174 | if (serverHookObject[options.target] !== true) { | ||
175 | logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName) | ||
176 | return | ||
177 | } | ||
178 | |||
179 | return this.onHookAdded(options) | ||
180 | } | ||
181 | } | ||
182 | |||
183 | private buildRegisterIdAndPassAuth () { | ||
184 | return (options: RegisterServerAuthPassOptions) => { | ||
185 | if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') { | ||
186 | logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options }) | ||
187 | return | ||
188 | } | ||
189 | |||
190 | this.idAndPassAuths.push(options) | ||
191 | } | ||
192 | } | ||
193 | |||
194 | private buildRegisterExternalAuth () { | ||
195 | const self = this | ||
196 | |||
197 | return (options: RegisterServerAuthExternalOptions) => { | ||
198 | if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') { | ||
199 | logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options }) | ||
200 | return | ||
201 | } | ||
202 | |||
203 | this.externalAuths.push(options) | ||
204 | |||
205 | return { | ||
206 | userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void { | ||
207 | onExternalUserAuthenticated({ | ||
208 | npmName: self.npmName, | ||
209 | authName: options.authName, | ||
210 | authResult: result | ||
211 | }).catch(err => { | ||
212 | logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err }) | ||
213 | }) | ||
214 | } | ||
215 | } as RegisterServerAuthExternalResult | ||
216 | } | ||
217 | } | ||
218 | |||
219 | private buildUnregisterExternalAuth () { | ||
220 | return (authName: string) => { | ||
221 | this.externalAuths = this.externalAuths.filter(a => a.authName !== authName) | ||
222 | } | ||
223 | } | ||
224 | |||
225 | private buildUnregisterIdAndPassAuth () { | ||
226 | return (authName: string) => { | ||
227 | this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | private buildSettingsManager (): PluginSettingsManager { | ||
232 | return { | ||
233 | getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings), | ||
234 | |||
235 | getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings), | ||
236 | |||
237 | setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value), | ||
238 | |||
239 | onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb) | ||
240 | } | ||
241 | } | ||
242 | |||
243 | private buildStorageManager (): PluginStorageManager { | ||
244 | return { | ||
245 | getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key), | ||
246 | |||
247 | storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data) | ||
248 | } | ||
249 | } | ||
250 | |||
251 | private buildVideoLanguageManager (): PluginVideoLanguageManager { | ||
252 | return { | ||
253 | addLanguage: (key: string, label: string) => { | ||
254 | return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }) | ||
255 | }, | ||
256 | |||
257 | deleteLanguage: (key: string) => { | ||
258 | return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) | ||
259 | } | ||
260 | } | ||
261 | } | ||
262 | |||
263 | private buildVideoCategoryManager (): PluginVideoCategoryManager { | ||
264 | return { | ||
265 | addCategory: (key: number, label: string) => { | ||
266 | return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }) | ||
267 | }, | ||
268 | |||
269 | deleteCategory: (key: number) => { | ||
270 | return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) | ||
271 | } | ||
272 | } | ||
273 | } | ||
274 | |||
275 | private buildVideoPrivacyManager (): PluginVideoPrivacyManager { | ||
276 | return { | ||
277 | deletePrivacy: (key: number) => { | ||
278 | return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key }) | ||
279 | } | ||
280 | } | ||
281 | } | ||
282 | |||
283 | private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager { | ||
284 | return { | ||
285 | deletePlaylistPrivacy: (key: number) => { | ||
286 | return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key }) | ||
287 | } | ||
288 | } | ||
289 | } | ||
290 | |||
291 | private buildVideoLicenceManager (): PluginVideoLicenceManager { | ||
292 | return { | ||
293 | addLicence: (key: number, label: string) => { | ||
294 | return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }) | ||
295 | }, | ||
296 | |||
297 | deleteLicence: (key: number) => { | ||
298 | return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key }) | ||
299 | } | ||
300 | } | ||
301 | } | ||
302 | |||
303 | private addConstant<T extends string | number> (parameters: { | ||
304 | npmName: string | ||
305 | type: AlterableVideoConstant | ||
306 | obj: VideoConstant | ||
307 | key: T | ||
308 | label: string | ||
309 | }) { | ||
310 | const { npmName, type, obj, key, label } = parameters | ||
311 | |||
312 | if (obj[key]) { | ||
313 | logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key) | ||
314 | return false | ||
315 | } | ||
316 | |||
317 | if (!this.updatedVideoConstants[type][npmName]) { | ||
318 | this.updatedVideoConstants[type][npmName] = { | ||
319 | added: [], | ||
320 | deleted: [] | ||
321 | } | ||
322 | } | ||
323 | |||
324 | this.updatedVideoConstants[type][npmName].added.push({ key, label }) | ||
325 | obj[key] = label | ||
326 | |||
327 | return true | ||
328 | } | ||
329 | |||
330 | private deleteConstant<T extends string | number> (parameters: { | ||
331 | npmName: string | ||
332 | type: AlterableVideoConstant | ||
333 | obj: VideoConstant | ||
334 | key: T | ||
335 | }) { | ||
336 | const { npmName, type, obj, key } = parameters | ||
337 | |||
338 | if (!obj[key]) { | ||
339 | logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key) | ||
340 | return false | ||
341 | } | ||
342 | |||
343 | if (!this.updatedVideoConstants[type][npmName]) { | ||
344 | this.updatedVideoConstants[type][npmName] = { | ||
345 | added: [], | ||
346 | deleted: [] | ||
347 | } | ||
348 | } | ||
349 | |||
350 | this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] }) | ||
351 | delete obj[key] | ||
352 | |||
353 | return true | ||
354 | } | ||
355 | } | ||
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index f77d0b62c..b4cd6f8e7 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -6,13 +6,14 @@ import { | |||
6 | CONTACT_FORM_LIFETIME, | 6 | CONTACT_FORM_LIFETIME, |
7 | USER_EMAIL_VERIFY_LIFETIME, | 7 | USER_EMAIL_VERIFY_LIFETIME, |
8 | USER_PASSWORD_RESET_LIFETIME, | 8 | USER_PASSWORD_RESET_LIFETIME, |
9 | USER_PASSWORD_CREATE_LIFETIME, | ||
9 | VIDEO_VIEW_LIFETIME, | 10 | VIDEO_VIEW_LIFETIME, |
10 | WEBSERVER | 11 | WEBSERVER |
11 | } from '../initializers/constants' | 12 | } from '../initializers/constants' |
12 | import { CONFIG } from '../initializers/config' | 13 | import { CONFIG } from '../initializers/config' |
13 | 14 | ||
14 | type CachedRoute = { | 15 | type CachedRoute = { |
15 | body: string, | 16 | body: string |
16 | contentType?: string | 17 | contentType?: string |
17 | statusCode?: string | 18 | statusCode?: string |
18 | } | 19 | } |
@@ -24,7 +25,8 @@ class Redis { | |||
24 | private client: RedisClient | 25 | private client: RedisClient |
25 | private prefix: string | 26 | private prefix: string |
26 | 27 | ||
27 | private constructor () {} | 28 | private constructor () { |
29 | } | ||
28 | 30 | ||
29 | init () { | 31 | init () { |
30 | // Already initialized | 32 | // Already initialized |
@@ -49,9 +51,9 @@ class Redis { | |||
49 | return Object.assign({}, | 51 | return Object.assign({}, |
50 | (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {}, | 52 | (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {}, |
51 | (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {}, | 53 | (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {}, |
52 | (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ? | 54 | (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) |
53 | { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } : | 55 | ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } |
54 | { path: CONFIG.REDIS.SOCKET } | 56 | : { path: CONFIG.REDIS.SOCKET } |
55 | ) | 57 | ) |
56 | } | 58 | } |
57 | 59 | ||
@@ -63,7 +65,7 @@ class Redis { | |||
63 | return this.prefix | 65 | return this.prefix |
64 | } | 66 | } |
65 | 67 | ||
66 | /************* Forgot password *************/ | 68 | /* ************ Forgot password ************ */ |
67 | 69 | ||
68 | async setResetPasswordVerificationString (userId: number) { | 70 | async setResetPasswordVerificationString (userId: number) { |
69 | const generatedString = await generateRandomString(32) | 71 | const generatedString = await generateRandomString(32) |
@@ -73,11 +75,19 @@ class Redis { | |||
73 | return generatedString | 75 | return generatedString |
74 | } | 76 | } |
75 | 77 | ||
78 | async setCreatePasswordVerificationString (userId: number) { | ||
79 | const generatedString = await generateRandomString(32) | ||
80 | |||
81 | await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME) | ||
82 | |||
83 | return generatedString | ||
84 | } | ||
85 | |||
76 | async getResetPasswordLink (userId: number) { | 86 | async getResetPasswordLink (userId: number) { |
77 | return this.getValue(this.generateResetPasswordKey(userId)) | 87 | return this.getValue(this.generateResetPasswordKey(userId)) |
78 | } | 88 | } |
79 | 89 | ||
80 | /************* Email verification *************/ | 90 | /* ************ Email verification ************ */ |
81 | 91 | ||
82 | async setVerifyEmailVerificationString (userId: number) { | 92 | async setVerifyEmailVerificationString (userId: number) { |
83 | const generatedString = await generateRandomString(32) | 93 | const generatedString = await generateRandomString(32) |
@@ -91,7 +101,7 @@ class Redis { | |||
91 | return this.getValue(this.generateVerifyEmailKey(userId)) | 101 | return this.getValue(this.generateVerifyEmailKey(userId)) |
92 | } | 102 | } |
93 | 103 | ||
94 | /************* Contact form per IP *************/ | 104 | /* ************ Contact form per IP ************ */ |
95 | 105 | ||
96 | async setContactFormIp (ip: string) { | 106 | async setContactFormIp (ip: string) { |
97 | return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) | 107 | return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) |
@@ -101,7 +111,7 @@ class Redis { | |||
101 | return this.exists(this.generateContactFormKey(ip)) | 111 | return this.exists(this.generateContactFormKey(ip)) |
102 | } | 112 | } |
103 | 113 | ||
104 | /************* Views per IP *************/ | 114 | /* ************ Views per IP ************ */ |
105 | 115 | ||
106 | setIPVideoView (ip: string, videoUUID: string) { | 116 | setIPVideoView (ip: string, videoUUID: string) { |
107 | return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) | 117 | return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) |
@@ -111,7 +121,7 @@ class Redis { | |||
111 | return this.exists(this.generateViewKey(ip, videoUUID)) | 121 | return this.exists(this.generateViewKey(ip, videoUUID)) |
112 | } | 122 | } |
113 | 123 | ||
114 | /************* API cache *************/ | 124 | /* ************ API cache ************ */ |
115 | 125 | ||
116 | async getCachedRoute (req: express.Request) { | 126 | async getCachedRoute (req: express.Request) { |
117 | const cached = await this.getObject(this.generateCachedRouteKey(req)) | 127 | const cached = await this.getObject(this.generateCachedRouteKey(req)) |
@@ -120,17 +130,17 @@ class Redis { | |||
120 | } | 130 | } |
121 | 131 | ||
122 | setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) { | 132 | setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) { |
123 | const cached: CachedRoute = Object.assign({}, { | 133 | const cached: CachedRoute = Object.assign( |
124 | body: body.toString() | 134 | {}, |
125 | }, | 135 | { body: body.toString() }, |
126 | (contentType) ? { contentType } : null, | 136 | (contentType) ? { contentType } : null, |
127 | (statusCode) ? { statusCode: statusCode.toString() } : null | 137 | (statusCode) ? { statusCode: statusCode.toString() } : null |
128 | ) | 138 | ) |
129 | 139 | ||
130 | return this.setObject(this.generateCachedRouteKey(req), cached, lifetime) | 140 | return this.setObject(this.generateCachedRouteKey(req), cached, lifetime) |
131 | } | 141 | } |
132 | 142 | ||
133 | /************* Video views *************/ | 143 | /* ************ Video views ************ */ |
134 | 144 | ||
135 | addVideoView (videoId: number) { | 145 | addVideoView (videoId: number) { |
136 | const keyIncr = this.generateVideoViewKey(videoId) | 146 | const keyIncr = this.generateVideoViewKey(videoId) |
@@ -173,7 +183,7 @@ class Redis { | |||
173 | ]) | 183 | ]) |
174 | } | 184 | } |
175 | 185 | ||
176 | /************* Keys generation *************/ | 186 | /* ************ Keys generation ************ */ |
177 | 187 | ||
178 | generateCachedRouteKey (req: express.Request) { | 188 | generateCachedRouteKey (req: express.Request) { |
179 | return req.method + '-' + req.originalUrl | 189 | return req.method + '-' + req.originalUrl |
@@ -207,7 +217,7 @@ class Redis { | |||
207 | return 'contact-form-' + ip | 217 | return 'contact-form-' + ip |
208 | } | 218 | } |
209 | 219 | ||
210 | /************* Redis helpers *************/ | 220 | /* ************ Redis helpers ************ */ |
211 | 221 | ||
212 | private getValue (key: string) { | 222 | private getValue (key: string) { |
213 | return new Promise<string>((res, rej) => { | 223 | return new Promise<string>((res, rej) => { |
@@ -265,7 +275,7 @@ class Redis { | |||
265 | }) | 275 | }) |
266 | } | 276 | } |
267 | 277 | ||
268 | private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) { | 278 | private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) { |
269 | return new Promise<void>((res, rej) => { | 279 | return new Promise<void>((res, rej) => { |
270 | this.client.hmset(this.prefix + key, obj, (err, ok) => { | 280 | this.client.hmset(this.prefix + key, obj, (err, ok) => { |
271 | if (err) return rej(err) | 281 | if (err) return rej(err) |
@@ -282,7 +292,7 @@ class Redis { | |||
282 | } | 292 | } |
283 | 293 | ||
284 | private getObject (key: string) { | 294 | private getObject (key: string) { |
285 | return new Promise<{ [ id: string ]: string }>((res, rej) => { | 295 | return new Promise<{ [id: string]: string }>((res, rej) => { |
286 | this.client.hgetall(this.prefix + key, (err, value) => { | 296 | this.client.hgetall(this.prefix + key, (err, value) => { |
287 | if (err) return rej(err) | 297 | if (err) return rej(err) |
288 | 298 | ||
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts index 1b4ecd7c0..361b401a5 100644 --- a/server/lib/redundancy.ts +++ b/server/lib/redundancy.ts | |||
@@ -1,8 +1,12 @@ | |||
1 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 1 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
2 | import { sendUndoCacheFile } from './activitypub/send' | 2 | import { sendUndoCacheFile } from './activitypub/send' |
3 | import { Transaction } from 'sequelize' | 3 | import { Transaction } from 'sequelize' |
4 | import { getServerActor } from '../helpers/utils' | 4 | import { MActorSignature, MVideoRedundancyVideo } from '@server/typings/models' |
5 | import { MVideoRedundancyVideo } from '@server/typings/models' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { logger } from '@server/helpers/logger' | ||
7 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | ||
8 | import { Activity } from '@shared/models' | ||
9 | import { getServerActor } from '@server/models/application/application' | ||
6 | 10 | ||
7 | async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { | 11 | async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { |
8 | const serverActor = await getServerActor() | 12 | const serverActor = await getServerActor() |
@@ -13,17 +17,38 @@ async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t? | |||
13 | await videoRedundancy.destroy({ transaction: t }) | 17 | await videoRedundancy.destroy({ transaction: t }) |
14 | } | 18 | } |
15 | 19 | ||
16 | async function removeRedundancyOf (serverId: number) { | 20 | async function removeRedundanciesOfServer (serverId: number) { |
17 | const videosRedundancy = await VideoRedundancyModel.listLocalOfServer(serverId) | 21 | const redundancies = await VideoRedundancyModel.listLocalOfServer(serverId) |
18 | 22 | ||
19 | for (const redundancy of videosRedundancy) { | 23 | for (const redundancy of redundancies) { |
20 | await removeVideoRedundancy(redundancy) | 24 | await removeVideoRedundancy(redundancy) |
21 | } | 25 | } |
22 | } | 26 | } |
23 | 27 | ||
28 | async function isRedundancyAccepted (activity: Activity, byActor: MActorSignature) { | ||
29 | const configAcceptFrom = CONFIG.REMOTE_REDUNDANCY.VIDEOS.ACCEPT_FROM | ||
30 | if (configAcceptFrom === 'nobody') { | ||
31 | logger.info('Do not accept remote redundancy %s due instance accept policy.', activity.id) | ||
32 | return false | ||
33 | } | ||
34 | |||
35 | if (configAcceptFrom === 'followings') { | ||
36 | const serverActor = await getServerActor() | ||
37 | const allowed = await ActorFollowModel.isFollowedBy(byActor.id, serverActor.id) | ||
38 | |||
39 | if (allowed !== true) { | ||
40 | logger.info('Do not accept remote redundancy %s because actor %s is not followed by our instance.', activity.id, byActor.url) | ||
41 | return false | ||
42 | } | ||
43 | } | ||
44 | |||
45 | return true | ||
46 | } | ||
47 | |||
24 | // --------------------------------------------------------------------------- | 48 | // --------------------------------------------------------------------------- |
25 | 49 | ||
26 | export { | 50 | export { |
27 | removeRedundancyOf, | 51 | isRedundancyAccepted, |
52 | removeRedundanciesOfServer, | ||
28 | removeVideoRedundancy | 53 | removeVideoRedundancy |
29 | } | 54 | } |
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index dd326bc1e..a57436a45 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts | |||
@@ -6,7 +6,7 @@ import { chunk } from 'lodash' | |||
6 | import { doRequest } from '@server/helpers/requests' | 6 | import { doRequest } from '@server/helpers/requests' |
7 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 7 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
8 | import { JobQueue } from '@server/lib/job-queue' | 8 | import { JobQueue } from '@server/lib/job-queue' |
9 | import { getServerActor } from '@server/helpers/utils' | 9 | import { getServerActor } from '@server/models/application/application' |
10 | 10 | ||
11 | export class AutoFollowIndexInstances extends AbstractScheduler { | 11 | export class AutoFollowIndexInstances extends AbstractScheduler { |
12 | 12 | ||
@@ -41,7 +41,11 @@ export class AutoFollowIndexInstances extends AbstractScheduler { | |||
41 | 41 | ||
42 | this.lastCheck = new Date() | 42 | this.lastCheck = new Date() |
43 | 43 | ||
44 | const { body } = await doRequest({ uri, qs, json: true }) | 44 | const { body } = await doRequest<any>({ uri, qs, json: true }) |
45 | if (!body.data || Array.isArray(body.data) === false) { | ||
46 | logger.error('Cannot auto follow instances of index %s: bad URL format. Please check the auto follow URL.', indexUrl) | ||
47 | return | ||
48 | } | ||
45 | 49 | ||
46 | const hosts: string[] = body.data.map(o => o.host) | 50 | const hosts: string[] = body.data.map(o => o.host) |
47 | const chunks = chunk(hosts, 20) | 51 | const chunks = chunk(hosts, 20) |
@@ -57,8 +61,7 @@ export class AutoFollowIndexInstances extends AbstractScheduler { | |||
57 | isAutoFollow: true | 61 | isAutoFollow: true |
58 | } | 62 | } |
59 | 63 | ||
60 | await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | 64 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) |
61 | .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err)) | ||
62 | } | 65 | } |
63 | } | 66 | } |
64 | 67 | ||
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts index 7ff41e639..014993e94 100644 --- a/server/lib/schedulers/plugins-check-scheduler.ts +++ b/server/lib/schedulers/plugins-check-scheduler.ts | |||
@@ -43,7 +43,7 @@ export class PluginsCheckScheduler extends AbstractScheduler { | |||
43 | const results = await getLatestPluginsVersion(npmNames) | 43 | const results = await getLatestPluginsVersion(npmNames) |
44 | 44 | ||
45 | for (const result of results) { | 45 | for (const result of results) { |
46 | const plugin = pluginIndex[ result.npmName ] | 46 | const plugin = pluginIndex[result.npmName] |
47 | if (!result.latestVersion) continue | 47 | if (!result.latestVersion) continue |
48 | 48 | ||
49 | if ( | 49 | if ( |
diff --git a/server/lib/schedulers/remove-old-views-scheduler.ts b/server/lib/schedulers/remove-old-views-scheduler.ts index 39fbb9163..5ae87fe50 100644 --- a/server/lib/schedulers/remove-old-views-scheduler.ts +++ b/server/lib/schedulers/remove-old-views-scheduler.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | import { logger } from '../../helpers/logger' | 1 | import { logger } from '../../helpers/logger' |
2 | import { AbstractScheduler } from './abstract-scheduler' | 2 | import { AbstractScheduler } from './abstract-scheduler' |
3 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 3 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
4 | import { UserVideoHistoryModel } from '../../models/account/user-video-history' | ||
5 | import { CONFIG } from '../../initializers/config' | 4 | import { CONFIG } from '../../initializers/config' |
6 | import { isTestInstance } from '../../helpers/core-utils' | ||
7 | import { VideoViewModel } from '../../models/video/video-views' | 5 | import { VideoViewModel } from '../../models/video/video-views' |
8 | 6 | ||
9 | export class RemoveOldViewsScheduler extends AbstractScheduler { | 7 | export class RemoveOldViewsScheduler extends AbstractScheduler { |
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 350a335d3..d32c1c068 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -2,9 +2,8 @@ import { logger } from '../../helpers/logger' | |||
2 | import { AbstractScheduler } from './abstract-scheduler' | 2 | import { AbstractScheduler } from './abstract-scheduler' |
3 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' | 3 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' |
4 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
5 | import { federateVideoIfNeeded } from '../activitypub' | 5 | import { federateVideoIfNeeded } from '../activitypub/videos' |
6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | ||
8 | import { Notifier } from '../notifier' | 7 | import { Notifier } from '../notifier' |
9 | import { sequelizeTypescript } from '../../initializers/database' | 8 | import { sequelizeTypescript } from '../../initializers/database' |
10 | import { MVideoFullLight } from '@server/typings/models' | 9 | import { MVideoFullLight } from '@server/typings/models' |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index c1c91b656..8da9d52b5 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -1,16 +1,15 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { AbstractScheduler } from './abstract-scheduler' |
2 | import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' | 2 | import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' |
3 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
4 | import { VideosRedundancy } from '../../../shared/models/redundancy' | 4 | import { VideosRedundancyStrategy } from '../../../shared/models/redundancy' |
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
6 | import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' | 6 | import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' |
7 | import { join } from 'path' | 7 | import { join } from 'path' |
8 | import { move } from 'fs-extra' | 8 | import { move } from 'fs-extra' |
9 | import { getServerActor } from '../../helpers/utils' | ||
10 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 9 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
11 | import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' | 10 | import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
12 | import { removeVideoRedundancy } from '../redundancy' | 11 | import { removeVideoRedundancy } from '../redundancy' |
13 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' | 12 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos' |
14 | import { downloadPlaylistSegments } from '../hls' | 13 | import { downloadPlaylistSegments } from '../hls' |
15 | import { CONFIG } from '../../initializers/config' | 14 | import { CONFIG } from '../../initializers/config' |
16 | import { | 15 | import { |
@@ -25,11 +24,13 @@ import { | |||
25 | MVideoWithAllFiles | 24 | MVideoWithAllFiles |
26 | } from '@server/typings/models' | 25 | } from '@server/typings/models' |
27 | import { getVideoFilename } from '../video-paths' | 26 | import { getVideoFilename } from '../video-paths' |
27 | import { VideoModel } from '@server/models/video/video' | ||
28 | import { getServerActor } from '@server/models/application/application' | ||
28 | 29 | ||
29 | type CandidateToDuplicate = { | 30 | type CandidateToDuplicate = { |
30 | redundancy: VideosRedundancy, | 31 | redundancy: VideosRedundancyStrategy |
31 | video: MVideoWithAllFiles, | 32 | video: MVideoWithAllFiles |
32 | files: MVideoFile[], | 33 | files: MVideoFile[] |
33 | streamingPlaylists: MStreamingPlaylistFiles[] | 34 | streamingPlaylists: MStreamingPlaylistFiles[] |
34 | } | 35 | } |
35 | 36 | ||
@@ -41,7 +42,7 @@ function isMVideoRedundancyFileVideo ( | |||
41 | 42 | ||
42 | export class VideosRedundancyScheduler extends AbstractScheduler { | 43 | export class VideosRedundancyScheduler extends AbstractScheduler { |
43 | 44 | ||
44 | private static instance: AbstractScheduler | 45 | private static instance: VideosRedundancyScheduler |
45 | 46 | ||
46 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL | 47 | protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL |
47 | 48 | ||
@@ -49,6 +50,22 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
49 | super() | 50 | super() |
50 | } | 51 | } |
51 | 52 | ||
53 | async createManualRedundancy (videoId: number) { | ||
54 | const videoToDuplicate = await VideoModel.loadWithFiles(videoId) | ||
55 | |||
56 | if (!videoToDuplicate) { | ||
57 | logger.warn('Video to manually duplicate %d does not exist anymore.', videoId) | ||
58 | return | ||
59 | } | ||
60 | |||
61 | return this.createVideoRedundancies({ | ||
62 | video: videoToDuplicate, | ||
63 | redundancy: null, | ||
64 | files: videoToDuplicate.VideoFiles, | ||
65 | streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists | ||
66 | }) | ||
67 | } | ||
68 | |||
52 | protected async internalExecute () { | 69 | protected async internalExecute () { |
53 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { | 70 | for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { |
54 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) | 71 | logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) |
@@ -94,7 +111,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
94 | for (const redundancyModel of expired) { | 111 | for (const redundancyModel of expired) { |
95 | try { | 112 | try { |
96 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) | 113 | const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) |
97 | const candidate = { | 114 | const candidate: CandidateToDuplicate = { |
98 | redundancy: redundancyConfig, | 115 | redundancy: redundancyConfig, |
99 | video: null, | 116 | video: null, |
100 | files: [], | 117 | files: [], |
@@ -140,7 +157,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
140 | } | 157 | } |
141 | } | 158 | } |
142 | 159 | ||
143 | private findVideoToDuplicate (cache: VideosRedundancy) { | 160 | private findVideoToDuplicate (cache: VideosRedundancyStrategy) { |
144 | if (cache.strategy === 'most-views') { | 161 | if (cache.strategy === 'most-views') { |
145 | return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) | 162 | return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) |
146 | } | 163 | } |
@@ -187,13 +204,21 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
187 | } | 204 | } |
188 | } | 205 | } |
189 | 206 | ||
190 | private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) { | 207 | private async createVideoFileRedundancy (redundancy: VideosRedundancyStrategy | null, video: MVideoAccountLight, fileArg: MVideoFile) { |
208 | let strategy = 'manual' | ||
209 | let expiresOn: Date = null | ||
210 | |||
211 | if (redundancy) { | ||
212 | strategy = redundancy.strategy | ||
213 | expiresOn = this.buildNewExpiration(redundancy.minLifetime) | ||
214 | } | ||
215 | |||
191 | const file = fileArg as MVideoFileVideo | 216 | const file = fileArg as MVideoFileVideo |
192 | file.Video = video | 217 | file.Video = video |
193 | 218 | ||
194 | const serverActor = await getServerActor() | 219 | const serverActor = await getServerActor() |
195 | 220 | ||
196 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) | 221 | logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy) |
197 | 222 | ||
198 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | 223 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() |
199 | const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) | 224 | const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) |
@@ -204,10 +229,10 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
204 | await move(tmpPath, destPath, { overwrite: true }) | 229 | await move(tmpPath, destPath, { overwrite: true }) |
205 | 230 | ||
206 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ | 231 | const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ |
207 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 232 | expiresOn, |
208 | url: getVideoCacheFileActivityPubUrl(file), | 233 | url: getVideoCacheFileActivityPubUrl(file), |
209 | fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), | 234 | fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), |
210 | strategy: redundancy.strategy, | 235 | strategy, |
211 | videoFileId: file.id, | 236 | videoFileId: file.id, |
212 | actorId: serverActor.id | 237 | actorId: serverActor.id |
213 | }) | 238 | }) |
@@ -220,25 +245,33 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
220 | } | 245 | } |
221 | 246 | ||
222 | private async createStreamingPlaylistRedundancy ( | 247 | private async createStreamingPlaylistRedundancy ( |
223 | redundancy: VideosRedundancy, | 248 | redundancy: VideosRedundancyStrategy, |
224 | video: MVideoAccountLight, | 249 | video: MVideoAccountLight, |
225 | playlistArg: MStreamingPlaylist | 250 | playlistArg: MStreamingPlaylist |
226 | ) { | 251 | ) { |
252 | let strategy = 'manual' | ||
253 | let expiresOn: Date = null | ||
254 | |||
255 | if (redundancy) { | ||
256 | strategy = redundancy.strategy | ||
257 | expiresOn = this.buildNewExpiration(redundancy.minLifetime) | ||
258 | } | ||
259 | |||
227 | const playlist = playlistArg as MStreamingPlaylistVideo | 260 | const playlist = playlistArg as MStreamingPlaylistVideo |
228 | playlist.Video = video | 261 | playlist.Video = video |
229 | 262 | ||
230 | const serverActor = await getServerActor() | 263 | const serverActor = await getServerActor() |
231 | 264 | ||
232 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) | 265 | logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy) |
233 | 266 | ||
234 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) | 267 | const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) |
235 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) | 268 | await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) |
236 | 269 | ||
237 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ | 270 | const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ |
238 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 271 | expiresOn, |
239 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), | 272 | url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), |
240 | fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), | 273 | fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), |
241 | strategy: redundancy.strategy, | 274 | strategy, |
242 | videoStreamingPlaylistId: playlist.id, | 275 | videoStreamingPlaylistId: playlist.id, |
243 | actorId: serverActor.id | 276 | actorId: serverActor.id |
244 | }) | 277 | }) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index a99f71629..8dbd41771 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -69,7 +69,7 @@ function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, | |||
69 | function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { | 69 | function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { |
70 | const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 70 | const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
71 | 71 | ||
72 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() | 72 | const thumbnail = existingThumbnail || new ThumbnailModel() |
73 | 73 | ||
74 | thumbnail.filename = filename | 74 | thumbnail.filename = filename |
75 | thumbnail.height = height | 75 | thumbnail.height = height |
@@ -142,18 +142,18 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
142 | } | 142 | } |
143 | 143 | ||
144 | async function createThumbnailFromFunction (parameters: { | 144 | async function createThumbnailFromFunction (parameters: { |
145 | thumbnailCreator: () => Promise<any>, | 145 | thumbnailCreator: () => Promise<any> |
146 | filename: string, | 146 | filename: string |
147 | height: number, | 147 | height: number |
148 | width: number, | 148 | width: number |
149 | type: ThumbnailType, | 149 | type: ThumbnailType |
150 | automaticallyGenerated?: boolean, | 150 | automaticallyGenerated?: boolean |
151 | fileUrl?: string, | 151 | fileUrl?: string |
152 | existingThumbnail?: MThumbnail | 152 | existingThumbnail?: MThumbnail |
153 | }) { | 153 | }) { |
154 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters | 154 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters |
155 | 155 | ||
156 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() | 156 | const thumbnail = existingThumbnail || new ThumbnailModel() |
157 | 157 | ||
158 | thumbnail.filename = filename | 158 | thumbnail.filename = filename |
159 | thumbnail.height = height | 159 | thumbnail.height = height |
diff --git a/server/lib/user.ts b/server/lib/user.ts index c45438d95..8b447583e 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as uuidv4 from 'uuid/v4' | 1 | import { v4 as uuidv4 } from 'uuid' |
2 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 2 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
3 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' | 3 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' |
4 | import { AccountModel } from '../models/account/account' | 4 | import { AccountModel } from '../models/account/account' |
5 | import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' | 5 | import { buildActorInstance, setAsyncActorKeys } from './activitypub/actor' |
6 | import { createLocalVideoChannel } from './video-channel' | 6 | import { createLocalVideoChannel } from './video-channel' |
7 | import { ActorModel } from '../models/activitypub/actor' | 7 | import { ActorModel } from '../models/activitypub/actor' |
8 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 8 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' |
@@ -14,13 +14,14 @@ import { Redis } from './redis' | |||
14 | import { Emailer } from './emailer' | 14 | import { Emailer } from './emailer' |
15 | import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models' | 15 | import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models' |
16 | import { MUser, MUserDefault, MUserId } from '../typings/models/user' | 16 | import { MUser, MUserDefault, MUserId } from '../typings/models/user' |
17 | import { getAccountActivityPubUrl } from './activitypub/url' | ||
17 | 18 | ||
18 | type ChannelNames = { name: string, displayName: string } | 19 | type ChannelNames = { name: string, displayName: string } |
19 | 20 | ||
20 | async function createUserAccountAndChannelAndPlaylist (parameters: { | 21 | async function createUserAccountAndChannelAndPlaylist (parameters: { |
21 | userToCreate: MUser, | 22 | userToCreate: MUser |
22 | userDisplayName?: string, | 23 | userDisplayName?: string |
23 | channelNames?: ChannelNames, | 24 | channelNames?: ChannelNames |
24 | validateUser?: boolean | 25 | validateUser?: boolean |
25 | }): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> { | 26 | }): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> { |
26 | const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters | 27 | const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters |
@@ -63,11 +64,11 @@ async function createUserAccountAndChannelAndPlaylist (parameters: { | |||
63 | } | 64 | } |
64 | 65 | ||
65 | async function createLocalAccountWithoutKeys (parameters: { | 66 | async function createLocalAccountWithoutKeys (parameters: { |
66 | name: string, | 67 | name: string |
67 | displayName?: string, | 68 | displayName?: string |
68 | userId: number | null, | 69 | userId: number | null |
69 | applicationId: number | null, | 70 | applicationId: number | null |
70 | t: Transaction | undefined, | 71 | t: Transaction | undefined |
71 | type?: ActivityPubActorType | 72 | type?: ActivityPubActorType |
72 | }) { | 73 | }) { |
73 | const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters | 74 | const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index 1dd45b76d..bd60c6201 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -1,23 +1,33 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | import { | ||
4 | MUser, | ||
5 | MVideoAccountLight, | ||
6 | MVideoBlacklist, | ||
7 | MVideoBlacklistVideo, | ||
8 | MVideoFullLight, | ||
9 | MVideoWithBlacklistLight | ||
10 | } from '@server/typings/models' | ||
11 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' | ||
12 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | ||
13 | import { logger } from '../helpers/logger' | ||
2 | import { CONFIG } from '../initializers/config' | 14 | import { CONFIG } from '../initializers/config' |
3 | import { UserRight, VideoBlacklistType } from '../../shared/models' | ||
4 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 15 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
5 | import { logger } from '../helpers/logger' | 16 | import { sendDeleteVideo } from './activitypub/send' |
6 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | 17 | import { federateVideoIfNeeded } from './activitypub/videos' |
7 | import { Hooks } from './plugins/hooks' | ||
8 | import { Notifier } from './notifier' | 18 | import { Notifier } from './notifier' |
9 | import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models' | 19 | import { Hooks } from './plugins/hooks' |
10 | 20 | ||
11 | async function autoBlacklistVideoIfNeeded (parameters: { | 21 | async function autoBlacklistVideoIfNeeded (parameters: { |
12 | video: MVideoWithBlacklistLight, | 22 | video: MVideoWithBlacklistLight |
13 | user?: MUser, | 23 | user?: MUser |
14 | isRemote: boolean, | 24 | isRemote: boolean |
15 | isNew: boolean, | 25 | isNew: boolean |
16 | notify?: boolean, | 26 | notify?: boolean |
17 | transaction?: Transaction | 27 | transaction?: Transaction |
18 | }) { | 28 | }) { |
19 | const { video, user, isRemote, isNew, notify = true, transaction } = parameters | 29 | const { video, user, isRemote, isNew, notify = true, transaction } = parameters |
20 | const doAutoBlacklist = await Hooks.wrapPromiseFun( | 30 | const doAutoBlacklist = await Hooks.wrapFun( |
21 | autoBlacklistNeeded, | 31 | autoBlacklistNeeded, |
22 | { video, user, isRemote, isNew }, | 32 | { video, user, isRemote, isNew }, |
23 | 'filter:video.auto-blacklist.result' | 33 | 'filter:video.auto-blacklist.result' |
@@ -49,10 +59,64 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
49 | return true | 59 | return true |
50 | } | 60 | } |
51 | 61 | ||
52 | async function autoBlacklistNeeded (parameters: { | 62 | async function blacklistVideo (videoInstance: MVideoAccountLight, options: VideoBlacklistCreate) { |
53 | video: MVideoWithBlacklistLight, | 63 | const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create({ |
54 | isRemote: boolean, | 64 | videoId: videoInstance.id, |
55 | isNew: boolean, | 65 | unfederated: options.unfederate === true, |
66 | reason: options.reason, | ||
67 | type: VideoBlacklistType.MANUAL | ||
68 | } | ||
69 | ) | ||
70 | blacklist.Video = videoInstance | ||
71 | |||
72 | if (options.unfederate === true) { | ||
73 | await sendDeleteVideo(videoInstance, undefined) | ||
74 | } | ||
75 | |||
76 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) | ||
77 | } | ||
78 | |||
79 | async function unblacklistVideo (videoBlacklist: MVideoBlacklist, video: MVideoFullLight) { | ||
80 | const videoBlacklistType = await sequelizeTypescript.transaction(async t => { | ||
81 | const unfederated = videoBlacklist.unfederated | ||
82 | const videoBlacklistType = videoBlacklist.type | ||
83 | |||
84 | await videoBlacklist.destroy({ transaction: t }) | ||
85 | video.VideoBlacklist = undefined | ||
86 | |||
87 | // Re federate the video | ||
88 | if (unfederated === true) { | ||
89 | await federateVideoIfNeeded(video, true, t) | ||
90 | } | ||
91 | |||
92 | return videoBlacklistType | ||
93 | }) | ||
94 | |||
95 | Notifier.Instance.notifyOnVideoUnblacklist(video) | ||
96 | |||
97 | if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) { | ||
98 | Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video) | ||
99 | |||
100 | // Delete on object so new video notifications will send | ||
101 | delete video.VideoBlacklist | ||
102 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) | ||
103 | } | ||
104 | } | ||
105 | |||
106 | // --------------------------------------------------------------------------- | ||
107 | |||
108 | export { | ||
109 | autoBlacklistVideoIfNeeded, | ||
110 | blacklistVideo, | ||
111 | unblacklistVideo | ||
112 | } | ||
113 | |||
114 | // --------------------------------------------------------------------------- | ||
115 | |||
116 | function autoBlacklistNeeded (parameters: { | ||
117 | video: MVideoWithBlacklistLight | ||
118 | isRemote: boolean | ||
119 | isNew: boolean | ||
56 | user?: MUser | 120 | user?: MUser |
57 | }) { | 121 | }) { |
58 | const { user, video, isRemote, isNew } = parameters | 122 | const { user, video, isRemote, isNew } = parameters |
@@ -66,9 +130,3 @@ async function autoBlacklistNeeded (parameters: { | |||
66 | 130 | ||
67 | return true | 131 | return true |
68 | } | 132 | } |
69 | |||
70 | // --------------------------------------------------------------------------- | ||
71 | |||
72 | export { | ||
73 | autoBlacklistVideoIfNeeded | ||
74 | } | ||
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 41eab456b..102c1088d 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts | |||
@@ -1,13 +1,14 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import * as uuidv4 from 'uuid/v4' | 2 | import { v4 as uuidv4 } from 'uuid' |
3 | import { VideoChannelCreate } from '../../shared/models' | 3 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoChannelModel } from '../models/video/video-channel' | 4 | import { VideoChannelModel } from '../models/video/video-channel' |
5 | import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub' | 5 | import { buildActorInstance } from './activitypub/actor' |
6 | import { VideoModel } from '../models/video/video' | 6 | import { VideoModel } from '../models/video/video' |
7 | import { MAccountId, MChannelDefault, MChannelId } from '../typings/models' | 7 | import { MAccountId, MChannelDefault, MChannelId } from '../typings/models' |
8 | import { getVideoChannelActivityPubUrl } from './activitypub/url' | ||
9 | import { federateVideoIfNeeded } from './activitypub/videos' | ||
8 | 10 | ||
9 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & | 11 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } |
10 | { Account?: T } | ||
11 | 12 | ||
12 | async function createLocalVideoChannel <T extends MAccountId> ( | 13 | async function createLocalVideoChannel <T extends MAccountId> ( |
13 | videoChannelInfo: VideoChannelCreate, | 14 | videoChannelInfo: VideoChannelCreate, |
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index b8074e6d2..516c912a9 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -2,14 +2,14 @@ import * as Sequelize from 'sequelize' | |||
2 | import { ResultList } from '../../shared/models' | 2 | import { ResultList } from '../../shared/models' |
3 | import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' | 3 | import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' |
4 | import { VideoCommentModel } from '../models/video/video-comment' | 4 | import { VideoCommentModel } from '../models/video/video-comment' |
5 | import { getVideoCommentActivityPubUrl } from './activitypub' | 5 | import { getVideoCommentActivityPubUrl } from './activitypub/url' |
6 | import { sendCreateVideoComment } from './activitypub/send' | 6 | import { sendCreateVideoComment } from './activitypub/send' |
7 | import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' | 7 | import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' |
8 | 8 | ||
9 | async function createVideoComment (obj: { | 9 | async function createVideoComment (obj: { |
10 | text: string, | 10 | text: string |
11 | inReplyToComment: MComment | null, | 11 | inReplyToComment: MComment | null |
12 | video: MVideoFullLight, | 12 | video: MVideoFullLight |
13 | account: MAccountDefault | 13 | account: MAccountDefault |
14 | }, t: Sequelize.Transaction) { | 14 | }, t: Sequelize.Transaction) { |
15 | let originCommentId: number | null = null | 15 | let originCommentId: number | null = null |
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts index fe0a004e4..05aaca8af 100644 --- a/server/lib/video-paths.ts +++ b/server/lib/video-paths.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models' | 1 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models' |
2 | import { extractVideo } from './videos' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
5 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' | 4 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' |
5 | import { extractVideo } from '@server/helpers/video' | ||
6 | 6 | ||
7 | // ################## Video file name ################## | 7 | // ################## Video file name ################## |
8 | 8 | ||
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts index 29b70cfda..75fbd6896 100644 --- a/server/lib/video-playlist.ts +++ b/server/lib/video-playlist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 2 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
3 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' | 3 | import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' |
4 | import { getVideoPlaylistActivityPubUrl } from './activitypub' | 4 | import { getVideoPlaylistActivityPubUrl } from './activitypub/url' |
5 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' | 5 | import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' |
6 | import { MAccount } from '../typings/models' | 6 | import { MAccount } from '../typings/models' |
7 | import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist' | 7 | import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist' |
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts index 0d5b3ae39..dcda82e0a 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/video-transcoding.ts | |||
@@ -3,6 +3,7 @@ import { basename, extname as extnameUtil, join } from 'path' | |||
3 | import { | 3 | import { |
4 | canDoQuickTranscode, | 4 | canDoQuickTranscode, |
5 | getDurationFromVideoFile, | 5 | getDurationFromVideoFile, |
6 | getMetadataFromFile, | ||
6 | getVideoFileFPS, | 7 | getVideoFileFPS, |
7 | transcode, | 8 | transcode, |
8 | TranscodeOptions, | 9 | TranscodeOptions, |
@@ -202,10 +203,11 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso | |||
202 | 203 | ||
203 | newVideoFile.size = stats.size | 204 | newVideoFile.size = stats.size |
204 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) | 205 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) |
206 | newVideoFile.metadata = await getMetadataFromFile(videoFilePath) | ||
205 | 207 | ||
206 | await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) | 208 | await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) |
207 | 209 | ||
208 | await newVideoFile.save() | 210 | await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) |
209 | videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') | 211 | videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') |
210 | 212 | ||
211 | video.setHLSPlaylist(videoStreamingPlaylist) | 213 | video.setHLSPlaylist(videoStreamingPlaylist) |
@@ -230,11 +232,13 @@ export { | |||
230 | async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { | 232 | async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { |
231 | const stats = await stat(transcodingPath) | 233 | const stats = await stat(transcodingPath) |
232 | const fps = await getVideoFileFPS(transcodingPath) | 234 | const fps = await getVideoFileFPS(transcodingPath) |
235 | const metadata = await getMetadataFromFile(transcodingPath) | ||
233 | 236 | ||
234 | await move(transcodingPath, outputPath) | 237 | await move(transcodingPath, outputPath) |
235 | 238 | ||
236 | videoFile.size = stats.size | 239 | videoFile.size = stats.size |
237 | videoFile.fps = fps | 240 | videoFile.fps = fps |
241 | videoFile.metadata = metadata | ||
238 | 242 | ||
239 | await createTorrentAndSetInfoHash(video, videoFile) | 243 | await createTorrentAndSetInfoHash(video, videoFile) |
240 | 244 | ||
diff --git a/server/lib/videos.ts b/server/lib/videos.ts deleted file mode 100644 index 22e9afbf9..000000000 --- a/server/lib/videos.ts +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models' | ||
2 | |||
3 | function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { | ||
4 | return isStreamingPlaylist(videoOrPlaylist) | ||
5 | ? videoOrPlaylist.Video | ||
6 | : videoOrPlaylist | ||
7 | } | ||
8 | |||
9 | export { | ||
10 | extractVideo | ||
11 | } | ||