aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/jobs.ts4
-rw-r--r--server/controllers/api/plugins.ts1
-rw-r--r--server/controllers/api/search.ts43
-rw-r--r--server/controllers/api/users/index.ts8
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/controllers/api/users/token.ts72
-rw-r--r--server/controllers/api/videos/index.ts11
-rw-r--r--server/controllers/client.ts28
-rw-r--r--server/controllers/download.ts85
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/plugins.ts14
-rw-r--r--server/helpers/activitypub.ts5
-rw-r--r--server/helpers/core-utils.ts19
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts110
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts18
-rw-r--r--server/helpers/custom-validators/activitypub/share.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/view.ts13
-rw-r--r--server/helpers/custom-validators/user-notifications.ts5
-rw-r--r--server/helpers/ffmpeg-utils.ts21
-rw-r--r--server/helpers/logger.ts10
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/helpers/requests.ts199
-rw-r--r--server/helpers/youtube-dl.ts71
-rw-r--r--server/initializers/checker-after-init.ts27
-rw-r--r--server/initializers/checker-before-init.ts6
-rw-r--r--server/initializers/config.ts6
-rw-r--r--server/initializers/constants.ts6
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/migrations/0610-views-index copy.ts (renamed from server/initializers/migrations/0610-views-index.ts)0
-rw-r--r--server/initializers/migrations/0615-latest-versions-notification-settings.ts44
-rw-r--r--server/initializers/migrations/0620-latest-versions-application.ts27
-rw-r--r--server/initializers/migrations/0625-latest-versions-notification.ts26
-rw-r--r--server/lib/activitypub/actor.ts61
-rw-r--r--server/lib/activitypub/crawl.ts25
-rw-r--r--server/lib/activitypub/playlist.ts69
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/share.ts30
-rw-r--r--server/lib/activitypub/video-comments.ts22
-rw-r--r--server/lib/activitypub/video-rates.ts22
-rw-r--r--server/lib/activitypub/videos.ts44
-rw-r--r--server/lib/auth/external-auth.ts (renamed from server/lib/auth.ts)129
-rw-r--r--server/lib/auth/oauth-model.ts (renamed from server/lib/oauth-model.ts)137
-rw-r--r--server/lib/auth/oauth.ts180
-rw-r--r--server/lib/auth/tokens-cache.ts52
-rw-r--r--server/lib/emailer.ts79
-rw-r--r--server/lib/emails/peertube-version-new/html.pug9
-rw-r--r--server/lib/emails/plugin-version-new/html.pug9
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts2
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/files-cache/videos-torrent-cache.ts15
-rw-r--r--server/lib/hls.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts63
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts5
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts15
-rw-r--r--server/lib/notifier.ts74
-rw-r--r--server/lib/plugins/plugin-index.ts26
-rw-r--r--server/lib/plugins/register-helpers.ts2
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts8
-rw-r--r--server/lib/schedulers/peertube-version-check-scheduler.ts55
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts6
-rw-r--r--server/lib/user.ts4
-rw-r--r--server/lib/video-blacklist.ts6
-rw-r--r--server/middlewares/auth.ts (renamed from server/middlewares/oauth.ts)24
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/validators/activitypub/signature.ts2
-rw-r--r--server/middlewares/validators/jobs.ts6
-rw-r--r--server/middlewares/validators/pagination.ts33
-rw-r--r--server/middlewares/validators/sort.ts2
-rw-r--r--server/middlewares/validators/utils.ts4
-rw-r--r--server/middlewares/validators/videos/video-comments.ts2
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts2
-rw-r--r--server/models/account/user-notification-setting.ts26
-rw-r--r--server/models/account/user-notification.ts90
-rw-r--r--server/models/account/user.ts4
-rw-r--r--server/models/application/application.ts4
-rw-r--r--server/models/oauth/oauth-token.ts11
-rw-r--r--server/tests/api/activitypub/security.ts116
-rw-r--r--server/tests/api/check-params/user-notifications.ts4
-rw-r--r--server/tests/api/check-params/users.ts2
-rw-r--r--server/tests/api/notifications/admin-notifications.ts165
-rw-r--r--server/tests/api/notifications/index.ts1
-rw-r--r--server/tests/api/server/handle-down.ts2
-rw-r--r--server/tests/api/users/users.ts51
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/cli/regenerate-thumbnails.ts110
-rw-r--r--server/tests/fixtures/peertube-plugin-test/main.js70
-rw-r--r--server/tests/helpers/request.ts16
-rw-r--r--server/tests/plugins/external-auth.ts2
-rw-r--r--server/tests/plugins/filter-hooks.ts178
-rw-r--r--server/tools/peertube-import-videos.ts5
-rw-r--r--server/types/models/application/application.ts5
-rw-r--r--server/types/models/application/index.ts1
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts12
-rw-r--r--server/typings/express/index.d.ts17
98 files changed, 2223 insertions, 842 deletions
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index 861cc22b9..d7cee1605 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -9,10 +9,10 @@ import {
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 jobsSortValidator, 11 jobsSortValidator,
12 paginationValidatorBuilder,
12 setDefaultPagination, 13 setDefaultPagination,
13 setDefaultSort 14 setDefaultSort
14} from '../../middlewares' 15} from '../../middlewares'
15import { paginationValidator } from '../../middlewares/validators'
16import { listJobsValidator } from '../../middlewares/validators/jobs' 16import { listJobsValidator } from '../../middlewares/validators/jobs'
17 17
18const jobsRouter = express.Router() 18const jobsRouter = express.Router()
@@ -20,7 +20,7 @@ const jobsRouter = express.Router()
20jobsRouter.get('/:state?', 20jobsRouter.get('/:state?',
21 authenticate, 21 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 22 ensureUserHasRight(UserRight.MANAGE_JOBS),
23 paginationValidator, 23 paginationValidatorBuilder([ 'jobs' ]),
24 jobsSortValidator, 24 jobsSortValidator,
25 setDefaultSort, 25 setDefaultSort,
26 setDefaultPagination, 26 setDefaultPagination,
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 1c0b5edb1..bb69f25a1 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -205,7 +205,6 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
205 if (!resultList) { 205 if (!resultList) {
206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) 206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503)
207 .json({ error: 'Plugin index unavailable. Please retry later' }) 207 .json({ error: 'Plugin index unavailable. Please retry later' })
208 .end()
209 } 208 }
210 209
211 return res.json(resultList) 210 return res.json(resultList)
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 7e1b7b230..f0cdf3a89 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils' 2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doRequest } from '@server/helpers/requests' 3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' 5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
6import { Hooks } from '@server/lib/plugins/hooks'
6import { AccountBlocklistModel } from '@server/models/account/account-blocklist' 7import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
7import { getServerActor } from '@server/models/application/application' 8import { getServerActor } from '@server/models/application/application'
8import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
@@ -22,8 +23,8 @@ import {
22 paginationValidator, 23 paginationValidator,
23 setDefaultPagination, 24 setDefaultPagination,
24 setDefaultSearchSort, 25 setDefaultSearchSort,
25 videoChannelsSearchSortValidator,
26 videoChannelsListSearchValidator, 26 videoChannelsListSearchValidator,
27 videoChannelsSearchSortValidator,
27 videosSearchSortValidator, 28 videosSearchSortValidator,
28 videosSearchValidator 29 videosSearchValidator
29} from '../../middlewares' 30} from '../../middlewares'
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
87async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { 88async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
88 const result = await buildMutedForSearchIndex(res) 89 const result = await buildMutedForSearchIndex(res)
89 90
90 const body = Object.assign(query, result) 91 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
91 92
92 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' 93 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
93 94
94 try { 95 try {
95 logger.debug('Doing video channels search index request on %s.', url, { body }) 96 logger.debug('Doing video channels search index request on %s.', url, { body })
96 97
97 const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true }) 98 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
99 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
98 100
99 return res.json(searchIndexResult.body) 101 return res.json(jsonResult)
100 } catch (err) { 102 } catch (err) {
101 logger.warn('Cannot use search index to make video channels search.', { err }) 103 logger.warn('Cannot use search index to make video channels search.', { err })
102 104
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
107async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { 109async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
108 const serverActor = await getServerActor() 110 const serverActor = await getServerActor()
109 111
110 const options = { 112 const apiOptions = await Hooks.wrapObject({
111 actorId: serverActor.id, 113 actorId: serverActor.id,
112 search: query.search, 114 search: query.search,
113 start: query.start, 115 start: query.start,
114 count: query.count, 116 count: query.count,
115 sort: query.sort 117 sort: query.sort
116 } 118 }, 'filter:api.search.video-channels.local.list.params')
117 const resultList = await VideoChannelModel.searchForApi(options) 119
120 const resultList = await Hooks.wrapPromiseFun(
121 VideoChannelModel.searchForApi,
122 apiOptions,
123 'filter:api.search.video-channels.local.list.result'
124 )
118 125
119 return res.json(getFormattedObjects(resultList.data, resultList.total)) 126 return res.json(getFormattedObjects(resultList.data, resultList.total))
120} 127}
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) {
168async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { 175async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
169 const result = await buildMutedForSearchIndex(res) 176 const result = await buildMutedForSearchIndex(res)
170 177
171 const body: VideosSearchQuery = Object.assign(query, result) 178 let body: VideosSearchQuery = Object.assign(query, result)
172 179
173 // Use the default instance NSFW policy if not specified 180 // Use the default instance NSFW policy if not specified
174 if (!body.nsfw) { 181 if (!body.nsfw) {
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
181 : 'both' 188 : 'both'
182 } 189 }
183 190
191 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
192
184 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' 193 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
185 194
186 try { 195 try {
187 logger.debug('Doing videos search index request on %s.', url, { body }) 196 logger.debug('Doing videos search index request on %s.', url, { body })
188 197
189 const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true }) 198 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
199 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
190 200
191 return res.json(searchIndexResult.body) 201 return res.json(jsonResult)
192 } catch (err) { 202 } catch (err) {
193 logger.warn('Cannot use search index to make video search.', { err }) 203 logger.warn('Cannot use search index to make video search.', { err })
194 204
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
197} 207}
198 208
199async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 209async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
200 const options = Object.assign(query, { 210 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
201 includeLocalVideos: true, 211 includeLocalVideos: true,
202 nsfw: buildNSFWFilter(res, query.nsfw), 212 nsfw: buildNSFWFilter(res, query.nsfw),
203 filter: query.filter, 213 filter: query.filter,
204 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 214 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
205 }) 215 }), 'filter:api.search.videos.local.list.params')
206 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 216
217 const resultList = await Hooks.wrapPromiseFun(
218 VideoModel.searchAndPopulateAccountAndServer,
219 apiOptions,
220 'filter:api.search.videos.local.list.result'
221 )
207 222
208 return res.json(getFormattedObjects(resultList.data, resultList.total)) 223 return res.json(getFormattedObjects(resultList.data, resultList.total))
209} 224}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 3be1d55ae..e2b1ea7cd 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -2,8 +2,10 @@ import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 3import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
5import { MUser, MUserAccountDefault } from '@server/types/models' 6import { MUser, MUserAccountDefault } from '@server/types/models'
6import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 7import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 9import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
8import { UserRegister } from '../../../../shared/models/users/user-register.model' 10import { UserRegister } from '../../../../shared/models/users/user-register.model'
9import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 11import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
14import { sequelizeTypescript } from '../../../initializers/database' 16import { sequelizeTypescript } from '../../../initializers/database'
15import { Emailer } from '../../../lib/emailer' 17import { Emailer } from '../../../lib/emailer'
16import { Notifier } from '../../../lib/notifier' 18import { Notifier } from '../../../lib/notifier'
17import { deleteUserToken } from '../../../lib/oauth-model'
18import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
19import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 20import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
20import { 21import {
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
52import { myNotificationsRouter } from './my-notifications' 53import { myNotificationsRouter } from './my-notifications'
53import { mySubscriptionsRouter } from './my-subscriptions' 54import { mySubscriptionsRouter } from './my-subscriptions'
54import { myVideoPlaylistsRouter } from './my-video-playlists' 55import { myVideoPlaylistsRouter } from './my-video-playlists'
55import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
56 56
57const auditLogger = auditLoggerFactory('users') 57const auditLogger = auditLoggerFactory('users')
58 58
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
335 const user = await userToUpdate.save() 335 const user = await userToUpdate.save()
336 336
337 // Destroy user token to refresh rights 337 // Destroy user token to refresh rights
338 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) 338 if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
339 339
340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
341 341
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
395 user.blockedReason = reason || null 395 user.blockedReason = reason || null
396 396
397 await sequelizeTypescript.transaction(async t => { 397 await sequelizeTypescript.transaction(async t => {
398 await deleteUserToken(user.id, t) 398 await OAuthTokenModel.deleteUserToken(user.id, t)
399 399
400 await user.save({ transaction: t }) 400 await user.save({ transaction: t })
401 }) 401 })
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 5f5e4c5e6..0a9101a46 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
80 newInstanceFollower: body.newInstanceFollower, 80 newInstanceFollower: body.newInstanceFollower,
81 autoInstanceFollowing: body.autoInstanceFollowing, 81 autoInstanceFollowing: body.autoInstanceFollowing,
82 abuseNewMessage: body.abuseNewMessage, 82 abuseNewMessage: body.abuseNewMessage,
83 abuseStateChange: body.abuseStateChange 83 abuseStateChange: body.abuseStateChange,
84 newPeerTubeVersion: body.newPeerTubeVersion,
85 newPluginVersion: body.newPluginVersion
84 } 86 }
85 87
86 await UserNotificationSettingModel.update(values, query) 88 await UserNotificationSettingModel.update(values, query)
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 821429358..694bb0a92 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,11 +1,14 @@
1import { handleLogin, handleTokenRevocation } from '@server/lib/auth' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { v4 as uuidv4 } from 'uuid'
4import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
4import * as express from 'express' 6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
7import { handleOAuthToken } from '@server/lib/auth/oauth'
8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
5import { Hooks } from '@server/lib/plugins/hooks' 9import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares' 10import { asyncMiddleware, authenticate } from '@server/middlewares'
7import { ScopedToken } from '@shared/models/users/user-scoped-token' 11import { ScopedToken } from '@shared/models/users/user-scoped-token'
8import { v4 as uuidv4 } from 'uuid'
9 12
10const tokensRouter = express.Router() 13const tokensRouter = express.Router()
11 14
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
16 19
17tokensRouter.post('/token', 20tokensRouter.post('/token',
18 loginRateLimiter, 21 loginRateLimiter,
19 handleLogin, 22 asyncMiddleware(handleToken)
20 tokenSuccess
21) 23)
22 24
23tokensRouter.post('/revoke-token', 25tokensRouter.post('/revoke-token',
@@ -42,10 +44,53 @@ export {
42} 44}
43// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
44 46
45function tokenSuccess (req: express.Request) { 47async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
46 const username = req.body.username 48 const grantType = req.body.grant_type
49
50 try {
51 const bypassLogin = await buildByPassLogin(req, grantType)
52
53 const refreshTokenAuthName = grantType === 'refresh_token'
54 ? await getAuthNameFromRefreshGrant(req.body.refresh_token)
55 : undefined
56
57 const options = {
58 refreshTokenAuthName,
59 bypassLogin
60 }
61
62 const token = await handleOAuthToken(req, options)
63
64 res.set('Cache-Control', 'no-store')
65 res.set('Pragma', 'no-cache')
66
67 Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
68
69 return res.json({
70 token_type: 'Bearer',
47 71
48 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) 72 access_token: token.accessToken,
73 refresh_token: token.refreshToken,
74
75 expires_in: token.accessTokenExpiresIn,
76 refresh_token_expires_in: token.refreshTokenExpiresIn
77 })
78 } catch (err) {
79 logger.warn('Login error', { err })
80
81 return res.status(err.code || 400).json({
82 code: err.name,
83 error: err.message
84 })
85 }
86}
87
88async function handleTokenRevocation (req: express.Request, res: express.Response) {
89 const token = res.locals.oauth.token
90
91 const result = await revokeToken(token, { req, explicitLogout: true })
92
93 return res.json(result)
49} 94}
50 95
51function getScopedTokens (req: express.Request, res: express.Response) { 96function getScopedTokens (req: express.Request, res: express.Response) {
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
66 feedToken: user.feedToken 111 feedToken: user.feedToken
67 } as ScopedToken) 112 } as ScopedToken)
68} 113}
114
115async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
116 if (grantType !== 'password') return undefined
117
118 if (req.body.externalAuthToken) {
119 // Consistency with the getBypassFromPasswordGrant promise
120 return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
121 }
122
123 return getBypassFromPasswordGrant(req.body.username, req.body.password)
124}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 2447c1288..7fee278f2 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -17,7 +17,7 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
20import { logger } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { getFormattedObjects } from '../../../helpers/utils' 21import { getFormattedObjects } from '../../../helpers/utils'
22import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
23import { 23import {
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership'
67import { rateVideoRouter } from './rate' 67import { rateVideoRouter } from './rate'
68import { watchingRouter } from './watching' 68import { watchingRouter } from './watching'
69 69
70const lTags = loggerTagsFactory('api', 'video')
70const auditLogger = auditLoggerFactory('videos') 71const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 72const videosRouter = express.Router()
72 73
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) {
257 }) 258 })
258 259
259 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 260 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 261 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
261 262
262 return { videoCreated } 263 return { videoCreated }
263 }) 264 })
264 265
265 // Create the torrent file in async way because it could be long 266 // Create the torrent file in async way because it could be long
266 createTorrentAndSetInfoHashAsync(video, videoFile) 267 createTorrentAndSetInfoHashAsync(video, videoFile)
267 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err })) 268 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
268 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) 269 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
269 .then(refreshedVideo => { 270 .then(refreshedVideo => {
270 if (!refreshedVideo) return 271 if (!refreshedVideo) return
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) {
276 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) 277 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
277 }) 278 })
278 }) 279 })
279 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err })) 280 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
280 281
281 if (video.state === VideoState.TO_TRANSCODE) { 282 if (video.state === VideoState.TO_TRANSCODE) {
282 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) 283 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
389 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), 390 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
390 oldVideoAuditView 391 oldVideoAuditView
391 ) 392 )
392 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 393 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
393 394
394 return videoInstanceUpdated 395 return videoInstanceUpdated
395 }) 396 })
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 557cbfdfb..022a17ff4 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -2,7 +2,9 @@ import * as express from 'express'
2import { constants, promises as fs } from 'fs' 2import { constants, promises as fs } from 'fs'
3import { readFile } from 'fs-extra' 3import { readFile } from 'fs-extra'
4import { join } from 'path' 4import { join } from 'path'
5import { logger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { Hooks } from '@server/lib/plugins/hooks'
6import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode } from '@shared/core-utils'
7import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' 9import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
8import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
@@ -27,6 +29,7 @@ const embedMiddlewares = [
27 ? embedCSP 29 ? embedCSP
28 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), 30 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
29 31
32 // Set headers
30 (req: express.Request, res: express.Response, next: express.NextFunction) => { 33 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 res.removeHeader('X-Frame-Options') 34 res.removeHeader('X-Frame-Options')
32 35
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
105} 108}
106 109
107async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { 110async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
111 const hookName = req.originalUrl.startsWith('/video-playlists/')
112 ? 'filter:html.embed.video-playlist.allowed.result'
113 : 'filter:html.embed.video.allowed.result'
114
115 const allowParameters = { req }
116
117 const allowedResult = await Hooks.wrapFun(
118 isEmbedAllowed,
119 allowParameters,
120 hookName
121 )
122
123 if (!allowedResult || allowedResult.allowed !== true) {
124 logger.info('Embed is not allowed.', { allowedResult })
125
126 return sendHTML(allowedResult?.html || '', res)
127 }
128
108 const html = await ClientHtml.getEmbedHTML() 129 const html = await ClientHtml.getEmbedHTML()
109 130
110 return sendHTML(html, res) 131 return sendHTML(html, res)
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) {
158 } 179 }
159 } 180 }
160} 181}
182
183type AllowedResult = { allowed: boolean, html?: string }
184function isEmbedAllowed (_object: {
185 req: express.Request
186}): AllowedResult {
187 return { allowed: true }
188}
diff --git a/server/controllers/download.ts b/server/controllers/download.ts
index 27caa1518..9a8194c5c 100644
--- a/server/controllers/download.ts
+++ b/server/controllers/download.ts
@@ -1,8 +1,10 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '@server/helpers/logger'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 4import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5import { Hooks } from '@server/lib/plugins/hooks'
4import { getVideoFilePath } from '@server/lib/video-paths' 6import { getVideoFilePath } from '@server/lib/video-paths'
5import { MVideoFile, MVideoFullLight } from '@server/types/models' 7import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { VideoStreamingPlaylistType } from '@shared/models' 9import { VideoStreamingPlaylistType } from '@shared/models'
8import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' 10import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@@ -14,19 +16,19 @@ downloadRouter.use(cors())
14 16
15downloadRouter.use( 17downloadRouter.use(
16 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', 18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
17 downloadTorrent 19 asyncMiddleware(downloadTorrent)
18) 20)
19 21
20downloadRouter.use( 22downloadRouter.use(
21 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
22 asyncMiddleware(videosDownloadValidator), 24 asyncMiddleware(videosDownloadValidator),
23 downloadVideoFile 25 asyncMiddleware(downloadVideoFile)
24) 26)
25 27
26downloadRouter.use( 28downloadRouter.use(
27 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', 29 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
28 asyncMiddleware(videosDownloadValidator), 30 asyncMiddleware(videosDownloadValidator),
29 downloadHLSVideoFile 31 asyncMiddleware(downloadHLSVideoFile)
30) 32)
31 33
32// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
41 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
42 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 44 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
43 45
46 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
47
48 const allowedResult = await Hooks.wrapFun(
49 isTorrentDownloadAllowed,
50 allowParameters,
51 'filter:api.download.torrent.allowed.result'
52 )
53
54 if (!checkAllowResult(res, allowParameters, allowedResult)) return
55
44 return res.download(result.path, result.downloadName) 56 return res.download(result.path, result.downloadName)
45} 57}
46 58
47function downloadVideoFile (req: express.Request, res: express.Response) { 59async function downloadVideoFile (req: express.Request, res: express.Response) {
48 const video = res.locals.videoAll 60 const video = res.locals.videoAll
49 61
50 const videoFile = getVideoFile(req, video.VideoFiles) 62 const videoFile = getVideoFile(req, video.VideoFiles)
51 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 63 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
52 64
65 const allowParameters = { video, videoFile }
66
67 const allowedResult = await Hooks.wrapFun(
68 isVideoDownloadAllowed,
69 allowParameters,
70 'filter:api.download.video.allowed.result'
71 )
72
73 if (!checkAllowResult(res, allowParameters, allowedResult)) return
74
53 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) 75 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
54} 76}
55 77
56function downloadHLSVideoFile (req: express.Request, res: express.Response) { 78async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
57 const video = res.locals.videoAll 79 const video = res.locals.videoAll
58 const playlist = getHLSPlaylist(video) 80 const streamingPlaylist = getHLSPlaylist(video)
59 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end 81 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
60 82
61 const videoFile = getVideoFile(req, playlist.VideoFiles) 83 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
62 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 84 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63 85
64 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` 86 const allowParameters = { video, streamingPlaylist, videoFile }
65 return res.download(getVideoFilePath(playlist, videoFile), filename) 87
88 const allowedResult = await Hooks.wrapFun(
89 isVideoDownloadAllowed,
90 allowParameters,
91 'filter:api.download.video.allowed.result'
92 )
93
94 if (!checkAllowResult(res, allowParameters, allowedResult)) return
95
96 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
97 return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
66} 98}
67 99
68function getVideoFile (req: express.Request, files: MVideoFile[]) { 100function getVideoFile (req: express.Request, files: MVideoFile[]) {
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
76 108
77 return Object.assign(playlist, { Video: video }) 109 return Object.assign(playlist, { Video: video })
78} 110}
111
112type AllowedResult = {
113 allowed: boolean
114 errorMessage?: string
115}
116
117function isTorrentDownloadAllowed (_object: {
118 torrentPath: string
119}): AllowedResult {
120 return { allowed: true }
121}
122
123function isVideoDownloadAllowed (_object: {
124 video: MVideo
125 videoFile: MVideoFile
126 streamingPlaylist?: MStreamingPlaylist
127}): AllowedResult {
128 return { allowed: true }
129}
130
131function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
132 if (!result || result.allowed !== true) {
133 logger.info('Download is not allowed.', { result, allowParameters })
134 res.status(HttpStatusCode.FORBIDDEN_403)
135 .json({ error: result?.errorMessage || 'Refused download' })
136
137 return false
138 }
139
140 return true
141}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index e29a8fe1d..921067e65 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Feed from 'pfeed' 2import * as Feed from 'pfeed'
3import { VideoFilter } from '../../shared/models/videos/video-query.type'
3import { buildNSFWFilter } from '../helpers/express-utils' 4import { buildNSFWFilter } from '../helpers/express-utils'
4import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
5import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' 6import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
6import { 7import {
7 asyncMiddleware, 8 asyncMiddleware,
8 commonVideosFiltersValidator, 9 commonVideosFiltersValidator,
@@ -17,7 +18,6 @@ import {
17import { cacheRoute } from '../middlewares/cache' 18import { cacheRoute } from '../middlewares/cache'
18import { VideoModel } from '../models/video/video' 19import { VideoModel } from '../models/video/video'
19import { VideoCommentModel } from '../models/video/video-comment' 20import { VideoCommentModel } from '../models/video/video-comment'
20import { VideoFilter } from '../../shared/models/videos/video-query.type'
21 21
22const feedsRouter = express.Router() 22const feedsRouter = express.Router()
23 23
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
318 }, 318 },
319 thumbnail: [ 319 thumbnail: [
320 { 320 {
321 url: WEBSERVER.URL + video.getMiniatureStaticPath(), 321 url: WEBSERVER.URL + video.getPreviewStaticPath(),
322 height: THUMBNAILS_SIZE.height, 322 height: PREVIEWS_SIZE.height,
323 width: THUMBNAILS_SIZE.width 323 width: PREVIEWS_SIZE.width
324 } 324 }
325 ] 325 ]
326 }) 326 })
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index 6a1ccc0bf..105f51518 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -1,15 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 2import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 3import { logger } from '@server/helpers/logger'
5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' 4import { optionalAuthenticate } from '@server/middlewares/auth'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
8import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' 5import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
9import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
10import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
11import { logger } from '@server/helpers/logger' 9import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
12import { optionalAuthenticate } from '@server/middlewares/oauth' 10import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
11import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
12import { serveThemeCSSValidator } from '../middlewares/validators/themes'
13 13
14const sendFileOptions = { 14const sendFileOptions = {
15 maxAge: '30 days', 15 maxAge: '30 days',
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 08aef2908..e0754b501 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -3,7 +3,6 @@ import { URL } from 'url'
3import validator from 'validator' 3import validator from 'validator'
4import { ContextType } from '@shared/models/activitypub/context' 4import { ContextType } from '@shared/models/activitypub/context'
5import { ResultList } from '../../shared/models' 5import { ResultList } from '../../shared/models'
6import { Activity } from '../../shared/models/activitypub'
7import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' 6import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
8import { MActor, MVideoWithHost } from '../types/models' 7import { MActor, MVideoWithHost } from '../types/models'
9import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination (
182 181
183} 182}
184 183
185function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { 184function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) {
186 const activity = activityPubContextify(data, contextType) 185 const activity = activityPubContextify(data, contextType)
187 186
188 return signJsonLDObject(byActor, activity) as Promise<Activity> 187 return signJsonLDObject(byActor, activity)
189} 188}
190 189
191function getAPId (activity: string | { id: string }) { 190function getAPId (activity: string | { id: string }) {
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 935fd22d9..0bd84ffaa 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import { basename, isAbsolute, join, resolve } from 'path' 11import { basename, isAbsolute, join, resolve } from 'path'
12import * as pem from 'pem' 12import * as pem from 'pem'
13import { pipeline } from 'stream'
13import { URL } from 'url' 14import { URL } from 'url'
15import { promisify } from 'util'
14 16
15const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { 17const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
16 if (!oldObject || typeof oldObject !== 'object') { 18 if (!oldObject || typeof oldObject !== 'object') {
@@ -249,11 +251,23 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
249 } 251 }
250} 252}
251 253
254type SemVersion = { major: number, minor: number, patch: number }
255function parseSemVersion (s: string) {
256 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
257
258 return {
259 major: parseInt(parsed[1]),
260 minor: parseInt(parsed[2]),
261 patch: parseInt(parsed[3])
262 } as SemVersion
263}
264
252const randomBytesPromise = promisify1<number, Buffer>(randomBytes) 265const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
253const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) 266const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
254const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) 267const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
255const execPromise2 = promisify2<string, any, string>(exec) 268const execPromise2 = promisify2<string, any, string>(exec)
256const execPromise = promisify1<string, string>(exec) 269const execPromise = promisify1<string, string>(exec)
270const pipelinePromise = promisify(pipeline)
257 271
258// --------------------------------------------------------------------------- 272// ---------------------------------------------------------------------------
259 273
@@ -284,5 +298,8 @@ export {
284 createPrivateKey, 298 createPrivateKey,
285 getPublicKey, 299 getPublicKey,
286 execPromise2, 300 execPromise2,
287 execPromise 301 execPromise,
302 pipelinePromise,
303
304 parseSemVersion
288} 305}
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index da79b2782..b5c96f6e7 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,16 +1,13 @@
1import validator from 'validator' 1import validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { isAbuseReasonValid } from '../abuses'
3import { exists } from '../misc' 4import { exists } from '../misc'
4import { sanitizeAndCheckActorObject } from './actor' 5import { sanitizeAndCheckActorObject } from './actor'
5import { isCacheFileObjectValid } from './cache-file' 6import { isCacheFileObjectValid } from './cache-file'
6import { isFlagActivityValid } from './flag'
7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' 7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
8import { isPlaylistObjectValid } from './playlist' 8import { isPlaylistObjectValid } from './playlist'
9import { isDislikeActivityValid, isLikeActivityValid } from './rate'
10import { isShareActivityValid } from './share'
11import { sanitizeAndCheckVideoCommentObject } from './video-comments' 9import { sanitizeAndCheckVideoCommentObject } from './video-comments'
12import { sanitizeAndCheckVideoTorrentObject } from './videos' 10import { sanitizeAndCheckVideoTorrentObject } from './videos'
13import { isViewActivityValid } from './view'
14 11
15function isRootActivityValid (activity: any) { 12function isRootActivityValid (activity: any) {
16 return isCollection(activity) || isActivity(activity) 13 return isCollection(activity) || isActivity(activity)
@@ -29,18 +26,18 @@ function isActivity (activity: any) {
29} 26}
30 27
31const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { 28const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
32 Create: checkCreateActivity, 29 Create: isCreateActivityValid,
33 Update: checkUpdateActivity, 30 Update: isUpdateActivityValid,
34 Delete: checkDeleteActivity, 31 Delete: isDeleteActivityValid,
35 Follow: checkFollowActivity, 32 Follow: isFollowActivityValid,
36 Accept: checkAcceptActivity, 33 Accept: isAcceptActivityValid,
37 Reject: checkRejectActivity, 34 Reject: isRejectActivityValid,
38 Announce: checkAnnounceActivity, 35 Announce: isAnnounceActivityValid,
39 Undo: checkUndoActivity, 36 Undo: isUndoActivityValid,
40 Like: checkLikeActivity, 37 Like: isLikeActivityValid,
41 View: checkViewActivity, 38 View: isViewActivityValid,
42 Flag: checkFlagActivity, 39 Flag: isFlagActivityValid,
43 Dislike: checkDislikeActivity 40 Dislike: isDislikeActivityValid
44} 41}
45 42
46function isActivityValid (activity: any) { 43function isActivityValid (activity: any) {
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) {
51 return checker(activity) 48 return checker(activity)
52} 49}
53 50
54// --------------------------------------------------------------------------- 51function isFlagActivityValid (activity: any) {
55 52 return isBaseActivityValid(activity, 'Flag') &&
56export { 53 isAbuseReasonValid(activity.content) &&
57 isRootActivityValid, 54 isActivityPubUrlValid(activity.object)
58 isActivityValid
59} 55}
60 56
61// --------------------------------------------------------------------------- 57function isLikeActivityValid (activity: any) {
62 58 return isBaseActivityValid(activity, 'Like') &&
63function checkViewActivity (activity: any) { 59 isObjectValid(activity.object)
64 return isBaseActivityValid(activity, 'View') &&
65 isViewActivityValid(activity)
66} 60}
67 61
68function checkFlagActivity (activity: any) { 62function isDislikeActivityValid (activity: any) {
69 return isBaseActivityValid(activity, 'Flag') && 63 return isBaseActivityValid(activity, 'Dislike') &&
70 isFlagActivityValid(activity) 64 isObjectValid(activity.object)
71} 65}
72 66
73function checkDislikeActivity (activity: any) { 67function isAnnounceActivityValid (activity: any) {
74 return isDislikeActivityValid(activity) 68 return isBaseActivityValid(activity, 'Announce') &&
69 isObjectValid(activity.object)
75} 70}
76 71
77function checkLikeActivity (activity: any) { 72function isViewActivityValid (activity: any) {
78 return isLikeActivityValid(activity) 73 return isBaseActivityValid(activity, 'View') &&
74 isActivityPubUrlValid(activity.actor) &&
75 isActivityPubUrlValid(activity.object)
79} 76}
80 77
81function checkCreateActivity (activity: any) { 78function isCreateActivityValid (activity: any) {
82 return isBaseActivityValid(activity, 'Create') && 79 return isBaseActivityValid(activity, 'Create') &&
83 ( 80 (
84 isViewActivityValid(activity.object) || 81 isViewActivityValid(activity.object) ||
@@ -92,7 +89,7 @@ function checkCreateActivity (activity: any) {
92 ) 89 )
93} 90}
94 91
95function checkUpdateActivity (activity: any) { 92function isUpdateActivityValid (activity: any) {
96 return isBaseActivityValid(activity, 'Update') && 93 return isBaseActivityValid(activity, 'Update') &&
97 ( 94 (
98 isCacheFileObjectValid(activity.object) || 95 isCacheFileObjectValid(activity.object) ||
@@ -102,36 +99,51 @@ function checkUpdateActivity (activity: any) {
102 ) 99 )
103} 100}
104 101
105function checkDeleteActivity (activity: any) { 102function isDeleteActivityValid (activity: any) {
106 // We don't really check objects 103 // We don't really check objects
107 return isBaseActivityValid(activity, 'Delete') && 104 return isBaseActivityValid(activity, 'Delete') &&
108 isObjectValid(activity.object) 105 isObjectValid(activity.object)
109} 106}
110 107
111function checkFollowActivity (activity: any) { 108function isFollowActivityValid (activity: any) {
112 return isBaseActivityValid(activity, 'Follow') && 109 return isBaseActivityValid(activity, 'Follow') &&
113 isObjectValid(activity.object) 110 isObjectValid(activity.object)
114} 111}
115 112
116function checkAcceptActivity (activity: any) { 113function isAcceptActivityValid (activity: any) {
117 return isBaseActivityValid(activity, 'Accept') 114 return isBaseActivityValid(activity, 'Accept')
118} 115}
119 116
120function checkRejectActivity (activity: any) { 117function isRejectActivityValid (activity: any) {
121 return isBaseActivityValid(activity, 'Reject') 118 return isBaseActivityValid(activity, 'Reject')
122} 119}
123 120
124function checkAnnounceActivity (activity: any) { 121function isUndoActivityValid (activity: any) {
125 return isShareActivityValid(activity)
126}
127
128function checkUndoActivity (activity: any) {
129 return isBaseActivityValid(activity, 'Undo') && 122 return isBaseActivityValid(activity, 'Undo') &&
130 ( 123 (
131 checkFollowActivity(activity.object) || 124 isFollowActivityValid(activity.object) ||
132 checkLikeActivity(activity.object) || 125 isLikeActivityValid(activity.object) ||
133 checkDislikeActivity(activity.object) || 126 isDislikeActivityValid(activity.object) ||
134 checkAnnounceActivity(activity.object) || 127 isAnnounceActivityValid(activity.object) ||
135 checkCreateActivity(activity.object) 128 isCreateActivityValid(activity.object)
136 ) 129 )
137} 130}
131
132// ---------------------------------------------------------------------------
133
134export {
135 isRootActivityValid,
136 isActivityValid,
137 isFlagActivityValid,
138 isLikeActivityValid,
139 isDislikeActivityValid,
140 isAnnounceActivityValid,
141 isViewActivityValid,
142 isCreateActivityValid,
143 isUpdateActivityValid,
144 isDeleteActivityValid,
145 isFollowActivityValid,
146 isAcceptActivityValid,
147 isRejectActivityValid,
148 isUndoActivityValid
149}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
deleted file mode 100644
index dc90b3667..000000000
--- a/server/helpers/custom-validators/activitypub/flag.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2import { isAbuseReasonValid } from '../abuses'
3
4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' &&
6 isAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isFlagActivityValid
14}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
deleted file mode 100644
index aafdda443..000000000
--- a/server/helpers/custom-validators/activitypub/rate.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isObjectValid(activity.object)
6}
7
8function isDislikeActivityValid (activity: any) {
9 return isBaseActivityValid(activity, 'Dislike') &&
10 isObjectValid(activity.object)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 isDislikeActivityValid,
17 isLikeActivityValid
18}
diff --git a/server/helpers/custom-validators/activitypub/share.ts b/server/helpers/custom-validators/activitypub/share.ts
deleted file mode 100644
index fb5e4c05e..000000000
--- a/server/helpers/custom-validators/activitypub/share.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isShareActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Announce') &&
5 isObjectValid(activity.object)
6}
7// ---------------------------------------------------------------------------
8
9export {
10 isShareActivityValid
11}
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts
deleted file mode 100644
index 41d16469f..000000000
--- a/server/helpers/custom-validators/activitypub/view.ts
+++ /dev/null
@@ -1,13 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2
3function isViewActivityValid (activity: any) {
4 return activity.type === 'View' &&
5 isActivityPubUrlValid(activity.actor) &&
6 isActivityPubUrlValid(activity.object)
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 isViewActivityValid
13}
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts
index 8a33b895b..252c107db 100644
--- a/server/helpers/custom-validators/user-notifications.ts
+++ b/server/helpers/custom-validators/user-notifications.ts
@@ -1,10 +1,9 @@
1import { exists } from './misc'
2import validator from 'validator' 1import validator from 'validator'
3import { UserNotificationType } from '../../../shared/models/users'
4import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 2import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
3import { exists } from './misc'
5 4
6function isUserNotificationTypeValid (value: any) { 5function isUserNotificationTypeValid (value: any) {
7 return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined 6 return exists(value) && validator.isInt('' + value)
8} 7}
9 8
10function isUserNotificationSettingValid (value: any) { 9function isUserNotificationSettingValid (value: any) {
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 620025966..69cd397b9 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -5,7 +5,7 @@ import { dirname, join } from 'path'
5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' 5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' 6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { promisify0 } from './core-utils' 8import { execPromise, promisify0 } from './core-utils'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
@@ -649,6 +649,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') {
649 return command 649 return command
650} 650}
651 651
652function getFFmpegVersion () {
653 return new Promise<string>((res, rej) => {
654 (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {
655 if (err) return rej(err)
656 if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path'))
657
658 return execPromise(`${ffmpegPath} -version`)
659 .then(stdout => {
660 const parsed = stdout.match(/ffmpeg version .(\d+\.\d+\.\d+)/)
661 if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`))
662
663 return res(parsed[1])
664 })
665 .catch(err => rej(err))
666 })
667 })
668}
669
652async function runCommand (options: { 670async function runCommand (options: {
653 command: ffmpeg.FfmpegCommand 671 command: ffmpeg.FfmpegCommand
654 silent?: boolean // false 672 silent?: boolean // false
@@ -695,6 +713,7 @@ export {
695 TranscodeOptionsType, 713 TranscodeOptionsType,
696 transcode, 714 transcode,
697 runCommand, 715 runCommand,
716 getFFmpegVersion,
698 717
699 resetSupportedEncoders, 718 resetSupportedEncoders,
700 719
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 6917a64d9..a112fd300 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -48,7 +48,7 @@ function getLoggerReplacer () {
48} 48}
49 49
50const consoleLoggerFormat = winston.format.printf(info => { 50const consoleLoggerFormat = winston.format.printf(info => {
51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql' ] 51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ]
52 52
53 const obj = omit(info, ...toOmit) 53 const obj = omit(info, ...toOmit)
54 54
@@ -150,6 +150,13 @@ const bunyanLogger = {
150 error: bunyanLogFactory('error'), 150 error: bunyanLogFactory('error'),
151 fatal: bunyanLogFactory('error') 151 fatal: bunyanLogFactory('error')
152} 152}
153
154function loggerTagsFactory (...defaultTags: string[]) {
155 return (...tags: string[]) => {
156 return { tags: defaultTags.concat(tags) }
157 }
158}
159
153// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
154 161
155export { 162export {
@@ -159,5 +166,6 @@ export {
159 consoleLoggerFormat, 166 consoleLoggerFormat,
160 jsonLoggerFormat, 167 jsonLoggerFormat,
161 logger, 168 logger,
169 loggerTagsFactory,
162 bunyanLogger 170 bunyanLogger
163} 171}
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 994f725d8..bc6f1d074 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -84,7 +84,7 @@ async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any)
84 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') 84 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
85} 85}
86 86
87async function signJsonLDObject (byActor: MActor, data: any) { 87async function signJsonLDObject <T> (byActor: MActor, data: T) {
88 const signature = { 88 const signature = {
89 type: 'RsaSignature2017', 89 type: 'RsaSignature2017',
90 creator: byActor.url, 90 creator: byActor.url,
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index b556c392e..fd2a56f30 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,58 +1,141 @@
1import * as Bluebird from 'bluebird'
2import { createWriteStream, remove } from 'fs-extra' 1import { createWriteStream, remove } from 'fs-extra'
3import * as request from 'request' 2import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got'
3import { join } from 'path'
4import { CONFIG } from '../initializers/config'
4import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' 5import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants'
6import { pipelinePromise } from './core-utils'
5import { processImage } from './image-utils' 7import { processImage } from './image-utils'
6import { join } from 'path'
7import { logger } from './logger' 8import { logger } from './logger'
8import { CONFIG } from '../initializers/config'
9 9
10function doRequest <T> ( 10export interface PeerTubeRequestError extends Error {
11 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }, 11 statusCode?: number
12 bodyKBLimit = 1000 // 1MB 12 responseBody?: any
13): Bluebird<{ response: request.RequestResponse, body: T }> { 13}
14 if (!(requestOptions.headers)) requestOptions.headers = {}
15 requestOptions.headers['User-Agent'] = getUserAgent()
16 14
17 if (requestOptions.activityPub === true) { 15const httpSignature = require('http-signature')
18 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 16
17type PeerTubeRequestOptions = {
18 activityPub?: boolean
19 bodyKBLimit?: number // 1MB
20 httpSignature?: {
21 algorithm: string
22 authorizationHeaderName: string
23 keyId: string
24 key: string
25 headers: string[]
19 } 26 }
27 jsonResponse?: boolean
28} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
29
30const peertubeGot = got.extend({
31 headers: {
32 'user-agent': getUserAgent()
33 },
34
35 handlers: [
36 (options, next) => {
37 const promiseOrStream = next(options) as CancelableRequest<any>
38 const bodyKBLimit = options.context?.bodyKBLimit as number
39 if (!bodyKBLimit) throw new Error('No KB limit for this request')
40
41 const bodyLimit = bodyKBLimit * 1000
42
43 /* eslint-disable @typescript-eslint/no-floating-promises */
44 promiseOrStream.on('downloadProgress', progress => {
45 if (progress.transferred > bodyLimit && progress.percent !== 1) {
46 const message = `Exceeded the download limit of ${bodyLimit} B`
47 logger.warn(message)
48
49 // CancelableRequest
50 if (promiseOrStream.cancel) {
51 promiseOrStream.cancel()
52 return
53 }
54
55 // Stream
56 (promiseOrStream as any).destroy()
57 }
58 })
20 59
21 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { 60 return promiseOrStream
22 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 61 }
23 .on('data', onRequestDataLengthCheck(bodyKBLimit)) 62 ],
24 }) 63
64 hooks: {
65 beforeRequest: [
66 options => {
67 const headers = options.headers || {}
68 headers['host'] = options.url.host
69 },
70
71 options => {
72 const httpSignatureOptions = options.context?.httpSignature
73
74 if (httpSignatureOptions) {
75 const method = options.method ?? 'GET'
76 const path = options.path ?? options.url.pathname
77
78 if (!method || !path) {
79 throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
80 }
81
82 httpSignature.signRequest({
83 getHeader: function (header) {
84 return options.headers[header]
85 },
86
87 setHeader: function (header, value) {
88 options.headers[header] = value
89 },
90
91 method,
92 path
93 }, httpSignatureOptions)
94 }
95 }
96 ]
97 }
98})
99
100function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
101 const gotOptions = buildGotOptions(options)
102
103 return peertubeGot(url, gotOptions)
104 .catch(err => { throw buildRequestError(err) })
25} 105}
26 106
27function doRequestAndSaveToFile ( 107function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
28 requestOptions: request.CoreOptions & request.UriOptions, 108 const gotOptions = buildGotOptions(options)
109
110 return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
111 .catch(err => { throw buildRequestError(err) })
112}
113
114async function doRequestAndSaveToFile (
115 url: string,
29 destPath: string, 116 destPath: string,
30 bodyKBLimit = 10000 // 10MB 117 options: PeerTubeRequestOptions = {}
31) { 118) {
32 if (!requestOptions.headers) requestOptions.headers = {} 119 const gotOptions = buildGotOptions(options)
33 requestOptions.headers['User-Agent'] = getUserAgent()
34
35 return new Bluebird<void>((res, rej) => {
36 const file = createWriteStream(destPath)
37 file.on('finish', () => res())
38 120
39 request(requestOptions) 121 const outFile = createWriteStream(destPath)
40 .on('data', onRequestDataLengthCheck(bodyKBLimit))
41 .on('error', err => {
42 file.close()
43 122
44 remove(destPath) 123 try {
45 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) 124 await pipelinePromise(
125 peertubeGot.stream(url, gotOptions),
126 outFile
127 )
128 } catch (err) {
129 remove(destPath)
130 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
46 131
47 return rej(err) 132 throw buildRequestError(err)
48 }) 133 }
49 .pipe(file)
50 })
51} 134}
52 135
53async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { 136async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
54 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) 137 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
55 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 138 await doRequestAndSaveToFile(url, tmpPath)
56 139
57 const destPath = join(destDir, destName) 140 const destPath = join(destDir, destName)
58 141
@@ -73,24 +156,46 @@ function getUserAgent () {
73 156
74export { 157export {
75 doRequest, 158 doRequest,
159 doJSONRequest,
76 doRequestAndSaveToFile, 160 doRequestAndSaveToFile,
77 downloadImage 161 downloadImage
78} 162}
79 163
80// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
81 165
82// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3 166function buildGotOptions (options: PeerTubeRequestOptions) {
83function onRequestDataLengthCheck (bodyKBLimit: number) { 167 const { activityPub, bodyKBLimit = 1000 } = options
84 let bufferLength = 0
85 const bytesLimit = bodyKBLimit * 1000
86 168
87 return function (chunk) { 169 const context = { bodyKBLimit, httpSignature: options.httpSignature }
88 bufferLength += chunk.length
89 if (bufferLength > bytesLimit) {
90 this.abort()
91 170
92 const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`) 171 let headers = options.headers || {}
93 this.emit('error', error) 172
94 } 173 if (!headers.date) {
174 headers = { ...headers, date: new Date().toUTCString() }
175 }
176
177 if (activityPub && !headers.accept) {
178 headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
95 } 179 }
180
181 return {
182 method: options.method,
183 json: options.json,
184 searchParams: options.searchParams,
185 headers,
186 context
187 }
188}
189
190function buildRequestError (error: RequestError) {
191 const newError: PeerTubeRequestError = new Error(error.message)
192 newError.name = error.name
193 newError.stack = error.stack
194
195 if (error.response) {
196 newError.responseBody = error.response.body
197 newError.statusCode = error.response.statusCode
198 }
199
200 return newError
96} 201}
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 8537a5772..9d2e54fb5 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,13 +1,13 @@
1import { createWriteStream } from 'fs' 1import { createWriteStream } from 'fs'
2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' 2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
3import got from 'got'
3import { join } from 'path' 4import { join } from 'path'
4import * as request from 'request'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoResolution } from '../../shared/models/videos' 7import { VideoResolution } from '../../shared/models/videos'
8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' 8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
9import { getEnabledResolutions } from '../lib/video-transcoding' 9import { getEnabledResolutions } from '../lib/video-transcoding'
10import { peertubeTruncate, root } from './core-utils' 10import { peertubeTruncate, pipelinePromise, root } from './core-utils'
11import { isVideoFileExtnameValid } from './custom-validators/videos' 11import { isVideoFileExtnameValid } from './custom-validators/videos'
12import { logger } from './logger' 12import { logger } from './logger'
13import { generateVideoImportTmpPath } from './utils' 13import { generateVideoImportTmpPath } from './utils'
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () {
195 195
196 await ensureDir(binDirectory) 196 await ensureDir(binDirectory)
197 197
198 return new Promise<void>(res => { 198 try {
199 request.get(url, { followRedirect: false }, (err, result) => { 199 const result = await got(url, { followRedirect: false })
200 if (err) {
201 logger.error('Cannot update youtube-dl.', { err })
202 return res()
203 }
204
205 if (result.statusCode !== HttpStatusCode.FOUND_302) {
206 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
207 return res()
208 }
209
210 const url = result.headers.location
211 const downloadFile = request.get(url)
212 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
213
214 downloadFile.on('response', result => {
215 if (result.statusCode !== HttpStatusCode.OK_200) {
216 logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode)
217 return res()
218 }
219
220 const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => {
221 logger.error('youtube-dl update error in write stream', { err })
222 return res()
223 })
224 200
225 downloadFile.pipe(writeStream) 201 if (result.statusCode !== HttpStatusCode.FOUND_302) {
226 }) 202 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
203 return
204 }
227 205
228 downloadFile.on('error', err => { 206 const newUrl = result.headers.location
229 logger.error('youtube-dl update error.', { err }) 207 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
230 return res()
231 })
232 208
233 downloadFile.on('end', () => { 209 const downloadFileStream = got.stream(newUrl)
234 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) 210 const writeStream = createWriteStream(bin, { mode: 493 })
235 writeFile(detailsPath, details, { encoding: 'utf8' }, err => {
236 if (err) {
237 logger.error('youtube-dl update error: cannot write details.', { err })
238 return res()
239 }
240 211
241 logger.info('youtube-dl updated to version %s.', newVersion) 212 await pipelinePromise(
242 return res() 213 downloadFileStream,
243 }) 214 writeStream
244 }) 215 )
245 }) 216
246 }) 217 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
218 await writeFile(detailsPath, details, { encoding: 'utf8' })
219
220 logger.info('youtube-dl updated to version %s.', newVersion)
221 } catch (err) {
222 logger.error('Cannot update youtube-dl.', { err })
223 }
247} 224}
248 225
249async function safeGetYoutubeDL () { 226async function safeGetYoutubeDL () {
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 2b00e2047..a93c8b7fd 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -1,16 +1,17 @@
1import * as config from 'config' 1import * as config from 'config'
2import { isProdInstance, isTestInstance } from '../helpers/core-utils' 2import { uniq } from 'lodash'
3import { UserModel } from '../models/account/user'
4import { getServerActor, ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { URL } from 'url' 3import { URL } from 'url'
7import { CONFIG, isEmailEnabled } from './config' 4import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils'
8import { logger } from '../helpers/logger' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { RecentlyAddedStrategy } from '../../shared/models/redundancy' 6import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
7import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
10import { isArray } from '../helpers/custom-validators/misc' 8import { isArray } from '../helpers/custom-validators/misc'
11import { uniq } from 'lodash' 9import { logger } from '../helpers/logger'
10import { UserModel } from '../models/account/user'
11import { ApplicationModel, getServerActor } from '../models/application/application'
12import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { CONFIG, isEmailEnabled } from './config'
12import { WEBSERVER } from './constants' 14import { WEBSERVER } from './constants'
13import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
14 15
15async function checkActivityPubUrls () { 16async function checkActivityPubUrls () {
16 const actor = await getServerActor() 17 const actor = await getServerActor()
@@ -176,11 +177,21 @@ async function applicationExist () {
176 return totalApplication !== 0 177 return totalApplication !== 0
177} 178}
178 179
180async function checkFFmpegVersion () {
181 const version = await getFFmpegVersion()
182 const { major, minor } = parseSemVersion(version)
183
184 if (major < 4 || (major === 4 && minor < 1)) {
185 logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade.', version)
186 }
187}
188
179// --------------------------------------------------------------------------- 189// ---------------------------------------------------------------------------
180 190
181export { 191export {
182 checkConfig, 192 checkConfig,
183 clientsExist, 193 clientsExist,
194 checkFFmpegVersion,
184 usersExist, 195 usersExist,
185 applicationExist, 196 applicationExist,
186 checkActivityPubUrls 197 checkActivityPubUrls
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 565e0d1fa..e92cc4d2c 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -1,5 +1,5 @@
1import * as config from 'config' 1import * as config from 'config'
2import { promisify0 } from '../helpers/core-utils' 2import { parseSemVersion, promisify0 } from '../helpers/core-utils'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4 4
5// ONLY USE CORE MODULES IN THIS FILE! 5// ONLY USE CORE MODULES IN THIS FILE!
@@ -37,6 +37,7 @@ function checkMissedConfig () {
37 'theme.default', 37 'theme.default',
38 'remote_redundancy.videos.accept_from', 38 'remote_redundancy.videos.accept_from',
39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
40 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
40 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', 41 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
41 'search.search_index.disable_local_search', 'search.search_index.is_default_search', 42 'search.search_index.disable_local_search', 'search.search_index.is_default_search',
42 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', 43 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
@@ -102,8 +103,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
102 103
103function checkNodeVersion () { 104function checkNodeVersion () {
104 const v = process.version 105 const v = process.version
105 const majorString = v.split('.')[0].replace('v', '') 106 const { major } = parseSemVersion(v)
106 const major = parseInt(majorString, 10)
107 107
108 logger.debug('Checking NodeJS version %s.', v) 108 logger.debug('Checking NodeJS version %s.', v)
109 109
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index c16b63c33..48e7f7397 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -163,6 +163,12 @@ const CONFIG = {
163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') 163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
164 } 164 }
165 }, 165 },
166 PEERTUBE: {
167 CHECK_LATEST_VERSION: {
168 ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
169 URL: config.get<string>('peertube.check_latest_version.url')
170 }
171 },
166 ADMIN: { 172 ADMIN: {
167 get EMAIL () { return config.get<string>('admin.email') } 173 get EMAIL () { return config.get<string>('admin.email') }
168 }, 174 },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 1623e6f42..b37aeb622 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,12 +24,12 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 610 27const LAST_MIGRATION_VERSION = 625
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31const API_VERSION = 'v1' 31const API_VERSION = 'v1'
32const PEERTUBE_VERSION = require(join(root(), 'package.json')).version 32const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version
33 33
34const PAGINATION = { 34const PAGINATION = {
35 GLOBAL: { 35 GLOBAL: {
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
207 updateVideos: 60000, // 1 minute 207 updateVideos: 60000, // 1 minute
208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day 208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, 209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
210 checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
210 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day 211 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
211 removeOldViews: 60000 * 60 * 24, // 1 day 212 removeOldViews: 60000 * 60 * 24, // 1 day
212 removeOldHistory: 60000 * 60 * 24, // 1 day 213 removeOldHistory: 60000 * 60 * 24, // 1 day
@@ -763,6 +764,7 @@ if (isTestInstance() === true) {
763 SCHEDULER_INTERVALS_MS.updateVideos = 5000 764 SCHEDULER_INTERVALS_MS.updateVideos = 5000
764 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 765 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
765 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 766 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
767 SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
766 REPEAT_JOBS['videos-views'] = { every: 5000 } 768 REPEAT_JOBS['videos-views'] = { every: 5000 }
767 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } 769 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
768 770
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 1f2b6d521..8378fa982 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -76,7 +76,7 @@ const sequelizeTypescript = new SequelizeTypescript({
76 newMessage += ' in ' + benchmark + 'ms' 76 newMessage += ' in ' + benchmark + 'ms'
77 } 77 }
78 78
79 logger.debug(newMessage, { sql: message }) 79 logger.debug(newMessage, { sql: message, tags: [ 'sql' ] })
80 } 80 }
81}) 81})
82 82
diff --git a/server/initializers/migrations/0610-views-index.ts b/server/initializers/migrations/0610-views-index copy.ts
index 02ee21172..02ee21172 100644
--- a/server/initializers/migrations/0610-views-index.ts
+++ b/server/initializers/migrations/0610-views-index copy.ts
diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
new file mode 100644
index 000000000..86bf56009
--- /dev/null
+++ b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
@@ -0,0 +1,44 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
11
12 for (const column of notificationSettingColumns) {
13 const data = {
14 type: Sequelize.INTEGER,
15 defaultValue: null,
16 allowNull: true
17 }
18 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
19 }
20
21 {
22 const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
23 await utils.sequelize.query(query)
24 }
25
26 for (const column of notificationSettingColumns) {
27 const data = {
28 type: Sequelize.INTEGER,
29 defaultValue: null,
30 allowNull: false
31 }
32 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
33 }
34 }
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
new file mode 100644
index 000000000..a689b18fc
--- /dev/null
+++ b/server/initializers/migrations/0620-latest-versions-application.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
new file mode 100644
index 000000000..77f395ce4
--- /dev/null
+++ b/server/initializers/migrations/0625-latest-versions-notification.ts
@@ -0,0 +1,26 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`
12 ALTER TABLE "userNotification"
13 ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
15 `)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index a726f9e20..3c9a7ba02 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,26 +1,28 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
2import { Op, Transaction } from 'sequelize' 3import { Op, Transaction } from 'sequelize'
3import { URL } from 'url' 4import { URL } from 'url'
4import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 8import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 9import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 12import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 13import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 14import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 15import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 16import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest } from '../../helpers/requests' 17import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 18import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 19import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
20import { sequelizeTypescript } from '../../initializers/database'
16import { AccountModel } from '../../models/account/account' 21import { AccountModel } from '../../models/account/account'
17import { ActorModel } from '../../models/activitypub/actor' 22import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar' 23import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 24import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 25import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue'
22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23import { sequelizeTypescript } from '../../initializers/database'
24import { 26import {
25 MAccount, 27 MAccount,
26 MAccountDefault, 28 MAccountDefault,
@@ -34,9 +36,7 @@ import {
34 MActorId, 36 MActorId,
35 MChannel 37 MChannel
36} from '../../types/models' 38} from '../../types/models'
37import { extname } from 'path' 39import { JobQueue } from '../job-queue'
38import { getServerActor } from '@server/models/application/application'
39import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
40 40
41// Set account keys, this could be long so process after the account creation and do not block the client 41// Set account keys, this could be long so process after the account creation and do not block the client
42async function generateAndSaveActorKeys <T extends MActor> (actor: T) { 42async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -209,16 +209,10 @@ async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction)
209} 209}
210 210
211async function fetchActorTotalItems (url: string) { 211async function fetchActorTotalItems (url: string) {
212 const options = {
213 uri: url,
214 method: 'GET',
215 json: true,
216 activityPub: true
217 }
218
219 try { 212 try {
220 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) 213 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
221 return body.totalItems ? body.totalItems : 0 214
215 return body.totalItems || 0
222 } catch (err) { 216 } catch (err) {
223 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 217 logger.warn('Cannot fetch remote actor count %s.', url, { err })
224 return 0 218 return 0
@@ -285,16 +279,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
285 actorUrl = actor.url 279 actorUrl = actor.url
286 } 280 }
287 281
288 const { result, statusCode } = await fetchRemoteActor(actorUrl) 282 const { result } = await fetchRemoteActor(actorUrl)
289
290 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
291 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
292 actor.Account
293 ? await actor.Account.destroy()
294 : await actor.VideoChannel.destroy()
295
296 return { actor: undefined, refreshed: false }
297 }
298 283
299 if (result === undefined) { 284 if (result === undefined) {
300 logger.warn('Cannot fetch remote actor in refresh actor.') 285 logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -334,6 +319,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
334 return { refreshed: true, actor } 319 return { refreshed: true, actor }
335 }) 320 })
336 } catch (err) { 321 } catch (err) {
322 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
323 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
324 actor.Account
325 ? await actor.Account.destroy()
326 : await actor.VideoChannel.destroy()
327
328 return { actor: undefined, refreshed: false }
329 }
330
337 logger.warn('Cannot refresh actor %s.', actor.url, { err }) 331 logger.warn('Cannot refresh actor %s.', actor.url, { err })
338 return { actor, refreshed: false } 332 return { actor, refreshed: false }
339 } 333 }
@@ -449,26 +443,19 @@ type FetchRemoteActorResult = {
449 attributedTo: ActivityPubAttributedTo[] 443 attributedTo: ActivityPubAttributedTo[]
450} 444}
451async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 445async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
452 const options = {
453 uri: actorUrl,
454 method: 'GET',
455 json: true,
456 activityPub: true
457 }
458
459 logger.info('Fetching remote actor %s.', actorUrl) 446 logger.info('Fetching remote actor %s.', actorUrl)
460 447
461 const requestResult = await doRequest<ActivityPubActor>(options) 448 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
462 const actorJSON = requestResult.body 449 const actorJSON = requestResult.body
463 450
464 if (sanitizeAndCheckActorObject(actorJSON) === false) { 451 if (sanitizeAndCheckActorObject(actorJSON) === false) {
465 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 452 logger.debug('Remote actor JSON is not valid.', { actorJSON })
466 return { result: undefined, statusCode: requestResult.response.statusCode } 453 return { result: undefined, statusCode: requestResult.statusCode }
467 } 454 }
468 455
469 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 456 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
470 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) 457 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
471 return { result: undefined, statusCode: requestResult.response.statusCode } 458 return { result: undefined, statusCode: requestResult.statusCode }
472 } 459 }
473 460
474 const followersCount = await fetchActorTotalItems(actorJSON.followers) 461 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -496,7 +483,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
496 483
497 const name = actorJSON.name || actorJSON.preferredUsername 484 const name = actorJSON.name || actorJSON.preferredUsername
498 return { 485 return {
499 statusCode: requestResult.response.statusCode, 486 statusCode: requestResult.statusCode,
500 result: { 487 result: {
501 actor, 488 actor,
502 name, 489 name,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1ed105bbe..278abf7de 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,27 +1,26 @@
1import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { URL } from 'url' 2import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
4import { logger } from '../../helpers/logger'
5import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
10 10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { 11async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
12 logger.info('Crawling ActivityPub data on %s.', uri) 12 let url = argUrl
13
14 logger.info('Crawling ActivityPub data on %s.', url)
13 15
14 const options = { 16 const options = {
15 method: 'GET',
16 uri,
17 json: true,
18 activityPub: true, 17 activityPub: true,
19 timeout: REQUEST_TIMEOUT 18 timeout: REQUEST_TIMEOUT
20 } 19 }
21 20
22 const startDate = new Date() 21 const startDate = new Date()
23 22
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 23 const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
25 const firstBody = response.body 24 const firstBody = response.body
26 25
27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 26 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
35 const remoteHost = new URL(nextLink).host 34 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 35 if (remoteHost === WEBSERVER.HOST) continue
37 36
38 options.uri = nextLink 37 url = nextLink
39 38
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options) 39 const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
41 body = res.body 40 body = res.body
42 } else { 41 } else {
43 // nextLink is already the object we want 42 // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
49 48
50 if (Array.isArray(body.orderedItems)) { 49 if (Array.isArray(body.orderedItems)) {
51 const items = body.orderedItems 50 const items = body.orderedItems
52 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) 51 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
53 52
54 await handler(items) 53 await handler(items)
55 } 54 }
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index d5a3ef7c8..7166c68a6 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,24 +1,24 @@
1import * as Bluebird from 'bluebird'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 5import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { checkUrlsSameHost } from '../../helpers/activitypub'
7import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
4import { isArray } from '../../helpers/custom-validators/misc' 8import { isArray } from '../../helpers/custom-validators/misc'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
11import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { sequelizeTypescript } from '../../initializers/database'
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 13import { VideoPlaylistModel } from '../../models/video/video-playlist'
8import { doRequest } from '../../helpers/requests'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import * as Bluebird from 'bluebird'
11import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
12import { getOrCreateVideoAndAccountAndChannel } from './videos'
13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
16import { sequelizeTypescript } from '../../initializers/database'
17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
18import { FilteredModelAttributes } from '../../types/sequelize'
19import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' 15import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' 16import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
21import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 17import { FilteredModelAttributes } from '../../types/sequelize'
18import { createPlaylistMiniatureFromUrl } from '../thumbnail'
19import { getOrCreateActorAndServerAndModel } from './actor'
20import { crawlCollectionPage } from './crawl'
21import { getOrCreateVideoAndAccountAndChannel } from './videos'
22 22
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
56 if (exists === true) return 56 if (exists === true) return
57 57
58 // Fetch url 58 // Fetch url
59 const { body } = await doRequest<PlaylistObject>({ 59 const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
60 uri: playlistUrl,
61 json: true,
62 activityPub: true
63 })
64 60
65 if (!isPlaylistObjectValid(body)) { 61 if (!isPlaylistObjectValid(body)) {
66 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) 62 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
120 if (!videoPlaylist.isOutdated()) return videoPlaylist 116 if (!videoPlaylist.isOutdated()) return videoPlaylist
121 117
122 try { 118 try {
123 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) 119 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
124 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
125 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
126
127 await videoPlaylist.destroy()
128 return undefined
129 }
130 120
131 if (playlistObject === undefined) { 121 if (playlistObject === undefined) {
132 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) 122 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
140 130
141 return videoPlaylist 131 return videoPlaylist
142 } catch (err) { 132 } catch (err) {
133 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
134 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
135
136 await videoPlaylist.destroy()
137 return undefined
138 }
139
143 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) 140 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
144 141
145 await videoPlaylist.setAsRefreshed() 142 await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
164 161
165 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
166 try { 163 try {
167 // Fetch url 164 const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
168 const { body } = await doRequest<PlaylistElementObject>({
169 uri: elementUrl,
170 json: true,
171 activityPub: true
172 })
173 165
174 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) 166 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
175 167
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
199} 191}
200 192
201async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 193async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
202 const options = {
203 uri: playlistUrl,
204 method: 'GET',
205 json: true,
206 activityPub: true
207 }
208
209 logger.info('Fetching remote playlist %s.', playlistUrl) 194 logger.info('Fetching remote playlist %s.', playlistUrl)
210 195
211 const { response, body } = await doRequest<any>(options) 196 const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
212 197
213 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 198 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
214 logger.debug('Remote video playlist JSON is not valid.', { body }) 199 logger.debug('Remote video playlist JSON is not valid.', { body })
215 return { statusCode: response.statusCode, playlistObject: undefined } 200 return { statusCode, playlistObject: undefined }
216 } 201 }
217 202
218 return { statusCode: response.statusCode, playlistObject: body } 203 return { statusCode, playlistObject: body }
219} 204}
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9fb218224..baded642a 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
4import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { 9import {
10 MActorLight, 10 MActorLight,
@@ -18,10 +18,12 @@ import {
18import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context' 19import { ContextType } from '@shared/models/activitypub/context'
20 20
21const lTags = loggerTagsFactory('ap', 'create')
22
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 23async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 24 if (!video.hasPrivacyForFederation()) return undefined
23 25
24 logger.info('Creating job to send video creation of %s.', video.url) 26 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
25 27
26 const byActor = video.VideoChannel.Account.Actor 28 const byActor = video.VideoChannel.Account.Actor
27 const videoObject = video.toActivityPubObject() 29 const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
37 video: MVideoAccountLight, 39 video: MVideoAccountLight,
38 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 40 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
39) { 41) {
40 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 42 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
41 43
42 return sendVideoRelatedCreateActivity({ 44 return sendVideoRelatedCreateActivity({
43 byActor, 45 byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
51async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { 53async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
52 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 54 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
53 55
54 logger.info('Creating job to send create video playlist of %s.', playlist.url) 56 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
55 57
56 const byActor = playlist.OwnerAccount.Actor 58 const byActor = playlist.OwnerAccount.Actor
57 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 59 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1f8a8f3c4..c22fa0893 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,15 +1,17 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { logger, loggerTagsFactory } from '../../helpers/logger'
6import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
2import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor'
3import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
4import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
5import * as Bluebird from 'bluebird' 13
6import { doRequest } from '../../helpers/requests' 14const lTags = loggerTagsFactory('share')
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { logger } from '../../helpers/logger'
9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
12import { getServerActor } from '@server/models/application/application'
13 15
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 16async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 17 if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
25 oldVideoChannel: MChannelActorLight, 27 oldVideoChannel: MChannelActorLight,
26 t: Transaction 28 t: Transaction
27) { 29) {
28 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 30 logger.info(
31 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
32 lTags(video.uuid)
33 )
29 34
30 await undoShareByVideoChannel(video, oldVideoChannel, t) 35 await undoShareByVideoChannel(video, oldVideoChannel, t)
31 36
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
35async function addVideoShares (shareUrls: string[], video: MVideoId) { 40async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 41 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 42 try {
38 // Fetch url 43 const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
39 const { body } = await doRequest<any>({
40 uri: shareUrl,
41 json: true,
42 activityPub: true
43 })
44 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 44 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
45 45
46 const actorUrl = getAPId(body.actor) 46 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d025ed7f1..e23e0c0e7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird'
2import { checkUrlsSameHost } from '../../helpers/activitypub'
1import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
2import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 5import { doJSONRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
6import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
7import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
8import * as Bluebird from 'bluebird'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string 13 url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, async commentUrl => {
22 return resolveThread({ url: commentUrl, isVideo: false }) 22 try {
23 await resolveThread({ url: commentUrl, isVideo: false })
24 } catch (err) {
25 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
26 }
23 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 27 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
24} 28}
25 29
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
126 throw new Error('Recursion limit reached when resolving a thread') 130 throw new Error('Recursion limit reached when resolving a thread')
127 } 131 }
128 132
129 const { body } = await doRequest<any>({ 133 const { body } = await doJSONRequest<any>(url, { activityPub: true })
130 uri: url,
131 json: true,
132 activityPub: true
133 })
134 134
135 if (sanitizeAndCheckVideoCommentObject(body) === false) { 135 if (sanitizeAndCheckVideoCommentObject(body) === false) {
136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) 136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index e246b1313..f40c07fea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,26 +1,22 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
2import { sendLike, sendUndoDislike, sendUndoLike } from './send' 3import { doJSONRequest } from '@server/helpers/requests'
3import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
4import * as Bluebird from 'bluebird' 5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { AccountVideoRateModel } from '../../models/account/account-video-rate'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
9import { doRequest } from '../../helpers/requests' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 14
15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
16 await Bluebird.map(ratesUrl, async rateUrl => { 16 await Bluebird.map(ratesUrl, async rateUrl => {
17 try { 17 try {
18 // Fetch url 18 // Fetch url
19 const { body } = await doRequest<any>({ 19 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
20 uri: rateUrl,
21 json: true,
22 activityPub: true
23 })
24 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 20 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
25 21
26 const actorUrl = getAPId(body.actor) 22 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c02578aad..d484edd36 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -2,7 +2,6 @@ import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename, join } from 'path'
5import * as request from 'request'
6import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -31,7 +30,7 @@ import { isArray } from '../../helpers/custom-validators/misc'
31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 30import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 31import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
33import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
34import { doRequest } from '../../helpers/requests' 33import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
@@ -115,36 +114,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
115 } 114 }
116} 115}
117 116
118async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { 117async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
119 const options = {
120 uri: videoUrl,
121 method: 'GET',
122 json: true,
123 activityPub: true
124 }
125
126 logger.info('Fetching remote video %s.', videoUrl) 118 logger.info('Fetching remote video %s.', videoUrl)
127 119
128 const { response, body } = await doRequest<any>(options) 120 const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
129 121
130 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 122 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
131 logger.debug('Remote video JSON is not valid.', { body }) 123 logger.debug('Remote video JSON is not valid.', { body })
132 return { response, videoObject: undefined } 124 return { statusCode, videoObject: undefined }
133 } 125 }
134 126
135 return { response, videoObject: body } 127 return { statusCode, videoObject: body }
136} 128}
137 129
138async function fetchRemoteVideoDescription (video: MVideoAccountLight) { 130async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
139 const host = video.VideoChannel.Account.Actor.Server.host 131 const host = video.VideoChannel.Account.Actor.Server.host
140 const path = video.getDescriptionAPIPath() 132 const path = video.getDescriptionAPIPath()
141 const options = { 133 const url = REMOTE_SCHEME.HTTP + '://' + host + path
142 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
143 json: true
144 }
145 134
146 const { body } = await doRequest<any>(options) 135 const { body } = await doJSONRequest<any>(url)
147 return body.description ? body.description : '' 136 return body.description || ''
148} 137}
149 138
150function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { 139function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -534,14 +523,7 @@ async function refreshVideoIfNeeded (options: {
534 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 523 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
535 524
536 try { 525 try {
537 const { response, videoObject } = await fetchRemoteVideo(video.url) 526 const { videoObject } = await fetchRemoteVideo(video.url)
538 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
539 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
540
541 // Video does not exist anymore
542 await video.destroy()
543 return undefined
544 }
545 527
546 if (videoObject === undefined) { 528 if (videoObject === undefined) {
547 logger.warn('Cannot refresh remote video %s: invalid body.', video.url) 529 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -565,6 +547,14 @@ async function refreshVideoIfNeeded (options: {
565 547
566 return video 548 return video
567 } catch (err) { 549 } catch (err) {
550 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
551 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
552
553 // Video does not exist anymore
554 await video.destroy()
555 return undefined
556 }
557
568 logger.warn('Cannot refresh video %s.', options.video.url, { err }) 558 logger.warn('Cannot refresh video %s.', options.video.url, { err })
569 559
570 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) 560 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts
index dbd421a7b..80f5064b6 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth/external-auth.ts
@@ -1,28 +1,16 @@
1
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 2import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 4import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' 5import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import { 8import {
10 RegisterServerAuthenticatedResult, 9 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions, 10 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult 11 RegisterServerExternalAuthenticatedResult
13} from '@server/types/plugins/register-server-auth.model' 12} from '@server/types/plugins/register-server-auth.model'
14import * as express from 'express' 13import { UserRole } from '@shared/models'
15import * as OAuthServer from 'express-oauth-server'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17
18const oAuthServer = new OAuthServer({
19 useErrorHandler: true,
20 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
21 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
22 allowExtendedTokenAttributes: true,
23 continueMiddleware: true,
24 model: require('./oauth-model')
25})
26 14
27// Token is the key, expiration date is the value 15// Token is the key, expiration date is the value
28const authBypassTokens = new Map<string, { 16const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
37 npmName: string 25 npmName: string
38}>() 26}>()
39 27
40async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
41 const grantType = req.body.grant_type
42
43 if (grantType === 'password') {
44 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
45 else await proxifyPasswordGrant(req, res)
46 } else if (grantType === 'refresh_token') {
47 await proxifyRefreshGrant(req, res)
48 }
49
50 return forwardTokenReq(req, res, next)
51}
52
53async function handleTokenRevocation (req: express.Request, res: express.Response) {
54 const token = res.locals.oauth.token
55
56 res.locals.explicitLogout = true
57 const result = await revokeToken(token)
58
59 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
60 // oAuthServer.revoke(req, res, err => {
61 // if (err) {
62 // logger.warn('Error in revoke token handler.', { err })
63 //
64 // return res.status(err.status)
65 // .json({
66 // error: err.message,
67 // code: err.name
68 // })
69 // .end()
70 // }
71 // })
72
73 return res.json(result)
74}
75
76async function onExternalUserAuthenticated (options: { 28async function onExternalUserAuthenticated (options: {
77 npmName: string 29 npmName: string
78 authName: string 30 authName: string
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: {
107 authName 59 authName
108 }) 60 })
109 61
110 // Cleanup 62 // Cleanup expired tokens
111 const now = new Date() 63 const now = new Date()
112 for (const [ key, value ] of authBypassTokens) { 64 for (const [ key, value ] of authBypassTokens) {
113 if (value.expires.getTime() < now.getTime()) { 65 if (value.expires.getTime() < now.getTime()) {
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: {
118 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) 70 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
119} 71}
120 72
121// --------------------------------------------------------------------------- 73async function getAuthNameFromRefreshGrant (refreshToken?: string) {
122 74 if (!refreshToken) return undefined
123export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
124
125// ---------------------------------------------------------------------------
126
127function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
128 return oAuthServer.token()(req, res, err => {
129 if (err) {
130 logger.warn('Login error.', { err })
131
132 return res.status(err.status)
133 .json({
134 error: err.message,
135 code: err.name
136 })
137 }
138
139 if (next) return next()
140 })
141}
142
143async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
144 const refreshToken = req.body.refresh_token
145 if (!refreshToken) return
146 75
147 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) 76 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
148 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName 77
78 return tokenModel?.authName
149} 79}
150 80
151async function proxifyPasswordGrant (req: express.Request, res: express.Response) { 81async function getBypassFromPasswordGrant (username: string, password: string) {
152 const plugins = PluginManager.Instance.getIdAndPassAuths() 82 const plugins = PluginManager.Instance.getIdAndPassAuths()
153 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 83 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
154 84
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
174 }) 104 })
175 105
176 const loginOptions = { 106 const loginOptions = {
177 id: req.body.username, 107 id: username,
178 password: req.body.password 108 password
179 } 109 }
180 110
181 for (const pluginAuth of pluginAuths) { 111 for (const pluginAuth of pluginAuths) {
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
199 authName, npmName, loginOptions.id 129 authName, npmName, loginOptions.id
200 ) 130 )
201 131
202 res.locals.bypassLogin = { 132 return {
203 bypass: true, 133 bypass: true,
204 pluginName: pluginAuth.npmName, 134 pluginName: pluginAuth.npmName,
205 authName: authOptions.authName, 135 authName: authOptions.authName,
206 user: buildUserResult(loginResult) 136 user: buildUserResult(loginResult)
207 } 137 }
208
209 return
210 } catch (err) { 138 } catch (err) {
211 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) 139 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
212 } 140 }
213 } 141 }
142
143 return undefined
214} 144}
215 145
216function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { 146function getBypassFromExternalAuth (username: string, externalAuthToken: string) {
217 const obj = authBypassTokens.get(req.body.externalAuthToken) 147 const obj = authBypassTokens.get(externalAuthToken)
218 if (!obj) { 148 if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
219 logger.error('Cannot authenticate user with unknown bypass token')
220 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
221 }
222 149
223 const { expires, user, authName, npmName } = obj 150 const { expires, user, authName, npmName } = obj
224 151
225 const now = new Date() 152 const now = new Date()
226 if (now.getTime() > expires.getTime()) { 153 if (now.getTime() > expires.getTime()) {
227 logger.error('Cannot authenticate user with an expired external auth token') 154 throw new Error('Cannot authenticate user with an expired external auth token')
228 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
229 } 155 }
230 156
231 if (user.username !== req.body.username) { 157 if (user.username !== username) {
232 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) 158 throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`)
233 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
234 } 159 }
235 160
236 // Bypass oauth library validation
237 req.body.password = 'fake'
238
239 logger.info( 161 logger.info(
240 'Auth success with external auth method %s of plugin %s for %s.', 162 'Auth success with external auth method %s of plugin %s for %s.',
241 authName, npmName, user.email 163 authName, npmName, user.email
242 ) 164 )
243 165
244 res.locals.bypassLogin = { 166 return {
245 bypass: true, 167 bypass: true,
246 pluginName: npmName, 168 pluginName: npmName,
247 authName: authName, 169 authName: authName,
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
286 displayName: pluginResult.displayName || pluginResult.username 208 displayName: pluginResult.displayName || pluginResult.username
287 } 209 }
288} 210}
211
212// ---------------------------------------------------------------------------
213
214export {
215 onExternalUserAuthenticated,
216 getBypassFromExternalAuth,
217 getAuthNameFromRefreshGrant,
218 getBypassFromPasswordGrant
219}
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts
index a2c53a2c9..b9c69eb2d 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -1,49 +1,36 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as LRUCache from 'lru-cache'
3import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
4import { Transaction } from 'sequelize'
5import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
6import { ActorModel } from '@server/models/activitypub/actor' 4import { ActorModel } from '@server/models/activitypub/actor'
5import { MOAuthClient } from '@server/types/models'
7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
8import { MUser } from '@server/types/models/user/user' 7import { MUser } from '@server/types/models/user/user'
9import { UserAdminFlag } from '@shared/models/users/user-flag.model' 8import { UserAdminFlag } from '@shared/models/users/user-flag.model'
10import { UserRole } from '@shared/models/users/user-role' 9import { UserRole } from '@shared/models/users/user-role'
11import { logger } from '../helpers/logger' 10import { logger } from '../../helpers/logger'
12import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../../initializers/config'
13import { LRU_CACHE } from '../initializers/constants' 12import { UserModel } from '../../models/account/user'
14import { UserModel } from '../models/account/user' 13import { OAuthClientModel } from '../../models/oauth/oauth-client'
15import { OAuthClientModel } from '../models/oauth/oauth-client' 14import { OAuthTokenModel } from '../../models/oauth/oauth-token'
16import { OAuthTokenModel } from '../models/oauth/oauth-token' 15import { createUserAccountAndChannelAndPlaylist } from '../user'
17import { createUserAccountAndChannelAndPlaylist } from './user' 16import { TokensCache } from './tokens-cache'
18 17
19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = {
20 19 accessToken: string
21const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 20 refreshToken: string
22const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 21 accessTokenExpiresAt: Date
23 22 refreshTokenExpiresAt: Date
24// ---------------------------------------------------------------------------
25
26function deleteUserToken (userId: number, t?: Transaction) {
27 clearCacheByUserId(userId)
28
29 return OAuthTokenModel.deleteUserToken(userId, t)
30} 23}
31 24
32function clearCacheByUserId (userId: number) { 25export type BypassLogin = {
33 const token = userHavingToken.get(userId) 26 bypass: boolean
34 27 pluginName: string
35 if (token !== undefined) { 28 authName?: string
36 accessTokenCache.del(token) 29 user: {
37 userHavingToken.del(userId) 30 username: string
38 } 31 email: string
39} 32 displayName: string
40 33 role: UserRole
41function clearCacheByToken (token: string) {
42 const tokenModel = accessTokenCache.get(token)
43
44 if (tokenModel !== undefined) {
45 userHavingToken.del(tokenModel.userId)
46 accessTokenCache.del(token)
47 } 34 }
48} 35}
49 36
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) {
54 41
55 let tokenModel: MOAuthTokenUser 42 let tokenModel: MOAuthTokenUser
56 43
57 if (accessTokenCache.has(bearerToken)) { 44 if (TokensCache.Instance.hasToken(bearerToken)) {
58 tokenModel = accessTokenCache.get(bearerToken) 45 tokenModel = TokensCache.Instance.getByToken(bearerToken)
59 } else { 46 } else {
60 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 47 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
61 48
62 if (tokenModel) { 49 if (tokenModel) TokensCache.Instance.setToken(tokenModel)
63 accessTokenCache.set(bearerToken, tokenModel)
64 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
65 }
66 } 50 }
67 51
68 if (!tokenModel) return undefined 52 if (!tokenModel) return undefined
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) {
99 return tokenInfo 83 return tokenInfo
100} 84}
101 85
102async function getUser (usernameOrEmail?: string, password?: string) { 86async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) {
103 const res: express.Response = this.request.res
104
105 // Special treatment coming from a plugin 87 // Special treatment coming from a plugin
106 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { 88 if (bypassLogin && bypassLogin.bypass === true) {
107 const obj = res.locals.bypassLogin 89 logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
108 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
109 90
110 let user = await UserModel.loadByEmail(obj.user.email) 91 let user = await UserModel.loadByEmail(bypassLogin.user.email)
111 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) 92 if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
112 93
113 // Cannot create a user 94 // Cannot create a user
114 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') 95 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) {
117 // Then we just go through a regular login process 98 // Then we just go through a regular login process
118 if (user.pluginAuth !== null) { 99 if (user.pluginAuth !== null) {
119 // This user does not belong to this plugin, skip it 100 // This user does not belong to this plugin, skip it
120 if (user.pluginAuth !== obj.pluginName) return null 101 if (user.pluginAuth !== bypassLogin.pluginName) return null
121 102
122 checkUserValidityOrThrow(user) 103 checkUserValidityOrThrow(user)
123 104
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) {
143 return user 124 return user
144} 125}
145 126
146async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { 127async function revokeToken (
147 const res: express.Response = this.request.res 128 tokenInfo: { refreshToken: string },
129 options: {
130 req?: express.Request
131 explicitLogout?: boolean
132 } = {}
133): Promise<{ success: boolean, redirectUrl?: string }> {
134 const { req, explicitLogout } = options
135
148 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 136 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
149 137
150 if (token) { 138 if (token) {
151 let redirectUrl: string 139 let redirectUrl: string
152 140
153 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { 141 if (explicitLogout === true && token.User.pluginAuth && token.authName) {
154 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) 142 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req)
155 } 143 }
156 144
157 clearCacheByToken(token.accessToken) 145 TokensCache.Instance.clearCacheByToken(token.accessToken)
158 146
159 token.destroy() 147 token.destroy()
160 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 148 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ
165 return { success: false } 153 return { success: false }
166} 154}
167 155
168async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 156async function saveToken (
169 const res: express.Response = this.request.res 157 token: TokenInfo,
170 158 client: MOAuthClient,
159 user: MUser,
160 options: {
161 refreshTokenAuthName?: string
162 bypassLogin?: BypassLogin
163 } = {}
164) {
165 const { refreshTokenAuthName, bypassLogin } = options
171 let authName: string = null 166 let authName: string = null
172 if (res.locals.bypassLogin?.bypass === true) { 167
173 authName = res.locals.bypassLogin.authName 168 if (bypassLogin?.bypass === true) {
174 } else if (res.locals.refreshTokenAuthName) { 169 authName = bypassLogin.authName
175 authName = res.locals.refreshTokenAuthName 170 } else if (refreshTokenAuthName) {
171 authName = refreshTokenAuthName
176 } 172 }
177 173
178 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 174 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
199 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, 195 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt,
200 client, 196 client,
201 user, 197 user,
202 refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) 198 accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt),
199 refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt)
203 } 200 }
204} 201}
205 202
206// ---------------------------------------------------------------------------
207
208// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
209export { 203export {
210 deleteUserToken,
211 clearCacheByUserId,
212 clearCacheByToken,
213 getAccessToken, 204 getAccessToken,
214 getClient, 205 getClient,
215 getRefreshToken, 206 getRefreshToken,
@@ -218,6 +209,8 @@ export {
218 saveToken 209 saveToken
219} 210}
220 211
212// ---------------------------------------------------------------------------
213
221async function createUserFromExternal (pluginAuth: string, options: { 214async function createUserFromExternal (pluginAuth: string, options: {
222 username: string 215 username: string
223 email: string 216 email: string
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: {
252function checkUserValidityOrThrow (user: MUser) { 245function checkUserValidityOrThrow (user: MUser) {
253 if (user.blocked) throw new AccessDeniedError('User is blocked.') 246 if (user.blocked) throw new AccessDeniedError('User is blocked.')
254} 247}
248
249function buildExpiresIn (expiresAt: Date) {
250 return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000)
251}
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
new file mode 100644
index 000000000..5b6130d56
--- /dev/null
+++ b/server/lib/auth/oauth.ts
@@ -0,0 +1,180 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
15
16/**
17 *
18 * Reimplement some functions of OAuth2Server to inject external auth methods
19 *
20 */
21
22const oAuthServer = new (require('oauth2-server'))({
23 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
24 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
25
26 // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
27 model: require('./oauth-model')
28})
29
30// ---------------------------------------------------------------------------
31
32async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) {
33 const request = new Request(req)
34 const { refreshTokenAuthName, bypassLogin } = options
35
36 if (request.method !== 'POST') {
37 throw new InvalidRequestError('Invalid request: method must be POST')
38 }
39
40 if (!request.is([ 'application/x-www-form-urlencoded' ])) {
41 throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')
42 }
43
44 const clientId = request.body.client_id
45 const clientSecret = request.body.client_secret
46
47 if (!clientId || !clientSecret) {
48 throw new InvalidClientError('Invalid client: cannot retrieve client credentials')
49 }
50
51 const client = await getClient(clientId, clientSecret)
52 if (!client) {
53 throw new InvalidClientError('Invalid client: client is invalid')
54 }
55
56 const grantType = request.body.grant_type
57 if (!grantType) {
58 throw new InvalidRequestError('Missing parameter: `grant_type`')
59 }
60
61 if (![ 'password', 'refresh_token' ].includes(grantType)) {
62 throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid')
63 }
64
65 if (!client.grants.includes(grantType)) {
66 throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid')
67 }
68
69 if (grantType === 'password') {
70 return handlePasswordGrant({
71 request,
72 client,
73 bypassLogin
74 })
75 }
76
77 return handleRefreshGrant({
78 request,
79 client,
80 refreshTokenAuthName
81 })
82}
83
84async function handleOAuthAuthenticate (
85 req: express.Request,
86 res: express.Response,
87 authenticateInQuery = false
88) {
89 const options = authenticateInQuery
90 ? { allowBearerTokensInQueryString: true }
91 : {}
92
93 return oAuthServer.authenticate(new Request(req), new Response(res), options)
94}
95
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async function handlePasswordGrant (options: {
104 request: Request
105 client: MOAuthClient
106 bypassLogin?: BypassLogin
107}) {
108 const { request, client, bypassLogin } = options
109
110 if (!request.body.username) {
111 throw new InvalidRequestError('Missing parameter: `username`')
112 }
113
114 if (!bypassLogin && !request.body.password) {
115 throw new InvalidRequestError('Missing parameter: `password`')
116 }
117
118 const user = await getUser(request.body.username, request.body.password, bypassLogin)
119 if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid')
120
121 const token = await buildToken()
122
123 return saveToken(token, client, user, { bypassLogin })
124}
125
126async function handleRefreshGrant (options: {
127 request: Request
128 client: MOAuthClient
129 refreshTokenAuthName: string
130}) {
131 const { request, client, refreshTokenAuthName } = options
132
133 if (!request.body.refresh_token) {
134 throw new InvalidRequestError('Missing parameter: `refresh_token`')
135 }
136
137 const refreshToken = await getRefreshToken(request.body.refresh_token)
138
139 if (!refreshToken) {
140 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
141 }
142
143 if (refreshToken.client.id !== client.id) {
144 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
145 }
146
147 if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) {
148 throw new InvalidGrantError('Invalid grant: refresh token has expired')
149 }
150
151 await revokeToken({ refreshToken: refreshToken.refreshToken })
152
153 const token = await buildToken()
154
155 return saveToken(token, client, refreshToken.user, { refreshTokenAuthName })
156}
157
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function getTokenExpiresAt (type: 'access' | 'refresh') {
164 const lifetime = type === 'access'
165 ? OAUTH_LIFETIME.ACCESS_TOKEN
166 : OAUTH_LIFETIME.REFRESH_TOKEN
167
168 return new Date(Date.now() + lifetime * 1000)
169}
170
171async function buildToken () {
172 const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ])
173
174 return {
175 accessToken,
176 refreshToken,
177 accessTokenExpiresAt: getTokenExpiresAt('access'),
178 refreshTokenExpiresAt: getTokenExpiresAt('refresh')
179 }
180}
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts
new file mode 100644
index 000000000..b027ce69a
--- /dev/null
+++ b/server/lib/auth/tokens-cache.ts
@@ -0,0 +1,52 @@
1import * as LRUCache from 'lru-cache'
2import { MOAuthTokenUser } from '@server/types/models'
3import { LRU_CACHE } from '../../initializers/constants'
4
5export class TokensCache {
6
7 private static instance: TokensCache
8
9 private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
10 private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
11
12 private constructor () { }
13
14 static get Instance () {
15 return this.instance || (this.instance = new this())
16 }
17
18 hasToken (token: string) {
19 return this.accessTokenCache.has(token)
20 }
21
22 getByToken (token: string) {
23 return this.accessTokenCache.get(token)
24 }
25
26 setToken (token: MOAuthTokenUser) {
27 this.accessTokenCache.set(token.accessToken, token)
28 this.userHavingToken.set(token.userId, token.accessToken)
29 }
30
31 deleteUserToken (userId: number) {
32 this.clearCacheByUserId(userId)
33 }
34
35 clearCacheByUserId (userId: number) {
36 const token = this.userHavingToken.get(userId)
37
38 if (token !== undefined) {
39 this.accessTokenCache.del(token)
40 this.userHavingToken.del(userId)
41 }
42 }
43
44 clearCacheByToken (token: string) {
45 const tokenModel = this.accessTokenCache.get(token)
46
47 if (tokenModel !== undefined) {
48 this.userHavingToken.del(tokenModel.userId)
49 this.accessTokenCache.del(token)
50 }
51 }
52}
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 969eae77b..ce4134d59 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' 7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' 8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' 9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
10import { SendEmailOptions } from '../../shared/models/server/emailer.model' 10import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
11import { isTestInstance, root } from '../helpers/core-utils' 11import { isTestInstance, root } from '../helpers/core-utils'
12import { bunyanLogger, logger } from '../helpers/logger' 12import { bunyanLogger, logger } from '../helpers/logger'
13import { CONFIG, isEmailEnabled } from '../initializers/config' 13import { CONFIG, isEmailEnabled } from '../initializers/config'
14import { WEBSERVER } from '../initializers/constants' 14import { WEBSERVER } from '../initializers/constants'
15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue' 17import { JobQueue } from './job-queue'
18 18
@@ -403,7 +403,7 @@ class Emailer {
403 } 403 }
404 404
405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
406 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 406 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() 408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
409 409
@@ -417,7 +417,7 @@ class Emailer {
417 videoName: videoBlacklist.Video.name, 417 videoName: videoBlacklist.Video.name,
418 action: { 418 action: {
419 text: 'Review autoblacklist', 419 text: 'Review autoblacklist',
420 url: VIDEO_AUTO_BLACKLIST_URL 420 url: videoAutoBlacklistUrl
421 } 421 }
422 } 422 }
423 } 423 }
@@ -472,6 +472,36 @@ class Emailer {
472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
473 } 473 }
474 474
475 addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
476 const emailPayload: EmailPayload = {
477 to,
478 template: 'peertube-version-new',
479 subject: `A new PeerTube version is available: ${latestVersion}`,
480 locals: {
481 latestVersion
482 }
483 }
484
485 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
486 }
487
488 addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
489 const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
490
491 const emailPayload: EmailPayload = {
492 to,
493 template: 'plugin-version-new',
494 subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`,
495 locals: {
496 pluginName: plugin.name,
497 latestVersion: plugin.latestVersion,
498 pluginUrl
499 }
500 }
501
502 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
503 }
504
475 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { 505 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
476 const emailPayload: EmailPayload = { 506 const emailPayload: EmailPayload = {
477 template: 'password-reset', 507 template: 'password-reset',
@@ -569,26 +599,27 @@ class Emailer {
569 }) 599 })
570 600
571 for (const to of options.to) { 601 for (const to of options.to) {
572 await email 602 const baseOptions: SendEmailDefaultOptions = {
573 .send(merge( 603 template: 'common',
574 { 604 message: {
575 template: 'common', 605 to,
576 message: { 606 from: options.from,
577 to, 607 subject: options.subject,
578 from: options.from, 608 replyTo: options.replyTo
579 subject: options.subject, 609 },
580 replyTo: options.replyTo 610 locals: { // default variables available in all templates
581 }, 611 WEBSERVER,
582 locals: { // default variables available in all templates 612 EMAIL: CONFIG.EMAIL,
583 WEBSERVER, 613 instanceName: CONFIG.INSTANCE.NAME,
584 EMAIL: CONFIG.EMAIL, 614 text: options.text,
585 instanceName: CONFIG.INSTANCE.NAME, 615 subject: options.subject
586 text: options.text, 616 }
587 subject: options.subject 617 }
588 } 618
589 }, 619 // overriden/new variables given for a specific template in the payload
590 options // overriden/new variables given for a specific template in the payload 620 const sendOptions = merge(baseOptions, options)
591 ) as SendEmailOptions) 621
622 await email.send(sendOptions)
592 .then(res => logger.debug('Sent email.', { res })) 623 .then(res => logger.debug('Sent email.', { res }))
593 .catch(err => logger.error('Error in email sender.', { err })) 624 .catch(err => logger.error('Error in email sender.', { err }))
594 } 625 }
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug
new file mode 100644
index 000000000..2f4d9399d
--- /dev/null
+++ b/server/lib/emails/peertube-version-new/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New PeerTube version available
5
6block content
7 p
8 | A new version of PeerTube is available: #{latestVersion}.
9 | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug
new file mode 100644
index 000000000..86d3d87e8
--- /dev/null
+++ b/server/lib/emails/plugin-version-new/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New plugin version available
5
6block content
7 p
8 | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
9 | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index ee0447010..58e2260b6 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = videoCaption.getFileUrl(video) 41 const remoteUrl = videoCaption.getFileUrl(video)
42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) 42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename)
43 43
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 44 await doRequestAndSaveToFile(remoteUrl, destPath)
45 45
46 return { isOwned: false, path: destPath } 46 return { isOwned: false, path: destPath }
47 } 47 }
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index ee72cd3f9..dd3a84aca 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) 39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
40 40
41 const remoteUrl = preview.getFileUrl(video) 41 const remoteUrl = preview.getFileUrl(video)
42 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 42 await doRequestAndSaveToFile(remoteUrl, destPath)
43 43
44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) 44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
45 45
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts
index ca0e1770d..23217f140 100644
--- a/server/lib/files-cache/videos-torrent-cache.ts
+++ b/server/lib/files-cache/videos-torrent-cache.ts
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
5import { FILES_CACHE } from '../../initializers/constants' 5import { FILES_CACHE } from '../../initializers/constants'
6import { VideoModel } from '../../models/video/video' 6import { VideoModel } from '../../models/video/video'
7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
8import { MVideo, MVideoFile } from '@server/types/models'
8 9
9class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { 10class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
10 11
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
22 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) 23 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
23 if (!file) return undefined 24 if (!file) return undefined
24 25
25 if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } 26 if (file.getVideo().isOwned()) {
27 const downloadName = this.buildDownloadName(file.getVideo(), file)
28
29 return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
30 }
26 31
27 return this.loadRemoteFile(filename) 32 return this.loadRemoteFile(filename)
28 } 33 }
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = file.getRemoteTorrentUrl(video) 46 const remoteUrl = file.getRemoteTorrentUrl(video)
42 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) 47 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
43 48
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 49 await doRequestAndSaveToFile(remoteUrl, destPath)
45 50
46 const downloadName = `${video.name}-${file.resolution}p.torrent` 51 const downloadName = this.buildDownloadName(video, file)
47 52
48 return { isOwned: false, path: destPath, downloadName } 53 return { isOwned: false, path: destPath, downloadName }
49 } 54 }
55
56 private buildDownloadName (video: MVideo, file: MVideoFile) {
57 return `${video.name}-${file.resolution}p.torrent`
58 }
50} 59}
51 60
52export { 61export {
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 04187668c..84539e2c1 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
135 const destPath = join(tmpDirectory, basename(fileUrl)) 135 const destPath = join(tmpDirectory, basename(fileUrl))
136 136
137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB 137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB
138 await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) 138 await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit })
139 } 139 }
140 140
141 clearTimeout(timer) 141 clearTimeout(timer)
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
156 } 156 }
157 157
158 async function fetchUniqUrls (playlistUrl: string) { 158 async function fetchUniqUrls (playlistUrl: string) {
159 const { body } = await doRequest<string>({ uri: playlistUrl }) 159 const { body } = await doRequest(playlistUrl)
160 160
161 if (!body) return [] 161 if (!body) return []
162 162
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts
index b58bbc983..1caca1dcc 100644
--- a/server/lib/job-queue/handlers/activitypub-cleaner.ts
+++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts
@@ -1,10 +1,13 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Bull from 'bull' 2import * as Bull from 'bull'
3import { checkUrlsSameHost } from '@server/helpers/activitypub' 3import { checkUrlsSameHost } from '@server/helpers/activitypub'
4import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' 4import {
5import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' 5 isAnnounceActivityValid,
6 isDislikeActivityValid,
7 isLikeActivityValid
8} from '@server/helpers/custom-validators/activitypub/activity'
6import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' 9import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
7import { doRequest } from '@server/helpers/requests' 10import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
8import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' 11import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
9import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
10import { VideoCommentModel } from '@server/models/video/video-comment' 13import { VideoCommentModel } from '@server/models/video/video-comment'
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> (
78 updater: (url: string, newUrl: string) => Promise<T>, 81 updater: (url: string, newUrl: string) => Promise<T>,
79 deleter: (url: string) => Promise<T> 82 deleter: (url: string) => Promise<T>
80): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { 83): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
81 // Fetch url 84 const on404OrTombstone = async () => {
82 const { response, body } = await doRequest<any>({
83 uri: url,
84 json: true,
85 activityPub: true
86 })
87
88 // Does not exist anymore, remove entry
89 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
90 logger.info('Removing remote AP object %s.', url) 85 logger.info('Removing remote AP object %s.', url)
91 const data = await deleter(url) 86 const data = await deleter(url)
92 87
93 return { status: 'deleted', data } 88 return { status: 'deleted' as 'deleted', data }
94 } 89 }
95 90
96 // If not same id, check same host and update 91 try {
97 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) 92 const { body } = await doJSONRequest<any>(url, { activityPub: true })
98 93
99 if (body.type === 'Tombstone') { 94 // If not same id, check same host and update
100 logger.info('Removing remote AP object %s.', url) 95 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
101 const data = await deleter(url)
102 96
103 return { status: 'deleted', data } 97 if (body.type === 'Tombstone') {
104 } 98 return on404OrTombstone()
99 }
105 100
106 const newUrl = body.id 101 const newUrl = body.id
107 if (newUrl !== url) { 102 if (newUrl !== url) {
108 if (checkUrlsSameHost(newUrl, url) !== true) { 103 if (checkUrlsSameHost(newUrl, url) !== true) {
109 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) 104 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
105 }
106
107 logger.info('Updating remote AP object %s.', url)
108 const data = await updater(url, newUrl)
109
110 return { status: 'updated', data }
110 } 111 }
111 112
112 logger.info('Updating remote AP object %s.', url) 113 return null
113 const data = await updater(url, newUrl) 114 } catch (err) {
115 // Does not exist anymore, remove entry
116 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
117 return on404OrTombstone()
118 }
114 119
115 return { status: 'updated', data } 120 throw err
116 } 121 }
117
118 return null
119} 122}
120 123
121function rateOptionsFactory () { 124function rateOptionsFactory () {
@@ -149,7 +152,7 @@ function rateOptionsFactory () {
149 152
150function shareOptionsFactory () { 153function shareOptionsFactory () {
151 return { 154 return {
152 bodyValidator: (body: any) => isShareActivityValid(body), 155 bodyValidator: (body: any) => isAnnounceActivityValid(body),
153 156
154 updater: async (url: string, newUrl: string) => { 157 updater: async (url: string, newUrl: string) => {
155 const share = await VideoShareModel.loadByUrl(url, undefined) 158 const share = await VideoShareModel.loadByUrl(url, undefined)
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 7174786d6..c69ff9e83 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri: '',
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
28 const goodUrls: string[] = [] 27 const goodUrls: string[] = []
29 28
30 await Bluebird.map(payload.uris, uri => { 29 await Bluebird.map(payload.uris, uri => {
31 return doRequest(Object.assign({}, options, { uri })) 30 return doRequest(uri, options)
32 .then(() => goodUrls.push(uri)) 31 .then(() => goodUrls.push(uri))
33 .catch(() => badUrls.push(uri)) 32 .catch(() => badUrls.push(uri))
34 }, { concurrency: BROADCAST_CONCURRENCY }) 33 }, { concurrency: BROADCAST_CONCURRENCY })
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index 74989d62e..585dad671 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri,
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
25 } 24 }
26 25
27 try { 26 try {
28 await doRequest(options) 27 await doRequest(uri, options)
29 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) 28 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], [])
30 } catch (err) { 29 } catch (err) {
31 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) 30 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ])
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index c030d31ef..e8a91450d 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto' 6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context' 7import { ContextType } from '@shared/models/activitypub/context'
8 8
9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } 9type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
10 10
11async function computeBody (payload: Payload) { 11async function computeBody <T> (
12 payload: Payload<T>
13): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> {
12 let body = payload.body 14 let body = payload.body
13 15
14 if (payload.signatureActorId) { 16 if (payload.signatureActorId) {
15 const actorSignature = await ActorModel.load(payload.signatureActorId) 17 const actorSignature = await ActorModel.load(payload.signatureActorId)
16 if (!actorSignature) throw new Error('Unknown signature actor id.') 18 if (!actorSignature) throw new Error('Unknown signature actor id.')
19
17 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) 20 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
18 } 21 }
19 22
20 return body 23 return body
21} 24}
22 25
23async function buildSignedRequestOptions (payload: Payload) { 26async function buildSignedRequestOptions (payload: Payload<any>) {
24 let actor: MActor | null 27 let actor: MActor | null
25 28
26 if (payload.signatureActorId) { 29 if (payload.signatureActorId) {
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) {
43 46
44function buildGlobalHeaders (body: any) { 47function buildGlobalHeaders (body: any) {
45 return { 48 return {
46 'Digest': buildDigest(body), 49 'digest': buildDigest(body),
47 'Content-Type': 'application/activity+json', 50 'content-type': 'application/activity+json',
48 'Accept': ACTIVITY_PUB.ACCEPT_HEADER 51 'accept': ACTIVITY_PUB.ACCEPT_HEADER
49 } 52 }
50} 53}
51 54
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 740c274d7..da7f7cc05 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
19import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
21import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { Emailer } from './emailer'
@@ -144,6 +144,20 @@ class Notifier {
144 }) 144 })
145 } 145 }
146 146
147 notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
148 this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
149 .catch(err => {
150 logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
151 })
152 }
153
154 notifyOfNewPluginVersion (plugin: MPlugin) {
155 this.notifyAdminsOfNewPluginVersion(plugin)
156 .catch(err => {
157 logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
158 })
159 }
160
147 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { 161 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
148 // List all followers that are users 162 // List all followers that are users
149 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 163 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -667,6 +681,64 @@ class Notifier {
667 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 681 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
668 } 682 }
669 683
684 private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
685 // Use the debug right to know who is an administrator
686 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
687 if (admins.length === 0) return
688
689 logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
690
691 function settingGetter (user: MUserWithNotificationSetting) {
692 return user.NotificationSetting.newPeerTubeVersion
693 }
694
695 async function notificationCreator (user: MUserWithNotificationSetting) {
696 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
697 type: UserNotificationType.NEW_PEERTUBE_VERSION,
698 userId: user.id,
699 applicationId: application.id
700 })
701 notification.Application = application
702
703 return notification
704 }
705
706 function emailSender (emails: string[]) {
707 return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
708 }
709
710 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
711 }
712
713 private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
714 // Use the debug right to know who is an administrator
715 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
716 if (admins.length === 0) return
717
718 logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
719
720 function settingGetter (user: MUserWithNotificationSetting) {
721 return user.NotificationSetting.newPluginVersion
722 }
723
724 async function notificationCreator (user: MUserWithNotificationSetting) {
725 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
726 type: UserNotificationType.NEW_PLUGIN_VERSION,
727 userId: user.id,
728 pluginId: plugin.id
729 })
730 notification.Plugin = plugin
731
732 return notification
733 }
734
735 function emailSender (emails: string[]) {
736 return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
737 }
738
739 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
740 }
741
670 private async notify<T extends MUserWithNotificationSetting> (options: { 742 private async notify<T extends MUserWithNotificationSetting> (options: {
671 users: T[] 743 users: T[]
672 notificationCreator: (user: T) => Promise<UserNotificationModelForApi> 744 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 7bcb6ed4c..624f5da1d 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -1,22 +1,22 @@
1import { doRequest } from '../../helpers/requests' 1import { sanitizeUrl } from '@server/helpers/core-utils'
2import { CONFIG } from '../../initializers/config' 2import { ResultList } from '../../../shared/models'
3import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
4import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
3import { 5import {
4 PeertubePluginLatestVersionRequest, 6 PeertubePluginLatestVersionRequest,
5 PeertubePluginLatestVersionResponse 7 PeertubePluginLatestVersionResponse
6} from '../../../shared/models/plugins/peertube-plugin-latest-version.model' 8} from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
7import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
8import { ResultList } from '../../../shared/models'
9import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
10import { PluginModel } from '../../models/server/plugin'
11import { PluginManager } from './plugin-manager'
12import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest } from '../../helpers/requests'
11import { CONFIG } from '../../initializers/config'
13import { PEERTUBE_VERSION } from '../../initializers/constants' 12import { PEERTUBE_VERSION } from '../../initializers/constants'
14import { sanitizeUrl } from '@server/helpers/core-utils' 13import { PluginModel } from '../../models/server/plugin'
14import { PluginManager } from './plugin-manager'
15 15
16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { 16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options 17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options
18 18
19 const qs: PeertubePluginIndexList = { 19 const searchParams: PeertubePluginIndexList & Record<string, string | number> = {
20 start, 20 start,
21 count, 21 count,
22 sort, 22 sort,
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
29 29
30 try { 30 try {
31 const { body } = await doRequest<any>({ uri, qs, json: true }) 31 const { body } = await doJSONRequest<any>(uri, { searchParams })
32 32
33 logger.debug('Got result from PeerTube index.', { body }) 33 logger.debug('Got result from PeerTube index.', { body })
34 34
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
58 58
59 const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' 59 const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version'
60 60
61 const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) 61 const options = {
62 json: bodyRequest,
63 method: 'POST' as 'POST'
64 }
65 const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options)
62 66
63 return body 67 return body
64} 68}
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
index 1f2a88c27..9b5e1a546 100644
--- a/server/lib/plugins/register-helpers.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -7,7 +7,7 @@ import {
7 VIDEO_PLAYLIST_PRIVACIES, 7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES 8 VIDEO_PRIVACIES
9} from '@server/initializers/constants' 9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
11import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
12import { 12import {
13 RegisterServerAuthExternalOptions, 13 RegisterServerAuthExternalOptions,
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
index f62f52f9c..0b8cd1389 100644
--- a/server/lib/schedulers/auto-follow-index-instances.ts
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -1,5 +1,5 @@
1import { chunk } from 'lodash' 1import { chunk } from 'lodash'
2import { doRequest } from '@server/helpers/requests' 2import { doJSONRequest } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 4import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
34 try { 34 try {
35 const serverActor = await getServerActor() 35 const serverActor = await getServerActor()
36 36
37 const qs = { count: 1000 } 37 const searchParams = { count: 1000 }
38 if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) 38 if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() })
39 39
40 this.lastCheck = new Date() 40 this.lastCheck = new Date()
41 41
42 const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) 42 const { body } = await doJSONRequest<any>(indexUrl, { searchParams })
43 if (!body.data || Array.isArray(body.data) === false) { 43 if (!body.data || Array.isArray(body.data) === false) {
44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) 44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body })
45 return 45 return
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts
new file mode 100644
index 000000000..c8960465c
--- /dev/null
+++ b/server/lib/schedulers/peertube-version-check-scheduler.ts
@@ -0,0 +1,55 @@
1
2import { doJSONRequest } from '@server/helpers/requests'
3import { ApplicationModel } from '@server/models/application/application'
4import { compareSemVer } from '@shared/core-utils'
5import { JoinPeerTubeVersions } from '@shared/models'
6import { logger } from '../../helpers/logger'
7import { CONFIG } from '../../initializers/config'
8import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
9import { Notifier } from '../notifier'
10import { AbstractScheduler } from './abstract-scheduler'
11
12export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
13
14 private static instance: AbstractScheduler
15
16 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
17
18 private constructor () {
19 super()
20 }
21
22 protected async internalExecute () {
23 return this.checkLatestVersion()
24 }
25
26 private async checkLatestVersion () {
27 if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
28
29 logger.info('Checking latest PeerTube version.')
30
31 const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
32
33 if (!body?.peertube?.latestVersion) {
34 logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
35 return
36 }
37
38 const latestVersion = body.peertube.latestVersion
39 const application = await ApplicationModel.load()
40
41 // Already checked this version
42 if (application.latestPeerTubeVersion === latestVersion) return
43
44 if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
45 application.latestPeerTubeVersion = latestVersion
46 await application.save()
47
48 Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
49 }
50 }
51
52 static get Instance () {
53 return this.instance || (this.instance = new this())
54 }
55}
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 014993e94..9a1ae3ec5 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
6import { chunk } from 'lodash' 6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index' 7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' 8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier'
9 10
10export class PluginsCheckScheduler extends AbstractScheduler { 11export class PluginsCheckScheduler extends AbstractScheduler {
11 12
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
53 plugin.latestVersion = result.latestVersion 54 plugin.latestVersion = result.latestVersion
54 await plugin.save() 55 await plugin.save()
55 56
57 // Notify if there is an higher plugin version available
58 if (compareSemVer(plugin.version, result.latestVersion) < 0) {
59 Notifier.Instance.notifyOfNewPluginVersion(plugin)
60 }
61
56 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) 62 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
57 } 63 }
58 } 64 }
diff --git a/server/lib/user.ts b/server/lib/user.ts
index e1892f22c..9b0a0a2f1 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
193 newInstanceFollower: UserNotificationSettingValue.WEB, 193 newInstanceFollower: UserNotificationSettingValue.WEB,
194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
196 autoInstanceFollowing: UserNotificationSettingValue.WEB 196 autoInstanceFollowing: UserNotificationSettingValue.WEB,
197 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
198 newPluginVersion: UserNotificationSettingValue.WEB
197 } 199 }
198 200
199 return UserNotificationSettingModel.create(values, { transaction: t }) 201 return UserNotificationSettingModel.create(values, { transaction: t })
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index dbb37e0b2..37c43c3b0 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -11,7 +11,7 @@ import {
11} from '@server/types/models' 11} from '@server/types/models'
12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' 12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
13import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 13import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
14import { logger } from '../helpers/logger' 14import { logger, loggerTagsFactory } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 15import { CONFIG } from '../initializers/config'
16import { VideoBlacklistModel } from '../models/video/video-blacklist' 16import { VideoBlacklistModel } from '../models/video/video-blacklist'
17import { sendDeleteVideo } from './activitypub/send' 17import { sendDeleteVideo } from './activitypub/send'
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager'
20import { Notifier } from './notifier' 20import { Notifier } from './notifier'
21import { Hooks } from './plugins/hooks' 21import { Hooks } from './plugins/hooks'
22 22
23const lTags = loggerTagsFactory('blacklist')
24
23async function autoBlacklistVideoIfNeeded (parameters: { 25async function autoBlacklistVideoIfNeeded (parameters: {
24 video: MVideoWithBlacklistLight 26 video: MVideoWithBlacklistLight
25 user?: MUser 27 user?: MUser
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
60 }) 62 })
61 } 63 }
62 64
63 logger.info('Video %s auto-blacklisted.', video.uuid) 65 logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid))
64 66
65 return true 67 return true
66} 68}
diff --git a/server/middlewares/oauth.ts b/server/middlewares/auth.ts
index 280595acc..f38373624 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/auth.ts
@@ -1,15 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Socket } from 'socket.io' 2import { Socket } from 'socket.io'
3import { oAuthServer } from '@server/lib/auth' 3import { getAccessToken } from '@server/lib/auth/oauth-model'
4import { logger } from '../helpers/logger'
5import { getAccessToken } from '../lib/oauth-model'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger'
6import { handleOAuthAuthenticate } from '../lib/auth/oauth'
7 7
8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
9 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 9 handleOAuthAuthenticate(req, res, authenticateInQuery)
10 .then((token: any) => {
11 res.locals.oauth = { token }
12 res.locals.authenticated = true
10 13
11 oAuthServer.authenticate(options)(req, res, err => { 14 return next()
12 if (err) { 15 })
16 .catch(err => {
13 logger.warn('Cannot authenticate.', { err }) 17 logger.warn('Cannot authenticate.', { err })
14 18
15 return res.status(err.status) 19 return res.status(err.status)
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres
17 error: 'Token is invalid.', 21 error: 'Token is invalid.',
18 code: err.name 22 code: err.name
19 }) 23 })
20 .end() 24 })
21 }
22
23 res.locals.authenticated = true
24
25 return next()
26 })
27} 25}
28 26
29function authenticateSocket (socket: Socket, next: (err?: any) => void) { 27function authenticateSocket (socket: Socket, next: (err?: any) => void) {
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index b758a8586..3e280e16f 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -1,7 +1,7 @@
1export * from './validators' 1export * from './validators'
2export * from './activitypub' 2export * from './activitypub'
3export * from './async' 3export * from './async'
4export * from './oauth' 4export * from './auth'
5export * from './pagination' 5export * from './pagination'
6export * from './servers' 6export * from './servers'
7export * from './sort' 7export * from './sort'
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
index 02b191480..7c4e49463 100644
--- a/server/middlewares/validators/activitypub/signature.ts
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -23,7 +23,7 @@ const signatureValidator = [
23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), 23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
24 24
25 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) 26 logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
27 27
28 if (areValidationErrors(req, res)) return 28 if (areValidationErrors(req, res)) return
29 29
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts
index 99ef25e0a..d87b28c06 100644
--- a/server/middlewares/validators/jobs.ts
+++ b/server/middlewares/validators/jobs.ts
@@ -1,9 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' 3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
4import { logger } from '../../helpers/logger' 4import { logger, loggerTagsFactory } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const lTags = loggerTagsFactory('validators', 'jobs')
8
7const listJobsValidator = [ 9const listJobsValidator = [
8 param('state') 10 param('state')
9 .optional() 11 .optional()
@@ -14,7 +16,7 @@ const listJobsValidator = [
14 .custom(isValidJobType).withMessage('Should have a valid job state'), 16 .custom(isValidJobType).withMessage('Should have a valid job state'),
15 17
16 (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params }) 19 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params, ...lTags() })
18 20
19 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
20 22
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts
index 1cae7848c..6b0a83d80 100644
--- a/server/middlewares/validators/pagination.ts
+++ b/server/middlewares/validators/pagination.ts
@@ -4,25 +4,30 @@ import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { PAGINATION } from '@server/initializers/constants' 5import { PAGINATION } from '@server/initializers/constants'
6 6
7const paginationValidator = [ 7const paginationValidator = paginationValidatorBuilder()
8 query('start')
9 .optional()
10 .isInt({ min: 0 }).withMessage('Should have a number start'),
11 query('count')
12 .optional()
13 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
14 8
15 (req: express.Request, res: express.Response, next: express.NextFunction) => { 9function paginationValidatorBuilder (tags: string[] = []) {
16 logger.debug('Checking pagination parameters', { parameters: req.query }) 10 return [
11 query('start')
12 .optional()
13 .isInt({ min: 0 }).withMessage('Should have a number start'),
14 query('count')
15 .optional()
16 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
17 17
18 if (areValidationErrors(req, res)) return 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking pagination parameters', { parameters: req.query, tags })
19 20
20 return next() 21 if (areValidationErrors(req, res)) return
21 } 22
22] 23 return next()
24 }
25 ]
26}
23 27
24// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
25 29
26export { 30export {
27 paginationValidator 31 paginationValidator,
32 paginationValidatorBuilder
28} 33}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index e93ceb200..beecc155b 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
28 28
29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ])
32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) 32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts
index 2899bed6f..4167f6d43 100644
--- a/server/middlewares/validators/utils.ts
+++ b/server/middlewares/validators/utils.ts
@@ -17,12 +17,12 @@ function areValidationErrors (req: express.Request, res: express.Response) {
17 return false 17 return false
18} 18}
19 19
20function checkSort (sortableColumns: string[]) { 20function checkSort (sortableColumns: string[], tags: string[] = []) {
21 return [ 21 return [
22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'), 22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'),
23 23
24 (req: express.Request, res: express.Response, next: express.NextFunction) => { 24 (req: express.Request, res: express.Response, next: express.NextFunction) => {
25 logger.debug('Checking sort parameters', { parameters: req.query }) 25 logger.debug('Checking sort parameters', { parameters: req.query, tags })
26 26
27 if (areValidationErrors(req, res)) return 27 if (areValidationErrors(req, res)) return
28 28
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 226c9d436..1afacfed8 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -216,7 +216,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
216 if (!acceptedResult || acceptedResult.accepted !== true) { 216 if (!acceptedResult || acceptedResult.accepted !== true) {
217 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 217 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
218 res.status(HttpStatusCode.FORBIDDEN_403) 218 res.status(HttpStatusCode.FORBIDDEN_403)
219 .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) 219 .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
220 220
221 return false 221 return false
222 } 222 }
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 0fba4f5fd..c872d045e 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -29,7 +29,7 @@ import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoP
29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
31import { MVideoPlaylist } from '../../../types/models/video/video-playlist' 31import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
32import { authenticatePromiseIfNeeded } from '../../oauth' 32import { authenticatePromiseIfNeeded } from '../../auth'
33import { areValidationErrors } from '../utils' 33import { areValidationErrors } from '../utils'
34 34
35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 37cc07b94..4d31d3dcb 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation'
54import { Hooks } from '../../../lib/plugins/hooks' 54import { Hooks } from '../../../lib/plugins/hooks'
55import { AccountModel } from '../../../models/account/account' 55import { AccountModel } from '../../../models/account/account'
56import { VideoModel } from '../../../models/video/video' 56import { VideoModel } from '../../../models/video/video'
57import { authenticatePromiseIfNeeded } from '../../oauth' 57import { authenticatePromiseIfNeeded } from '../../auth'
58import { areValidationErrors } from '../utils' 58import { areValidationErrors } from '../utils'
59 59
60const videosAddValidator = getCommonVideoEditAttributes().concat([ 60const videosAddValidator = getCommonVideoEditAttributes().concat([
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index ebab8b6d2..138051528 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -12,10 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
15import { MNotificationSettingFormattable } from '@server/types/models' 16import { MNotificationSettingFormattable } from '@server/types/models'
16import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 17import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' 18import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { clearCacheByUserId } from '../../lib/oauth-model'
19import { throwIfNotValid } from '../utils' 19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user' 20import { UserModel } from './user'
21 21
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
156 @Column 156 @Column
157 abuseNewMessage: UserNotificationSettingValue 157 abuseNewMessage: UserNotificationSettingValue
158 158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
159 @ForeignKey(() => UserModel) 177 @ForeignKey(() => UserModel)
160 @Column 178 @Column
161 userId: number 179 userId: number
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model {
177 @AfterUpdate 195 @AfterUpdate
178 @AfterDestroy 196 @AfterDestroy
179 static removeTokenCache (instance: UserNotificationSettingModel) { 197 static removeTokenCache (instance: UserNotificationSettingModel) {
180 return clearCacheByUserId(instance.userId) 198 return TokensCache.Instance.clearCacheByUserId(instance.userId)
181 } 199 }
182 200
183 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { 201 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
195 newInstanceFollower: this.newInstanceFollower, 213 newInstanceFollower: this.newInstanceFollower,
196 autoInstanceFollowing: this.autoInstanceFollowing, 214 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage, 215 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange 216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
199 } 219 }
200 } 220 }
201} 221}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index add129644..25c523203 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -9,7 +9,9 @@ import { VideoAbuseModel } from '../abuse/video-abuse'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
11import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
12import { ApplicationModel } from '../application/application'
12import { AvatarModel } from '../avatar/avatar' 13import { AvatarModel } from '../avatar/avatar'
14import { PluginModel } from '../server/plugin'
13import { ServerModel } from '../server/server' 15import { ServerModel } from '../server/server'
14import { getSort, throwIfNotValid } from '../utils' 16import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video' 17import { VideoModel } from '../video/video'
@@ -96,7 +98,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
96 attributes: [ 'id' ], 98 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(), 99 model: VideoAbuseModel.unscoped(),
98 required: false, 100 required: false,
99 include: [ buildVideoInclude(true) ] 101 include: [ buildVideoInclude(false) ]
100 }, 102 },
101 { 103 {
102 attributes: [ 'id' ], 104 attributes: [ 'id' ],
@@ -106,12 +108,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
106 { 108 {
107 attributes: [ 'id', 'originCommentId' ], 109 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel.unscoped(), 110 model: VideoCommentModel.unscoped(),
109 required: true, 111 required: false,
110 include: [ 112 include: [
111 { 113 {
112 attributes: [ 'id', 'name', 'uuid' ], 114 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(), 115 model: VideoModel.unscoped(),
114 required: true 116 required: false
115 } 117 }
116 ] 118 ]
117 } 119 }
@@ -120,7 +122,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
120 { 122 {
121 model: AccountModel, 123 model: AccountModel,
122 as: 'FlaggedAccount', 124 as: 'FlaggedAccount',
123 required: true, 125 required: false,
124 include: [ buildActorWithAvatarInclude() ] 126 include: [ buildActorWithAvatarInclude() ]
125 } 127 }
126 ] 128 ]
@@ -141,6 +143,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
141 }, 143 },
142 144
143 { 145 {
146 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
147 model: PluginModel.unscoped(),
148 required: false
149 },
150
151 {
152 attributes: [ 'id', 'latestPeerTubeVersion' ],
153 model: ApplicationModel.unscoped(),
154 required: false
155 },
156
157 {
144 attributes: [ 'id', 'state' ], 158 attributes: [ 'id', 'state' ],
145 model: ActorFollowModel.unscoped(), 159 model: ActorFollowModel.unscoped(),
146 required: false, 160 required: false,
@@ -251,6 +265,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
251 [Op.ne]: null 265 [Op.ne]: null
252 } 266 }
253 } 267 }
268 },
269 {
270 fields: [ 'pluginId' ],
271 where: {
272 pluginId: {
273 [Op.ne]: null
274 }
275 }
276 },
277 {
278 fields: [ 'applicationId' ],
279 where: {
280 applicationId: {
281 [Op.ne]: null
282 }
283 }
254 } 284 }
255 ] as (ModelIndexesOptions & { where?: WhereOptions })[] 285 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
256}) 286})
@@ -370,6 +400,30 @@ export class UserNotificationModel extends Model {
370 }) 400 })
371 ActorFollow: ActorFollowModel 401 ActorFollow: ActorFollowModel
372 402
403 @ForeignKey(() => PluginModel)
404 @Column
405 pluginId: number
406
407 @BelongsTo(() => PluginModel, {
408 foreignKey: {
409 allowNull: true
410 },
411 onDelete: 'cascade'
412 })
413 Plugin: PluginModel
414
415 @ForeignKey(() => ApplicationModel)
416 @Column
417 applicationId: number
418
419 @BelongsTo(() => ApplicationModel, {
420 foreignKey: {
421 allowNull: true
422 },
423 onDelete: 'cascade'
424 })
425 Application: ApplicationModel
426
373 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { 427 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
374 const where = { userId } 428 const where = { userId }
375 429
@@ -524,6 +578,18 @@ export class UserNotificationModel extends Model {
524 } 578 }
525 : undefined 579 : undefined
526 580
581 const plugin = this.Plugin
582 ? {
583 name: this.Plugin.name,
584 type: this.Plugin.type,
585 latestVersion: this.Plugin.latestVersion
586 }
587 : undefined
588
589 const peertube = this.Application
590 ? { latestVersion: this.Application.latestPeerTubeVersion }
591 : undefined
592
527 return { 593 return {
528 id: this.id, 594 id: this.id,
529 type: this.type, 595 type: this.type,
@@ -535,6 +601,8 @@ export class UserNotificationModel extends Model {
535 videoBlacklist, 601 videoBlacklist,
536 account, 602 account,
537 actorFollow, 603 actorFollow,
604 plugin,
605 peertube,
538 createdAt: this.createdAt.toISOString(), 606 createdAt: this.createdAt.toISOString(),
539 updatedAt: this.updatedAt.toISOString() 607 updatedAt: this.updatedAt.toISOString()
540 } 608 }
@@ -553,17 +621,19 @@ export class UserNotificationModel extends Model {
553 ? { 621 ? {
554 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), 622 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
555 623
556 video: { 624 video: abuse.VideoCommentAbuse.VideoComment.Video
557 id: abuse.VideoCommentAbuse.VideoComment.Video.id, 625 ? {
558 name: abuse.VideoCommentAbuse.VideoComment.Video.name, 626 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
559 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid 627 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
560 } 628 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
629 }
630 : undefined
561 } 631 }
562 : undefined 632 : undefined
563 633
564 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined 634 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
565 635
566 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined 636 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
567 637
568 return { 638 return {
569 id: abuse.id, 639 id: abuse.id,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index c1f22b76a..a7a65c489 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -21,6 +21,7 @@ import {
21 Table, 21 Table,
22 UpdatedAt 22 UpdatedAt
23} from 'sequelize-typescript' 23} from 'sequelize-typescript'
24import { TokensCache } from '@server/lib/auth/tokens-cache'
24import { 25import {
25 MMyUserFormattable, 26 MMyUserFormattable,
26 MUser, 27 MUser,
@@ -58,7 +59,6 @@ import {
58} from '../../helpers/custom-validators/users' 59} from '../../helpers/custom-validators/users'
59import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 60import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
60import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 61import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
61import { clearCacheByUserId } from '../../lib/oauth-model'
62import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 62import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
63import { ActorModel } from '../activitypub/actor' 63import { ActorModel } from '../activitypub/actor'
64import { ActorFollowModel } from '../activitypub/actor-follow' 64import { ActorFollowModel } from '../activitypub/actor-follow'
@@ -411,7 +411,7 @@ export class UserModel extends Model {
411 @AfterUpdate 411 @AfterUpdate
412 @AfterDestroy 412 @AfterDestroy
413 static removeTokenCache (instance: UserModel) { 413 static removeTokenCache (instance: UserModel) {
414 return clearCacheByUserId(instance.id) 414 return TokensCache.Instance.clearCacheByUserId(instance.id)
415 } 415 }
416 416
417 static countTotal () { 417 static countTotal () {
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 909569de1..21f8b1cbc 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
32 @Column 32 @Column
33 migrationVersion: number 33 migrationVersion: number
34 34
35 @AllowNull(true)
36 @Column
37 latestPeerTubeVersion: string
38
35 @HasOne(() => AccountModel, { 39 @HasOne(() => AccountModel, {
36 foreignKey: { 40 foreignKey: {
37 allowNull: true 41 allowNull: true
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 6bc6cf27c..27e643aa7 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -12,9 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
16import { MUserAccountId } from '@server/types/models'
15import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 17import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
16import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
17import { clearCacheByToken } from '../../lib/oauth-model'
18import { AccountModel } from '../account/account' 19import { AccountModel } from '../account/account'
19import { UserModel } from '../account/user' 20import { UserModel } from '../account/user'
20import { ActorModel } from '../activitypub/actor' 21import { ActorModel } from '../activitypub/actor'
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = {
26 client: { 27 client: {
27 id: number 28 id: number
28 } 29 }
29 user: { 30 user: MUserAccountId
30 id: number
31 }
32 token: MOAuthTokenUser 31 token: MOAuthTokenUser
33} 32}
34 33
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model {
133 @AfterUpdate 132 @AfterUpdate
134 @AfterDestroy 133 @AfterDestroy
135 static removeTokenCache (token: OAuthTokenModel) { 134 static removeTokenCache (token: OAuthTokenModel) {
136 return clearCacheByToken(token.accessToken) 135 return TokensCache.Instance.clearCacheByToken(token.accessToken)
137 } 136 }
138 137
139 static loadByRefreshToken (refreshToken: string) { 138 static loadByRefreshToken (refreshToken: string) {
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model {
206 } 205 }
207 206
208 static deleteUserToken (userId: number, t?: Transaction) { 207 static deleteUserToken (userId: number, t?: Transaction) {
208 TokensCache.Instance.deleteUserToken(userId)
209
209 const query = { 210 const query = {
210 where: { 211 where: {
211 userId 212 userId
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index 8bde54a40..364b53e0f 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -8,6 +8,8 @@ import {
8 cleanupTests, 8 cleanupTests,
9 closeAllSequelize, 9 closeAllSequelize,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 killallServers,
12 reRunServer,
11 ServerInfo, 13 ServerInfo,
12 setActorField, 14 setActorField,
13 wait 15 wait
@@ -20,21 +22,32 @@ import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activi
20const expect = chai.expect 22const expect = chai.expect
21 23
22function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { 24function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
25 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
26
23 return Promise.all([ 27 return Promise.all([
24 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey), 28 setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey),
25 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey) 29 setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey)
26 ]) 30 ])
27} 31}
28 32
29function getAnnounceWithoutContext (server2: ServerInfo) { 33function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) {
34 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
35
36 return Promise.all([
37 setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt),
38 setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt)
39 ])
40}
41
42function getAnnounceWithoutContext (server: ServerInfo) {
30 const json = require('./json/peertube/announce-without-context.json') 43 const json = require('./json/peertube/announce-without-context.json')
31 const result: typeof json = {} 44 const result: typeof json = {}
32 45
33 for (const key of Object.keys(json)) { 46 for (const key of Object.keys(json)) {
34 if (Array.isArray(json[key])) { 47 if (Array.isArray(json[key])) {
35 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`)) 48 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
36 } else { 49 } else {
37 result[key] = json[key].replace(':9002', `:${server2.port}`) 50 result[key] = json[key].replace(':9002', `:${server.port}`)
38 } 51 }
39 } 52 }
40 53
@@ -64,7 +77,8 @@ describe('Test ActivityPub security', function () {
64 77
65 url = servers[0].url + '/inbox' 78 url = servers[0].url + '/inbox'
66 79
67 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) 80 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
81 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
68 82
69 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' } 83 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
70 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey } 84 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
@@ -79,9 +93,12 @@ describe('Test ActivityPub security', function () {
79 Digest: buildDigest({ hello: 'coucou' }) 93 Digest: buildDigest({ hello: 'coucou' })
80 } 94 }
81 95
82 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 96 try {
83 97 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
84 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 98 expect(true, 'Did not throw').to.be.false
99 } catch (err) {
100 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
101 }
85 }) 102 })
86 103
87 it('Should fail with an invalid date', async function () { 104 it('Should fail with an invalid date', async function () {
@@ -89,9 +106,12 @@ describe('Test ActivityPub security', function () {
89 const headers = buildGlobalHeaders(body) 106 const headers = buildGlobalHeaders(body)
90 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' 107 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
91 108
92 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 109 try {
93 110 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
94 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 111 expect(true, 'Did not throw').to.be.false
112 } catch (err) {
113 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
114 }
95 }) 115 })
96 116
97 it('Should fail with bad keys', async function () { 117 it('Should fail with bad keys', async function () {
@@ -101,9 +121,12 @@ describe('Test ActivityPub security', function () {
101 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 121 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
102 const headers = buildGlobalHeaders(body) 122 const headers = buildGlobalHeaders(body)
103 123
104 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 124 try {
105 125 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
106 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 126 expect(true, 'Did not throw').to.be.false
127 } catch (err) {
128 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
129 }
107 }) 130 })
108 131
109 it('Should reject requests without appropriate signed headers', async function () { 132 it('Should reject requests without appropriate signed headers', async function () {
@@ -123,8 +146,12 @@ describe('Test ActivityPub security', function () {
123 for (const badHeaders of badHeadersMatrix) { 146 for (const badHeaders of badHeadersMatrix) {
124 signatureOptions.headers = badHeaders 147 signatureOptions.headers = badHeaders
125 148
126 const { response } = await makePOSTAPRequest(url, body, signatureOptions, headers) 149 try {
127 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 150 await makePOSTAPRequest(url, body, signatureOptions, headers)
151 expect(true, 'Did not throw').to.be.false
152 } catch (err) {
153 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
154 }
128 } 155 }
129 }) 156 })
130 157
@@ -132,27 +159,32 @@ describe('Test ActivityPub security', function () {
132 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 159 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
133 const headers = buildGlobalHeaders(body) 160 const headers = buildGlobalHeaders(body)
134 161
135 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 162 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
136 163 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
137 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
138 }) 164 })
139 165
140 it('Should refresh the actor keys', async function () { 166 it('Should refresh the actor keys', async function () {
141 this.timeout(20000) 167 this.timeout(20000)
142 168
143 // Wait refresh invalidation
144 await wait(10000)
145
146 // Update keys of server 2 to invalid keys 169 // Update keys of server 2 to invalid keys
147 // Server 1 should refresh the actor and fail 170 // Server 1 should refresh the actor and fail
148 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) 171 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
172 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
173
174 // Invalid peertube actor cache
175 killallServers([ servers[1] ])
176 await reRunServer(servers[1])
149 177
150 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 178 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
151 const headers = buildGlobalHeaders(body) 179 const headers = buildGlobalHeaders(body)
152 180
153 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 181 try {
154 182 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
155 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 183 expect(true, 'Did not throw').to.be.false
184 } catch (err) {
185 console.error(err)
186 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
187 }
156 }) 188 })
157 }) 189 })
158 190
@@ -183,9 +215,12 @@ describe('Test ActivityPub security', function () {
183 215
184 const headers = buildGlobalHeaders(signedBody) 216 const headers = buildGlobalHeaders(signedBody)
185 217
186 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 218 try {
187 219 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
188 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 220 expect(true, 'Did not throw').to.be.false
221 } catch (err) {
222 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
223 }
189 }) 224 })
190 225
191 it('Should fail with an altered body', async function () { 226 it('Should fail with an altered body', async function () {
@@ -204,9 +239,12 @@ describe('Test ActivityPub security', function () {
204 239
205 const headers = buildGlobalHeaders(signedBody) 240 const headers = buildGlobalHeaders(signedBody)
206 241
207 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 242 try {
208 243 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
209 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 244 expect(true, 'Did not throw').to.be.false
245 } catch (err) {
246 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
247 }
210 }) 248 })
211 249
212 it('Should succeed with a valid signature', async function () { 250 it('Should succeed with a valid signature', async function () {
@@ -220,9 +258,8 @@ describe('Test ActivityPub security', function () {
220 258
221 const headers = buildGlobalHeaders(signedBody) 259 const headers = buildGlobalHeaders(signedBody)
222 260
223 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 261 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
224 262 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
225 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
226 }) 263 })
227 264
228 it('Should refresh the actor keys', async function () { 265 it('Should refresh the actor keys', async function () {
@@ -243,9 +280,12 @@ describe('Test ActivityPub security', function () {
243 280
244 const headers = buildGlobalHeaders(signedBody) 281 const headers = buildGlobalHeaders(signedBody)
245 282
246 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 283 try {
247 284 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
248 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 285 expect(true, 'Did not throw').to.be.false
286 } catch (err) {
287 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
288 }
249 }) 289 })
250 }) 290 })
251 291
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 05a78b0ad..26d4423f9 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () {
176 newInstanceFollower: UserNotificationSettingValue.WEB, 176 newInstanceFollower: UserNotificationSettingValue.WEB,
177 autoInstanceFollowing: UserNotificationSettingValue.WEB, 177 autoInstanceFollowing: UserNotificationSettingValue.WEB,
178 abuseNewMessage: UserNotificationSettingValue.WEB, 178 abuseNewMessage: UserNotificationSettingValue.WEB,
179 abuseStateChange: UserNotificationSettingValue.WEB 179 abuseStateChange: UserNotificationSettingValue.WEB,
180 newPeerTubeVersion: UserNotificationSettingValue.WEB,
181 newPluginVersion: UserNotificationSettingValue.WEB
180 } 182 }
181 183
182 it('Should fail with missing fields', async function () { 184 it('Should fail with missing fields', async function () {
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 0a13f5b67..2b03fde2d 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -241,7 +241,7 @@ describe('Test users API validators', function () {
241 }) 241 })
242 242
243 it('Should succeed with no password on a server with smtp enabled', async function () { 243 it('Should succeed with no password on a server with smtp enabled', async function () {
244 this.timeout(10000) 244 this.timeout(20000)
245 245
246 killallServers([ server ]) 246 killallServers([ server ])
247 247
diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts
new file mode 100644
index 000000000..e07327d74
--- /dev/null
+++ b/server/tests/api/notifications/admin-notifications.ts
@@ -0,0 +1,165 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
6import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
7import { ServerInfo } from '../../../../shared/extra-utils/index'
8import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
9import {
10 CheckerBaseParams,
11 checkNewPeerTubeVersion,
12 checkNewPluginVersion,
13 prepareNotificationsTest
14} from '../../../../shared/extra-utils/users/user-notifications'
15import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
16import { PluginType } from '@shared/models'
17
18describe('Test admin notifications', function () {
19 let server: ServerInfo
20 let userNotifications: UserNotification[] = []
21 let adminNotifications: UserNotification[] = []
22 let emails: object[] = []
23 let baseParams: CheckerBaseParams
24 let joinPeerTubeServer: MockJoinPeerTubeVersions
25
26 before(async function () {
27 this.timeout(120000)
28
29 const config = {
30 peertube: {
31 check_latest_version: {
32 enabled: true,
33 url: 'http://localhost:42102/versions.json'
34 }
35 },
36 plugins: {
37 index: {
38 enabled: true,
39 check_latest_versions_interval: '5 seconds'
40 }
41 }
42 }
43
44 const res = await prepareNotificationsTest(1, config)
45 emails = res.emails
46 server = res.servers[0]
47
48 userNotifications = res.userNotifications
49 adminNotifications = res.adminNotifications
50
51 baseParams = {
52 server: server,
53 emails,
54 socketNotifications: adminNotifications,
55 token: server.accessToken
56 }
57
58 await installPlugin({
59 url: server.url,
60 accessToken: server.accessToken,
61 npmName: 'peertube-plugin-hello-world'
62 })
63
64 await installPlugin({
65 url: server.url,
66 accessToken: server.accessToken,
67 npmName: 'peertube-theme-background-red'
68 })
69
70 joinPeerTubeServer = new MockJoinPeerTubeVersions()
71 await joinPeerTubeServer.initialize()
72 })
73
74 describe('Latest PeerTube version notification', function () {
75
76 it('Should not send a notification to admins if there is not a new version', async function () {
77 this.timeout(30000)
78
79 joinPeerTubeServer.setLatestVersion('1.4.2')
80
81 await wait(3000)
82 await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
83 })
84
85 it('Should send a notification to admins on new plugin version', async function () {
86 this.timeout(30000)
87
88 joinPeerTubeServer.setLatestVersion('15.4.2')
89
90 await wait(3000)
91 await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
92 })
93
94 it('Should not send the same notification to admins', async function () {
95 this.timeout(30000)
96
97 await wait(3000)
98 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
99 })
100
101 it('Should not have sent a notification to users', async function () {
102 this.timeout(30000)
103
104 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
105 })
106
107 it('Should send a new notification after a new release', async function () {
108 this.timeout(30000)
109
110 joinPeerTubeServer.setLatestVersion('15.4.3')
111
112 await wait(3000)
113 await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
114 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
115 })
116 })
117
118 describe('Latest plugin version notification', function () {
119
120 it('Should not send a notification to admins if there is no new plugin version', async function () {
121 this.timeout(30000)
122
123 await wait(6000)
124 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
125 })
126
127 it('Should send a notification to admins on new plugin version', async function () {
128 this.timeout(30000)
129
130 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
131 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
132 await wait(6000)
133
134 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
135 })
136
137 it('Should not send the same notification to admins', async function () {
138 this.timeout(30000)
139
140 await wait(6000)
141
142 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
143 })
144
145 it('Should not have sent a notification to users', async function () {
146 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
147 })
148
149 it('Should send a new notification after a new plugin release', async function () {
150 this.timeout(30000)
151
152 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
153 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
154 await wait(6000)
155
156 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
157 })
158 })
159
160 after(async function () {
161 MockSmtpServer.Instance.kill()
162
163 await cleanupTests([ server ])
164 })
165})
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts
index bd07a339e..8caa30a3d 100644
--- a/server/tests/api/notifications/index.ts
+++ b/server/tests/api/notifications/index.ts
@@ -1,3 +1,4 @@
1import './admin-notifications'
1import './comments-notifications' 2import './comments-notifications'
2import './moderation-notifications' 3import './moderation-notifications'
3import './notifications-api' 4import './notifications-api'
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index 043754e70..f3ba11950 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -348,8 +348,8 @@ describe('Test handle downs', function () {
348 348
349 for (let i = 0; i < 3; i++) { 349 for (let i = 0; i < 3; i++) {
350 await getVideo(servers[1].url, videoIdsServer1[i]) 350 await getVideo(servers[1].url, videoIdsServer1[i])
351 await wait(1000)
352 await waitJobs([ servers[1] ]) 351 await waitJobs([ servers[1] ])
352 await wait(1500)
353 } 353 }
354 354
355 for (const id of videoIdsServer1) { 355 for (const id of videoIdsServer1) {
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 62a59033f..cea98aac7 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -4,10 +4,12 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' 5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
6import { CustomConfig } from '@shared/models/server' 6import { CustomConfig } from '@shared/models/server'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { 8import {
8 addVideoCommentThread, 9 addVideoCommentThread,
9 blockUser, 10 blockUser,
10 cleanupTests, 11 cleanupTests,
12 closeAllSequelize,
11 createUser, 13 createUser,
12 deleteMe, 14 deleteMe,
13 flushAndRunServer, 15 flushAndRunServer,
@@ -24,6 +26,7 @@ import {
24 getVideoChannel, 26 getVideoChannel,
25 getVideosList, 27 getVideosList,
26 installPlugin, 28 installPlugin,
29 killallServers,
27 login, 30 login,
28 makePutBodyRequest, 31 makePutBodyRequest,
29 rateVideo, 32 rateVideo,
@@ -31,7 +34,9 @@ import {
31 removeUser, 34 removeUser,
32 removeVideo, 35 removeVideo,
33 reportAbuse, 36 reportAbuse,
37 reRunServer,
34 ServerInfo, 38 ServerInfo,
39 setTokenField,
35 testImage, 40 testImage,
36 unblockUser, 41 unblockUser,
37 updateAbuse, 42 updateAbuse,
@@ -44,10 +49,9 @@ import {
44 waitJobs 49 waitJobs
45} from '../../../../shared/extra-utils' 50} from '../../../../shared/extra-utils'
46import { follow } from '../../../../shared/extra-utils/server/follows' 51import { follow } from '../../../../shared/extra-utils/server/follows'
47import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 52import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
48import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 53import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 54import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
51 55
52const expect = chai.expect 56const expect = chai.expect
53 57
@@ -89,6 +93,7 @@ describe('Test users', function () {
89 const client = { id: 'client', secret: server.client.secret } 93 const client = { id: 'client', secret: server.client.secret }
90 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 94 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
91 95
96 expect(res.body.code).to.equal('invalid_client')
92 expect(res.body.error).to.contain('client is invalid') 97 expect(res.body.error).to.contain('client is invalid')
93 }) 98 })
94 99
@@ -96,6 +101,7 @@ describe('Test users', function () {
96 const client = { id: server.client.id, secret: 'coucou' } 101 const client = { id: server.client.id, secret: 'coucou' }
97 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 102 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
98 103
104 expect(res.body.code).to.equal('invalid_client')
99 expect(res.body.error).to.contain('client is invalid') 105 expect(res.body.error).to.contain('client is invalid')
100 }) 106 })
101 }) 107 })
@@ -106,6 +112,7 @@ describe('Test users', function () {
106 const user = { username: 'captain crochet', password: server.user.password } 112 const user = { username: 'captain crochet', password: server.user.password }
107 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 113 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
108 114
115 expect(res.body.code).to.equal('invalid_grant')
109 expect(res.body.error).to.contain('credentials are invalid') 116 expect(res.body.error).to.contain('credentials are invalid')
110 }) 117 })
111 118
@@ -113,6 +120,7 @@ describe('Test users', function () {
113 const user = { username: server.user.username, password: 'mew_three' } 120 const user = { username: server.user.username, password: 'mew_three' }
114 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 121 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
115 122
123 expect(res.body.code).to.equal('invalid_grant')
116 expect(res.body.error).to.contain('credentials are invalid') 124 expect(res.body.error).to.contain('credentials are invalid')
117 }) 125 })
118 126
@@ -245,12 +253,44 @@ describe('Test users', function () {
245 }) 253 })
246 254
247 it('Should be able to login again', async function () { 255 it('Should be able to login again', async function () {
248 server.accessToken = await serverLogin(server) 256 const res = await login(server.url, server.client, server.user)
257 server.accessToken = res.body.access_token
258 server.refreshToken = res.body.refresh_token
259 })
260
261 it('Should be able to get my user information again', async function () {
262 await getMyUserInformation(server.url, server.accessToken)
263 })
264
265 it('Should have an expired access token', async function () {
266 this.timeout(15000)
267
268 await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
269 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
270
271 killallServers([ server ])
272 await reRunServer(server)
273
274 await getMyUserInformation(server.url, server.accessToken, 401)
275 })
276
277 it('Should not be able to refresh an access token with an expired refresh token', async function () {
278 await refreshToken(server, server.refreshToken, 400)
249 }) 279 })
250 280
251 it('Should have an expired access token') 281 it('Should refresh the token', async function () {
282 this.timeout(15000)
283
284 const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
285 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate)
252 286
253 it('Should refresh the token') 287 killallServers([ server ])
288 await reRunServer(server)
289
290 const res = await refreshToken(server, server.refreshToken)
291 server.accessToken = res.body.access_token
292 server.refreshToken = res.body.refresh_token
293 })
254 294
255 it('Should be able to get my user information again', async function () { 295 it('Should be able to get my user information again', async function () {
256 await getMyUserInformation(server.url, server.accessToken) 296 await getMyUserInformation(server.url, server.accessToken)
@@ -976,6 +1016,7 @@ describe('Test users', function () {
976 }) 1016 })
977 1017
978 after(async function () { 1018 after(async function () {
1019 await closeAllSequelize([ server ])
979 await cleanupTests([ server ]) 1020 await cleanupTests([ server ])
980 }) 1021 })
981}) 1022})
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts
index 242589010..7e6eebd17 100644
--- a/server/tests/cli/index.ts
+++ b/server/tests/cli/index.ts
@@ -6,5 +6,6 @@ import './peertube'
6import './plugins' 6import './plugins'
7import './print-transcode-command' 7import './print-transcode-command'
8import './prune-storage' 8import './prune-storage'
9import './regenerate-thumbnails'
9import './reset-password' 10import './reset-password'
10import './update-host' 11import './update-host'
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts
new file mode 100644
index 000000000..56005518a
--- /dev/null
+++ b/server/tests/cli/regenerate-thumbnails.ts
@@ -0,0 +1,110 @@
1import 'mocha'
2import { expect } from 'chai'
3import { writeFile } from 'fs-extra'
4import { basename, join } from 'path'
5import { Video } from '@shared/models'
6import {
7 buildServerDirectory,
8 cleanupTests,
9 doubleFollow,
10 execCLI,
11 flushAndRunMultipleServers,
12 getEnvCli,
13 getVideo,
14 makeRawRequest,
15 ServerInfo,
16 setAccessTokensToServers,
17 uploadVideoAndGetId,
18 waitJobs
19} from '../../../shared/extra-utils'
20import { HttpStatusCode } from '@shared/core-utils'
21
22describe('Test regenerate thumbnails script', function () {
23 let servers: ServerInfo[]
24
25 let video1: Video
26 let video2: Video
27 let remoteVideo: Video
28
29 let thumbnail1Path: string
30 let thumbnailRemotePath: string
31
32 before(async function () {
33 this.timeout(60000)
34
35 servers = await flushAndRunMultipleServers(2)
36 await setAccessTokensToServers(servers)
37
38 await doubleFollow(servers[0], servers[1])
39
40 {
41 const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid
42 video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body))
43
44 thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath))
45
46 const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid
47 video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body))
48 }
49
50 {
51 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid
52 await waitJobs(servers)
53
54 remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body))
55
56 thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath))
57 }
58
59 await writeFile(thumbnail1Path, '')
60 await writeFile(thumbnailRemotePath, '')
61 })
62
63 it('Should have empty thumbnails', async function () {
64 {
65 const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200)
66 expect(res.body).to.have.lengthOf(0)
67 }
68
69 {
70 const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200)
71 expect(res.body).to.not.have.lengthOf(0)
72 }
73
74 {
75 const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
76 expect(res.body).to.have.lengthOf(0)
77 }
78 })
79
80 it('Should regenerate thumbnails from the CLI', async function () {
81 this.timeout(15000)
82
83 const env = getEnvCli(servers[0])
84 await execCLI(`${env} npm run regenerate-thumbnails`)
85 })
86
87 it('Should have regenerated thumbbnails', async function () {
88 {
89 const res1 = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200)
90 expect(res1.body).to.not.have.lengthOf(0)
91
92 const res2 = await makeRawRequest(join(servers[0].url, video1.previewPath), HttpStatusCode.OK_200)
93 expect(res2.body).to.not.have.lengthOf(0)
94 }
95
96 {
97 const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200)
98 expect(res.body).to.not.have.lengthOf(0)
99 }
100
101 {
102 const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
103 expect(res.body).to.have.lengthOf(0)
104 }
105 })
106
107 after(async function () {
108 await cleanupTests(servers)
109 })
110})
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js
index 305d92002..ee0bc39f3 100644
--- a/server/tests/fixtures/peertube-plugin-test/main.js
+++ b/server/tests/fixtures/peertube-plugin-test/main.js
@@ -184,6 +184,76 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
184 return result 184 return result
185 } 185 }
186 }) 186 })
187
188 registerHook({
189 target: 'filter:api.download.torrent.allowed.result',
190 handler: (result, params) => {
191 if (params && params.downloadName.includes('bad torrent')) {
192 return { allowed: false, errorMessage: 'Liu Bei' }
193 }
194
195 return result
196 }
197 })
198
199 registerHook({
200 target: 'filter:api.download.video.allowed.result',
201 handler: (result, params) => {
202 if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) {
203 return { allowed: false, errorMessage: 'Cao Cao' }
204 }
205
206 if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) {
207 return { allowed: false, errorMessage: 'Sun Jian' }
208 }
209
210 return result
211 }
212 })
213
214 registerHook({
215 target: 'filter:html.embed.video.allowed.result',
216 handler: (result, params) => {
217 return {
218 allowed: false,
219 html: 'Lu Bu'
220 }
221 }
222 })
223
224 registerHook({
225 target: 'filter:html.embed.video-playlist.allowed.result',
226 handler: (result, params) => {
227 return {
228 allowed: false,
229 html: 'Diao Chan'
230 }
231 }
232 })
233
234 {
235 const searchHooks = [
236 'filter:api.search.videos.local.list.params',
237 'filter:api.search.videos.local.list.result',
238 'filter:api.search.videos.index.list.params',
239 'filter:api.search.videos.index.list.result',
240 'filter:api.search.video-channels.local.list.params',
241 'filter:api.search.video-channels.local.list.result',
242 'filter:api.search.video-channels.index.list.params',
243 'filter:api.search.video-channels.index.list.result',
244 ]
245
246 for (const h of searchHooks) {
247 registerHook({
248 target: h,
249 handler: (obj) => {
250 peertubeHelpers.logger.debug('Run hook %s.', h)
251
252 return obj
253 }
254 })
255 }
256 }
187} 257}
188 258
189async function unregister () { 259async function unregister () {
diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts
index f8b2d599b..5e77f129e 100644
--- a/server/tests/helpers/request.ts
+++ b/server/tests/helpers/request.ts
@@ -1,11 +1,11 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
5import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
6import { join } from 'path'
7import { pathExists, remove } from 'fs-extra'
8import { expect } from 'chai' 4import { expect } from 'chai'
5import { pathExists, remove } from 'fs-extra'
6import { join } from 'path'
7import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
8import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
9 9
10describe('Request helpers', function () { 10describe('Request helpers', function () {
11 const destPath1 = join(root(), 'test-output-1.txt') 11 const destPath1 = join(root(), 'test-output-1.txt')
@@ -13,7 +13,7 @@ describe('Request helpers', function () {
13 13
14 it('Should throw an error when the bytes limit is exceeded for request', async function () { 14 it('Should throw an error when the bytes limit is exceeded for request', async function () {
15 try { 15 try {
16 await doRequest({ uri: get4KFileUrl() }, 3) 16 await doRequest(get4KFileUrl(), { bodyKBLimit: 3 })
17 } catch { 17 } catch {
18 return 18 return
19 } 19 }
@@ -23,7 +23,7 @@ describe('Request helpers', function () {
23 23
24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { 24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
25 try { 25 try {
26 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3) 26 await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 })
27 } catch { 27 } catch {
28 28
29 await wait(500) 29 await wait(500)
@@ -35,8 +35,8 @@ describe('Request helpers', function () {
35 }) 35 })
36 36
37 it('Should succeed if the file is below the limit', async function () { 37 it('Should succeed if the file is below the limit', async function () {
38 await doRequest({ uri: get4KFileUrl() }, 5) 38 await doRequest(get4KFileUrl(), { bodyKBLimit: 5 })
39 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5) 39 await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 })
40 40
41 expect(await pathExists(destPath2)).to.be.true 41 expect(await pathExists(destPath2)).to.be.true
42 }) 42 })
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index a1b5e8f5d..5addb45c7 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () {
137 137
138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) 138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
139 139
140 await waitUntilLog(server, 'expired external auth token') 140 await waitUntilLog(server, 'expired external auth token', 2)
141 }) 141 })
142 142
143 it('Should auto login Cyan, create the user and use the token', async function () { 143 it('Should auto login Cyan, create the user and use the token', async function () {
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts
index d88170201..ac958c5f5 100644
--- a/server/tests/plugins/filter-hooks.ts
+++ b/server/tests/plugins/filter-hooks.ts
@@ -2,11 +2,15 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
5import { ServerConfig } from '@shared/models' 6import { ServerConfig } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { 8import {
7 addVideoCommentReply, 9 addVideoCommentReply,
8 addVideoCommentThread, 10 addVideoCommentThread,
11 advancedVideosSearch,
9 createLive, 12 createLive,
13 createVideoPlaylist,
10 doubleFollow, 14 doubleFollow,
11 getAccountVideos, 15 getAccountVideos,
12 getConfig, 16 getConfig,
@@ -15,24 +19,33 @@ import {
15 getVideo, 19 getVideo,
16 getVideoChannelVideos, 20 getVideoChannelVideos,
17 getVideoCommentThreads, 21 getVideoCommentThreads,
22 getVideoPlaylist,
18 getVideosList, 23 getVideosList,
19 getVideosListPagination, 24 getVideosListPagination,
20 getVideoThreadComments, 25 getVideoThreadComments,
21 getVideoWithToken, 26 getVideoWithToken,
22 installPlugin, 27 installPlugin,
28 makeRawRequest,
23 registerUser, 29 registerUser,
24 setAccessTokensToServers, 30 setAccessTokensToServers,
25 setDefaultVideoChannel, 31 setDefaultVideoChannel,
26 updateCustomSubConfig, 32 updateCustomSubConfig,
27 updateVideo, 33 updateVideo,
28 uploadVideo, 34 uploadVideo,
35 uploadVideoAndGetId,
29 waitJobs 36 waitJobs
30} from '../../../shared/extra-utils' 37} from '../../../shared/extra-utils'
31import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' 38import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
32import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' 39import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
33import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos' 40import {
41 VideoDetails,
42 VideoImport,
43 VideoImportState,
44 VideoPlaylist,
45 VideoPlaylistPrivacy,
46 VideoPrivacy
47} from '../../../shared/models/videos'
34import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' 48import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
35import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
36 49
37const expect = chai.expect 50const expect = chai.expect
38 51
@@ -355,6 +368,165 @@ describe('Test plugin filter hooks', function () {
355 }) 368 })
356 }) 369 })
357 370
371 describe('Download hooks', function () {
372 const downloadVideos: VideoDetails[] = []
373
374 before(async function () {
375 this.timeout(60000)
376
377 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
378 transcoding: {
379 webtorrent: {
380 enabled: true
381 },
382 hls: {
383 enabled: true
384 }
385 }
386 })
387
388 const uuids: string[] = []
389
390 for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
391 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
392 uuids.push(uuid)
393 }
394
395 await waitJobs(servers)
396
397 for (const uuid of uuids) {
398 const res = await getVideo(servers[0].url, uuid)
399 downloadVideos.push(res.body)
400 }
401 })
402
403 it('Should run filter:api.download.torrent.allowed.result', async function () {
404 const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
405 expect(res.body.error).to.equal('Liu Bei')
406
407 await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
408 await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
409 })
410
411 it('Should run filter:api.download.video.allowed.result', async function () {
412 {
413 const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
414 expect(res.body.error).to.equal('Cao Cao')
415
416 await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
417 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
418 }
419
420 {
421 const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
422 expect(res.body.error).to.equal('Sun Jian')
423
424 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
425
426 await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
427 await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
428 }
429 })
430 })
431
432 describe('Embed filters', function () {
433 const embedVideos: VideoDetails[] = []
434 const embedPlaylists: VideoPlaylist[] = []
435
436 before(async function () {
437 this.timeout(60000)
438
439 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
440 transcoding: {
441 enabled: false
442 }
443 })
444
445 for (const name of [ 'bad embed', 'good embed' ]) {
446 {
447 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
448 const res = await getVideo(servers[0].url, uuid)
449 embedVideos.push(res.body)
450 }
451
452 {
453 const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
454 const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
455
456 const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id)
457 embedPlaylists.push(resPlaylist.body)
458 }
459 }
460 })
461
462 it('Should run filter:html.embed.video.allowed.result', async function () {
463 const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200)
464 expect(res.text).to.equal('Lu Bu')
465 })
466
467 it('Should run filter:html.embed.video-playlist.allowed.result', async function () {
468 const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200)
469 expect(res.text).to.equal('Diao Chan')
470 })
471 })
472
473 describe('Search filters', function () {
474
475 before(async function () {
476 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
477 search: {
478 searchIndex: {
479 enabled: true,
480 isDefaultSearch: false,
481 disableLocalSearch: false
482 }
483 }
484 })
485 })
486
487 it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
488 await advancedVideosSearch(servers[0].url, {
489 search: 'Sun Quan'
490 })
491
492 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
493 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
494 })
495
496 it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
497 await advancedVideosSearch(servers[0].url, {
498 search: 'Sun Quan',
499 searchTarget: 'search-index'
500 })
501
502 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
503 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
504 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1)
505 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1)
506 })
507
508 it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
509 await advancedVideoChannelSearch(servers[0].url, {
510 search: 'Sun Ce'
511 })
512
513 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
514 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
515 })
516
517 it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
518 await advancedVideoChannelSearch(servers[0].url, {
519 search: 'Sun Ce',
520 searchTarget: 'search-index'
521 })
522
523 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
524 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
525 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1)
526 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1)
527 })
528 })
529
358 after(async function () { 530 after(async function () {
359 await cleanupTests(servers) 531 await cleanupTests(servers)
360 }) 532 })
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 9be0834ba..915995031 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -202,10 +202,7 @@ async function uploadVideoOnPeerTube (parameters: {
202 if (videoInfo.thumbnail) { 202 if (videoInfo.thumbnail) {
203 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') 203 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg')
204 204
205 await doRequestAndSaveToFile({ 205 await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile)
206 method: 'GET',
207 uri: videoInfo.thumbnail
208 }, thumbnailfile)
209 } 206 }
210 207
211 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) 208 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
diff --git a/server/types/models/application/application.ts b/server/types/models/application/application.ts
new file mode 100644
index 000000000..9afb9ad70
--- /dev/null
+++ b/server/types/models/application/application.ts
@@ -0,0 +1,5 @@
1import { ApplicationModel } from '@server/models/application/application'
2
3// ############################################################################
4
5export type MApplication = Omit<ApplicationModel, 'Account'>
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts
new file mode 100644
index 000000000..26e4b031f
--- /dev/null
+++ b/server/types/models/application/index.ts
@@ -0,0 +1 @@
export * from './application'
diff --git a/server/types/models/index.ts b/server/types/models/index.ts
index affa17425..b4fdb1ff3 100644
--- a/server/types/models/index.ts
+++ b/server/types/models/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './application'
2export * from './moderation' 3export * from './moderation'
3export * from './oauth' 4export * from './oauth'
4export * from './server' 5export * from './server'
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index 58764a748..6988086f1 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -1,5 +1,7 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' 2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { ApplicationModel } from '@server/models/application/application'
4import { PluginModel } from '@server/models/server/plugin'
3import { PickWith, PickWithOpt } from '@shared/core-utils' 5import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse' 6import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
@@ -85,13 +87,19 @@ export module UserNotificationIncludes {
85 Pick<ActorFollowModel, 'id' | 'state'> & 87 Pick<ActorFollowModel, 'id' | 'state'> &
86 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & 88 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
87 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> 89 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
90
91 export type PluginInclude =
92 Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
93
94 export type ApplicationInclude =
95 Pick<ApplicationModel, 'latestPeerTubeVersion'>
88} 96}
89 97
90// ############################################################################ 98// ############################################################################
91 99
92export type MUserNotification = 100export type MUserNotification =
93 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | 101 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
94 'VideoImport' | 'Account' | 'ActorFollow'> 102 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
95 103
96// ############################################################################ 104// ############################################################################
97 105
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
103 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & 111 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
104 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & 112 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
105 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & 113 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
114 Use<'Plugin', UserNotificationIncludes.PluginInclude> &
115 Use<'Application', UserNotificationIncludes.ApplicationInclude> &
106 Use<'Account', UserNotificationIncludes.AccountIncludeActor> 116 Use<'Account', UserNotificationIncludes.AccountIncludeActor>
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index 66acfb3f5..b0004dc7b 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -17,7 +17,6 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
17import { MVideoImportDefault } from '@server/types/models/video/video-import' 17import { MVideoImportDefault } from '@server/types/models/video/video-import'
18import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' 18import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
19import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' 19import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
20import { UserRole } from '@shared/models'
21import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 20import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
22import { 21import {
23 MAccountDefault, 22 MAccountDefault,
@@ -49,22 +48,6 @@ declare module 'express' {
49} 48}
50 49
51interface PeerTubeLocals { 50interface PeerTubeLocals {
52 bypassLogin?: {
53 bypass: boolean
54 pluginName: string
55 authName?: string
56 user: {
57 username: string
58 email: string
59 displayName: string
60 role: UserRole
61 }
62 }
63
64 refreshTokenAuthName?: string
65
66 explicitLogout?: boolean
67
68 videoAll?: MVideoFullLight 51 videoAll?: MVideoFullLight
69 onlyImmutableVideo?: MVideoImmutable 52 onlyImmutableVideo?: MVideoImmutable
70 onlyVideo?: MVideoThumbnail 53 onlyVideo?: MVideoThumbnail