diff options
Diffstat (limited to 'server')
157 files changed, 3266 insertions, 1465 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index fb108ca1c..e28f7502d 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -158,9 +158,17 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
158 | avatar: { | 158 | avatar: { |
159 | file: { | 159 | file: { |
160 | size: { | 160 | size: { |
161 | max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max | 161 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max |
162 | }, | 162 | }, |
163 | extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | 163 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
164 | } | ||
165 | }, | ||
166 | banner: { | ||
167 | file: { | ||
168 | size: { | ||
169 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
170 | }, | ||
171 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
164 | } | 172 | } |
165 | }, | 173 | }, |
166 | video: { | 174 | video: { |
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' |
15 | import { paginationValidator } from '../../middlewares/validators' | ||
16 | import { listJobsValidator } from '../../middlewares/validators/jobs' | 16 | import { listJobsValidator } from '../../middlewares/validators/jobs' |
17 | 17 | ||
18 | const jobsRouter = express.Router() | 18 | const jobsRouter = express.Router() |
@@ -20,7 +20,7 @@ const jobsRouter = express.Router() | |||
20 | jobsRouter.get('/:state?', | 20 | jobsRouter.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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | 2 | import { sanitizeUrl } from '@server/helpers/core-utils' |
3 | import { doRequest } from '@server/helpers/requests' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' | 5 | import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' |
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | 7 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' |
7 | import { getServerActor } from '@server/models/application/application' | 8 | import { getServerActor } from '@server/models/application/application' |
8 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | 9 | import { 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) { | |||
87 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | 88 | async 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 | |||
107 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | 109 | async 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) { | |||
168 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | 175 | async 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 | ||
199 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | 209 | async 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' | |||
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { tokensRouter } from '@server/controllers/api/users/token' | 3 | import { tokensRouter } from '@server/controllers/api/users/token' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | ||
5 | import { MUser, MUserAccountDefault } from '@server/types/models' | 6 | import { MUser, MUserAccountDefault } from '@server/types/models' |
6 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 7 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 9 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
8 | import { UserRegister } from '../../../../shared/models/users/user-register.model' | 10 | import { UserRegister } from '../../../../shared/models/users/user-register.model' |
9 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' |
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants' | |||
14 | import { sequelizeTypescript } from '../../../initializers/database' | 16 | import { sequelizeTypescript } from '../../../initializers/database' |
15 | import { Emailer } from '../../../lib/emailer' | 17 | import { Emailer } from '../../../lib/emailer' |
16 | import { Notifier } from '../../../lib/notifier' | 18 | import { Notifier } from '../../../lib/notifier' |
17 | import { deleteUserToken } from '../../../lib/oauth-model' | ||
18 | import { Redis } from '../../../lib/redis' | 19 | import { Redis } from '../../../lib/redis' |
19 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' | 20 | import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' |
20 | import { | 21 | import { |
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history' | |||
52 | import { myNotificationsRouter } from './my-notifications' | 53 | import { myNotificationsRouter } from './my-notifications' |
53 | import { mySubscriptionsRouter } from './my-subscriptions' | 54 | import { mySubscriptionsRouter } from './my-subscriptions' |
54 | import { myVideoPlaylistsRouter } from './my-video-playlists' | 55 | import { myVideoPlaylistsRouter } from './my-video-playlists' |
55 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
56 | 56 | ||
57 | const auditLogger = auditLoggerFactory('users') | 57 | const 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/me.ts b/server/controllers/api/users/me.ts index 5a3e9e51a..9f9d2d77f 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -2,7 +2,7 @@ import 'multer' | |||
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' | 3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' | 5 | import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' |
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
8 | import { createReqFiles } from '../../../helpers/express-utils' | 8 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config' | |||
11 | import { MIMETYPES } from '../../../initializers/constants' | 11 | import { MIMETYPES } from '../../../initializers/constants' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 13 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
14 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' | 14 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' |
15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
16 | import { | 16 | import { |
17 | asyncMiddleware, | 17 | asyncMiddleware, |
@@ -25,7 +25,7 @@ import { | |||
25 | usersVideoRatingValidator | 25 | usersVideoRatingValidator |
26 | } from '../../../middlewares' | 26 | } from '../../../middlewares' |
27 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' | 27 | import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' |
28 | import { updateAvatarValidator } from '../../../middlewares/validators/avatar' | 28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' |
29 | import { AccountModel } from '../../../models/account/account' | 29 | import { AccountModel } from '../../../models/account/account' |
30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
31 | import { UserModel } from '../../../models/account/user' | 31 | import { UserModel } from '../../../models/account/user' |
@@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) { | |||
238 | 238 | ||
239 | const userAccount = await AccountModel.load(user.Account.id) | 239 | const userAccount = await AccountModel.load(user.Account.id) |
240 | 240 | ||
241 | const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile) | 241 | const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR) |
242 | 242 | ||
243 | return res.json({ avatar: avatar.toFormattedJSON() }) | 243 | return res.json({ avatar: avatar.toFormattedJSON() }) |
244 | } | 244 | } |
@@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { | |||
247 | const user = res.locals.oauth.token.user | 247 | const user = res.locals.oauth.token.user |
248 | 248 | ||
249 | const userAccount = await AccountModel.load(user.Account.id) | 249 | const userAccount = await AccountModel.load(user.Account.id) |
250 | await deleteLocalActorAvatarFile(userAccount) | 250 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) |
251 | 251 | ||
252 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 252 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
253 | } | 253 | } |
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/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index ec77ddd7a..e8949ee59 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -1,5 +1,8 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
4 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 6 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
4 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 8 | import { getFormattedObjects } from '../../../helpers/utils' |
@@ -26,8 +29,6 @@ import { | |||
26 | } from '../../../middlewares/validators' | 29 | } from '../../../middlewares/validators' |
27 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 30 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
28 | import { VideoModel } from '../../../models/video/video' | 31 | import { VideoModel } from '../../../models/video/video' |
29 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
30 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
31 | 32 | ||
32 | const mySubscriptionsRouter = express.Router() | 33 | const mySubscriptionsRouter = express.Router() |
33 | 34 | ||
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions', | |||
66 | mySubscriptionsRouter.get('/me/subscriptions/:uri', | 67 | mySubscriptionsRouter.get('/me/subscriptions/:uri', |
67 | authenticate, | 68 | authenticate, |
68 | userSubscriptionGetValidator, | 69 | userSubscriptionGetValidator, |
69 | getUserSubscription | 70 | asyncMiddleware(getUserSubscription) |
70 | ) | 71 | ) |
71 | 72 | ||
72 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', | 73 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', |
@@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) { | |||
130 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 131 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
131 | } | 132 | } |
132 | 133 | ||
133 | function getUserSubscription (req: express.Request, res: express.Response) { | 134 | async function getUserSubscription (req: express.Request, res: express.Response) { |
134 | const subscription = res.locals.subscription | 135 | const subscription = res.locals.subscription |
136 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id) | ||
135 | 137 | ||
136 | return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) | 138 | return res.json(videoChannel.toFormattedJSON()) |
137 | } | 139 | } |
138 | 140 | ||
139 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | 141 | async function deleteUserSubscription (req: express.Request, res: express.Response) { |
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 @@ | |||
1 | import { handleLogin, handleTokenRevocation } from '@server/lib/auth' | 1 | import * as express from 'express' |
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { logger } from '@server/helpers/logger' | ||
3 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
4 | import * as express from 'express' | 6 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' |
7 | import { handleOAuthToken } from '@server/lib/auth/oauth' | ||
8 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | 9 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { asyncMiddleware, authenticate } from '@server/middlewares' | 10 | import { asyncMiddleware, authenticate } from '@server/middlewares' |
7 | import { ScopedToken } from '@shared/models/users/user-scoped-token' | 11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' |
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | 12 | ||
10 | const tokensRouter = express.Router() | 13 | const tokensRouter = express.Router() |
11 | 14 | ||
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({ | |||
16 | 19 | ||
17 | tokensRouter.post('/token', | 20 | tokensRouter.post('/token', |
18 | loginRateLimiter, | 21 | loginRateLimiter, |
19 | handleLogin, | 22 | asyncMiddleware(handleToken) |
20 | tokenSuccess | ||
21 | ) | 23 | ) |
22 | 24 | ||
23 | tokensRouter.post('/revoke-token', | 25 | tokensRouter.post('/revoke-token', |
@@ -42,10 +44,53 @@ export { | |||
42 | } | 44 | } |
43 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
44 | 46 | ||
45 | function tokenSuccess (req: express.Request) { | 47 | async 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 | |||
88 | async 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 | ||
51 | function getScopedTokens (req: express.Request, res: express.Response) { | 96 | function 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 | |||
115 | async 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/video-channel.ts b/server/controllers/api/video-channel.ts index 03617dc8d..149d6cfb4 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Hooks } from '@server/lib/plugins/hooks' | 2 | import { Hooks } from '@server/lib/plugins/hooks' |
3 | import { getServerActor } from '@server/models/application/application' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { MChannelAccountDefault } from '@server/types/models' | 4 | import { MChannelBannerAccountDefault } from '@server/types/models' |
5 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 5 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
8 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 8 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config' | |||
13 | import { MIMETYPES } from '../../initializers/constants' | 13 | import { MIMETYPES } from '../../initializers/constants' |
14 | import { sequelizeTypescript } from '../../initializers/database' | 14 | import { sequelizeTypescript } from '../../initializers/database' |
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | 15 | import { sendUpdateActor } from '../../lib/activitypub/send' |
16 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' | 16 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image' |
17 | import { JobQueue } from '../../lib/job-queue' | 17 | import { JobQueue } from '../../lib/job-queue' |
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
19 | import { | 19 | import { |
@@ -33,7 +33,7 @@ import { | |||
33 | videoPlaylistsSortValidator | 33 | videoPlaylistsSortValidator |
34 | } from '../../middlewares' | 34 | } from '../../middlewares' |
35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' | 35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' |
36 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' | 36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
38 | import { AccountModel } from '../../models/account/account' | 38 | import { AccountModel } from '../../models/account/account' |
39 | import { VideoModel } from '../../models/video/video' | 39 | import { VideoModel } from '../../models/video/video' |
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist' | |||
42 | 42 | ||
43 | const auditLogger = auditLoggerFactory('channels') | 43 | const auditLogger = auditLoggerFactory('channels') |
44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) | 44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
45 | const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR }) | ||
45 | 46 | ||
46 | const videoChannelRouter = express.Router() | 47 | const videoChannelRouter = express.Router() |
47 | 48 | ||
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick', | |||
69 | asyncMiddleware(updateVideoChannelAvatar) | 70 | asyncMiddleware(updateVideoChannelAvatar) |
70 | ) | 71 | ) |
71 | 72 | ||
73 | videoChannelRouter.post('/:nameWithHost/banner/pick', | ||
74 | authenticate, | ||
75 | reqBannerFile, | ||
76 | // Check the rights | ||
77 | asyncMiddleware(videoChannelsUpdateValidator), | ||
78 | updateBannerValidator, | ||
79 | asyncMiddleware(updateVideoChannelBanner) | ||
80 | ) | ||
81 | |||
72 | videoChannelRouter.delete('/:nameWithHost/avatar', | 82 | videoChannelRouter.delete('/:nameWithHost/avatar', |
73 | authenticate, | 83 | authenticate, |
74 | // Check the rights | 84 | // Check the rights |
@@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar', | |||
76 | asyncMiddleware(deleteVideoChannelAvatar) | 86 | asyncMiddleware(deleteVideoChannelAvatar) |
77 | ) | 87 | ) |
78 | 88 | ||
89 | videoChannelRouter.delete('/:nameWithHost/banner', | ||
90 | authenticate, | ||
91 | // Check the rights | ||
92 | asyncMiddleware(videoChannelsUpdateValidator), | ||
93 | asyncMiddleware(deleteVideoChannelBanner) | ||
94 | ) | ||
95 | |||
79 | videoChannelRouter.put('/:nameWithHost', | 96 | videoChannelRouter.put('/:nameWithHost', |
80 | authenticate, | 97 | authenticate, |
81 | asyncMiddleware(videoChannelsUpdateValidator), | 98 | asyncMiddleware(videoChannelsUpdateValidator), |
@@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) { | |||
134 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 151 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
135 | } | 152 | } |
136 | 153 | ||
154 | async function updateVideoChannelBanner (req: express.Request, res: express.Response) { | ||
155 | const bannerPhysicalFile = req.files['bannerfile'][0] | ||
156 | const videoChannel = res.locals.videoChannel | ||
157 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | ||
158 | |||
159 | const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) | ||
160 | |||
161 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | ||
162 | |||
163 | return res.json({ banner: banner.toFormattedJSON() }) | ||
164 | } | ||
137 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 165 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
138 | const avatarPhysicalFile = req.files['avatarfile'][0] | 166 | const avatarPhysicalFile = req.files['avatarfile'][0] |
139 | const videoChannel = res.locals.videoChannel | 167 | const videoChannel = res.locals.videoChannel |
140 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | 168 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) |
141 | 169 | ||
142 | const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile) | 170 | const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) |
143 | 171 | ||
144 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | 172 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) |
145 | 173 | ||
146 | return res | 174 | return res.json({ avatar: avatar.toFormattedJSON() }) |
147 | .json({ | ||
148 | avatar: avatar.toFormattedJSON() | ||
149 | }) | ||
150 | .end() | ||
151 | } | 175 | } |
152 | 176 | ||
153 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | 177 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { |
154 | const videoChannel = res.locals.videoChannel | 178 | const videoChannel = res.locals.videoChannel |
155 | 179 | ||
156 | await deleteLocalActorAvatarFile(videoChannel) | 180 | await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) |
181 | |||
182 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | ||
183 | } | ||
184 | |||
185 | async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { | ||
186 | const videoChannel = res.locals.videoChannel | ||
187 | |||
188 | await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) | ||
157 | 189 | ||
158 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 190 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) |
159 | } | 191 | } |
@@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) { | |||
177 | videoChannel: { | 209 | videoChannel: { |
178 | id: videoChannelCreated.id | 210 | id: videoChannelCreated.id |
179 | } | 211 | } |
180 | }).end() | 212 | }) |
181 | } | 213 | } |
182 | 214 | ||
183 | async function updateVideoChannel (req: express.Request, res: express.Response) { | 215 | async function updateVideoChannel (req: express.Request, res: express.Response) { |
@@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
206 | } | 238 | } |
207 | } | 239 | } |
208 | 240 | ||
209 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault | 241 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault |
210 | await sendUpdateActor(videoChannelInstanceUpdated, t) | 242 | await sendUpdateActor(videoChannelInstanceUpdated, t) |
211 | 243 | ||
212 | auditLogger.update( | 244 | auditLogger.update( |
@@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response) | |||
252 | } | 284 | } |
253 | 285 | ||
254 | async function getVideoChannel (req: express.Request, res: express.Response) { | 286 | async function getVideoChannel (req: express.Request, res: express.Response) { |
255 | const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) | 287 | const videoChannel = res.locals.videoChannel |
256 | 288 | ||
257 | if (videoChannelWithVideos.isOutdated()) { | 289 | if (videoChannel.isOutdated()) { |
258 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) | 290 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } }) |
259 | } | 291 | } |
260 | 292 | ||
261 | return res.json(videoChannelWithVideos.toFormattedJSON()) | 293 | return res.json(videoChannel.toFormattedJSON()) |
262 | } | 294 | } |
263 | 295 | ||
264 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { | 296 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { |
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 '../../../ | |||
17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | 19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' |
20 | import { logger } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { getFormattedObjects } from '../../../helpers/utils' | 21 | import { getFormattedObjects } from '../../../helpers/utils' |
22 | import { CONFIG } from '../../../initializers/config' | 22 | import { CONFIG } from '../../../initializers/config' |
23 | import { | 23 | import { |
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership' | |||
67 | import { rateVideoRouter } from './rate' | 67 | import { rateVideoRouter } from './rate' |
68 | import { watchingRouter } from './watching' | 68 | import { watchingRouter } from './watching' |
69 | 69 | ||
70 | const lTags = loggerTagsFactory('api', 'video') | ||
70 | const auditLogger = auditLoggerFactory('videos') | 71 | const auditLogger = auditLoggerFactory('videos') |
71 | const videosRouter = express.Router() | 72 | const 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/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index 86adb6c69..a85d7c30b 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) { | |||
107 | // We need more attributes for federation | 107 | // We need more attributes for federation |
108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) | 108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) |
109 | 109 | ||
110 | const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) | 110 | const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId) |
111 | 111 | ||
112 | targetVideo.channelId = channel.id | 112 | targetVideo.channelId = channel.id |
113 | 113 | ||
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' | |||
2 | import { constants, promises as fs } from 'fs' | 2 | import { constants, promises as fs } from 'fs' |
3 | import { readFile } from 'fs-extra' | 3 | import { readFile } from 'fs-extra' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { logger } from '@server/helpers/logger' | ||
5 | import { CONFIG } from '@server/initializers/config' | 6 | import { CONFIG } from '@server/initializers/config' |
7 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { HttpStatusCode } from '@shared/core-utils' | 8 | import { HttpStatusCode } from '@shared/core-utils' |
7 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' | 9 | import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' |
8 | import { root } from '../helpers/core-utils' | 10 | import { 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 | ||
107 | async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { | 110 | async 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 | |||
183 | type AllowedResult = { allowed: boolean, html?: string } | ||
184 | function 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 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { logger } from '@server/helpers/logger' | ||
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 4 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' |
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { getVideoFilePath } from '@server/lib/video-paths' | 6 | import { getVideoFilePath } from '@server/lib/video-paths' |
5 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
7 | import { VideoStreamingPlaylistType } from '@shared/models' | 9 | import { VideoStreamingPlaylistType } from '@shared/models' |
8 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 10 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
@@ -14,19 +16,19 @@ downloadRouter.use(cors()) | |||
14 | 16 | ||
15 | downloadRouter.use( | 17 | downloadRouter.use( |
16 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', | 18 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', |
17 | downloadTorrent | 19 | asyncMiddleware(downloadTorrent) |
18 | ) | 20 | ) |
19 | 21 | ||
20 | downloadRouter.use( | 22 | downloadRouter.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 | ||
26 | downloadRouter.use( | 28 | downloadRouter.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 | ||
47 | function downloadVideoFile (req: express.Request, res: express.Response) { | 59 | async 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 | ||
56 | function downloadHLSVideoFile (req: express.Request, res: express.Response) { | 78 | async 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 | ||
68 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | 100 | function 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 | |||
112 | type AllowedResult = { | ||
113 | allowed: boolean | ||
114 | errorMessage?: string | ||
115 | } | ||
116 | |||
117 | function isTorrentDownloadAllowed (_object: { | ||
118 | torrentPath: string | ||
119 | }): AllowedResult { | ||
120 | return { allowed: true } | ||
121 | } | ||
122 | |||
123 | function isVideoDownloadAllowed (_object: { | ||
124 | video: MVideo | ||
125 | videoFile: MVideoFile | ||
126 | streamingPlaylist?: MStreamingPlaylist | ||
127 | }): AllowedResult { | ||
128 | return { allowed: true } | ||
129 | } | ||
130 | |||
131 | function 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Feed from 'pfeed' | 2 | import * as Feed from 'pfeed' |
3 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | ||
3 | import { buildNSFWFilter } from '../helpers/express-utils' | 4 | import { buildNSFWFilter } from '../helpers/express-utils' |
4 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
5 | import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' | 6 | import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' |
6 | import { | 7 | import { |
7 | asyncMiddleware, | 8 | asyncMiddleware, |
8 | commonVideosFiltersValidator, | 9 | commonVideosFiltersValidator, |
@@ -17,7 +18,6 @@ import { | |||
17 | import { cacheRoute } from '../middlewares/cache' | 18 | import { cacheRoute } from '../middlewares/cache' |
18 | import { VideoModel } from '../models/video/video' | 19 | import { VideoModel } from '../models/video/video' |
19 | import { VideoCommentModel } from '../models/video/video-comment' | 20 | import { VideoCommentModel } from '../models/video/video-comment' |
20 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | ||
21 | 21 | ||
22 | const feedsRouter = express.Router() | 22 | const 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/lazy-static.ts b/server/controllers/lazy-static.ts index 4e553479b..6f71fdb16 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache | |||
4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' | 7 | import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image' |
8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' |
9 | import { asyncMiddleware } from '../middlewares' | 9 | import { asyncMiddleware } from '../middlewares' |
10 | import { AvatarModel } from '../models/avatar/avatar' | 10 | import { ActorImageModel } from '../models/account/actor-image' |
11 | 11 | ||
12 | const lazyStaticRouter = express.Router() | 12 | const lazyStaticRouter = express.Router() |
13 | 13 | ||
@@ -15,7 +15,12 @@ lazyStaticRouter.use(cors()) | |||
15 | 15 | ||
16 | lazyStaticRouter.use( | 16 | lazyStaticRouter.use( |
17 | LAZY_STATIC_PATHS.AVATARS + ':filename', | 17 | LAZY_STATIC_PATHS.AVATARS + ':filename', |
18 | asyncMiddleware(getAvatar) | 18 | asyncMiddleware(getActorImage) |
19 | ) | ||
20 | |||
21 | lazyStaticRouter.use( | ||
22 | LAZY_STATIC_PATHS.BANNERS + ':filename', | ||
23 | asyncMiddleware(getActorImage) | ||
19 | ) | 24 | ) |
20 | 25 | ||
21 | lazyStaticRouter.use( | 26 | lazyStaticRouter.use( |
@@ -43,36 +48,36 @@ export { | |||
43 | 48 | ||
44 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
45 | 50 | ||
46 | async function getAvatar (req: express.Request, res: express.Response) { | 51 | async function getActorImage (req: express.Request, res: express.Response) { |
47 | const filename = req.params.filename | 52 | const filename = req.params.filename |
48 | 53 | ||
49 | if (avatarPathUnsafeCache.has(filename)) { | 54 | if (actorImagePathUnsafeCache.has(filename)) { |
50 | return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) | 55 | return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) |
51 | } | 56 | } |
52 | 57 | ||
53 | const avatar = await AvatarModel.loadByName(filename) | 58 | const image = await ActorImageModel.loadByName(filename) |
54 | if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 59 | if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
55 | 60 | ||
56 | if (avatar.onDisk === false) { | 61 | if (image.onDisk === false) { |
57 | if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 62 | if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
58 | 63 | ||
59 | logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl) | 64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
60 | 65 | ||
61 | try { | 66 | try { |
62 | await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl }) | 67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) |
63 | } catch (err) { | 68 | } catch (err) { |
64 | logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err }) | 69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) |
65 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 70 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
66 | } | 71 | } |
67 | 72 | ||
68 | avatar.onDisk = true | 73 | image.onDisk = true |
69 | avatar.save() | 74 | image.save() |
70 | .catch(err => logger.error('Cannot save new avatar disk state.', { err })) | 75 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) |
71 | } | 76 | } |
72 | 77 | ||
73 | const path = avatar.getPath() | 78 | const path = image.getPath() |
74 | 79 | ||
75 | avatarPathUnsafeCache.set(filename, path) | 80 | actorImagePathUnsafeCache.set(filename, path) |
76 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 81 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
77 | } | 82 | } |
78 | 83 | ||
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' | ||
3 | import { join } from 'path' | 2 | import { join } from 'path' |
4 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' | 3 | import { logger } from '@server/helpers/logger' |
5 | import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' | 4 | import { optionalAuthenticate } from '@server/middlewares/auth' |
6 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
7 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
8 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' | 5 | import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
9 | import { PluginType } from '../../shared/models/plugins/plugin.type' | 7 | import { PluginType } from '../../shared/models/plugins/plugin.type' |
10 | import { isTestInstance } from '../helpers/core-utils' | 8 | import { isTestInstance } from '../helpers/core-utils' |
11 | import { logger } from '@server/helpers/logger' | 9 | import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' |
12 | import { optionalAuthenticate } from '@server/middlewares/oauth' | 10 | import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' |
11 | import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins' | ||
12 | import { serveThemeCSSValidator } from '../middlewares/validators/themes' | ||
13 | 13 | ||
14 | const sendFileOptions = { | 14 | const sendFileOptions = { |
15 | maxAge: '30 days', | 15 | maxAge: '30 days', |
diff --git a/server/controllers/services.ts b/server/controllers/services.ts index d0217c30a..189e1651b 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts | |||
@@ -3,6 +3,7 @@ import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initia | |||
3 | import { asyncMiddleware, oembedValidator } from '../middlewares' | 3 | import { asyncMiddleware, oembedValidator } from '../middlewares' |
4 | import { accountNameWithHostGetValidator } from '../middlewares/validators' | 4 | import { accountNameWithHostGetValidator } from '../middlewares/validators' |
5 | import { MChannelSummary } from '@server/types/models' | 5 | import { MChannelSummary } from '@server/types/models' |
6 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
6 | 7 | ||
7 | const servicesRouter = express.Router() | 8 | const servicesRouter = express.Router() |
8 | 9 | ||
@@ -79,6 +80,7 @@ function buildOEmbed (options: { | |||
79 | const embedUrl = webserverUrl + embedPath | 80 | const embedUrl = webserverUrl + embedPath |
80 | let embedWidth = EMBED_SIZE.width | 81 | let embedWidth = EMBED_SIZE.width |
81 | let embedHeight = EMBED_SIZE.height | 82 | let embedHeight = EMBED_SIZE.height |
83 | const embedTitle = escapeHTML(title) | ||
82 | 84 | ||
83 | let thumbnailUrl = previewPath | 85 | let thumbnailUrl = previewPath |
84 | ? webserverUrl + previewPath | 86 | ? webserverUrl + previewPath |
@@ -96,7 +98,7 @@ function buildOEmbed (options: { | |||
96 | } | 98 | } |
97 | 99 | ||
98 | const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + | 100 | const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + |
99 | `src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` | 101 | `title="${embedTitle}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` |
100 | 102 | ||
101 | const json: any = { | 103 | const json: any = { |
102 | type: 'video', | 104 | type: 'video', |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 4baa31117..e6a0628e6 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
252 | avatar: { | 252 | avatar: { |
253 | file: { | 253 | file: { |
254 | size: { | 254 | size: { |
255 | max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max | 255 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max |
256 | }, | 256 | }, |
257 | extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | 257 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
258 | } | 258 | } |
259 | }, | 259 | }, |
260 | video: { | 260 | video: { |
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' | |||
3 | import validator from 'validator' | 3 | import validator from 'validator' |
4 | import { ContextType } from '@shared/models/activitypub/context' | 4 | import { ContextType } from '@shared/models/activitypub/context' |
5 | import { ResultList } from '../../shared/models' | 5 | import { ResultList } from '../../shared/models' |
6 | import { Activity } from '../../shared/models/activitypub' | ||
7 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' | 6 | import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' |
8 | import { MActor, MVideoWithHost } from '../types/models' | 7 | import { MActor, MVideoWithHost } from '../types/models' |
9 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination ( | |||
182 | 181 | ||
183 | } | 182 | } |
184 | 183 | ||
185 | function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { | 184 | function 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 | ||
191 | function getAPId (activity: string | { id: string }) { | 190 | function getAPId (activity: string | { id: string }) { |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 935fd22d9..b93868c12 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto' | |||
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { basename, isAbsolute, join, resolve } from 'path' | 11 | import { basename, isAbsolute, join, resolve } from 'path' |
12 | import * as pem from 'pem' | 12 | import * as pem from 'pem' |
13 | import { pipeline } from 'stream' | ||
13 | import { URL } from 'url' | 14 | import { URL } from 'url' |
15 | import { promisify } from 'util' | ||
14 | 16 | ||
15 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { | 17 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { |
16 | if (!oldObject || typeof oldObject !== 'object') { | 18 | if (!oldObject || typeof oldObject !== 'object') { |
@@ -152,24 +154,6 @@ function root () { | |||
152 | return rootPath | 154 | return rootPath |
153 | } | 155 | } |
154 | 156 | ||
155 | // Thanks: https://stackoverflow.com/a/12034334 | ||
156 | function escapeHTML (stringParam) { | ||
157 | if (!stringParam) return '' | ||
158 | |||
159 | const entityMap = { | ||
160 | '&': '&', | ||
161 | '<': '<', | ||
162 | '>': '>', | ||
163 | '"': '"', | ||
164 | '\'': ''', | ||
165 | '/': '/', | ||
166 | '`': '`', | ||
167 | '=': '=' | ||
168 | } | ||
169 | |||
170 | return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s]) | ||
171 | } | ||
172 | |||
173 | function pageToStartAndCount (page: number, itemsPerPage: number) { | 157 | function pageToStartAndCount (page: number, itemsPerPage: number) { |
174 | const start = (page - 1) * itemsPerPage | 158 | const start = (page - 1) * itemsPerPage |
175 | 159 | ||
@@ -249,11 +233,23 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) | |||
249 | } | 233 | } |
250 | } | 234 | } |
251 | 235 | ||
236 | type SemVersion = { major: number, minor: number, patch: number } | ||
237 | function parseSemVersion (s: string) { | ||
238 | const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) | ||
239 | |||
240 | return { | ||
241 | major: parseInt(parsed[1]), | ||
242 | minor: parseInt(parsed[2]), | ||
243 | patch: parseInt(parsed[3]) | ||
244 | } as SemVersion | ||
245 | } | ||
246 | |||
252 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) | 247 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) |
253 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) | 248 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) |
254 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | 249 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) |
255 | const execPromise2 = promisify2<string, any, string>(exec) | 250 | const execPromise2 = promisify2<string, any, string>(exec) |
256 | const execPromise = promisify1<string, string>(exec) | 251 | const execPromise = promisify1<string, string>(exec) |
252 | const pipelinePromise = promisify(pipeline) | ||
257 | 253 | ||
258 | // --------------------------------------------------------------------------- | 254 | // --------------------------------------------------------------------------- |
259 | 255 | ||
@@ -264,7 +260,6 @@ export { | |||
264 | 260 | ||
265 | objectConverter, | 261 | objectConverter, |
266 | root, | 262 | root, |
267 | escapeHTML, | ||
268 | pageToStartAndCount, | 263 | pageToStartAndCount, |
269 | sanitizeUrl, | 264 | sanitizeUrl, |
270 | sanitizeHost, | 265 | sanitizeHost, |
@@ -284,5 +279,8 @@ export { | |||
284 | createPrivateKey, | 279 | createPrivateKey, |
285 | getPublicKey, | 280 | getPublicKey, |
286 | execPromise2, | 281 | execPromise2, |
287 | execPromise | 282 | execPromise, |
283 | pipelinePromise, | ||
284 | |||
285 | parseSemVersion | ||
288 | } | 286 | } |
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 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { isAbuseReasonValid } from '../abuses' | ||
3 | import { exists } from '../misc' | 4 | import { exists } from '../misc' |
4 | import { sanitizeAndCheckActorObject } from './actor' | 5 | import { sanitizeAndCheckActorObject } from './actor' |
5 | import { isCacheFileObjectValid } from './cache-file' | 6 | import { isCacheFileObjectValid } from './cache-file' |
6 | import { isFlagActivityValid } from './flag' | ||
7 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' | 7 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' |
8 | import { isPlaylistObjectValid } from './playlist' | 8 | import { isPlaylistObjectValid } from './playlist' |
9 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
10 | import { isShareActivityValid } from './share' | ||
11 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' | 9 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' |
12 | import { sanitizeAndCheckVideoTorrentObject } from './videos' | 10 | import { sanitizeAndCheckVideoTorrentObject } from './videos' |
13 | import { isViewActivityValid } from './view' | ||
14 | 11 | ||
15 | function isRootActivityValid (activity: any) { | 12 | function 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 | ||
31 | const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { | 28 | const 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 | ||
46 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) { | |||
51 | return checker(activity) | 48 | return checker(activity) |
52 | } | 49 | } |
53 | 50 | ||
54 | // --------------------------------------------------------------------------- | 51 | function isFlagActivityValid (activity: any) { |
55 | 52 | return isBaseActivityValid(activity, 'Flag') && | |
56 | export { | 53 | isAbuseReasonValid(activity.content) && |
57 | isRootActivityValid, | 54 | isActivityPubUrlValid(activity.object) |
58 | isActivityValid | ||
59 | } | 55 | } |
60 | 56 | ||
61 | // --------------------------------------------------------------------------- | 57 | function isLikeActivityValid (activity: any) { |
62 | 58 | return isBaseActivityValid(activity, 'Like') && | |
63 | function checkViewActivity (activity: any) { | 59 | isObjectValid(activity.object) |
64 | return isBaseActivityValid(activity, 'View') && | ||
65 | isViewActivityValid(activity) | ||
66 | } | 60 | } |
67 | 61 | ||
68 | function checkFlagActivity (activity: any) { | 62 | function isDislikeActivityValid (activity: any) { |
69 | return isBaseActivityValid(activity, 'Flag') && | 63 | return isBaseActivityValid(activity, 'Dislike') && |
70 | isFlagActivityValid(activity) | 64 | isObjectValid(activity.object) |
71 | } | 65 | } |
72 | 66 | ||
73 | function checkDislikeActivity (activity: any) { | 67 | function isAnnounceActivityValid (activity: any) { |
74 | return isDislikeActivityValid(activity) | 68 | return isBaseActivityValid(activity, 'Announce') && |
69 | isObjectValid(activity.object) | ||
75 | } | 70 | } |
76 | 71 | ||
77 | function checkLikeActivity (activity: any) { | 72 | function isViewActivityValid (activity: any) { |
78 | return isLikeActivityValid(activity) | 73 | return isBaseActivityValid(activity, 'View') && |
74 | isActivityPubUrlValid(activity.actor) && | ||
75 | isActivityPubUrlValid(activity.object) | ||
79 | } | 76 | } |
80 | 77 | ||
81 | function checkCreateActivity (activity: any) { | 78 | function 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 | ||
95 | function checkUpdateActivity (activity: any) { | 92 | function 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 | ||
105 | function checkDeleteActivity (activity: any) { | 102 | function 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 | ||
111 | function checkFollowActivity (activity: any) { | 108 | function 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 | ||
116 | function checkAcceptActivity (activity: any) { | 113 | function isAcceptActivityValid (activity: any) { |
117 | return isBaseActivityValid(activity, 'Accept') | 114 | return isBaseActivityValid(activity, 'Accept') |
118 | } | 115 | } |
119 | 116 | ||
120 | function checkRejectActivity (activity: any) { | 117 | function isRejectActivityValid (activity: any) { |
121 | return isBaseActivityValid(activity, 'Reject') | 118 | return isBaseActivityValid(activity, 'Reject') |
122 | } | 119 | } |
123 | 120 | ||
124 | function checkAnnounceActivity (activity: any) { | 121 | function isUndoActivityValid (activity: any) { |
125 | return isShareActivityValid(activity) | ||
126 | } | ||
127 | |||
128 | function 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 | |||
134 | export { | ||
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 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | import { isAbuseReasonValid } from '../abuses' | ||
3 | |||
4 | function isFlagActivityValid (activity: any) { | ||
5 | return activity.type === 'Flag' && | ||
6 | isAbuseReasonValid(activity.content) && | ||
7 | isActivityPubUrlValid(activity.object) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
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 @@ | |||
1 | import { isBaseActivityValid, isObjectValid } from './misc' | ||
2 | |||
3 | function isLikeActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Like') && | ||
5 | isObjectValid(activity.object) | ||
6 | } | ||
7 | |||
8 | function isDislikeActivityValid (activity: any) { | ||
9 | return isBaseActivityValid(activity, 'Dislike') && | ||
10 | isObjectValid(activity.object) | ||
11 | } | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
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 @@ | |||
1 | import { isBaseActivityValid, isObjectValid } from './misc' | ||
2 | |||
3 | function isShareActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Announce') && | ||
5 | isObjectValid(activity.object) | ||
6 | } | ||
7 | // --------------------------------------------------------------------------- | ||
8 | |||
9 | export { | ||
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 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | |||
3 | function isViewActivityValid (activity: any) { | ||
4 | return activity.type === 'View' && | ||
5 | isActivityPubUrlValid(activity.actor) && | ||
6 | isActivityPubUrlValid(activity.object) | ||
7 | } | ||
8 | |||
9 | // --------------------------------------------------------------------------- | ||
10 | |||
11 | export { | ||
12 | isViewActivityValid | ||
13 | } | ||
diff --git a/server/helpers/custom-validators/actor-images.ts b/server/helpers/custom-validators/actor-images.ts new file mode 100644 index 000000000..4fb0b7c70 --- /dev/null +++ b/server/helpers/custom-validators/actor-images.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | |||
2 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
3 | import { isFileValid } from './misc' | ||
4 | |||
5 | const imageMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
6 | .map(v => v.replace('.', '')) | ||
7 | .join('|') | ||
8 | const imageMimeTypesRegex = `image/(${imageMimeTypes})` | ||
9 | function isActorImageFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], fieldname: string) { | ||
10 | return isFileValid(files, imageMimeTypesRegex, fieldname, CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max) | ||
11 | } | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | isActorImageFile | ||
17 | } | ||
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 @@ | |||
1 | import { exists } from './misc' | ||
2 | import validator from 'validator' | 1 | import validator from 'validator' |
3 | import { UserNotificationType } from '../../../shared/models/users' | ||
4 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 2 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
3 | import { exists } from './misc' | ||
5 | 4 | ||
6 | function isUserNotificationTypeValid (value: any) { | 5 | function isUserNotificationTypeValid (value: any) { |
7 | return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined | 6 | return exists(value) && validator.isInt('' + value) |
8 | } | 7 | } |
9 | 8 | ||
10 | function isUserNotificationSettingValid (value: any) { | 9 | function isUserNotificationSettingValid (value: any) { |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index d6e91ad35..5b21c3529 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { values } from 'lodash' | ||
1 | import validator from 'validator' | 2 | import validator from 'validator' |
2 | import { UserRole } from '../../../shared' | 3 | import { UserRole } from '../../../shared' |
3 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | ||
4 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' | ||
5 | import { values } from 'lodash' | ||
6 | import { isEmailEnabled } from '../../initializers/config' | 4 | import { isEmailEnabled } from '../../initializers/config' |
5 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | ||
6 | import { exists, isArray, isBooleanValid } from './misc' | ||
7 | 7 | ||
8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS |
9 | 9 | ||
@@ -97,14 +97,6 @@ function isUserRoleValid (value: any) { | |||
97 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined | 97 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined |
98 | } | 98 | } |
99 | 99 | ||
100 | const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | ||
101 | .map(v => v.replace('.', '')) | ||
102 | .join('|') | ||
103 | const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` | ||
104 | function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | ||
105 | return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max) | ||
106 | } | ||
107 | |||
108 | // --------------------------------------------------------------------------- | 100 | // --------------------------------------------------------------------------- |
109 | 101 | ||
110 | export { | 102 | export { |
@@ -128,6 +120,5 @@ export { | |||
128 | isUserDisplayNameValid, | 120 | isUserDisplayNameValid, |
129 | isUserDescriptionValid, | 121 | isUserDescriptionValid, |
130 | isNoInstanceConfigWarningModal, | 122 | isNoInstanceConfigWarningModal, |
131 | isNoWelcomeModal, | 123 | isNoWelcomeModal |
132 | isAvatarFile | ||
133 | } | 124 | } |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 620025966..01c3aa5f7 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -5,7 +5,7 @@ import { dirname, join } from 'path' | |||
5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' | 6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { promisify0 } from './core-utils' | 8 | import { execPromise, promisify0 } from './core-utils' |
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
@@ -649,6 +649,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') { | |||
649 | return command | 649 | return command |
650 | } | 650 | } |
651 | 651 | ||
652 | function 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 | |||
652 | async function runCommand (options: { | 670 | async 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/image-utils.ts b/server/helpers/image-utils.ts index 9285c12fc..6f6f8d4da 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,9 +1,14 @@ | |||
1 | import { copy, readFile, remove, rename } from 'fs-extra' | 1 | import { copy, readFile, remove, rename } from 'fs-extra' |
2 | import * as Jimp from 'jimp' | 2 | import * as Jimp from 'jimp' |
3 | import { extname } from 'path' | 3 | import { extname } from 'path' |
4 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' | 5 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' |
5 | import { logger } from './logger' | 6 | import { logger } from './logger' |
6 | 7 | ||
8 | function generateImageFilename (extension = '.jpg') { | ||
9 | return uuidv4() + extension | ||
10 | } | ||
11 | |||
7 | async function processImage ( | 12 | async function processImage ( |
8 | path: string, | 13 | path: string, |
9 | destination: string, | 14 | destination: string, |
@@ -31,6 +36,7 @@ async function processImage ( | |||
31 | // --------------------------------------------------------------------------- | 36 | // --------------------------------------------------------------------------- |
32 | 37 | ||
33 | export { | 38 | export { |
39 | generateImageFilename, | ||
34 | processImage | 40 | processImage |
35 | } | 41 | } |
36 | 42 | ||
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 | ||
50 | const consoleLoggerFormat = winston.format.printf(info => { | 50 | const 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 | |||
154 | function loggerTagsFactory (...defaultTags: string[]) { | ||
155 | return (...tags: string[]) => { | ||
156 | return { tags: defaultTags.concat(tags) } | ||
157 | } | ||
158 | } | ||
159 | |||
153 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
154 | 161 | ||
155 | export { | 162 | export { |
@@ -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/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts index 05499bb74..e6eab65a2 100644 --- a/server/helpers/middlewares/video-channels.ts +++ b/server/helpers/middlewares/video-channels.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { VideoChannelModel } from '../../models/video/video-channel' | 2 | import { MChannelBannerAccountDefault } from '@server/types/models' |
3 | import { MChannelAccountDefault } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | 6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { |
7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | 7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) |
@@ -29,11 +29,10 @@ export { | |||
29 | doesVideoChannelNameWithHostExist | 29 | doesVideoChannelNameWithHostExist |
30 | } | 30 | } |
31 | 31 | ||
32 | function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { | 32 | function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { |
33 | if (!videoChannel) { | 33 | if (!videoChannel) { |
34 | res.status(HttpStatusCode.NOT_FOUND_404) | 34 | res.status(HttpStatusCode.NOT_FOUND_404) |
35 | .json({ error: 'Video channel not found' }) | 35 | .json({ error: 'Video channel not found' }) |
36 | .end() | ||
37 | 36 | ||
38 | return false | 37 | return false |
39 | } | 38 | } |
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts index c5eb0607a..403cae092 100644 --- a/server/helpers/middlewares/videos.ts +++ b/server/helpers/middlewares/videos.ts | |||
@@ -66,25 +66,24 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st | |||
66 | } | 66 | } |
67 | 67 | ||
68 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { | 68 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { |
69 | if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { | 69 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) |
70 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) | ||
71 | if (videoChannel === null) { | ||
72 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
73 | .json({ error: 'Unknown video `video channel` on this instance.' }) | ||
74 | .end() | ||
75 | 70 | ||
76 | return false | 71 | if (videoChannel === null) { |
77 | } | 72 | res.status(HttpStatusCode.BAD_REQUEST_400) |
73 | .json({ error: 'Unknown video "video channel" for this instance.' }) | ||
78 | 74 | ||
75 | return false | ||
76 | } | ||
77 | |||
78 | // Don't check account id if the user can update any video | ||
79 | if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { | ||
79 | res.locals.videoChannel = videoChannel | 80 | res.locals.videoChannel = videoChannel |
80 | return true | 81 | return true |
81 | } | 82 | } |
82 | 83 | ||
83 | const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id) | 84 | if (videoChannel.Account.id !== user.Account.id) { |
84 | if (videoChannel === null) { | ||
85 | res.status(HttpStatusCode.BAD_REQUEST_400) | 85 | res.status(HttpStatusCode.BAD_REQUEST_400) |
86 | .json({ error: 'Unknown video `video channel` for this account.' }) | 86 | .json({ error: 'Unknown video "video channel" for this account.' }) |
87 | .end() | ||
88 | 87 | ||
89 | return false | 88 | return false |
90 | } | 89 | } |
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 | ||
87 | async function signJsonLDObject (byActor: MActor, data: any) { | 87 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
3 | import * as request from 'request' | 2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' |
3 | import { join } from 'path' | ||
4 | import { CONFIG } from '../initializers/config' | ||
4 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' | 5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' |
6 | import { pipelinePromise } from './core-utils' | ||
5 | import { processImage } from './image-utils' | 7 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
7 | import { logger } from './logger' | 8 | import { logger } from './logger' |
8 | import { CONFIG } from '../initializers/config' | ||
9 | 9 | ||
10 | function doRequest <T> ( | 10 | export 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) { | 15 | const httpSignature = require('http-signature') |
18 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 16 | |
17 | type 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 | |||
30 | const 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 | |||
100 | function 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 | ||
27 | function doRequestAndSaveToFile ( | 107 | function 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 | |||
114 | async 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 | ||
53 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | 136 | async 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 | ||
74 | export { | 157 | export { |
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 | 166 | function buildGotOptions (options: PeerTubeRequestOptions) { |
83 | function 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 | |||
190 | function 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 5b46f704a..fac3da6ba 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { createWriteStream } from 'fs' | 1 | import { createWriteStream } from 'fs' |
2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' | 2 | import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' |
3 | import got from 'got' | ||
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import * as request from 'request' | ||
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../shared/models/videos' |
8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
9 | import { getEnabledResolutions } from '../lib/video-transcoding' | 9 | import { getEnabledResolutions } from '../lib/video-transcoding' |
10 | import { peertubeTruncate, root } from './core-utils' | 10 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' |
11 | import { isVideoFileExtnameValid } from './custom-validators/videos' | 11 | import { isVideoFileExtnameValid } from './custom-validators/videos' |
12 | import { logger } from './logger' | 12 | import { logger } from './logger' |
13 | import { generateVideoImportTmpPath } from './utils' | 13 | import { 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 | ||
249 | async function safeGetYoutubeDL () { | 226 | async 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 @@ | |||
1 | import * as config from 'config' | 1 | import * as config from 'config' |
2 | import { isProdInstance, isTestInstance } from '../helpers/core-utils' | 2 | import { uniq } from 'lodash' |
3 | import { UserModel } from '../models/account/user' | ||
4 | import { getServerActor, ApplicationModel } from '../models/application/application' | ||
5 | import { OAuthClientModel } from '../models/oauth/oauth-client' | ||
6 | import { URL } from 'url' | 3 | import { URL } from 'url' |
7 | import { CONFIG, isEmailEnabled } from './config' | 4 | import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils' |
8 | import { logger } from '../helpers/logger' | 5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' |
9 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' | 6 | import { RecentlyAddedStrategy } from '../../shared/models/redundancy' |
7 | import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils' | ||
10 | import { isArray } from '../helpers/custom-validators/misc' | 8 | import { isArray } from '../helpers/custom-validators/misc' |
11 | import { uniq } from 'lodash' | 9 | import { logger } from '../helpers/logger' |
10 | import { UserModel } from '../models/account/user' | ||
11 | import { ApplicationModel, getServerActor } from '../models/application/application' | ||
12 | import { OAuthClientModel } from '../models/oauth/oauth-client' | ||
13 | import { CONFIG, isEmailEnabled } from './config' | ||
12 | import { WEBSERVER } from './constants' | 14 | import { WEBSERVER } from './constants' |
13 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | ||
14 | 15 | ||
15 | async function checkActivityPubUrls () { | 16 | async 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 | ||
180 | async 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 | ||
181 | export { | 191 | export { |
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 @@ | |||
1 | import * as config from 'config' | 1 | import * as config from 'config' |
2 | import { promisify0 } from '../helpers/core-utils' | 2 | import { parseSemVersion, promisify0 } from '../helpers/core-utils' |
3 | import { logger } from '../helpers/logger' | 3 | import { 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 | ||
103 | function checkNodeVersion () { | 104 | function 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..93dd5ac04 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -59,7 +59,7 @@ const CONFIG = { | |||
59 | }, | 59 | }, |
60 | STORAGE: { | 60 | STORAGE: { |
61 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | 61 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), |
62 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 62 | ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), |
63 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 63 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
64 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 64 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
65 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), | 65 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), |
@@ -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 50467f408..1802257df 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 | ||
27 | const LAST_MIGRATION_VERSION = 612 | 27 | const LAST_MIGRATION_VERSION = 635 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
31 | const API_VERSION = 'v1' | 31 | const API_VERSION = 'v1' |
32 | const PEERTUBE_VERSION = require(join(root(), 'package.json')).version | 32 | const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version |
33 | 33 | ||
34 | const PAGINATION = { | 34 | const 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 |
@@ -304,7 +305,7 @@ const CONSTRAINTS_FIELDS = { | |||
304 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length | 305 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length |
305 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length | 306 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length |
306 | URL: { min: 3, max: 2000 }, // Length | 307 | URL: { min: 3, max: 2000 }, // Length |
307 | AVATAR: { | 308 | IMAGE: { |
308 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], | 309 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], |
309 | FILE_SIZE: { | 310 | FILE_SIZE: { |
310 | max: 2 * 1024 * 1024 // 2MB | 311 | max: 2 * 1024 * 1024 // 2MB |
@@ -465,6 +466,8 @@ const MIMETYPES = { | |||
465 | IMAGE: { | 466 | IMAGE: { |
466 | MIMETYPE_EXT: { | 467 | MIMETYPE_EXT: { |
467 | 'image/png': '.png', | 468 | 'image/png': '.png', |
469 | 'image/gif': '.gif', | ||
470 | 'image/webp': '.webp', | ||
468 | 'image/jpg': '.jpg', | 471 | 'image/jpg': '.jpg', |
469 | 'image/jpeg': '.jpg' | 472 | 'image/jpeg': '.jpg' |
470 | }, | 473 | }, |
@@ -579,6 +582,7 @@ const STATIC_DOWNLOAD_PATHS = { | |||
579 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' | 582 | HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' |
580 | } | 583 | } |
581 | const LAZY_STATIC_PATHS = { | 584 | const LAZY_STATIC_PATHS = { |
585 | BANNERS: '/lazy-static/banners/', | ||
582 | AVATARS: '/lazy-static/avatars/', | 586 | AVATARS: '/lazy-static/avatars/', |
583 | PREVIEWS: '/lazy-static/previews/', | 587 | PREVIEWS: '/lazy-static/previews/', |
584 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', | 588 | VIDEO_CAPTIONS: '/lazy-static/video-captions/', |
@@ -594,8 +598,8 @@ const STATIC_MAX_AGE = { | |||
594 | 598 | ||
595 | // Videos thumbnail size | 599 | // Videos thumbnail size |
596 | const THUMBNAILS_SIZE = { | 600 | const THUMBNAILS_SIZE = { |
597 | width: 223, | 601 | width: 280, |
598 | height: 122, | 602 | height: 157, |
599 | minWidth: 150 | 603 | minWidth: 150 |
600 | } | 604 | } |
601 | const PREVIEWS_SIZE = { | 605 | const PREVIEWS_SIZE = { |
@@ -603,9 +607,15 @@ const PREVIEWS_SIZE = { | |||
603 | height: 480, | 607 | height: 480, |
604 | minWidth: 400 | 608 | minWidth: 400 |
605 | } | 609 | } |
606 | const AVATARS_SIZE = { | 610 | const ACTOR_IMAGES_SIZE = { |
607 | width: 120, | 611 | AVATARS: { |
608 | height: 120 | 612 | width: 120, |
613 | height: 120 | ||
614 | }, | ||
615 | BANNERS: { | ||
616 | width: 1920, | ||
617 | height: 317 // 6/1 ratio | ||
618 | } | ||
609 | } | 619 | } |
610 | 620 | ||
611 | const EMBED_SIZE = { | 621 | const EMBED_SIZE = { |
@@ -633,7 +643,7 @@ const LRU_CACHE = { | |||
633 | USER_TOKENS: { | 643 | USER_TOKENS: { |
634 | MAX_SIZE: 1000 | 644 | MAX_SIZE: 1000 |
635 | }, | 645 | }, |
636 | AVATAR_STATIC: { | 646 | ACTOR_IMAGE_STATIC: { |
637 | MAX_SIZE: 500 | 647 | MAX_SIZE: 500 |
638 | } | 648 | } |
639 | } | 649 | } |
@@ -670,7 +680,7 @@ const MEMOIZE_LENGTH = { | |||
670 | } | 680 | } |
671 | 681 | ||
672 | const QUEUE_CONCURRENCY = { | 682 | const QUEUE_CONCURRENCY = { |
673 | AVATAR_PROCESS_IMAGE: 3 | 683 | ACTOR_PROCESS_IMAGE: 3 |
674 | } | 684 | } |
675 | 685 | ||
676 | const REDUNDANCY = { | 686 | const REDUNDANCY = { |
@@ -753,7 +763,7 @@ if (isTestInstance() === true) { | |||
753 | ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds | 763 | ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds |
754 | ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds | 764 | ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds |
755 | 765 | ||
756 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB | 766 | CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB |
757 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB | 767 | CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB |
758 | 768 | ||
759 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 | 769 | SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 |
@@ -763,6 +773,7 @@ if (isTestInstance() === true) { | |||
763 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 | 773 | SCHEDULER_INTERVALS_MS.updateVideos = 5000 |
764 | SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 | 774 | SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 |
765 | SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 | 775 | SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 |
776 | SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000 | ||
766 | REPEAT_JOBS['videos-views'] = { every: 5000 } | 777 | REPEAT_JOBS['videos-views'] = { every: 5000 } |
767 | REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } | 778 | REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } |
768 | 779 | ||
@@ -813,7 +824,7 @@ export { | |||
813 | SEARCH_INDEX, | 824 | SEARCH_INDEX, |
814 | HLS_REDUNDANCY_DIRECTORY, | 825 | HLS_REDUNDANCY_DIRECTORY, |
815 | P2P_MEDIA_LOADER_PEER_VERSION, | 826 | P2P_MEDIA_LOADER_PEER_VERSION, |
816 | AVATARS_SIZE, | 827 | ACTOR_IMAGES_SIZE, |
817 | ACCEPT_HEADERS, | 828 | ACCEPT_HEADERS, |
818 | BCRYPT_SALT_SIZE, | 829 | BCRYPT_SALT_SIZE, |
819 | TRACKER_RATE_LIMITS, | 830 | TRACKER_RATE_LIMITS, |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 1f2b6d521..4c9d7c610 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { TrackerModel } from '@server/models/server/tracker' | ||
2 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | ||
3 | import { QueryTypes, Transaction } from 'sequelize' | 1 | import { QueryTypes, Transaction } from 'sequelize' |
4 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' | 2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' |
3 | import { TrackerModel } from '@server/models/server/tracker' | ||
4 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | ||
5 | import { isTestInstance } from '../helpers/core-utils' | 5 | import { isTestInstance } from '../helpers/core-utils' |
6 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
7 | import { AbuseModel } from '../models/abuse/abuse' | 7 | import { AbuseModel } from '../models/abuse/abuse' |
@@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse' | |||
11 | import { AccountModel } from '../models/account/account' | 11 | import { AccountModel } from '../models/account/account' |
12 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 12 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
13 | import { AccountVideoRateModel } from '../models/account/account-video-rate' | 13 | import { AccountVideoRateModel } from '../models/account/account-video-rate' |
14 | import { ActorImageModel } from '../models/account/actor-image' | ||
14 | import { UserModel } from '../models/account/user' | 15 | import { UserModel } from '../models/account/user' |
15 | import { UserNotificationModel } from '../models/account/user-notification' | 16 | import { UserNotificationModel } from '../models/account/user-notification' |
16 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 17 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' |
@@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history' | |||
18 | import { ActorModel } from '../models/activitypub/actor' | 19 | import { ActorModel } from '../models/activitypub/actor' |
19 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | 20 | import { ActorFollowModel } from '../models/activitypub/actor-follow' |
20 | import { ApplicationModel } from '../models/application/application' | 21 | import { ApplicationModel } from '../models/application/application' |
21 | import { AvatarModel } from '../models/avatar/avatar' | ||
22 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 22 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
23 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 23 | import { OAuthTokenModel } from '../models/oauth/oauth-token' |
24 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' |
@@ -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 | ||
@@ -95,7 +95,7 @@ async function initDatabaseModels (silent: boolean) { | |||
95 | ApplicationModel, | 95 | ApplicationModel, |
96 | ActorModel, | 96 | ActorModel, |
97 | ActorFollowModel, | 97 | ActorFollowModel, |
98 | AvatarModel, | 98 | ActorImageModel, |
99 | AccountModel, | 99 | AccountModel, |
100 | OAuthClientModel, | 100 | OAuthClientModel, |
101 | OAuthTokenModel, | 101 | OAuthTokenModel, |
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
37 | function down (options) { | ||
38 | throw new Error('Not implemented.') | ||
39 | } | ||
40 | |||
41 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
19 | function down (options) { | ||
20 | throw new Error('Not implemented.') | ||
21 | } | ||
22 | |||
23 | export { | ||
24 | up, | ||
25 | down | ||
26 | } | ||
diff --git a/server/initializers/migrations/0630-banner.ts b/server/initializers/migrations/0630-banner.ts new file mode 100644 index 000000000..5766bb171 --- /dev/null +++ b/server/initializers/migrations/0630-banner.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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(`ALTER TABLE "avatar" RENAME to "actorImage"`) | ||
12 | } | ||
13 | |||
14 | { | ||
15 | const data = { | ||
16 | type: Sequelize.INTEGER, | ||
17 | defaultValue: null, | ||
18 | allowNull: true | ||
19 | } | ||
20 | await utils.queryInterface.addColumn('actorImage', 'type', data) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | await utils.sequelize.query(`UPDATE "actorImage" SET "type" = 1`) | ||
25 | } | ||
26 | |||
27 | { | ||
28 | const data = { | ||
29 | type: Sequelize.INTEGER, | ||
30 | defaultValue: null, | ||
31 | allowNull: false | ||
32 | } | ||
33 | await utils.queryInterface.changeColumn('actorImage', 'type', data) | ||
34 | } | ||
35 | |||
36 | { | ||
37 | await utils.sequelize.query( | ||
38 | `ALTER TABLE "actor" ADD COLUMN "bannerId" INTEGER REFERENCES "actorImage" ("id") ON DELETE SET NULL ON UPDATE CASCADE` | ||
39 | ) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | function down (options) { | ||
44 | throw new Error('Not implemented.') | ||
45 | } | ||
46 | |||
47 | export { | ||
48 | up, | ||
49 | down | ||
50 | } | ||
diff --git a/server/initializers/migrations/0635-actor-image-size.ts b/server/initializers/migrations/0635-actor-image-size.ts new file mode 100644 index 000000000..d7c5da8c3 --- /dev/null +++ b/server/initializers/migrations/0635-actor-image-size.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.INTEGER, | ||
12 | defaultValue: null, | ||
13 | allowNull: true | ||
14 | } | ||
15 | await utils.queryInterface.addColumn('actorImage', 'height', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const data = { | ||
20 | type: Sequelize.INTEGER, | ||
21 | defaultValue: null, | ||
22 | allowNull: true | ||
23 | } | ||
24 | await utils.queryInterface.addColumn('actorImage', 'width', data) | ||
25 | } | ||
26 | } | ||
27 | |||
28 | function down (options) { | ||
29 | throw new Error('Not implemented.') | ||
30 | } | ||
31 | |||
32 | export { | ||
33 | up, | ||
34 | down | ||
35 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index a726f9e20..eec951d4e 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,26 +1,29 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { extname } from 'path' | ||
2 | import { Op, Transaction } from 'sequelize' | 3 | import { Op, Transaction } from 'sequelize' |
3 | import { URL } from 'url' | 4 | import { URL } from 'url' |
4 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid' |
6 | import { getServerActor } from '@server/models/application/application' | ||
7 | import { ActorImageType } from '@shared/models' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 9 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
6 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 10 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
7 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
12 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
8 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' | 13 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 14 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | 15 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' |
11 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 17 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
13 | import { doRequest } from '../../helpers/requests' | 18 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
14 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | 19 | import { getUrlFromWebfinger } from '../../helpers/webfinger' |
15 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 20 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
21 | import { sequelizeTypescript } from '../../initializers/database' | ||
16 | import { AccountModel } from '../../models/account/account' | 22 | import { AccountModel } from '../../models/account/account' |
23 | import { ActorImageModel } from '../../models/account/actor-image' | ||
17 | import { ActorModel } from '../../models/activitypub/actor' | 24 | import { ActorModel } from '../../models/activitypub/actor' |
18 | import { AvatarModel } from '../../models/avatar/avatar' | ||
19 | import { ServerModel } from '../../models/server/server' | 25 | import { ServerModel } from '../../models/server/server' |
20 | import { VideoChannelModel } from '../../models/video/video-channel' | 26 | import { VideoChannelModel } from '../../models/video/video-channel' |
21 | import { JobQueue } from '../job-queue' | ||
22 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
23 | import { sequelizeTypescript } from '../../initializers/database' | ||
24 | import { | 27 | import { |
25 | MAccount, | 28 | MAccount, |
26 | MAccountDefault, | 29 | MAccountDefault, |
@@ -28,15 +31,14 @@ import { | |||
28 | MActorAccountChannelId, | 31 | MActorAccountChannelId, |
29 | MActorAccountChannelIdActor, | 32 | MActorAccountChannelIdActor, |
30 | MActorAccountId, | 33 | MActorAccountId, |
31 | MActorDefault, | ||
32 | MActorFull, | 34 | MActorFull, |
33 | MActorFullActor, | 35 | MActorFullActor, |
34 | MActorId, | 36 | MActorId, |
37 | MActorImage, | ||
38 | MActorImages, | ||
35 | MChannel | 39 | MChannel |
36 | } from '../../types/models' | 40 | } from '../../types/models' |
37 | import { extname } from 'path' | 41 | import { JobQueue } from '../job-queue' |
38 | import { getServerActor } from '@server/models/application/application' | ||
39 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
40 | 42 | ||
41 | // Set account keys, this could be long so process after the account creation and do not block the client | 43 | // Set account keys, this could be long so process after the account creation and do not block the client |
42 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { | 44 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { |
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ | |||
168 | } | 170 | } |
169 | } | 171 | } |
170 | 172 | ||
171 | type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } | 173 | type ImageInfo = { |
172 | async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { | 174 | name: string |
173 | if (!info.name) return actor | 175 | fileUrl: string |
176 | height: number | ||
177 | width: number | ||
178 | onDisk?: boolean | ||
179 | } | ||
180 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | ||
181 | const oldImageModel = type === ActorImageType.AVATAR | ||
182 | ? actor.Avatar | ||
183 | : actor.Banner | ||
174 | 184 | ||
175 | if (actor.Avatar) { | 185 | if (oldImageModel) { |
176 | // Don't update the avatar if the file URL did not change | 186 | // Don't update the avatar if the file URL did not change |
177 | if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor | 187 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor |
178 | 188 | ||
179 | try { | 189 | try { |
180 | await actor.Avatar.destroy({ transaction: t }) | 190 | await oldImageModel.destroy({ transaction: t }) |
191 | |||
192 | setActorImage(actor, type, null) | ||
181 | } catch (err) { | 193 | } catch (err) { |
182 | logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) | 194 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) |
183 | } | 195 | } |
184 | } | 196 | } |
185 | 197 | ||
186 | const avatar = await AvatarModel.create({ | 198 | if (imageInfo) { |
187 | filename: info.name, | 199 | const imageModel = await ActorImageModel.create({ |
188 | onDisk: info.onDisk, | 200 | filename: imageInfo.name, |
189 | fileUrl: info.fileUrl | 201 | onDisk: imageInfo.onDisk ?? false, |
190 | }, { transaction: t }) | 202 | fileUrl: imageInfo.fileUrl, |
191 | 203 | height: imageInfo.height, | |
192 | actor.avatarId = avatar.id | 204 | width: imageInfo.width, |
193 | actor.Avatar = avatar | 205 | type |
206 | }, { transaction: t }) | ||
207 | |||
208 | setActorImage(actor, type, imageModel) | ||
209 | } | ||
194 | 210 | ||
195 | return actor | 211 | return actor |
196 | } | 212 | } |
197 | 213 | ||
198 | async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { | 214 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { |
199 | try { | 215 | try { |
200 | await actor.Avatar.destroy({ transaction: t }) | 216 | if (type === ActorImageType.AVATAR) { |
217 | await actor.Avatar.destroy({ transaction: t }) | ||
218 | |||
219 | actor.avatarId = null | ||
220 | actor.Avatar = null | ||
221 | } else { | ||
222 | await actor.Banner.destroy({ transaction: t }) | ||
223 | |||
224 | actor.bannerId = null | ||
225 | actor.Banner = null | ||
226 | } | ||
201 | } catch (err) { | 227 | } catch (err) { |
202 | logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) | 228 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) |
203 | } | 229 | } |
204 | 230 | ||
205 | actor.avatarId = null | ||
206 | actor.Avatar = null | ||
207 | |||
208 | return actor | 231 | return actor |
209 | } | 232 | } |
210 | 233 | ||
211 | async function fetchActorTotalItems (url: string) { | 234 | async function fetchActorTotalItems (url: string) { |
212 | const options = { | ||
213 | uri: url, | ||
214 | method: 'GET', | ||
215 | json: true, | ||
216 | activityPub: true | ||
217 | } | ||
218 | |||
219 | try { | 235 | try { |
220 | const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) | 236 | const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true }) |
221 | return body.totalItems ? body.totalItems : 0 | 237 | |
238 | return body.totalItems || 0 | ||
222 | } catch (err) { | 239 | } catch (err) { |
223 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) | 240 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) |
224 | return 0 | 241 | return 0 |
225 | } | 242 | } |
226 | } | 243 | } |
227 | 244 | ||
228 | function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | 245 | function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) { |
229 | const mimetypes = MIMETYPES.IMAGE | 246 | const mimetypes = MIMETYPES.IMAGE |
230 | const icon = actorJSON.icon | 247 | const icon = type === ActorImageType.AVATAR |
248 | ? actorJSON.icon | ||
249 | : actorJSON.image | ||
231 | 250 | ||
232 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | 251 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined |
233 | 252 | ||
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | |||
245 | 264 | ||
246 | return { | 265 | return { |
247 | name: uuidv4() + extension, | 266 | name: uuidv4() + extension, |
248 | fileUrl: icon.url | 267 | fileUrl: icon.url, |
268 | height: icon.height, | ||
269 | width: icon.width, | ||
270 | type | ||
249 | } | 271 | } |
250 | } | 272 | } |
251 | 273 | ||
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
285 | actorUrl = actor.url | 307 | actorUrl = actor.url |
286 | } | 308 | } |
287 | 309 | ||
288 | const { result, statusCode } = await fetchRemoteActor(actorUrl) | 310 | const { result } = await fetchRemoteActor(actorUrl) |
289 | |||
290 | if (statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
291 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
292 | actor.Account | ||
293 | ? await actor.Account.destroy() | ||
294 | : await actor.VideoChannel.destroy() | ||
295 | |||
296 | return { actor: undefined, refreshed: false } | ||
297 | } | ||
298 | 311 | ||
299 | if (result === undefined) { | 312 | if (result === undefined) { |
300 | logger.warn('Cannot fetch remote actor in refresh actor.') | 313 | logger.warn('Cannot fetch remote actor in refresh actor.') |
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
304 | return sequelizeTypescript.transaction(async t => { | 317 | return sequelizeTypescript.transaction(async t => { |
305 | updateInstanceWithAnother(actor, result.actor) | 318 | updateInstanceWithAnother(actor, result.actor) |
306 | 319 | ||
307 | if (result.avatar !== undefined) { | 320 | await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t) |
308 | const avatarInfo = { | 321 | await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t) |
309 | name: result.avatar.name, | ||
310 | fileUrl: result.avatar.fileUrl, | ||
311 | onDisk: false | ||
312 | } | ||
313 | |||
314 | await updateActorAvatarInstance(actor, avatarInfo, t) | ||
315 | } | ||
316 | 322 | ||
317 | // Force update | 323 | // Force update |
318 | actor.setDataValue('updatedAt', new Date()) | 324 | actor.setDataValue('updatedAt', new Date()) |
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel | |||
334 | return { refreshed: true, actor } | 340 | return { refreshed: true, actor } |
335 | }) | 341 | }) |
336 | } catch (err) { | 342 | } catch (err) { |
343 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
344 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
345 | actor.Account | ||
346 | ? await actor.Account.destroy() | ||
347 | : await actor.VideoChannel.destroy() | ||
348 | |||
349 | return { actor: undefined, refreshed: false } | ||
350 | } | ||
351 | |||
337 | logger.warn('Cannot refresh actor %s.', actor.url, { err }) | 352 | logger.warn('Cannot refresh actor %s.', actor.url, { err }) |
338 | return { actor, refreshed: false } | 353 | return { actor, refreshed: false } |
339 | } | 354 | } |
@@ -344,16 +359,32 @@ export { | |||
344 | buildActorInstance, | 359 | buildActorInstance, |
345 | generateAndSaveActorKeys, | 360 | generateAndSaveActorKeys, |
346 | fetchActorTotalItems, | 361 | fetchActorTotalItems, |
347 | getAvatarInfoIfExists, | 362 | getImageInfoIfExists, |
348 | updateActorInstance, | 363 | updateActorInstance, |
349 | deleteActorAvatarInstance, | 364 | deleteActorImageInstance, |
350 | refreshActorIfNeeded, | 365 | refreshActorIfNeeded, |
351 | updateActorAvatarInstance, | 366 | updateActorImageInstance, |
352 | addFetchOutboxJob | 367 | addFetchOutboxJob |
353 | } | 368 | } |
354 | 369 | ||
355 | // --------------------------------------------------------------------------- | 370 | // --------------------------------------------------------------------------- |
356 | 371 | ||
372 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | ||
373 | const id = imageModel | ||
374 | ? imageModel.id | ||
375 | : null | ||
376 | |||
377 | if (type === ActorImageType.AVATAR) { | ||
378 | actorModel.avatarId = id | ||
379 | actorModel.Avatar = imageModel | ||
380 | } else { | ||
381 | actorModel.bannerId = id | ||
382 | actorModel.Banner = imageModel | ||
383 | } | ||
384 | |||
385 | return actorModel | ||
386 | } | ||
387 | |||
357 | function saveActorAndServerAndModelIfNotExist ( | 388 | function saveActorAndServerAndModelIfNotExist ( |
358 | result: FetchRemoteActorResult, | 389 | result: FetchRemoteActorResult, |
359 | ownerActor?: MActorFullActor, | 390 | ownerActor?: MActorFullActor, |
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist ( | |||
384 | 415 | ||
385 | // Avatar? | 416 | // Avatar? |
386 | if (result.avatar) { | 417 | if (result.avatar) { |
387 | const avatar = await AvatarModel.create({ | 418 | const avatar = await ActorImageModel.create({ |
388 | filename: result.avatar.name, | 419 | filename: result.avatar.name, |
389 | fileUrl: result.avatar.fileUrl, | 420 | fileUrl: result.avatar.fileUrl, |
390 | onDisk: false | 421 | width: result.avatar.width, |
422 | height: result.avatar.height, | ||
423 | onDisk: false, | ||
424 | type: ActorImageType.AVATAR | ||
391 | }, { transaction: t }) | 425 | }, { transaction: t }) |
392 | 426 | ||
393 | actor.avatarId = avatar.id | 427 | actor.avatarId = avatar.id |
394 | } | 428 | } |
395 | 429 | ||
430 | // Banner? | ||
431 | if (result.banner) { | ||
432 | const banner = await ActorImageModel.create({ | ||
433 | filename: result.banner.name, | ||
434 | fileUrl: result.banner.fileUrl, | ||
435 | width: result.banner.width, | ||
436 | height: result.banner.height, | ||
437 | onDisk: false, | ||
438 | type: ActorImageType.BANNER | ||
439 | }, { transaction: t }) | ||
440 | |||
441 | actor.bannerId = banner.id | ||
442 | } | ||
443 | |||
396 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists | 444 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists |
397 | // (which could be false in a retried query) | 445 | // (which could be false in a retried query) |
398 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ | 446 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ |
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist ( | |||
436 | } | 484 | } |
437 | } | 485 | } |
438 | 486 | ||
487 | type ImageResult = { | ||
488 | name: string | ||
489 | fileUrl: string | ||
490 | height: number | ||
491 | width: number | ||
492 | } | ||
493 | |||
439 | type FetchRemoteActorResult = { | 494 | type FetchRemoteActorResult = { |
440 | actor: MActor | 495 | actor: MActor |
441 | name: string | 496 | name: string |
442 | summary: string | 497 | summary: string |
443 | support?: string | 498 | support?: string |
444 | playlists?: string | 499 | playlists?: string |
445 | avatar?: { | 500 | avatar?: ImageResult |
446 | name: string | 501 | banner?: ImageResult |
447 | fileUrl: string | ||
448 | } | ||
449 | attributedTo: ActivityPubAttributedTo[] | 502 | attributedTo: ActivityPubAttributedTo[] |
450 | } | 503 | } |
451 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { | 504 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { |
452 | const options = { | ||
453 | uri: actorUrl, | ||
454 | method: 'GET', | ||
455 | json: true, | ||
456 | activityPub: true | ||
457 | } | ||
458 | |||
459 | logger.info('Fetching remote actor %s.', actorUrl) | 505 | logger.info('Fetching remote actor %s.', actorUrl) |
460 | 506 | ||
461 | const requestResult = await doRequest<ActivityPubActor>(options) | 507 | const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true }) |
462 | const actorJSON = requestResult.body | 508 | const actorJSON = requestResult.body |
463 | 509 | ||
464 | if (sanitizeAndCheckActorObject(actorJSON) === false) { | 510 | if (sanitizeAndCheckActorObject(actorJSON) === false) { |
465 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) | 511 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) |
466 | return { result: undefined, statusCode: requestResult.response.statusCode } | 512 | return { result: undefined, statusCode: requestResult.statusCode } |
467 | } | 513 | } |
468 | 514 | ||
469 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { | 515 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { |
470 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) | 516 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) |
471 | return { result: undefined, statusCode: requestResult.response.statusCode } | 517 | return { result: undefined, statusCode: requestResult.statusCode } |
472 | } | 518 | } |
473 | 519 | ||
474 | const followersCount = await fetchActorTotalItems(actorJSON.followers) | 520 | const followersCount = await fetchActorTotalItems(actorJSON.followers) |
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe | |||
492 | : null | 538 | : null |
493 | }) | 539 | }) |
494 | 540 | ||
495 | const avatarInfo = await getAvatarInfoIfExists(actorJSON) | 541 | const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR) |
542 | const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER) | ||
496 | 543 | ||
497 | const name = actorJSON.name || actorJSON.preferredUsername | 544 | const name = actorJSON.name || actorJSON.preferredUsername |
498 | return { | 545 | return { |
499 | statusCode: requestResult.response.statusCode, | 546 | statusCode: requestResult.statusCode, |
500 | result: { | 547 | result: { |
501 | actor, | 548 | actor, |
502 | name, | 549 | name, |
503 | avatar: avatarInfo, | 550 | avatar: avatarInfo, |
551 | banner: bannerInfo, | ||
504 | summary: actorJSON.summary, | 552 | summary: actorJSON.summary, |
505 | support: actorJSON.support, | 553 | support: actorJSON.support, |
506 | playlists: actorJSON.playlists, | 554 | playlists: actorJSON.playlists, |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 1ed105bbe..278abf7de 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -1,27 +1,26 @@ | |||
1 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
2 | import { doRequest } from '../../helpers/requests' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
5 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
6 | import { URL } from 'url' | 2 | import { URL } from 'url' |
3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { doJSONRequest } from '../../helpers/requests' | ||
6 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | ||
7 | 7 | ||
8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) | 8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) |
9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) | 9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) |
10 | 10 | ||
11 | async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { | 11 | async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { |
12 | logger.info('Crawling ActivityPub data on %s.', uri) | 12 | let url = argUrl |
13 | |||
14 | logger.info('Crawling ActivityPub data on %s.', url) | ||
13 | 15 | ||
14 | const options = { | 16 | const options = { |
15 | method: 'GET', | ||
16 | uri, | ||
17 | json: true, | ||
18 | activityPub: true, | 17 | activityPub: true, |
19 | timeout: REQUEST_TIMEOUT | 18 | timeout: REQUEST_TIMEOUT |
20 | } | 19 | } |
21 | 20 | ||
22 | const startDate = new Date() | 21 | const startDate = new Date() |
23 | 22 | ||
24 | const response = await doRequest<ActivityPubOrderedCollection<T>>(options) | 23 | const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) |
25 | const firstBody = response.body | 24 | const firstBody = response.body |
26 | 25 | ||
27 | const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT | 26 | const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT |
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
35 | const remoteHost = new URL(nextLink).host | 34 | const remoteHost = new URL(nextLink).host |
36 | if (remoteHost === WEBSERVER.HOST) continue | 35 | if (remoteHost === WEBSERVER.HOST) continue |
37 | 36 | ||
38 | options.uri = nextLink | 37 | url = nextLink |
39 | 38 | ||
40 | const res = await doRequest<ActivityPubOrderedCollection<T>>(options) | 39 | const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options) |
41 | body = res.body | 40 | body = res.body |
42 | } else { | 41 | } else { |
43 | // nextLink is already the object we want | 42 | // nextLink is already the object we want |
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T> | |||
49 | 48 | ||
50 | if (Array.isArray(body.orderedItems)) { | 49 | if (Array.isArray(body.orderedItems)) { |
51 | const items = body.orderedItems | 50 | const items = body.orderedItems |
52 | logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) | 51 | logger.info('Processing %i ActivityPub items for %s.', items.length, url) |
53 | 52 | ||
54 | await handler(items) | 53 | await handler(items) |
55 | } | 54 | } |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index d5a3ef7c8..7166c68a6 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -1,24 +1,24 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | 5 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { checkUrlsSameHost } from '../../helpers/activitypub' |
7 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
4 | import { isArray } from '../../helpers/custom-validators/misc' | 8 | import { isArray } from '../../helpers/custom-validators/misc' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
11 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../../initializers/database' | ||
7 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 13 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
8 | import { doRequest } from '../../helpers/requests' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import * as Bluebird from 'bluebird' | ||
11 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
12 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
13 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
15 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
16 | import { sequelizeTypescript } from '../../initializers/database' | ||
17 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
18 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
19 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' | 15 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' |
20 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' | 16 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' |
21 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 17 | import { FilteredModelAttributes } from '../../types/sequelize' |
18 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
19 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
20 | import { crawlCollectionPage } from './crawl' | ||
21 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
22 | 22 | ||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { |
24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | 24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount | |||
56 | if (exists === true) return | 56 | if (exists === true) return |
57 | 57 | ||
58 | // Fetch url | 58 | // Fetch url |
59 | const { body } = await doRequest<PlaylistObject>({ | 59 | const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true }) |
60 | uri: playlistUrl, | ||
61 | json: true, | ||
62 | activityPub: true | ||
63 | }) | ||
64 | 60 | ||
65 | if (!isPlaylistObjectValid(body)) { | 61 | if (!isPlaylistObjectValid(body)) { |
66 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | 62 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) |
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
120 | if (!videoPlaylist.isOutdated()) return videoPlaylist | 116 | if (!videoPlaylist.isOutdated()) return videoPlaylist |
121 | 117 | ||
122 | try { | 118 | try { |
123 | const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | 119 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) |
124 | if (statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
125 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
126 | |||
127 | await videoPlaylist.destroy() | ||
128 | return undefined | ||
129 | } | ||
130 | 120 | ||
131 | if (playlistObject === undefined) { | 121 | if (playlistObject === undefined) { |
132 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) | 122 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) |
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
140 | 130 | ||
141 | return videoPlaylist | 131 | return videoPlaylist |
142 | } catch (err) { | 132 | } catch (err) { |
133 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
134 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
135 | |||
136 | await videoPlaylist.destroy() | ||
137 | return undefined | ||
138 | } | ||
139 | |||
143 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) | 140 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) |
144 | 141 | ||
145 | await videoPlaylist.setAsRefreshed() | 142 | await videoPlaylist.setAsRefreshed() |
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid | |||
164 | 161 | ||
165 | await Bluebird.map(elementUrls, async elementUrl => { | 162 | await Bluebird.map(elementUrls, async elementUrl => { |
166 | try { | 163 | try { |
167 | // Fetch url | 164 | const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) |
168 | const { body } = await doRequest<PlaylistElementObject>({ | ||
169 | uri: elementUrl, | ||
170 | json: true, | ||
171 | activityPub: true | ||
172 | }) | ||
173 | 165 | ||
174 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | 166 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) |
175 | 167 | ||
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid | |||
199 | } | 191 | } |
200 | 192 | ||
201 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | 193 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { |
202 | const options = { | ||
203 | uri: playlistUrl, | ||
204 | method: 'GET', | ||
205 | json: true, | ||
206 | activityPub: true | ||
207 | } | ||
208 | |||
209 | logger.info('Fetching remote playlist %s.', playlistUrl) | 194 | logger.info('Fetching remote playlist %s.', playlistUrl) |
210 | 195 | ||
211 | const { response, body } = await doRequest<any>(options) | 196 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) |
212 | 197 | ||
213 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | 198 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { |
214 | logger.debug('Remote video playlist JSON is not valid.', { body }) | 199 | logger.debug('Remote video playlist JSON is not valid.', { body }) |
215 | return { statusCode: response.statusCode, playlistObject: undefined } | 200 | return { statusCode, playlistObject: undefined } |
216 | } | 201 | } |
217 | 202 | ||
218 | return { statusCode: response.statusCode, playlistObject: body } | 203 | return { statusCode, playlistObject: body } |
219 | } | 204 | } |
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index a86def936..070ee0f1d 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video' | |||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' | 10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models' |
11 | import { markCommentAsDeleted } from '../../video-comment' | 11 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 12 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | 13 | ||
@@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete | |||
30 | } else if (byActorFull.type === 'Group') { | 30 | } else if (byActorFull.type === 'Group') { |
31 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') | 31 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') |
32 | 32 | ||
33 | const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor | 33 | const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull }) |
34 | channelToDelete.Actor = byActorFull | ||
35 | |||
36 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) | 34 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) |
37 | } | 35 | } |
38 | } | 36 | } |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 849f70b94..6df9b93b2 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database' | |||
6 | import { AccountModel } from '../../../models/account/account' | 6 | import { AccountModel } from '../../../models/account/account' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
9 | import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' | 9 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' |
10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' | 10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' |
11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | 11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' |
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist' | |||
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' | 18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' |
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 19 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
20 | import { ActorImageType } from '@shared/models' | ||
20 | 21 | ||
21 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 22 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { |
22 | const { activity, byActor } = options | 23 | const { activity, byActor } = options |
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) | |||
119 | let accountOrChannelFieldsSave: object | 120 | let accountOrChannelFieldsSave: object |
120 | 121 | ||
121 | // Fetch icon? | 122 | // Fetch icon? |
122 | const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) | 123 | const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR) |
124 | const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER) | ||
123 | 125 | ||
124 | try { | 126 | try { |
125 | await sequelizeTypescript.transaction(async t => { | 127 | await sequelizeTypescript.transaction(async t => { |
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) | |||
132 | 134 | ||
133 | await updateActorInstance(actor, actorAttributesToUpdate) | 135 | await updateActorInstance(actor, actorAttributesToUpdate) |
134 | 136 | ||
135 | if (avatarInfo !== undefined) { | 137 | await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t) |
136 | const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) | 138 | await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t) |
137 | |||
138 | await updateActorAvatarInstance(actor, avatarOptions, t) | ||
139 | } | ||
140 | 139 | ||
141 | await actor.save({ transaction: t }) | 140 | await actor.save({ transaction: t }) |
142 | 141 | ||
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 9fb218224..baded642a 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos' | |||
4 | import { VideoCommentModel } from '../../../models/video/video-comment' | 4 | import { VideoCommentModel } from '../../../models/video/video-comment' |
5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' | 5 | import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' |
6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' | 6 | import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' |
7 | import { logger } from '../../../helpers/logger' | 7 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 8 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
9 | import { | 9 | import { |
10 | MActorLight, | 10 | MActorLight, |
@@ -18,10 +18,12 @@ import { | |||
18 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
19 | import { ContextType } from '@shared/models/activitypub/context' | 19 | import { ContextType } from '@shared/models/activitypub/context' |
20 | 20 | ||
21 | const lTags = loggerTagsFactory('ap', 'create') | ||
22 | |||
21 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { | 23 | async function sendCreateVideo (video: MVideoAP, t: Transaction) { |
22 | if (!video.hasPrivacyForFederation()) return undefined | 24 | if (!video.hasPrivacyForFederation()) return undefined |
23 | 25 | ||
24 | logger.info('Creating job to send video creation of %s.', video.url) | 26 | logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) |
25 | 27 | ||
26 | const byActor = video.VideoChannel.Account.Actor | 28 | const byActor = video.VideoChannel.Account.Actor |
27 | const videoObject = video.toActivityPubObject() | 29 | const videoObject = video.toActivityPubObject() |
@@ -37,7 +39,7 @@ async function sendCreateCacheFile ( | |||
37 | video: MVideoAccountLight, | 39 | video: MVideoAccountLight, |
38 | fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo | 40 | fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo |
39 | ) { | 41 | ) { |
40 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url) | 42 | logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid)) |
41 | 43 | ||
42 | return sendVideoRelatedCreateActivity({ | 44 | return sendVideoRelatedCreateActivity({ |
43 | byActor, | 45 | byActor, |
@@ -51,7 +53,7 @@ async function sendCreateCacheFile ( | |||
51 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { | 53 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { |
52 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined | 54 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined |
53 | 55 | ||
54 | logger.info('Creating job to send create video playlist of %s.', playlist.url) | 56 | logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid)) |
55 | 57 | ||
56 | const byActor = playlist.OwnerAccount.Actor | 58 | const byActor = playlist.OwnerAccount.Actor |
57 | const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) | 59 | const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 1f8a8f3c4..c22fa0893 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -1,15 +1,17 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
3 | import { getServerActor } from '@server/models/application/application' | ||
4 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
5 | import { logger, loggerTagsFactory } from '../../helpers/logger' | ||
6 | import { doJSONRequest } from '../../helpers/requests' | ||
7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
2 | import { VideoShareModel } from '../../models/video/video-share' | 8 | import { VideoShareModel } from '../../models/video/video-share' |
9 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
3 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 11 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
4 | import { getLocalVideoAnnounceActivityPubUrl } from './url' | 12 | import { getLocalVideoAnnounceActivityPubUrl } from './url' |
5 | import * as Bluebird from 'bluebird' | 13 | |
6 | import { doRequest } from '../../helpers/requests' | 14 | const lTags = loggerTagsFactory('share') |
7 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | ||
12 | import { getServerActor } from '@server/models/application/application' | ||
13 | 15 | ||
14 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { | 16 | async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { |
15 | if (!video.hasPrivacyForFederation()) return undefined | 17 | if (!video.hasPrivacyForFederation()) return undefined |
@@ -25,7 +27,10 @@ async function changeVideoChannelShare ( | |||
25 | oldVideoChannel: MChannelActorLight, | 27 | oldVideoChannel: MChannelActorLight, |
26 | t: Transaction | 28 | t: Transaction |
27 | ) { | 29 | ) { |
28 | logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) | 30 | logger.info( |
31 | 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name, | ||
32 | lTags(video.uuid) | ||
33 | ) | ||
29 | 34 | ||
30 | await undoShareByVideoChannel(video, oldVideoChannel, t) | 35 | await undoShareByVideoChannel(video, oldVideoChannel, t) |
31 | 36 | ||
@@ -35,12 +40,7 @@ async function changeVideoChannelShare ( | |||
35 | async function addVideoShares (shareUrls: string[], video: MVideoId) { | 40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { |
36 | await Bluebird.map(shareUrls, async shareUrl => { | 41 | await Bluebird.map(shareUrls, async shareUrl => { |
37 | try { | 42 | try { |
38 | // Fetch url | 43 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) |
39 | const { body } = await doRequest<any>({ | ||
40 | uri: shareUrl, | ||
41 | json: true, | ||
42 | activityPub: true | ||
43 | }) | ||
44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
45 | 45 | ||
46 | const actorUrl = getAPId(body.actor) | 46 | const actorUrl = getAPId(body.actor) |
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index d025ed7f1..e23e0c0e7 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
1 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' | 3 | import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' |
2 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
3 | import { doRequest } from '../../helpers/requests' | 5 | import { doJSONRequest } from '../../helpers/requests' |
4 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
5 | import { VideoCommentModel } from '../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../models/video/video-comment' |
8 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
6 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateActorAndServerAndModel } from './actor' |
7 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' |
8 | import * as Bluebird from 'bluebird' | ||
9 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
10 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | ||
11 | 11 | ||
12 | type ResolveThreadParams = { | 12 | type ResolveThreadParams = { |
13 | url: string | 13 | url: string |
@@ -18,8 +18,12 @@ type ResolveThreadParams = { | |||
18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> | 18 | type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> |
19 | 19 | ||
20 | async function addVideoComments (commentUrls: string[]) { | 20 | async function addVideoComments (commentUrls: string[]) { |
21 | return Bluebird.map(commentUrls, commentUrl => { | 21 | return Bluebird.map(commentUrls, async commentUrl => { |
22 | return resolveThread({ url: commentUrl, isVideo: false }) | 22 | try { |
23 | await resolveThread({ url: commentUrl, isVideo: false }) | ||
24 | } catch (err) { | ||
25 | logger.warn('Cannot resolve thread %s.', commentUrl, { err }) | ||
26 | } | ||
23 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 27 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
24 | } | 28 | } |
25 | 29 | ||
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) { | |||
126 | throw new Error('Recursion limit reached when resolving a thread') | 130 | throw new Error('Recursion limit reached when resolving a thread') |
127 | } | 131 | } |
128 | 132 | ||
129 | const { body } = await doRequest<any>({ | 133 | const { body } = await doJSONRequest<any>(url, { activityPub: true }) |
130 | uri: url, | ||
131 | json: true, | ||
132 | activityPub: true | ||
133 | }) | ||
134 | 134 | ||
135 | if (sanitizeAndCheckVideoCommentObject(body) === false) { | 135 | if (sanitizeAndCheckVideoCommentObject(body) === false) { |
136 | throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) | 136 | throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index e246b1313..f40c07fea 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -1,26 +1,22 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { Transaction } from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
2 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { VideoRateType } from '../../../shared/models/videos' | 4 | import { VideoRateType } from '../../../shared/models/videos' |
4 | import * as Bluebird from 'bluebird' | 5 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
5 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
6 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | ||
7 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
8 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
9 | import { doRequest } from '../../helpers/requests' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
10 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
11 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' | 9 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' |
10 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
11 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | ||
12 | import { sendDislike } from './send/send-dislike' | ||
13 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | ||
14 | 14 | ||
15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { | 15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { |
16 | await Bluebird.map(ratesUrl, async rateUrl => { | 16 | await Bluebird.map(ratesUrl, async rateUrl => { |
17 | try { | 17 | try { |
18 | // Fetch url | 18 | // Fetch url |
19 | const { body } = await doRequest<any>({ | 19 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) |
20 | uri: rateUrl, | ||
21 | json: true, | ||
22 | activityPub: true | ||
23 | }) | ||
24 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | 20 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') |
25 | 21 | ||
26 | const actorUrl = getAPId(body.actor) | 22 | const actorUrl = getAPId(body.actor) |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index c02578aad..9014791c0 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { basename, join } from 'path' | 4 | import { basename } from 'path' |
5 | import * as request from 'request' | ||
6 | import { Transaction } from 'sequelize/types' | 5 | import { Transaction } from 'sequelize/types' |
7 | import { TrackerModel } from '@server/models/server/tracker' | 6 | import { TrackerModel } from '@server/models/server/tracker' |
8 | import { VideoLiveModel } from '@server/models/video/video-live' | 7 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -17,7 +16,7 @@ import { | |||
17 | ActivityUrlObject, | 16 | ActivityUrlObject, |
18 | ActivityVideoUrlObject | 17 | ActivityVideoUrlObject |
19 | } from '../../../shared/index' | 18 | } from '../../../shared/index' |
20 | import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | 19 | import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' |
21 | import { VideoPrivacy } from '../../../shared/models/videos' | 20 | import { VideoPrivacy } from '../../../shared/models/videos' |
22 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
23 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 22 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc' | |||
31 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 30 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
32 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 31 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
33 | import { logger } from '../../helpers/logger' | 32 | import { logger } from '../../helpers/logger' |
34 | import { doRequest } from '../../helpers/requests' | 33 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' |
35 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' | 34 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' |
36 | import { | 35 | import { |
37 | ACTIVITY_PUB, | 36 | ACTIVITY_PUB, |
38 | LAZY_STATIC_PATHS, | ||
39 | MIMETYPES, | 37 | MIMETYPES, |
40 | P2P_MEDIA_LOADER_PEER_VERSION, | 38 | P2P_MEDIA_LOADER_PEER_VERSION, |
41 | PREVIEWS_SIZE, | 39 | PREVIEWS_SIZE, |
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid | |||
115 | } | 113 | } |
116 | } | 114 | } |
117 | 115 | ||
118 | async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { | 116 | async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> { |
119 | const options = { | ||
120 | uri: videoUrl, | ||
121 | method: 'GET', | ||
122 | json: true, | ||
123 | activityPub: true | ||
124 | } | ||
125 | |||
126 | logger.info('Fetching remote video %s.', videoUrl) | 117 | logger.info('Fetching remote video %s.', videoUrl) |
127 | 118 | ||
128 | const { response, body } = await doRequest<any>(options) | 119 | const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true }) |
129 | 120 | ||
130 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { | 121 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { |
131 | logger.debug('Remote video JSON is not valid.', { body }) | 122 | logger.debug('Remote video JSON is not valid.', { body }) |
132 | return { response, videoObject: undefined } | 123 | return { statusCode, videoObject: undefined } |
133 | } | 124 | } |
134 | 125 | ||
135 | return { response, videoObject: body } | 126 | return { statusCode, videoObject: body } |
136 | } | 127 | } |
137 | 128 | ||
138 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | 129 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { |
139 | const host = video.VideoChannel.Account.Actor.Server.host | 130 | const host = video.VideoChannel.Account.Actor.Server.host |
140 | const path = video.getDescriptionAPIPath() | 131 | const path = video.getDescriptionAPIPath() |
141 | const options = { | 132 | const url = REMOTE_SCHEME.HTTP + '://' + host + path |
142 | uri: REMOTE_SCHEME.HTTP + '://' + host + path, | ||
143 | json: true | ||
144 | } | ||
145 | 133 | ||
146 | const { body } = await doRequest<any>(options) | 134 | const { body } = await doJSONRequest<any>(url) |
147 | return body.description ? body.description : '' | 135 | return body.description || '' |
148 | } | 136 | } |
149 | 137 | ||
150 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { | 138 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { |
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: { | |||
378 | 366 | ||
379 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) | 367 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) |
380 | 368 | ||
381 | if (videoUpdated.getPreview()) { | 369 | const previewIcon = getPreviewFromIcons(videoObject) |
382 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) | 370 | if (videoUpdated.getPreview() && previewIcon) { |
383 | const previewModel = createPlaceholderThumbnail({ | 371 | const previewModel = createPlaceholderThumbnail({ |
384 | fileUrl: previewUrl, | 372 | fileUrl: previewIcon.url, |
385 | video, | 373 | video, |
386 | type: ThumbnailType.PREVIEW, | 374 | type: ThumbnailType.PREVIEW, |
387 | size: PREVIEWS_SIZE | 375 | size: previewIcon |
388 | }) | 376 | }) |
389 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | 377 | await videoUpdated.addAndSaveThumbnail(previewModel, t) |
390 | } | 378 | } |
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: { | |||
534 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | 522 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) |
535 | 523 | ||
536 | try { | 524 | try { |
537 | const { response, videoObject } = await fetchRemoteVideo(video.url) | 525 | const { videoObject } = await fetchRemoteVideo(video.url) |
538 | if (response.statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
539 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
540 | |||
541 | // Video does not exist anymore | ||
542 | await video.destroy() | ||
543 | return undefined | ||
544 | } | ||
545 | 526 | ||
546 | if (videoObject === undefined) { | 527 | if (videoObject === undefined) { |
547 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | 528 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) |
@@ -565,6 +546,14 @@ async function refreshVideoIfNeeded (options: { | |||
565 | 546 | ||
566 | return video | 547 | return video |
567 | } catch (err) { | 548 | } catch (err) { |
549 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
550 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
551 | |||
552 | // Video does not exist anymore | ||
553 | await video.destroy() | ||
554 | return undefined | ||
555 | } | ||
556 | |||
568 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | 557 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) |
569 | 558 | ||
570 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) | 559 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) |
@@ -638,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi | |||
638 | 627 | ||
639 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | 628 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) |
640 | 629 | ||
641 | const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) | 630 | const previewIcon = getPreviewFromIcons(videoObject) |
642 | const previewModel = createPlaceholderThumbnail({ | 631 | if (previewIcon) { |
643 | fileUrl: previewUrl, | 632 | const previewModel = createPlaceholderThumbnail({ |
644 | video: videoCreated, | 633 | fileUrl: previewIcon.url, |
645 | type: ThumbnailType.PREVIEW, | 634 | video: videoCreated, |
646 | size: PREVIEWS_SIZE | 635 | type: ThumbnailType.PREVIEW, |
647 | }) | 636 | size: previewIcon |
637 | }) | ||
648 | 638 | ||
649 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) | 639 | await videoCreated.addAndSaveThumbnail(previewModel, t) |
640 | } | ||
650 | 641 | ||
651 | // Process files | 642 | // Process files |
652 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) | 643 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) |
@@ -906,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) { | |||
906 | return maxBy(validIcons, 'width') | 897 | return maxBy(validIcons, 'width') |
907 | } | 898 | } |
908 | 899 | ||
909 | function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) { | ||
910 | return previewIcon | ||
911 | ? previewIcon.url | ||
912 | : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | ||
913 | } | ||
914 | |||
915 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { | 900 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { |
916 | let wsFound = false | 901 | let wsFound = false |
917 | 902 | ||
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..f271f0b5b --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | ||
3 | import * as LRUCache from 'lru-cache' | ||
4 | import { extname, join } from 'path' | ||
5 | import { v4 as uuidv4 } from 'uuid' | ||
6 | import { ActorImageType } from '@shared/models' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { processImage } from '../helpers/image-utils' | ||
9 | import { downloadImage } from '../helpers/requests' | ||
10 | import { CONFIG } from '../initializers/config' | ||
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../initializers/database' | ||
13 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
14 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' | ||
15 | import { sendUpdateActor } from './activitypub/send' | ||
16 | |||
17 | async function updateLocalActorImageFile ( | ||
18 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
19 | imagePhysicalFile: Express.Multer.File, | ||
20 | type: ActorImageType | ||
21 | ) { | ||
22 | const imageSize = type === ActorImageType.AVATAR | ||
23 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
24 | : ACTOR_IMAGES_SIZE.BANNERS | ||
25 | |||
26 | const extension = extname(imagePhysicalFile.filename) | ||
27 | |||
28 | const imageName = uuidv4() + extension | ||
29 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | ||
30 | await processImage(imagePhysicalFile.path, destination, imageSize) | ||
31 | |||
32 | return retryTransactionWrapper(() => { | ||
33 | return sequelizeTypescript.transaction(async t => { | ||
34 | const actorImageInfo = { | ||
35 | name: imageName, | ||
36 | fileUrl: null, | ||
37 | height: imageSize.height, | ||
38 | width: imageSize.width, | ||
39 | onDisk: true | ||
40 | } | ||
41 | |||
42 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | ||
43 | await updatedActor.save({ transaction: t }) | ||
44 | |||
45 | await sendUpdateActor(accountOrChannel, t) | ||
46 | |||
47 | return type === ActorImageType.AVATAR | ||
48 | ? updatedActor.Avatar | ||
49 | : updatedActor.Banner | ||
50 | }) | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | ||
55 | return retryTransactionWrapper(() => { | ||
56 | return sequelizeTypescript.transaction(async t => { | ||
57 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | ||
58 | await updatedActor.save({ transaction: t }) | ||
59 | |||
60 | await sendUpdateActor(accountOrChannel, t) | ||
61 | |||
62 | return updatedActor.Avatar | ||
63 | }) | ||
64 | }) | ||
65 | } | ||
66 | |||
67 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | ||
68 | |||
69 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
70 | const size = task.type === ActorImageType.AVATAR | ||
71 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
72 | : ACTOR_IMAGES_SIZE.BANNERS | ||
73 | |||
74 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
75 | .then(() => cb()) | ||
76 | .catch(err => cb(err)) | ||
77 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | ||
78 | |||
79 | function pushActorImageProcessInQueue (task: DownloadImageQueueTask) { | ||
80 | return new Promise<void>((res, rej) => { | ||
81 | downloadImageQueue.push(task, err => { | ||
82 | if (err) return rej(err) | ||
83 | |||
84 | return res() | ||
85 | }) | ||
86 | }) | ||
87 | } | ||
88 | |||
89 | // Unsafe so could returns paths that does not exist anymore | ||
90 | const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE }) | ||
91 | |||
92 | export { | ||
93 | actorImagePathUnsafeCache, | ||
94 | updateLocalActorImageFile, | ||
95 | deleteLocalActorImageFile, | ||
96 | pushActorImageProcessInQueue | ||
97 | } | ||
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts index dbd421a7b..80f5064b6 100644 --- a/server/lib/auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,28 +1,16 @@ | |||
1 | |||
1 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' |
2 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
3 | import { generateRandomString } from '@server/helpers/utils' | 4 | import { generateRandomString } from '@server/helpers/utils' |
4 | import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
5 | import { revokeToken } from '@server/lib/oauth-model' | ||
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
8 | import { UserRole } from '@shared/models' | ||
9 | import { | 8 | import { |
10 | RegisterServerAuthenticatedResult, | 9 | RegisterServerAuthenticatedResult, |
11 | RegisterServerAuthPassOptions, | 10 | RegisterServerAuthPassOptions, |
12 | RegisterServerExternalAuthenticatedResult | 11 | RegisterServerExternalAuthenticatedResult |
13 | } from '@server/types/plugins/register-server-auth.model' | 12 | } from '@server/types/plugins/register-server-auth.model' |
14 | import * as express from 'express' | 13 | import { UserRole } from '@shared/models' |
15 | import * as OAuthServer from 'express-oauth-server' | ||
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | |||
18 | const oAuthServer = new OAuthServer({ | ||
19 | useErrorHandler: true, | ||
20 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
21 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
22 | allowExtendedTokenAttributes: true, | ||
23 | continueMiddleware: true, | ||
24 | model: require('./oauth-model') | ||
25 | }) | ||
26 | 14 | ||
27 | // Token is the key, expiration date is the value | 15 | // Token is the key, expiration date is the value |
28 | const authBypassTokens = new Map<string, { | 16 | const authBypassTokens = new Map<string, { |
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, { | |||
37 | npmName: string | 25 | npmName: string |
38 | }>() | 26 | }>() |
39 | 27 | ||
40 | async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
41 | const grantType = req.body.grant_type | ||
42 | |||
43 | if (grantType === 'password') { | ||
44 | if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res) | ||
45 | else await proxifyPasswordGrant(req, res) | ||
46 | } else if (grantType === 'refresh_token') { | ||
47 | await proxifyRefreshGrant(req, res) | ||
48 | } | ||
49 | |||
50 | return forwardTokenReq(req, res, next) | ||
51 | } | ||
52 | |||
53 | async function handleTokenRevocation (req: express.Request, res: express.Response) { | ||
54 | const token = res.locals.oauth.token | ||
55 | |||
56 | res.locals.explicitLogout = true | ||
57 | const result = await revokeToken(token) | ||
58 | |||
59 | // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released | ||
60 | // oAuthServer.revoke(req, res, err => { | ||
61 | // if (err) { | ||
62 | // logger.warn('Error in revoke token handler.', { err }) | ||
63 | // | ||
64 | // return res.status(err.status) | ||
65 | // .json({ | ||
66 | // error: err.message, | ||
67 | // code: err.name | ||
68 | // }) | ||
69 | // .end() | ||
70 | // } | ||
71 | // }) | ||
72 | |||
73 | return res.json(result) | ||
74 | } | ||
75 | |||
76 | async function onExternalUserAuthenticated (options: { | 28 | async function onExternalUserAuthenticated (options: { |
77 | npmName: string | 29 | npmName: string |
78 | authName: string | 30 | authName: string |
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: { | |||
107 | authName | 59 | authName |
108 | }) | 60 | }) |
109 | 61 | ||
110 | // Cleanup | 62 | // Cleanup expired tokens |
111 | const now = new Date() | 63 | const now = new Date() |
112 | for (const [ key, value ] of authBypassTokens) { | 64 | for (const [ key, value ] of authBypassTokens) { |
113 | if (value.expires.getTime() < now.getTime()) { | 65 | if (value.expires.getTime() < now.getTime()) { |
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: { | |||
118 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) | 70 | res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) |
119 | } | 71 | } |
120 | 72 | ||
121 | // --------------------------------------------------------------------------- | 73 | async function getAuthNameFromRefreshGrant (refreshToken?: string) { |
122 | 74 | if (!refreshToken) return undefined | |
123 | export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation } | ||
124 | |||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
127 | function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) { | ||
128 | return oAuthServer.token()(req, res, err => { | ||
129 | if (err) { | ||
130 | logger.warn('Login error.', { err }) | ||
131 | |||
132 | return res.status(err.status) | ||
133 | .json({ | ||
134 | error: err.message, | ||
135 | code: err.name | ||
136 | }) | ||
137 | } | ||
138 | |||
139 | if (next) return next() | ||
140 | }) | ||
141 | } | ||
142 | |||
143 | async function proxifyRefreshGrant (req: express.Request, res: express.Response) { | ||
144 | const refreshToken = req.body.refresh_token | ||
145 | if (!refreshToken) return | ||
146 | 75 | ||
147 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) | 76 | const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) |
148 | if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName | 77 | |
78 | return tokenModel?.authName | ||
149 | } | 79 | } |
150 | 80 | ||
151 | async function proxifyPasswordGrant (req: express.Request, res: express.Response) { | 81 | async function getBypassFromPasswordGrant (username: string, password: string) { |
152 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 82 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
153 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
154 | 84 | ||
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
174 | }) | 104 | }) |
175 | 105 | ||
176 | const loginOptions = { | 106 | const loginOptions = { |
177 | id: req.body.username, | 107 | id: username, |
178 | password: req.body.password | 108 | password |
179 | } | 109 | } |
180 | 110 | ||
181 | for (const pluginAuth of pluginAuths) { | 111 | for (const pluginAuth of pluginAuths) { |
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response | |||
199 | authName, npmName, loginOptions.id | 129 | authName, npmName, loginOptions.id |
200 | ) | 130 | ) |
201 | 131 | ||
202 | res.locals.bypassLogin = { | 132 | return { |
203 | bypass: true, | 133 | bypass: true, |
204 | pluginName: pluginAuth.npmName, | 134 | pluginName: pluginAuth.npmName, |
205 | authName: authOptions.authName, | 135 | authName: authOptions.authName, |
206 | user: buildUserResult(loginResult) | 136 | user: buildUserResult(loginResult) |
207 | } | 137 | } |
208 | |||
209 | return | ||
210 | } catch (err) { | 138 | } catch (err) { |
211 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
212 | } | 140 | } |
213 | } | 141 | } |
142 | |||
143 | return undefined | ||
214 | } | 144 | } |
215 | 145 | ||
216 | function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { | 146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { |
217 | const obj = authBypassTokens.get(req.body.externalAuthToken) | 147 | const obj = authBypassTokens.get(externalAuthToken) |
218 | if (!obj) { | 148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
219 | logger.error('Cannot authenticate user with unknown bypass token') | ||
220 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
221 | } | ||
222 | 149 | ||
223 | const { expires, user, authName, npmName } = obj | 150 | const { expires, user, authName, npmName } = obj |
224 | 151 | ||
225 | const now = new Date() | 152 | const now = new Date() |
226 | if (now.getTime() > expires.getTime()) { | 153 | if (now.getTime() > expires.getTime()) { |
227 | logger.error('Cannot authenticate user with an expired external auth token') | 154 | throw new Error('Cannot authenticate user with an expired external auth token') |
228 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
229 | } | 155 | } |
230 | 156 | ||
231 | if (user.username !== req.body.username) { | 157 | if (user.username !== username) { |
232 | logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) | 158 | throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`) |
233 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | ||
234 | } | 159 | } |
235 | 160 | ||
236 | // Bypass oauth library validation | ||
237 | req.body.password = 'fake' | ||
238 | |||
239 | logger.info( | 161 | logger.info( |
240 | 'Auth success with external auth method %s of plugin %s for %s.', | 162 | 'Auth success with external auth method %s of plugin %s for %s.', |
241 | authName, npmName, user.email | 163 | authName, npmName, user.email |
242 | ) | 164 | ) |
243 | 165 | ||
244 | res.locals.bypassLogin = { | 166 | return { |
245 | bypass: true, | 167 | bypass: true, |
246 | pluginName: npmName, | 168 | pluginName: npmName, |
247 | authName: authName, | 169 | authName: authName, |
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
286 | displayName: pluginResult.displayName || pluginResult.username | 208 | displayName: pluginResult.displayName || pluginResult.username |
287 | } | 209 | } |
288 | } | 210 | } |
211 | |||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
214 | export { | ||
215 | onExternalUserAuthenticated, | ||
216 | getBypassFromExternalAuth, | ||
217 | getAuthNameFromRefreshGrant, | ||
218 | getBypassFromPasswordGrant | ||
219 | } | ||
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts index a2c53a2c9..b9c69eb2d 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,49 +1,36 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as LRUCache from 'lru-cache' | ||
3 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
4 | import { Transaction } from 'sequelize' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
6 | import { ActorModel } from '@server/models/activitypub/actor' | 4 | import { ActorModel } from '@server/models/activitypub/actor' |
5 | import { MOAuthClient } from '@server/types/models' | ||
7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
8 | import { MUser } from '@server/types/models/user/user' | 7 | import { MUser } from '@server/types/models/user/user' |
9 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 8 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' |
10 | import { UserRole } from '@shared/models/users/user-role' | 9 | import { UserRole } from '@shared/models/users/user-role' |
11 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../../initializers/config' |
13 | import { LRU_CACHE } from '../initializers/constants' | 12 | import { UserModel } from '../../models/account/user' |
14 | import { UserModel } from '../models/account/user' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
15 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
16 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 15 | import { createUserAccountAndChannelAndPlaylist } from '../user' |
17 | import { createUserAccountAndChannelAndPlaylist } from './user' | 16 | import { TokensCache } from './tokens-cache' |
18 | 17 | ||
19 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 18 | type TokenInfo = { |
20 | 19 | accessToken: string | |
21 | const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 20 | refreshToken: string |
22 | const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | 21 | accessTokenExpiresAt: Date |
23 | 22 | refreshTokenExpiresAt: Date | |
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function deleteUserToken (userId: number, t?: Transaction) { | ||
27 | clearCacheByUserId(userId) | ||
28 | |||
29 | return OAuthTokenModel.deleteUserToken(userId, t) | ||
30 | } | 23 | } |
31 | 24 | ||
32 | function clearCacheByUserId (userId: number) { | 25 | export type BypassLogin = { |
33 | const token = userHavingToken.get(userId) | 26 | bypass: boolean |
34 | 27 | pluginName: string | |
35 | if (token !== undefined) { | 28 | authName?: string |
36 | accessTokenCache.del(token) | 29 | user: { |
37 | userHavingToken.del(userId) | 30 | username: string |
38 | } | 31 | email: string |
39 | } | 32 | displayName: string |
40 | 33 | role: UserRole | |
41 | function clearCacheByToken (token: string) { | ||
42 | const tokenModel = accessTokenCache.get(token) | ||
43 | |||
44 | if (tokenModel !== undefined) { | ||
45 | userHavingToken.del(tokenModel.userId) | ||
46 | accessTokenCache.del(token) | ||
47 | } | 34 | } |
48 | } | 35 | } |
49 | 36 | ||
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) { | |||
54 | 41 | ||
55 | let tokenModel: MOAuthTokenUser | 42 | let tokenModel: MOAuthTokenUser |
56 | 43 | ||
57 | if (accessTokenCache.has(bearerToken)) { | 44 | if (TokensCache.Instance.hasToken(bearerToken)) { |
58 | tokenModel = accessTokenCache.get(bearerToken) | 45 | tokenModel = TokensCache.Instance.getByToken(bearerToken) |
59 | } else { | 46 | } else { |
60 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) | 47 | tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) |
61 | 48 | ||
62 | if (tokenModel) { | 49 | if (tokenModel) TokensCache.Instance.setToken(tokenModel) |
63 | accessTokenCache.set(bearerToken, tokenModel) | ||
64 | userHavingToken.set(tokenModel.userId, tokenModel.accessToken) | ||
65 | } | ||
66 | } | 50 | } |
67 | 51 | ||
68 | if (!tokenModel) return undefined | 52 | if (!tokenModel) return undefined |
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) { | |||
99 | return tokenInfo | 83 | return tokenInfo |
100 | } | 84 | } |
101 | 85 | ||
102 | async function getUser (usernameOrEmail?: string, password?: string) { | 86 | async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) { |
103 | const res: express.Response = this.request.res | ||
104 | |||
105 | // Special treatment coming from a plugin | 87 | // Special treatment coming from a plugin |
106 | if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { | 88 | if (bypassLogin && bypassLogin.bypass === true) { |
107 | const obj = res.locals.bypassLogin | 89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
108 | logger.info('Bypassing oauth login by plugin %s.', obj.pluginName) | ||
109 | 90 | ||
110 | let user = await UserModel.loadByEmail(obj.user.email) | 91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
111 | if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
112 | 93 | ||
113 | // Cannot create a user | 94 | // Cannot create a user |
114 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 95 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
117 | // Then we just go through a regular login process | 98 | // Then we just go through a regular login process |
118 | if (user.pluginAuth !== null) { | 99 | if (user.pluginAuth !== null) { |
119 | // This user does not belong to this plugin, skip it | 100 | // This user does not belong to this plugin, skip it |
120 | if (user.pluginAuth !== obj.pluginName) return null | 101 | if (user.pluginAuth !== bypassLogin.pluginName) return null |
121 | 102 | ||
122 | checkUserValidityOrThrow(user) | 103 | checkUserValidityOrThrow(user) |
123 | 104 | ||
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) { | |||
143 | return user | 124 | return user |
144 | } | 125 | } |
145 | 126 | ||
146 | async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { | 127 | async function revokeToken ( |
147 | const res: express.Response = this.request.res | 128 | tokenInfo: { refreshToken: string }, |
129 | options: { | ||
130 | req?: express.Request | ||
131 | explicitLogout?: boolean | ||
132 | } = {} | ||
133 | ): Promise<{ success: boolean, redirectUrl?: string }> { | ||
134 | const { req, explicitLogout } = options | ||
135 | |||
148 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) | 136 | const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) |
149 | 137 | ||
150 | if (token) { | 138 | if (token) { |
151 | let redirectUrl: string | 139 | let redirectUrl: string |
152 | 140 | ||
153 | if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { | 141 | if (explicitLogout === true && token.User.pluginAuth && token.authName) { |
154 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) | 142 | redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req) |
155 | } | 143 | } |
156 | 144 | ||
157 | clearCacheByToken(token.accessToken) | 145 | TokensCache.Instance.clearCacheByToken(token.accessToken) |
158 | 146 | ||
159 | token.destroy() | 147 | token.destroy() |
160 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) | 148 | .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) |
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ | |||
165 | return { success: false } | 153 | return { success: false } |
166 | } | 154 | } |
167 | 155 | ||
168 | async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { | 156 | async function saveToken ( |
169 | const res: express.Response = this.request.res | 157 | token: TokenInfo, |
170 | 158 | client: MOAuthClient, | |
159 | user: MUser, | ||
160 | options: { | ||
161 | refreshTokenAuthName?: string | ||
162 | bypassLogin?: BypassLogin | ||
163 | } = {} | ||
164 | ) { | ||
165 | const { refreshTokenAuthName, bypassLogin } = options | ||
171 | let authName: string = null | 166 | let authName: string = null |
172 | if (res.locals.bypassLogin?.bypass === true) { | 167 | |
173 | authName = res.locals.bypassLogin.authName | 168 | if (bypassLogin?.bypass === true) { |
174 | } else if (res.locals.refreshTokenAuthName) { | 169 | authName = bypassLogin.authName |
175 | authName = res.locals.refreshTokenAuthName | 170 | } else if (refreshTokenAuthName) { |
171 | authName = refreshTokenAuthName | ||
176 | } | 172 | } |
177 | 173 | ||
178 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') | 174 | logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') |
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User | |||
199 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, | 195 | refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, |
200 | client, | 196 | client, |
201 | user, | 197 | user, |
202 | refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) | 198 | accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt), |
199 | refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt) | ||
203 | } | 200 | } |
204 | } | 201 | } |
205 | 202 | ||
206 | // --------------------------------------------------------------------------- | ||
207 | |||
208 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
209 | export { | 203 | export { |
210 | deleteUserToken, | ||
211 | clearCacheByUserId, | ||
212 | clearCacheByToken, | ||
213 | getAccessToken, | 204 | getAccessToken, |
214 | getClient, | 205 | getClient, |
215 | getRefreshToken, | 206 | getRefreshToken, |
@@ -218,6 +209,8 @@ export { | |||
218 | saveToken | 209 | saveToken |
219 | } | 210 | } |
220 | 211 | ||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
221 | async function createUserFromExternal (pluginAuth: string, options: { | 214 | async function createUserFromExternal (pluginAuth: string, options: { |
222 | username: string | 215 | username: string |
223 | email: string | 216 | email: string |
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
252 | function checkUserValidityOrThrow (user: MUser) { | 245 | function checkUserValidityOrThrow (user: MUser) { |
253 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 246 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
254 | } | 247 | } |
248 | |||
249 | function buildExpiresIn (expiresAt: Date) { | ||
250 | return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) | ||
251 | } | ||
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts new file mode 100644 index 000000000..5b6130d56 --- /dev/null +++ b/server/lib/auth/oauth.ts | |||
@@ -0,0 +1,180 @@ | |||
1 | import * as express from 'express' | ||
2 | import { | ||
3 | InvalidClientError, | ||
4 | InvalidGrantError, | ||
5 | InvalidRequestError, | ||
6 | Request, | ||
7 | Response, | ||
8 | UnauthorizedClientError, | ||
9 | UnsupportedGrantTypeError | ||
10 | } from 'oauth2-server' | ||
11 | import { randomBytesPromise, sha1 } from '@server/helpers/core-utils' | ||
12 | import { MOAuthClient } from '@server/types/models' | ||
13 | import { OAUTH_LIFETIME } from '../../initializers/constants' | ||
14 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | ||
15 | |||
16 | /** | ||
17 | * | ||
18 | * Reimplement some functions of OAuth2Server to inject external auth methods | ||
19 | * | ||
20 | */ | ||
21 | |||
22 | const oAuthServer = new (require('oauth2-server'))({ | ||
23 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | ||
24 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | ||
25 | |||
26 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | ||
27 | model: require('./oauth-model') | ||
28 | }) | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) { | ||
33 | const request = new Request(req) | ||
34 | const { refreshTokenAuthName, bypassLogin } = options | ||
35 | |||
36 | if (request.method !== 'POST') { | ||
37 | throw new InvalidRequestError('Invalid request: method must be POST') | ||
38 | } | ||
39 | |||
40 | if (!request.is([ 'application/x-www-form-urlencoded' ])) { | ||
41 | throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded') | ||
42 | } | ||
43 | |||
44 | const clientId = request.body.client_id | ||
45 | const clientSecret = request.body.client_secret | ||
46 | |||
47 | if (!clientId || !clientSecret) { | ||
48 | throw new InvalidClientError('Invalid client: cannot retrieve client credentials') | ||
49 | } | ||
50 | |||
51 | const client = await getClient(clientId, clientSecret) | ||
52 | if (!client) { | ||
53 | throw new InvalidClientError('Invalid client: client is invalid') | ||
54 | } | ||
55 | |||
56 | const grantType = request.body.grant_type | ||
57 | if (!grantType) { | ||
58 | throw new InvalidRequestError('Missing parameter: `grant_type`') | ||
59 | } | ||
60 | |||
61 | if (![ 'password', 'refresh_token' ].includes(grantType)) { | ||
62 | throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid') | ||
63 | } | ||
64 | |||
65 | if (!client.grants.includes(grantType)) { | ||
66 | throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid') | ||
67 | } | ||
68 | |||
69 | if (grantType === 'password') { | ||
70 | return handlePasswordGrant({ | ||
71 | request, | ||
72 | client, | ||
73 | bypassLogin | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | return handleRefreshGrant({ | ||
78 | request, | ||
79 | client, | ||
80 | refreshTokenAuthName | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | async function handleOAuthAuthenticate ( | ||
85 | req: express.Request, | ||
86 | res: express.Response, | ||
87 | authenticateInQuery = false | ||
88 | ) { | ||
89 | const options = authenticateInQuery | ||
90 | ? { allowBearerTokensInQueryString: true } | ||
91 | : {} | ||
92 | |||
93 | return oAuthServer.authenticate(new Request(req), new Response(res), options) | ||
94 | } | ||
95 | |||
96 | export { | ||
97 | handleOAuthToken, | ||
98 | handleOAuthAuthenticate | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | |||
103 | async function handlePasswordGrant (options: { | ||
104 | request: Request | ||
105 | client: MOAuthClient | ||
106 | bypassLogin?: BypassLogin | ||
107 | }) { | ||
108 | const { request, client, bypassLogin } = options | ||
109 | |||
110 | if (!request.body.username) { | ||
111 | throw new InvalidRequestError('Missing parameter: `username`') | ||
112 | } | ||
113 | |||
114 | if (!bypassLogin && !request.body.password) { | ||
115 | throw new InvalidRequestError('Missing parameter: `password`') | ||
116 | } | ||
117 | |||
118 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | ||
119 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
120 | |||
121 | const token = await buildToken() | ||
122 | |||
123 | return saveToken(token, client, user, { bypassLogin }) | ||
124 | } | ||
125 | |||
126 | async function handleRefreshGrant (options: { | ||
127 | request: Request | ||
128 | client: MOAuthClient | ||
129 | refreshTokenAuthName: string | ||
130 | }) { | ||
131 | const { request, client, refreshTokenAuthName } = options | ||
132 | |||
133 | if (!request.body.refresh_token) { | ||
134 | throw new InvalidRequestError('Missing parameter: `refresh_token`') | ||
135 | } | ||
136 | |||
137 | const refreshToken = await getRefreshToken(request.body.refresh_token) | ||
138 | |||
139 | if (!refreshToken) { | ||
140 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
141 | } | ||
142 | |||
143 | if (refreshToken.client.id !== client.id) { | ||
144 | throw new InvalidGrantError('Invalid grant: refresh token is invalid') | ||
145 | } | ||
146 | |||
147 | if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) { | ||
148 | throw new InvalidGrantError('Invalid grant: refresh token has expired') | ||
149 | } | ||
150 | |||
151 | await revokeToken({ refreshToken: refreshToken.refreshToken }) | ||
152 | |||
153 | const token = await buildToken() | ||
154 | |||
155 | return saveToken(token, client, refreshToken.user, { refreshTokenAuthName }) | ||
156 | } | ||
157 | |||
158 | function generateRandomToken () { | ||
159 | return randomBytesPromise(256) | ||
160 | .then(buffer => sha1(buffer)) | ||
161 | } | ||
162 | |||
163 | function getTokenExpiresAt (type: 'access' | 'refresh') { | ||
164 | const lifetime = type === 'access' | ||
165 | ? OAUTH_LIFETIME.ACCESS_TOKEN | ||
166 | : OAUTH_LIFETIME.REFRESH_TOKEN | ||
167 | |||
168 | return new Date(Date.now() + lifetime * 1000) | ||
169 | } | ||
170 | |||
171 | async function buildToken () { | ||
172 | const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ]) | ||
173 | |||
174 | return { | ||
175 | accessToken, | ||
176 | refreshToken, | ||
177 | accessTokenExpiresAt: getTokenExpiresAt('access'), | ||
178 | refreshTokenExpiresAt: getTokenExpiresAt('refresh') | ||
179 | } | ||
180 | } | ||
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts new file mode 100644 index 000000000..b027ce69a --- /dev/null +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import * as LRUCache from 'lru-cache' | ||
2 | import { MOAuthTokenUser } from '@server/types/models' | ||
3 | import { LRU_CACHE } from '../../initializers/constants' | ||
4 | |||
5 | export class TokensCache { | ||
6 | |||
7 | private static instance: TokensCache | ||
8 | |||
9 | private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
10 | private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) | ||
11 | |||
12 | private constructor () { } | ||
13 | |||
14 | static get Instance () { | ||
15 | return this.instance || (this.instance = new this()) | ||
16 | } | ||
17 | |||
18 | hasToken (token: string) { | ||
19 | return this.accessTokenCache.has(token) | ||
20 | } | ||
21 | |||
22 | getByToken (token: string) { | ||
23 | return this.accessTokenCache.get(token) | ||
24 | } | ||
25 | |||
26 | setToken (token: MOAuthTokenUser) { | ||
27 | this.accessTokenCache.set(token.accessToken, token) | ||
28 | this.userHavingToken.set(token.userId, token.accessToken) | ||
29 | } | ||
30 | |||
31 | deleteUserToken (userId: number) { | ||
32 | this.clearCacheByUserId(userId) | ||
33 | } | ||
34 | |||
35 | clearCacheByUserId (userId: number) { | ||
36 | const token = this.userHavingToken.get(userId) | ||
37 | |||
38 | if (token !== undefined) { | ||
39 | this.accessTokenCache.del(token) | ||
40 | this.userHavingToken.del(userId) | ||
41 | } | ||
42 | } | ||
43 | |||
44 | clearCacheByToken (token: string) { | ||
45 | const tokenModel = this.accessTokenCache.get(token) | ||
46 | |||
47 | if (tokenModel !== undefined) { | ||
48 | this.userHavingToken.del(tokenModel.userId) | ||
49 | this.accessTokenCache.del(token) | ||
50 | } | ||
51 | } | ||
52 | } | ||
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts deleted file mode 100644 index 86f1e7bdb..000000000 --- a/server/lib/avatar.ts +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | import 'multer' | ||
2 | import { sendUpdateActor } from './activitypub/send' | ||
3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | ||
4 | import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor' | ||
5 | import { processImage } from '../helpers/image-utils' | ||
6 | import { extname, join } from 'path' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | ||
8 | import { v4 as uuidv4 } from 'uuid' | ||
9 | import { CONFIG } from '../initializers/config' | ||
10 | import { sequelizeTypescript } from '../initializers/database' | ||
11 | import * as LRUCache from 'lru-cache' | ||
12 | import { queue } from 'async' | ||
13 | import { downloadImage } from '../helpers/requests' | ||
14 | import { MAccountDefault, MChannelDefault } from '../types/models' | ||
15 | |||
16 | async function updateLocalActorAvatarFile ( | ||
17 | accountOrChannel: MAccountDefault | MChannelDefault, | ||
18 | avatarPhysicalFile: Express.Multer.File | ||
19 | ) { | ||
20 | const extension = extname(avatarPhysicalFile.filename) | ||
21 | |||
22 | const avatarName = uuidv4() + extension | ||
23 | const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | ||
24 | await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE) | ||
25 | |||
26 | return retryTransactionWrapper(() => { | ||
27 | return sequelizeTypescript.transaction(async t => { | ||
28 | const avatarInfo = { | ||
29 | name: avatarName, | ||
30 | fileUrl: null, | ||
31 | onDisk: true | ||
32 | } | ||
33 | |||
34 | const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t) | ||
35 | await updatedActor.save({ transaction: t }) | ||
36 | |||
37 | await sendUpdateActor(accountOrChannel, t) | ||
38 | |||
39 | return updatedActor.Avatar | ||
40 | }) | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | async function deleteLocalActorAvatarFile ( | ||
45 | accountOrChannel: MAccountDefault | MChannelDefault | ||
46 | ) { | ||
47 | return retryTransactionWrapper(() => { | ||
48 | return sequelizeTypescript.transaction(async t => { | ||
49 | const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t) | ||
50 | await updatedActor.save({ transaction: t }) | ||
51 | |||
52 | await sendUpdateActor(accountOrChannel, t) | ||
53 | |||
54 | return updatedActor.Avatar | ||
55 | }) | ||
56 | }) | ||
57 | } | ||
58 | |||
59 | type DownloadImageQueueTask = { fileUrl: string, filename: string } | ||
60 | |||
61 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | ||
62 | downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE) | ||
63 | .then(() => cb()) | ||
64 | .catch(err => cb(err)) | ||
65 | }, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE) | ||
66 | |||
67 | function pushAvatarProcessInQueue (task: DownloadImageQueueTask) { | ||
68 | return new Promise<void>((res, rej) => { | ||
69 | downloadImageQueue.push(task, err => { | ||
70 | if (err) return rej(err) | ||
71 | |||
72 | return res() | ||
73 | }) | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | // Unsafe so could returns paths that does not exist anymore | ||
78 | const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE }) | ||
79 | |||
80 | export { | ||
81 | avatarPathUnsafeCache, | ||
82 | updateLocalActorAvatarFile, | ||
83 | deleteLocalActorAvatarFile, | ||
84 | pushAvatarProcessInQueue | ||
85 | } | ||
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index f19ec7df0..6ddaa82c8 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -5,12 +5,13 @@ import validator from 'validator' | |||
5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
8 | import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' | 8 | import { isTestInstance, sha256 } from '../helpers/core-utils' |
9 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
9 | import { logger } from '../helpers/logger' | 10 | import { logger } from '../helpers/logger' |
10 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
11 | import { | 12 | import { |
12 | ACCEPT_HEADERS, | 13 | ACCEPT_HEADERS, |
13 | AVATARS_SIZE, | 14 | ACTOR_IMAGES_SIZE, |
14 | CUSTOM_HTML_TAG_COMMENTS, | 15 | CUSTOM_HTML_TAG_COMMENTS, |
15 | EMBED_SIZE, | 16 | EMBED_SIZE, |
16 | FILES_CONTENT_HASH, | 17 | FILES_CONTENT_HASH, |
@@ -245,8 +246,8 @@ class ClientHtml { | |||
245 | 246 | ||
246 | const image = { | 247 | const image = { |
247 | url: entity.Actor.getAvatarUrl(), | 248 | url: entity.Actor.getAvatarUrl(), |
248 | width: AVATARS_SIZE.width, | 249 | width: ACTOR_IMAGES_SIZE.AVATARS.width, |
249 | height: AVATARS_SIZE.height | 250 | height: ACTOR_IMAGES_SIZE.AVATARS.height |
250 | } | 251 | } |
251 | 252 | ||
252 | const ogType = 'website' | 253 | const ogType = 'website' |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 969eae77b..9ca0d5d5b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m | |||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | 7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' |
8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 8 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | 9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' |
10 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | 10 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
11 | import { isTestInstance, root } from '../helpers/core-utils' | 11 | import { isTestInstance, root } from '../helpers/core-utils' |
12 | import { bunyanLogger, logger } from '../helpers/logger' | 12 | import { bunyanLogger, logger } from '../helpers/logger' |
13 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 13 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
14 | import { WEBSERVER } from '../initializers/constants' | 14 | import { WEBSERVER } from '../initializers/constants' |
15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' |
16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
17 | import { JobQueue } from './job-queue' | 17 | import { JobQueue } from './job-queue' |
18 | 18 | ||
@@ -403,9 +403,9 @@ class Emailer { | |||
403 | } | 403 | } |
404 | 404 | ||
405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | 405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
406 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 406 | const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | 407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
408 | const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | 408 | const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() |
409 | 409 | ||
410 | const emailPayload: EmailPayload = { | 410 | const emailPayload: EmailPayload = { |
411 | template: 'video-auto-blacklist-new', | 411 | template: 'video-auto-blacklist-new', |
@@ -417,7 +417,7 @@ class Emailer { | |||
417 | videoName: videoBlacklist.Video.name, | 417 | videoName: videoBlacklist.Video.name, |
418 | action: { | 418 | action: { |
419 | text: 'Review autoblacklist', | 419 | text: 'Review autoblacklist', |
420 | url: VIDEO_AUTO_BLACKLIST_URL | 420 | url: videoAutoBlacklistUrl |
421 | } | 421 | } |
422 | } | 422 | } |
423 | } | 423 | } |
@@ -472,6 +472,36 @@ class Emailer { | |||
472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
473 | } | 473 | } |
474 | 474 | ||
475 | addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { | ||
476 | const emailPayload: EmailPayload = { | ||
477 | to, | ||
478 | template: 'peertube-version-new', | ||
479 | subject: `A new PeerTube version is available: ${latestVersion}`, | ||
480 | locals: { | ||
481 | latestVersion | ||
482 | } | ||
483 | } | ||
484 | |||
485 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
486 | } | ||
487 | |||
488 | addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { | ||
489 | const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type | ||
490 | |||
491 | const emailPayload: EmailPayload = { | ||
492 | to, | ||
493 | template: 'plugin-version-new', | ||
494 | subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, | ||
495 | locals: { | ||
496 | pluginName: plugin.name, | ||
497 | latestVersion: plugin.latestVersion, | ||
498 | pluginUrl | ||
499 | } | ||
500 | } | ||
501 | |||
502 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
503 | } | ||
504 | |||
475 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { | 505 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { |
476 | const emailPayload: EmailPayload = { | 506 | const emailPayload: EmailPayload = { |
477 | template: 'password-reset', | 507 | template: 'password-reset', |
@@ -569,26 +599,27 @@ class Emailer { | |||
569 | }) | 599 | }) |
570 | 600 | ||
571 | for (const to of options.to) { | 601 | for (const to of options.to) { |
572 | await email | 602 | const baseOptions: SendEmailDefaultOptions = { |
573 | .send(merge( | 603 | template: 'common', |
574 | { | 604 | message: { |
575 | template: 'common', | 605 | to, |
576 | message: { | 606 | from: options.from, |
577 | to, | 607 | subject: options.subject, |
578 | from: options.from, | 608 | replyTo: options.replyTo |
579 | subject: options.subject, | 609 | }, |
580 | replyTo: options.replyTo | 610 | locals: { // default variables available in all templates |
581 | }, | 611 | WEBSERVER, |
582 | locals: { // default variables available in all templates | 612 | EMAIL: CONFIG.EMAIL, |
583 | WEBSERVER, | 613 | instanceName: CONFIG.INSTANCE.NAME, |
584 | EMAIL: CONFIG.EMAIL, | 614 | text: options.text, |
585 | instanceName: CONFIG.INSTANCE.NAME, | 615 | subject: options.subject |
586 | text: options.text, | 616 | } |
587 | subject: options.subject | 617 | } |
588 | } | 618 | |
589 | }, | 619 | // overriden/new variables given for a specific template in the payload |
590 | options // overriden/new variables given for a specific template in the payload | 620 | const sendOptions = merge(baseOptions, options) |
591 | ) as SendEmailOptions) | 621 | |
622 | await email.send(sendOptions) | ||
592 | .then(res => logger.debug('Sent email.', { res })) | 623 | .then(res => logger.debug('Sent email.', { res })) |
593 | .catch(err => logger.error('Error in email sender.', { err })) | 624 | .catch(err => logger.error('Error in email sender.', { err })) |
594 | } | 625 | } |
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug new file mode 100644 index 000000000..2f4d9399d --- /dev/null +++ b/server/lib/emails/peertube-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New PeerTube version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of PeerTube is available: #{latestVersion}. | ||
9 | | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube]. | ||
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug new file mode 100644 index 000000000..86d3d87e8 --- /dev/null +++ b/server/lib/emails/plugin-version-new/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | New plugin version available | ||
5 | |||
6 | block content | ||
7 | p | ||
8 | | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}. | ||
9 | | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface]. | ||
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts index ee0447010..58e2260b6 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = videoCaption.getFileUrl(video) | 41 | const remoteUrl = videoCaption.getFileUrl(video) |
42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) | 42 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) |
43 | 43 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 44 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 45 | ||
46 | return { isOwned: false, path: destPath } | 46 | return { isOwned: false, path: destPath } |
47 | } | 47 | } |
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index ee72cd3f9..dd3a84aca 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) | 39 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) |
40 | 40 | ||
41 | const remoteUrl = preview.getFileUrl(video) | 41 | const remoteUrl = preview.getFileUrl(video) |
42 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 42 | await doRequestAndSaveToFile(remoteUrl, destPath) |
43 | 43 | ||
44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) | 44 | logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) |
45 | 45 | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts index ca0e1770d..23217f140 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/videos-torrent-cache.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
8 | import { MVideo, MVideoFile } from '@server/types/models' | ||
8 | 9 | ||
9 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { |
10 | 11 | ||
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
22 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) | 23 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) |
23 | if (!file) return undefined | 24 | if (!file) return undefined |
24 | 25 | ||
25 | if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } | 26 | if (file.getVideo().isOwned()) { |
27 | const downloadName = this.buildDownloadName(file.getVideo(), file) | ||
28 | |||
29 | return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName } | ||
30 | } | ||
26 | 31 | ||
27 | return this.loadRemoteFile(filename) | 32 | return this.loadRemoteFile(filename) |
28 | } | 33 | } |
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
41 | const remoteUrl = file.getRemoteTorrentUrl(video) | 46 | const remoteUrl = file.getRemoteTorrentUrl(video) |
42 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) | 47 | const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) |
43 | 48 | ||
44 | await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) | 49 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 50 | ||
46 | const downloadName = `${video.name}-${file.resolution}p.torrent` | 51 | const downloadName = this.buildDownloadName(video, file) |
47 | 52 | ||
48 | return { isOwned: false, path: destPath, downloadName } | 53 | return { isOwned: false, path: destPath, downloadName } |
49 | } | 54 | } |
55 | |||
56 | private buildDownloadName (video: MVideo, file: MVideoFile) { | ||
57 | return `${video.name}-${file.resolution}p.torrent` | ||
58 | } | ||
50 | } | 59 | } |
51 | 60 | ||
52 | export { | 61 | export { |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 04187668c..84539e2c1 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
135 | const destPath = join(tmpDirectory, basename(fileUrl)) | 135 | const destPath = join(tmpDirectory, basename(fileUrl)) |
136 | 136 | ||
137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB | 137 | const bodyKBLimit = 10 * 1000 * 1000 // 10GB |
138 | await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) | 138 | await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit }) |
139 | } | 139 | } |
140 | 140 | ||
141 | clearTimeout(timer) | 141 | clearTimeout(timer) |
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
156 | } | 156 | } |
157 | 157 | ||
158 | async function fetchUniqUrls (playlistUrl: string) { | 158 | async function fetchUniqUrls (playlistUrl: string) { |
159 | const { body } = await doRequest<string>({ uri: playlistUrl }) | 159 | const { body } = await doRequest(playlistUrl) |
160 | 160 | ||
161 | if (!body) return [] | 161 | if (!body) return [] |
162 | 162 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts index b58bbc983..1caca1dcc 100644 --- a/server/lib/job-queue/handlers/activitypub-cleaner.ts +++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts | |||
@@ -1,10 +1,13 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Bull from 'bull' | 2 | import * as Bull from 'bull' |
3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | 3 | import { checkUrlsSameHost } from '@server/helpers/activitypub' |
4 | import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' | 4 | import { |
5 | import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' | 5 | isAnnounceActivityValid, |
6 | isDislikeActivityValid, | ||
7 | isLikeActivityValid | ||
8 | } from '@server/helpers/custom-validators/activitypub/activity' | ||
6 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' | 9 | import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' |
7 | import { doRequest } from '@server/helpers/requests' | 10 | import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests' |
8 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' | 11 | import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' |
9 | import { VideoModel } from '@server/models/video/video' | 12 | import { VideoModel } from '@server/models/video/video' |
10 | import { VideoCommentModel } from '@server/models/video/video-comment' | 13 | import { VideoCommentModel } from '@server/models/video/video-comment' |
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> ( | |||
78 | updater: (url: string, newUrl: string) => Promise<T>, | 81 | updater: (url: string, newUrl: string) => Promise<T>, |
79 | deleter: (url: string) => Promise<T> | 82 | deleter: (url: string) => Promise<T> |
80 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { | 83 | ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { |
81 | // Fetch url | 84 | const on404OrTombstone = async () => { |
82 | const { response, body } = await doRequest<any>({ | ||
83 | uri: url, | ||
84 | json: true, | ||
85 | activityPub: true | ||
86 | }) | ||
87 | |||
88 | // Does not exist anymore, remove entry | ||
89 | if (response.statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
90 | logger.info('Removing remote AP object %s.', url) | 85 | logger.info('Removing remote AP object %s.', url) |
91 | const data = await deleter(url) | 86 | const data = await deleter(url) |
92 | 87 | ||
93 | return { status: 'deleted', data } | 88 | return { status: 'deleted' as 'deleted', data } |
94 | } | 89 | } |
95 | 90 | ||
96 | // If not same id, check same host and update | 91 | try { |
97 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) | 92 | const { body } = await doJSONRequest<any>(url, { activityPub: true }) |
98 | 93 | ||
99 | if (body.type === 'Tombstone') { | 94 | // If not same id, check same host and update |
100 | logger.info('Removing remote AP object %s.', url) | 95 | if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) |
101 | const data = await deleter(url) | ||
102 | 96 | ||
103 | return { status: 'deleted', data } | 97 | if (body.type === 'Tombstone') { |
104 | } | 98 | return on404OrTombstone() |
99 | } | ||
105 | 100 | ||
106 | const newUrl = body.id | 101 | const newUrl = body.id |
107 | if (newUrl !== url) { | 102 | if (newUrl !== url) { |
108 | if (checkUrlsSameHost(newUrl, url) !== true) { | 103 | if (checkUrlsSameHost(newUrl, url) !== true) { |
109 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) | 104 | throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) |
105 | } | ||
106 | |||
107 | logger.info('Updating remote AP object %s.', url) | ||
108 | const data = await updater(url, newUrl) | ||
109 | |||
110 | return { status: 'updated', data } | ||
110 | } | 111 | } |
111 | 112 | ||
112 | logger.info('Updating remote AP object %s.', url) | 113 | return null |
113 | const data = await updater(url, newUrl) | 114 | } catch (err) { |
115 | // Does not exist anymore, remove entry | ||
116 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
117 | return on404OrTombstone() | ||
118 | } | ||
114 | 119 | ||
115 | return { status: 'updated', data } | 120 | throw err |
116 | } | 121 | } |
117 | |||
118 | return null | ||
119 | } | 122 | } |
120 | 123 | ||
121 | function rateOptionsFactory () { | 124 | function rateOptionsFactory () { |
@@ -149,7 +152,7 @@ function rateOptionsFactory () { | |||
149 | 152 | ||
150 | function shareOptionsFactory () { | 153 | function shareOptionsFactory () { |
151 | return { | 154 | return { |
152 | bodyValidator: (body: any) => isShareActivityValid(body), | 155 | bodyValidator: (body: any) => isAnnounceActivityValid(body), |
153 | 156 | ||
154 | updater: async (url: string, newUrl: string) => { | 157 | updater: async (url: string, newUrl: string) => { |
155 | const share = await VideoShareModel.loadByUrl(url, undefined) | 158 | const share = await VideoShareModel.loadByUrl(url, undefined) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index 7174786d6..c69ff9e83 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri: '', | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
28 | const goodUrls: string[] = [] | 27 | const goodUrls: string[] = [] |
29 | 28 | ||
30 | await Bluebird.map(payload.uris, uri => { | 29 | await Bluebird.map(payload.uris, uri => { |
31 | return doRequest(Object.assign({}, options, { uri })) | 30 | return doRequest(uri, options) |
32 | .then(() => goodUrls.push(uri)) | 31 | .then(() => goodUrls.push(uri)) |
33 | .catch(() => badUrls.push(uri)) | 32 | .catch(() => badUrls.push(uri)) |
34 | }, { concurrency: BROADCAST_CONCURRENCY }) | 33 | }, { concurrency: BROADCAST_CONCURRENCY }) |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index 74989d62e..585dad671 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) | 16 | const httpSignatureOptions = await buildSignedRequestOptions(payload) |
17 | 17 | ||
18 | const options = { | 18 | const options = { |
19 | method: 'POST', | 19 | method: 'POST' as 'POST', |
20 | uri, | ||
21 | json: body, | 20 | json: body, |
22 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
23 | timeout: REQUEST_TIMEOUT, | 22 | timeout: REQUEST_TIMEOUT, |
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
25 | } | 24 | } |
26 | 25 | ||
27 | try { | 26 | try { |
28 | await doRequest(options) | 27 | await doRequest(uri, options) |
29 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) | 28 | ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) |
30 | } catch (err) { | 29 | } catch (err) { |
31 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) | 30 | ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) |
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index c030d31ef..e8a91450d 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application' | |||
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | 6 | import { buildDigest } from '@server/helpers/peertube-crypto' |
7 | import { ContextType } from '@shared/models/activitypub/context' | 7 | import { ContextType } from '@shared/models/activitypub/context' |
8 | 8 | ||
9 | type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } | 9 | type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number } |
10 | 10 | ||
11 | async function computeBody (payload: Payload) { | 11 | async function computeBody <T> ( |
12 | payload: Payload<T> | ||
13 | ): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> { | ||
12 | let body = payload.body | 14 | let body = payload.body |
13 | 15 | ||
14 | if (payload.signatureActorId) { | 16 | if (payload.signatureActorId) { |
15 | const actorSignature = await ActorModel.load(payload.signatureActorId) | 17 | const actorSignature = await ActorModel.load(payload.signatureActorId) |
16 | if (!actorSignature) throw new Error('Unknown signature actor id.') | 18 | if (!actorSignature) throw new Error('Unknown signature actor id.') |
19 | |||
17 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) | 20 | body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) |
18 | } | 21 | } |
19 | 22 | ||
20 | return body | 23 | return body |
21 | } | 24 | } |
22 | 25 | ||
23 | async function buildSignedRequestOptions (payload: Payload) { | 26 | async function buildSignedRequestOptions (payload: Payload<any>) { |
24 | let actor: MActor | null | 27 | let actor: MActor | null |
25 | 28 | ||
26 | if (payload.signatureActorId) { | 29 | if (payload.signatureActorId) { |
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) { | |||
43 | 46 | ||
44 | function buildGlobalHeaders (body: any) { | 47 | function buildGlobalHeaders (body: any) { |
45 | return { | 48 | return { |
46 | 'Digest': buildDigest(body), | 49 | 'digest': buildDigest(body), |
47 | 'Content-Type': 'application/activity+json', | 50 | 'content-type': 'application/activity+json', |
48 | 'Accept': ACTIVITY_PUB.ACCEPT_HEADER | 51 | 'accept': ACTIVITY_PUB.ACCEPT_HEADER |
49 | } | 52 | } |
50 | } | 53 | } |
51 | 54 | ||
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 740c274d7..da7f7cc05 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config' | |||
19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
20 | import { UserModel } from '../models/account/user' | 20 | import { UserModel } from '../models/account/user' |
21 | import { UserNotificationModel } from '../models/account/user-notification' | 21 | import { UserNotificationModel } from '../models/account/user-notification' |
22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' | 22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' |
23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | 23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
24 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
25 | import { Emailer } from './emailer' | 25 | import { Emailer } from './emailer' |
@@ -144,6 +144,20 @@ class Notifier { | |||
144 | }) | 144 | }) |
145 | } | 145 | } |
146 | 146 | ||
147 | notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
148 | this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion) | ||
149 | .catch(err => { | ||
150 | logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err }) | ||
151 | }) | ||
152 | } | ||
153 | |||
154 | notifyOfNewPluginVersion (plugin: MPlugin) { | ||
155 | this.notifyAdminsOfNewPluginVersion(plugin) | ||
156 | .catch(err => { | ||
157 | logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }) | ||
158 | }) | ||
159 | } | ||
160 | |||
147 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { | 161 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { |
148 | // List all followers that are users | 162 | // List all followers that are users |
149 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) | 163 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) |
@@ -667,6 +681,64 @@ class Notifier { | |||
667 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 681 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
668 | } | 682 | } |
669 | 683 | ||
684 | private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) { | ||
685 | // Use the debug right to know who is an administrator | ||
686 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
687 | if (admins.length === 0) return | ||
688 | |||
689 | logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion) | ||
690 | |||
691 | function settingGetter (user: MUserWithNotificationSetting) { | ||
692 | return user.NotificationSetting.newPeerTubeVersion | ||
693 | } | ||
694 | |||
695 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
696 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
697 | type: UserNotificationType.NEW_PEERTUBE_VERSION, | ||
698 | userId: user.id, | ||
699 | applicationId: application.id | ||
700 | }) | ||
701 | notification.Application = application | ||
702 | |||
703 | return notification | ||
704 | } | ||
705 | |||
706 | function emailSender (emails: string[]) { | ||
707 | return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion) | ||
708 | } | ||
709 | |||
710 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
711 | } | ||
712 | |||
713 | private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) { | ||
714 | // Use the debug right to know who is an administrator | ||
715 | const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG) | ||
716 | if (admins.length === 0) return | ||
717 | |||
718 | logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion) | ||
719 | |||
720 | function settingGetter (user: MUserWithNotificationSetting) { | ||
721 | return user.NotificationSetting.newPluginVersion | ||
722 | } | ||
723 | |||
724 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
725 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
726 | type: UserNotificationType.NEW_PLUGIN_VERSION, | ||
727 | userId: user.id, | ||
728 | pluginId: plugin.id | ||
729 | }) | ||
730 | notification.Plugin = plugin | ||
731 | |||
732 | return notification | ||
733 | } | ||
734 | |||
735 | function emailSender (emails: string[]) { | ||
736 | return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin) | ||
737 | } | ||
738 | |||
739 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
740 | } | ||
741 | |||
670 | private async notify<T extends MUserWithNotificationSetting> (options: { | 742 | private async notify<T extends MUserWithNotificationSetting> (options: { |
671 | users: T[] | 743 | users: T[] |
672 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> | 744 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi> |
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 7bcb6ed4c..624f5da1d 100644 --- a/server/lib/plugins/plugin-index.ts +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -1,22 +1,22 @@ | |||
1 | import { doRequest } from '../../helpers/requests' | 1 | import { sanitizeUrl } from '@server/helpers/core-utils' |
2 | import { CONFIG } from '../../initializers/config' | 2 | import { ResultList } from '../../../shared/models' |
3 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
4 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
3 | import { | 5 | import { |
4 | PeertubePluginLatestVersionRequest, | 6 | PeertubePluginLatestVersionRequest, |
5 | PeertubePluginLatestVersionResponse | 7 | PeertubePluginLatestVersionResponse |
6 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' | 8 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' |
7 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | ||
8 | import { ResultList } from '../../../shared/models' | ||
9 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | ||
10 | import { PluginModel } from '../../models/server/plugin' | ||
11 | import { PluginManager } from './plugin-manager' | ||
12 | import { logger } from '../../helpers/logger' | 9 | import { logger } from '../../helpers/logger' |
10 | import { doJSONRequest } from '../../helpers/requests' | ||
11 | import { CONFIG } from '../../initializers/config' | ||
13 | import { PEERTUBE_VERSION } from '../../initializers/constants' | 12 | import { PEERTUBE_VERSION } from '../../initializers/constants' |
14 | import { sanitizeUrl } from '@server/helpers/core-utils' | 13 | import { PluginModel } from '../../models/server/plugin' |
14 | import { PluginManager } from './plugin-manager' | ||
15 | 15 | ||
16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { | 16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { |
17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options | 17 | const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options |
18 | 18 | ||
19 | const qs: PeertubePluginIndexList = { | 19 | const searchParams: PeertubePluginIndexList & Record<string, string | number> = { |
20 | start, | 20 | start, |
21 | count, | 21 | count, |
22 | sort, | 22 | sort, |
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) | |||
28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' | 28 | const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' |
29 | 29 | ||
30 | try { | 30 | try { |
31 | const { body } = await doRequest<any>({ uri, qs, json: true }) | 31 | const { body } = await doJSONRequest<any>(uri, { searchParams }) |
32 | 32 | ||
33 | logger.debug('Got result from PeerTube index.', { body }) | 33 | logger.debug('Got result from PeerTube index.', { body }) |
34 | 34 | ||
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu | |||
58 | 58 | ||
59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' | 59 | const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' |
60 | 60 | ||
61 | const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) | 61 | const options = { |
62 | json: bodyRequest, | ||
63 | method: 'POST' as 'POST' | ||
64 | } | ||
65 | const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options) | ||
62 | 66 | ||
63 | return body | 67 | return body |
64 | } | 68 | } |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index 1f2a88c27..9b5e1a546 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | VIDEO_PLAYLIST_PRIVACIES, | 7 | VIDEO_PLAYLIST_PRIVACIES, |
8 | VIDEO_PRIVACIES | 8 | VIDEO_PRIVACIES |
9 | } from '@server/initializers/constants' | 9 | } from '@server/initializers/constants' |
10 | import { onExternalUserAuthenticated } from '@server/lib/auth' | 10 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' |
11 | import { PluginModel } from '@server/models/server/plugin' | 11 | import { PluginModel } from '@server/models/server/plugin' |
12 | import { | 12 | import { |
13 | RegisterServerAuthExternalOptions, | 13 | RegisterServerAuthExternalOptions, |
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index f62f52f9c..0b8cd1389 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { chunk } from 'lodash' | 1 | import { chunk } from 'lodash' |
2 | import { doRequest } from '@server/helpers/requests' | 2 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler { | |||
34 | try { | 34 | try { |
35 | const serverActor = await getServerActor() | 35 | const serverActor = await getServerActor() |
36 | 36 | ||
37 | const qs = { count: 1000 } | 37 | const searchParams = { count: 1000 } |
38 | if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) | 38 | if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() }) |
39 | 39 | ||
40 | this.lastCheck = new Date() | 40 | this.lastCheck = new Date() |
41 | 41 | ||
42 | const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) | 42 | const { body } = await doJSONRequest<any>(indexUrl, { searchParams }) |
43 | if (!body.data || Array.isArray(body.data) === false) { | 43 | if (!body.data || Array.isArray(body.data) === false) { |
44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) | 44 | logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) |
45 | return | 45 | return |
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts new file mode 100644 index 000000000..c8960465c --- /dev/null +++ b/server/lib/schedulers/peertube-version-check-scheduler.ts | |||
@@ -0,0 +1,55 @@ | |||
1 | |||
2 | import { doJSONRequest } from '@server/helpers/requests' | ||
3 | import { ApplicationModel } from '@server/models/application/application' | ||
4 | import { compareSemVer } from '@shared/core-utils' | ||
5 | import { JoinPeerTubeVersions } from '@shared/models' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
9 | import { Notifier } from '../notifier' | ||
10 | import { AbstractScheduler } from './abstract-scheduler' | ||
11 | |||
12 | export class PeerTubeVersionCheckScheduler extends AbstractScheduler { | ||
13 | |||
14 | private static instance: AbstractScheduler | ||
15 | |||
16 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion | ||
17 | |||
18 | private constructor () { | ||
19 | super() | ||
20 | } | ||
21 | |||
22 | protected async internalExecute () { | ||
23 | return this.checkLatestVersion() | ||
24 | } | ||
25 | |||
26 | private async checkLatestVersion () { | ||
27 | if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return | ||
28 | |||
29 | logger.info('Checking latest PeerTube version.') | ||
30 | |||
31 | const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL) | ||
32 | |||
33 | if (!body?.peertube?.latestVersion) { | ||
34 | logger.warn('Cannot check latest PeerTube version: body is invalid.', { body }) | ||
35 | return | ||
36 | } | ||
37 | |||
38 | const latestVersion = body.peertube.latestVersion | ||
39 | const application = await ApplicationModel.load() | ||
40 | |||
41 | // Already checked this version | ||
42 | if (application.latestPeerTubeVersion === latestVersion) return | ||
43 | |||
44 | if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) { | ||
45 | application.latestPeerTubeVersion = latestVersion | ||
46 | await application.save() | ||
47 | |||
48 | Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion) | ||
49 | } | ||
50 | } | ||
51 | |||
52 | static get Instance () { | ||
53 | return this.instance || (this.instance = new this()) | ||
54 | } | ||
55 | } | ||
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts index 014993e94..9a1ae3ec5 100644 --- a/server/lib/schedulers/plugins-check-scheduler.ts +++ b/server/lib/schedulers/plugins-check-scheduler.ts | |||
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin' | |||
6 | import { chunk } from 'lodash' | 6 | import { chunk } from 'lodash' |
7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | 7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' |
8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' | 8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' |
9 | import { Notifier } from '../notifier' | ||
9 | 10 | ||
10 | export class PluginsCheckScheduler extends AbstractScheduler { | 11 | export class PluginsCheckScheduler extends AbstractScheduler { |
11 | 12 | ||
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler { | |||
53 | plugin.latestVersion = result.latestVersion | 54 | plugin.latestVersion = result.latestVersion |
54 | await plugin.save() | 55 | await plugin.save() |
55 | 56 | ||
57 | // Notify if there is an higher plugin version available | ||
58 | if (compareSemVer(plugin.version, result.latestVersion) < 0) { | ||
59 | Notifier.Instance.notifyOfNewPluginVersion(plugin) | ||
60 | } | ||
61 | |||
56 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) | 62 | logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) |
57 | } | 63 | } |
58 | } | 64 | } |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 106f5fdaa..cfee69cfc 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | |||
2 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 3 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
3 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 4 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
4 | import { processImage } from '../helpers/image-utils' | 5 | import { generateImageFilename, processImage } from '../helpers/image-utils' |
5 | import { downloadImage } from '../helpers/requests' | 6 | import { downloadImage } from '../helpers/requests' |
6 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
7 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 8 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail' | |||
11 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' | 12 | import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' |
12 | import { getVideoFilePath } from './video-paths' | 13 | import { getVideoFilePath } from './video-paths' |
13 | 14 | ||
14 | type ImageSize = { height: number, width: number } | 15 | type ImageSize = { height?: number, width?: number } |
15 | 16 | ||
16 | function createPlaylistMiniatureFromExisting (options: { | 17 | function createPlaylistMiniatureFromExisting (options: { |
17 | inputPath: string | 18 | inputPath: string |
@@ -200,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
200 | : undefined | 201 | : undefined |
201 | 202 | ||
202 | if (type === ThumbnailType.MINIATURE) { | 203 | if (type === ThumbnailType.MINIATURE) { |
203 | const filename = video.generateThumbnailName() | 204 | const filename = generateImageFilename() |
204 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR | 205 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR |
205 | 206 | ||
206 | return { | 207 | return { |
@@ -214,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
214 | } | 215 | } |
215 | 216 | ||
216 | if (type === ThumbnailType.PREVIEW) { | 217 | if (type === ThumbnailType.PREVIEW) { |
217 | const filename = video.generatePreviewName() | 218 | const filename = generateImageFilename() |
218 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR | 219 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR |
219 | 220 | ||
220 | return { | 221 | return { |
diff --git a/server/lib/user.ts b/server/lib/user.ts index e1892f22c..9b0a0a2f1 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
193 | newInstanceFollower: UserNotificationSettingValue.WEB, | 193 | newInstanceFollower: UserNotificationSettingValue.WEB, |
194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 194 | abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 195 | abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
196 | autoInstanceFollowing: UserNotificationSettingValue.WEB | 196 | autoInstanceFollowing: UserNotificationSettingValue.WEB, |
197 | newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | ||
198 | newPluginVersion: UserNotificationSettingValue.WEB | ||
197 | } | 199 | } |
198 | 200 | ||
199 | return UserNotificationSettingModel.create(values, { transaction: t }) | 201 | return UserNotificationSettingModel.create(values, { transaction: t }) |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index dbb37e0b2..37c43c3b0 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | } from '@server/types/models' | 11 | } from '@server/types/models' |
12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' | 12 | import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' |
13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | 13 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' |
14 | import { logger } from '../helpers/logger' | 14 | import { logger, loggerTagsFactory } from '../helpers/logger' |
15 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
17 | import { sendDeleteVideo } from './activitypub/send' | 17 | import { sendDeleteVideo } from './activitypub/send' |
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager' | |||
20 | import { Notifier } from './notifier' | 20 | import { Notifier } from './notifier' |
21 | import { Hooks } from './plugins/hooks' | 21 | import { Hooks } from './plugins/hooks' |
22 | 22 | ||
23 | const lTags = loggerTagsFactory('blacklist') | ||
24 | |||
23 | async function autoBlacklistVideoIfNeeded (parameters: { | 25 | async function autoBlacklistVideoIfNeeded (parameters: { |
24 | video: MVideoWithBlacklistLight | 26 | video: MVideoWithBlacklistLight |
25 | user?: MUser | 27 | user?: MUser |
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
60 | }) | 62 | }) |
61 | } | 63 | } |
62 | 64 | ||
63 | logger.info('Video %s auto-blacklisted.', video.uuid) | 65 | logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid)) |
64 | 66 | ||
65 | return true | 67 | return true |
66 | } | 68 | } |
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 49bdf4869..0476cb2d5 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts | |||
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid' | |||
3 | import { VideoChannelCreate } from '../../shared/models' | 3 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoModel } from '../models/video/video' | 4 | import { VideoModel } from '../models/video/video' |
5 | import { VideoChannelModel } from '../models/video/video-channel' | 5 | import { VideoChannelModel } from '../models/video/video-channel' |
6 | import { MAccountId, MChannelDefault, MChannelId } from '../types/models' | 6 | import { MAccountId, MChannelId } from '../types/models' |
7 | import { buildActorInstance } from './activitypub/actor' | 7 | import { buildActorInstance } from './activitypub/actor' |
8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' | 8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' |
9 | import { federateVideoIfNeeded } from './activitypub/videos' | 9 | import { federateVideoIfNeeded } from './activitypub/videos' |
10 | 10 | ||
11 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } | 11 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { |
12 | |||
13 | async function createLocalVideoChannel <T extends MAccountId> ( | ||
14 | videoChannelInfo: VideoChannelCreate, | ||
15 | account: T, | ||
16 | t: Sequelize.Transaction | ||
17 | ): Promise<CustomVideoChannelModelAccount<T>> { | ||
18 | const uuid = uuidv4() | 12 | const uuid = uuidv4() |
19 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) | 13 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) |
20 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) | 14 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) |
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> ( | |||
32 | const videoChannel = new VideoChannelModel(videoChannelData) | 26 | const videoChannel = new VideoChannelModel(videoChannelData) |
33 | 27 | ||
34 | const options = { transaction: t } | 28 | const options = { transaction: t } |
35 | const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault | 29 | const videoChannelCreated = await videoChannel.save(options) |
36 | 30 | ||
37 | // Do not forget to add Account/Actor information to the created video channel | ||
38 | videoChannelCreated.Account = account | ||
39 | videoChannelCreated.Actor = actorInstanceCreated | 31 | videoChannelCreated.Actor = actorInstanceCreated |
40 | 32 | ||
41 | // No need to seed this empty video channel to followers | 33 | // No need to send this empty video channel to followers |
42 | return videoChannelCreated | 34 | return videoChannelCreated |
43 | } | 35 | } |
44 | 36 | ||
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Socket } from 'socket.io' | 2 | import { Socket } from 'socket.io' |
3 | import { oAuthServer } from '@server/lib/auth' | 3 | import { getAccessToken } from '@server/lib/auth/oauth-model' |
4 | import { logger } from '../helpers/logger' | ||
5 | import { getAccessToken } from '../lib/oauth-model' | ||
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | ||
6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | ||
7 | 7 | ||
8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { | 8 | function 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 | ||
29 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { | 27 | function 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 @@ | |||
1 | export * from './validators' | 1 | export * from './validators' |
2 | export * from './activitypub' | 2 | export * from './activitypub' |
3 | export * from './async' | 3 | export * from './async' |
4 | export * from './oauth' | 4 | export * from './auth' |
5 | export * from './pagination' | 5 | export * from './pagination' |
6 | export * from './servers' | 6 | export * from './servers' |
7 | export * from './sort' | 7 | export * 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/actor-image.ts b/server/middlewares/validators/actor-image.ts new file mode 100644 index 000000000..961d7a7e5 --- /dev/null +++ b/server/middlewares/validators/actor-image.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isActorImageFile } from '@server/helpers/custom-validators/actor-images' | ||
4 | import { cleanUpReqFiles } from '../../helpers/express-utils' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
7 | import { areValidationErrors } from './utils' | ||
8 | |||
9 | const updateActorImageValidatorFactory = (fieldname: string) => ([ | ||
10 | body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage( | ||
11 | 'This file is not supported or too large. Please, make sure it is of the following type : ' + | ||
12 | CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ') | ||
13 | ), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking updateActorImageValidator parameters', { files: req.files }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ]) | ||
23 | |||
24 | const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile') | ||
25 | const updateBannerValidator = updateActorImageValidatorFactory('bannerfile') | ||
26 | |||
27 | export { | ||
28 | updateAvatarValidator, | ||
29 | updateBannerValidator | ||
30 | } | ||
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts deleted file mode 100644 index 2acb97483..000000000 --- a/server/middlewares/validators/avatar.ts +++ /dev/null | |||
@@ -1,26 +0,0 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator' | ||
3 | import { isAvatarFile } from '../../helpers/custom-validators/users' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { cleanUpReqFiles } from '../../helpers/express-utils' | ||
8 | |||
9 | const updateAvatarValidator = [ | ||
10 | body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage( | ||
11 | 'This file is not supported or too large. Please, make sure it is of the following type : ' + | ||
12 | CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ') | ||
13 | ), | ||
14 | |||
15 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
16 | logger.debug('Checking updateAvatarValidator parameters', { files: req.files }) | ||
17 | |||
18 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | ||
19 | |||
20 | return next() | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | export { | ||
25 | updateAvatarValidator | ||
26 | } | ||
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index a590aca99..bb849dc72 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts | |||
@@ -68,7 +68,6 @@ const removeFollowingValidator = [ | |||
68 | .json({ | 68 | .json({ |
69 | error: `Following ${req.params.host} not found.` | 69 | error: `Following ${req.params.host} not found.` |
70 | }) | 70 | }) |
71 | .end() | ||
72 | } | 71 | } |
73 | 72 | ||
74 | res.locals.follow = follow | 73 | res.locals.follow = follow |
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 4086d77aa..24faeea3e 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './abuse' | 1 | export * from './abuse' |
2 | export * from './account' | 2 | export * from './account' |
3 | export * from './actor-image' | ||
3 | export * from './blocklist' | 4 | export * from './blocklist' |
4 | export * from './oembed' | 5 | export * from './oembed' |
5 | export * from './activitypub' | 6 | export * from './activitypub' |
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param, query } from 'express-validator' | 2 | import { param, query } from 'express-validator' |
3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' | 3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger, loggerTagsFactory } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
7 | const lTags = loggerTagsFactory('validators', 'jobs') | ||
8 | |||
7 | const listJobsValidator = [ | 9 | const 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' | |||
4 | import { areValidationErrors } from './utils' | 4 | import { areValidationErrors } from './utils' |
5 | import { PAGINATION } from '@server/initializers/constants' | 5 | import { PAGINATION } from '@server/initializers/constants' |
6 | 6 | ||
7 | const paginationValidator = [ | 7 | const 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) => { | 9 | function 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 | ||
26 | export { | 30 | export { |
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 | ||
29 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 29 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
30 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 30 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
31 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 31 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ]) |
32 | const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) | 32 | const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) |
33 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 33 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
34 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | 34 | const 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 | ||
20 | function checkSort (sortableColumns: string[]) { | 20 | function 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-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 57ac548b9..2463d281c 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -73,13 +73,11 @@ const videoChannelsUpdateValidator = [ | |||
73 | if (res.locals.videoChannel.Actor.isOwned() === false) { | 73 | if (res.locals.videoChannel.Actor.isOwned() === false) { |
74 | return res.status(HttpStatusCode.FORBIDDEN_403) | 74 | return res.status(HttpStatusCode.FORBIDDEN_403) |
75 | .json({ error: 'Cannot update video channel of another server' }) | 75 | .json({ error: 'Cannot update video channel of another server' }) |
76 | .end() | ||
77 | } | 76 | } |
78 | 77 | ||
79 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { | 78 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { |
80 | return res.status(HttpStatusCode.FORBIDDEN_403) | 79 | return res.status(HttpStatusCode.FORBIDDEN_403) |
81 | .json({ error: 'Cannot update video channel of another user' }) | 80 | .json({ error: 'Cannot update video channel of another user' }) |
82 | .end() | ||
83 | } | 81 | } |
84 | 82 | ||
85 | return next() | 83 | return next() |
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 | |||
29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | 30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' |
31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | 31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' |
32 | import { authenticatePromiseIfNeeded } from '../../oauth' | 32 | import { authenticatePromiseIfNeeded } from '../../auth' |
33 | import { areValidationErrors } from '../utils' | 33 | import { areValidationErrors } from '../utils' |
34 | 34 | ||
35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | 35 | const 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' | |||
54 | import { Hooks } from '../../../lib/plugins/hooks' | 54 | import { Hooks } from '../../../lib/plugins/hooks' |
55 | import { AccountModel } from '../../../models/account/account' | 55 | import { AccountModel } from '../../../models/account/account' |
56 | import { VideoModel } from '../../../models/video/video' | 56 | import { VideoModel } from '../../../models/video/video' |
57 | import { authenticatePromiseIfNeeded } from '../../oauth' | 57 | import { authenticatePromiseIfNeeded } from '../../auth' |
58 | import { areValidationErrors } from '../utils' | 58 | import { areValidationErrors } from '../utils' |
59 | 59 | ||
60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ | 60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index c72f9c63d..312451abe 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -33,7 +33,7 @@ import { | |||
33 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../activitypub/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { ApplicationModel } from '../application/application' | 35 | import { ApplicationModel } from '../application/application' |
36 | import { AvatarModel } from '../avatar/avatar' | 36 | import { ActorImageModel } from './actor-image' |
37 | import { ServerModel } from '../server/server' | 37 | import { ServerModel } from '../server/server' |
38 | import { ServerBlocklistModel } from '../server/server-blocklist' | 38 | import { ServerBlocklistModel } from '../server/server-blocklist' |
39 | import { getSort, throwIfNotValid } from '../utils' | 39 | import { getSort, throwIfNotValid } from '../utils' |
@@ -82,7 +82,8 @@ export type SummaryOptions = { | |||
82 | serverInclude, | 82 | serverInclude, |
83 | 83 | ||
84 | { | 84 | { |
85 | model: AvatarModel.unscoped(), | 85 | model: ActorImageModel.unscoped(), |
86 | as: 'Avatar', | ||
86 | required: false | 87 | required: false |
87 | } | 88 | } |
88 | ] | 89 | ] |
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts new file mode 100644 index 000000000..ae05b4969 --- /dev/null +++ b/server/models/account/actor-image.ts | |||
@@ -0,0 +1,100 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { MActorImageFormattable } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONFIG } from '../../initializers/config' | ||
10 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
11 | import { throwIfNotValid } from '../utils' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'actorImage', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'filename' ], | ||
18 | unique: true | ||
19 | } | ||
20 | ] | ||
21 | }) | ||
22 | export class ActorImageModel extends Model { | ||
23 | |||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | filename: string | ||
27 | |||
28 | @AllowNull(true) | ||
29 | @Default(null) | ||
30 | @Column | ||
31 | height: number | ||
32 | |||
33 | @AllowNull(true) | ||
34 | @Default(null) | ||
35 | @Column | ||
36 | width: number | ||
37 | |||
38 | @AllowNull(true) | ||
39 | @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
40 | @Column | ||
41 | fileUrl: string | ||
42 | |||
43 | @AllowNull(false) | ||
44 | @Column | ||
45 | onDisk: boolean | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Column | ||
49 | type: ActorImageType | ||
50 | |||
51 | @CreatedAt | ||
52 | createdAt: Date | ||
53 | |||
54 | @UpdatedAt | ||
55 | updatedAt: Date | ||
56 | |||
57 | @AfterDestroy | ||
58 | static removeFilesAndSendDelete (instance: ActorImageModel) { | ||
59 | logger.info('Removing actor image file %s.', instance.filename) | ||
60 | |||
61 | // Don't block the transaction | ||
62 | instance.removeImage() | ||
63 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) | ||
64 | } | ||
65 | |||
66 | static loadByName (filename: string) { | ||
67 | const query = { | ||
68 | where: { | ||
69 | filename | ||
70 | } | ||
71 | } | ||
72 | |||
73 | return ActorImageModel.findOne(query) | ||
74 | } | ||
75 | |||
76 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | ||
77 | return { | ||
78 | path: this.getStaticPath(), | ||
79 | createdAt: this.createdAt, | ||
80 | updatedAt: this.updatedAt | ||
81 | } | ||
82 | } | ||
83 | |||
84 | getStaticPath () { | ||
85 | if (this.type === ActorImageType.AVATAR) { | ||
86 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
87 | } | ||
88 | |||
89 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
90 | } | ||
91 | |||
92 | getPath () { | ||
93 | return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
94 | } | ||
95 | |||
96 | removeImage () { | ||
97 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
98 | return remove(imagePath) | ||
99 | } | ||
100 | } | ||
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' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
15 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
16 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
18 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
19 | import { throwIfNotValid } from '../utils' | 19 | import { throwIfNotValid } from '../utils' |
20 | import { UserModel } from './user' | 20 | import { 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..805095002 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -9,7 +9,8 @@ import { VideoAbuseModel } from '../abuse/video-abuse' | |||
9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
10 | import { ActorModel } from '../activitypub/actor' | 10 | import { ActorModel } from '../activitypub/actor' |
11 | import { ActorFollowModel } from '../activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../activitypub/actor-follow' |
12 | import { AvatarModel } from '../avatar/avatar' | 12 | import { ApplicationModel } from '../application/application' |
13 | import { PluginModel } from '../server/plugin' | ||
13 | import { ServerModel } from '../server/server' | 14 | import { ServerModel } from '../server/server' |
14 | import { getSort, throwIfNotValid } from '../utils' | 15 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
18 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
19 | import { VideoImportModel } from '../video/video-import' | 20 | import { VideoImportModel } from '../video/video-import' |
20 | import { AccountModel } from './account' | 21 | import { AccountModel } from './account' |
22 | import { ActorImageModel } from './actor-image' | ||
21 | import { UserModel } from './user' | 23 | import { UserModel } from './user' |
22 | 24 | ||
23 | enum ScopeNames { | 25 | enum ScopeNames { |
@@ -32,7 +34,8 @@ function buildActorWithAvatarInclude () { | |||
32 | include: [ | 34 | include: [ |
33 | { | 35 | { |
34 | attributes: [ 'filename' ], | 36 | attributes: [ 'filename' ], |
35 | model: AvatarModel.unscoped(), | 37 | as: 'Avatar', |
38 | model: ActorImageModel.unscoped(), | ||
36 | required: false | 39 | required: false |
37 | }, | 40 | }, |
38 | { | 41 | { |
@@ -96,7 +99,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
96 | attributes: [ 'id' ], | 99 | attributes: [ 'id' ], |
97 | model: VideoAbuseModel.unscoped(), | 100 | model: VideoAbuseModel.unscoped(), |
98 | required: false, | 101 | required: false, |
99 | include: [ buildVideoInclude(true) ] | 102 | include: [ buildVideoInclude(false) ] |
100 | }, | 103 | }, |
101 | { | 104 | { |
102 | attributes: [ 'id' ], | 105 | attributes: [ 'id' ], |
@@ -106,12 +109,12 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
106 | { | 109 | { |
107 | attributes: [ 'id', 'originCommentId' ], | 110 | attributes: [ 'id', 'originCommentId' ], |
108 | model: VideoCommentModel.unscoped(), | 111 | model: VideoCommentModel.unscoped(), |
109 | required: true, | 112 | required: false, |
110 | include: [ | 113 | include: [ |
111 | { | 114 | { |
112 | attributes: [ 'id', 'name', 'uuid' ], | 115 | attributes: [ 'id', 'name', 'uuid' ], |
113 | model: VideoModel.unscoped(), | 116 | model: VideoModel.unscoped(), |
114 | required: true | 117 | required: false |
115 | } | 118 | } |
116 | ] | 119 | ] |
117 | } | 120 | } |
@@ -120,7 +123,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
120 | { | 123 | { |
121 | model: AccountModel, | 124 | model: AccountModel, |
122 | as: 'FlaggedAccount', | 125 | as: 'FlaggedAccount', |
123 | required: true, | 126 | required: false, |
124 | include: [ buildActorWithAvatarInclude() ] | 127 | include: [ buildActorWithAvatarInclude() ] |
125 | } | 128 | } |
126 | ] | 129 | ] |
@@ -141,6 +144,18 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
141 | }, | 144 | }, |
142 | 145 | ||
143 | { | 146 | { |
147 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
148 | model: PluginModel.unscoped(), | ||
149 | required: false | ||
150 | }, | ||
151 | |||
152 | { | ||
153 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
154 | model: ApplicationModel.unscoped(), | ||
155 | required: false | ||
156 | }, | ||
157 | |||
158 | { | ||
144 | attributes: [ 'id', 'state' ], | 159 | attributes: [ 'id', 'state' ], |
145 | model: ActorFollowModel.unscoped(), | 160 | model: ActorFollowModel.unscoped(), |
146 | required: false, | 161 | required: false, |
@@ -158,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
158 | }, | 173 | }, |
159 | { | 174 | { |
160 | attributes: [ 'filename' ], | 175 | attributes: [ 'filename' ], |
161 | model: AvatarModel.unscoped(), | 176 | as: 'Avatar', |
177 | model: ActorImageModel.unscoped(), | ||
162 | required: false | 178 | required: false |
163 | }, | 179 | }, |
164 | { | 180 | { |
@@ -251,6 +267,22 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
251 | [Op.ne]: null | 267 | [Op.ne]: null |
252 | } | 268 | } |
253 | } | 269 | } |
270 | }, | ||
271 | { | ||
272 | fields: [ 'pluginId' ], | ||
273 | where: { | ||
274 | pluginId: { | ||
275 | [Op.ne]: null | ||
276 | } | ||
277 | } | ||
278 | }, | ||
279 | { | ||
280 | fields: [ 'applicationId' ], | ||
281 | where: { | ||
282 | applicationId: { | ||
283 | [Op.ne]: null | ||
284 | } | ||
285 | } | ||
254 | } | 286 | } |
255 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 287 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
256 | }) | 288 | }) |
@@ -370,6 +402,30 @@ export class UserNotificationModel extends Model { | |||
370 | }) | 402 | }) |
371 | ActorFollow: ActorFollowModel | 403 | ActorFollow: ActorFollowModel |
372 | 404 | ||
405 | @ForeignKey(() => PluginModel) | ||
406 | @Column | ||
407 | pluginId: number | ||
408 | |||
409 | @BelongsTo(() => PluginModel, { | ||
410 | foreignKey: { | ||
411 | allowNull: true | ||
412 | }, | ||
413 | onDelete: 'cascade' | ||
414 | }) | ||
415 | Plugin: PluginModel | ||
416 | |||
417 | @ForeignKey(() => ApplicationModel) | ||
418 | @Column | ||
419 | applicationId: number | ||
420 | |||
421 | @BelongsTo(() => ApplicationModel, { | ||
422 | foreignKey: { | ||
423 | allowNull: true | ||
424 | }, | ||
425 | onDelete: 'cascade' | ||
426 | }) | ||
427 | Application: ApplicationModel | ||
428 | |||
373 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 429 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
374 | const where = { userId } | 430 | const where = { userId } |
375 | 431 | ||
@@ -524,6 +580,18 @@ export class UserNotificationModel extends Model { | |||
524 | } | 580 | } |
525 | : undefined | 581 | : undefined |
526 | 582 | ||
583 | const plugin = this.Plugin | ||
584 | ? { | ||
585 | name: this.Plugin.name, | ||
586 | type: this.Plugin.type, | ||
587 | latestVersion: this.Plugin.latestVersion | ||
588 | } | ||
589 | : undefined | ||
590 | |||
591 | const peertube = this.Application | ||
592 | ? { latestVersion: this.Application.latestPeerTubeVersion } | ||
593 | : undefined | ||
594 | |||
527 | return { | 595 | return { |
528 | id: this.id, | 596 | id: this.id, |
529 | type: this.type, | 597 | type: this.type, |
@@ -535,6 +603,8 @@ export class UserNotificationModel extends Model { | |||
535 | videoBlacklist, | 603 | videoBlacklist, |
536 | account, | 604 | account, |
537 | actorFollow, | 605 | actorFollow, |
606 | plugin, | ||
607 | peertube, | ||
538 | createdAt: this.createdAt.toISOString(), | 608 | createdAt: this.createdAt.toISOString(), |
539 | updatedAt: this.updatedAt.toISOString() | 609 | updatedAt: this.updatedAt.toISOString() |
540 | } | 610 | } |
@@ -553,17 +623,19 @@ export class UserNotificationModel extends Model { | |||
553 | ? { | 623 | ? { |
554 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 624 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
555 | 625 | ||
556 | video: { | 626 | video: abuse.VideoCommentAbuse.VideoComment.Video |
557 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | 627 | ? { |
558 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | 628 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, |
559 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | 629 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, |
560 | } | 630 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid |
631 | } | ||
632 | : undefined | ||
561 | } | 633 | } |
562 | : undefined | 634 | : undefined |
563 | 635 | ||
564 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 636 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined |
565 | 637 | ||
566 | const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined | 638 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined |
567 | 639 | ||
568 | return { | 640 | return { |
569 | id: abuse.id, | 641 | id: abuse.id, |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index c1f22b76a..00c6d73aa 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' |
24 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
24 | import { | 25 | import { |
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' |
59 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
60 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
61 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 63 | import { ActorModel } from '../activitypub/actor' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 64 | import { ActorFollowModel } from '../activitypub/actor-follow' |
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live' | |||
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 71 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | 72 | import { AccountModel } from './account' |
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { ActorImageModel } from './actor-image' | ||
74 | 75 | ||
75 | enum ScopeNames { | 76 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 77 | FOR_ME_API = 'FOR_ME_API', |
@@ -97,7 +98,20 @@ enum ScopeNames { | |||
97 | model: AccountModel, | 98 | model: AccountModel, |
98 | include: [ | 99 | include: [ |
99 | { | 100 | { |
100 | model: VideoChannelModel | 101 | model: VideoChannelModel.unscoped(), |
102 | include: [ | ||
103 | { | ||
104 | model: ActorModel, | ||
105 | required: true, | ||
106 | include: [ | ||
107 | { | ||
108 | model: ActorImageModel, | ||
109 | as: 'Banner', | ||
110 | required: false | ||
111 | } | ||
112 | ] | ||
113 | } | ||
114 | ] | ||
101 | }, | 115 | }, |
102 | { | 116 | { |
103 | attributes: [ 'id', 'name', 'type' ], | 117 | attributes: [ 'id', 'name', 'type' ], |
@@ -411,7 +425,7 @@ export class UserModel extends Model { | |||
411 | @AfterUpdate | 425 | @AfterUpdate |
412 | @AfterDestroy | 426 | @AfterDestroy |
413 | static removeTokenCache (instance: UserModel) { | 427 | static removeTokenCache (instance: UserModel) { |
414 | return clearCacheByUserId(instance.id) | 428 | return TokensCache.Instance.clearCacheByUserId(instance.id) |
415 | } | 429 | } |
416 | 430 | ||
417 | static countTotal () { | 431 | static countTotal () { |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index ce6a4e267..4c5f37620 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model { | |||
248 | } | 248 | } |
249 | 249 | ||
250 | return ActorFollowModel.findOne(query) | 250 | return ActorFollowModel.findOne(query) |
251 | .then(result => { | ||
252 | if (result?.ActorFollowing.VideoChannel) { | ||
253 | result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing | ||
254 | } | ||
255 | |||
256 | return result | ||
257 | }) | ||
258 | } | 251 | } |
259 | 252 | ||
260 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { | 253 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 3b98e8841..a6c724f26 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | 21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
22 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 22 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
23 | import { activityPubContextify } from '../../helpers/activitypub' | 23 | import { activityPubContextify } from '../../helpers/activitypub' |
24 | import { | 24 | import { |
25 | isActorFollowersCountValid, | 25 | isActorFollowersCountValid, |
@@ -29,11 +29,19 @@ import { | |||
29 | isActorPublicKeyValid | 29 | isActorPublicKeyValid |
30 | } from '../../helpers/custom-validators/activitypub/actor' | 30 | } from '../../helpers/custom-validators/activitypub/actor' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 32 | import { |
33 | ACTIVITY_PUB, | ||
34 | ACTIVITY_PUB_ACTOR_TYPES, | ||
35 | CONSTRAINTS_FIELDS, | ||
36 | MIMETYPES, | ||
37 | SERVER_ACTOR_NAME, | ||
38 | WEBSERVER | ||
39 | } from '../../initializers/constants' | ||
33 | import { | 40 | import { |
34 | MActor, | 41 | MActor, |
35 | MActorAccountChannelId, | 42 | MActorAccountChannelId, |
36 | MActorAP, | 43 | MActorAPAccount, |
44 | MActorAPChannel, | ||
37 | MActorFormattable, | 45 | MActorFormattable, |
38 | MActorFull, | 46 | MActorFull, |
39 | MActorHost, | 47 | MActorHost, |
@@ -43,7 +51,7 @@ import { | |||
43 | MActorWithInboxes | 51 | MActorWithInboxes |
44 | } from '../../types/models' | 52 | } from '../../types/models' |
45 | import { AccountModel } from '../account/account' | 53 | import { AccountModel } from '../account/account' |
46 | import { AvatarModel } from '../avatar/avatar' | 54 | import { ActorImageModel } from '../account/actor-image' |
47 | import { ServerModel } from '../server/server' | 55 | import { ServerModel } from '../server/server' |
48 | import { isOutdated, throwIfNotValid } from '../utils' | 56 | import { isOutdated, throwIfNotValid } from '../utils' |
49 | import { VideoModel } from '../video/video' | 57 | import { VideoModel } from '../video/video' |
@@ -73,7 +81,8 @@ export const unusedActorAttributesForAPI = [ | |||
73 | required: false | 81 | required: false |
74 | }, | 82 | }, |
75 | { | 83 | { |
76 | model: AvatarModel, | 84 | model: ActorImageModel, |
85 | as: 'Avatar', | ||
77 | required: false | 86 | required: false |
78 | } | 87 | } |
79 | ] | 88 | ] |
@@ -100,7 +109,13 @@ export const unusedActorAttributesForAPI = [ | |||
100 | required: false | 109 | required: false |
101 | }, | 110 | }, |
102 | { | 111 | { |
103 | model: AvatarModel, | 112 | model: ActorImageModel, |
113 | as: 'Avatar', | ||
114 | required: false | ||
115 | }, | ||
116 | { | ||
117 | model: ActorImageModel, | ||
118 | as: 'Banner', | ||
104 | required: false | 119 | required: false |
105 | } | 120 | } |
106 | ] | 121 | ] |
@@ -213,18 +228,35 @@ export class ActorModel extends Model { | |||
213 | @UpdatedAt | 228 | @UpdatedAt |
214 | updatedAt: Date | 229 | updatedAt: Date |
215 | 230 | ||
216 | @ForeignKey(() => AvatarModel) | 231 | @ForeignKey(() => ActorImageModel) |
217 | @Column | 232 | @Column |
218 | avatarId: number | 233 | avatarId: number |
219 | 234 | ||
220 | @BelongsTo(() => AvatarModel, { | 235 | @ForeignKey(() => ActorImageModel) |
236 | @Column | ||
237 | bannerId: number | ||
238 | |||
239 | @BelongsTo(() => ActorImageModel, { | ||
221 | foreignKey: { | 240 | foreignKey: { |
241 | name: 'avatarId', | ||
222 | allowNull: true | 242 | allowNull: true |
223 | }, | 243 | }, |
244 | as: 'Avatar', | ||
224 | onDelete: 'set null', | 245 | onDelete: 'set null', |
225 | hooks: true | 246 | hooks: true |
226 | }) | 247 | }) |
227 | Avatar: AvatarModel | 248 | Avatar: ActorImageModel |
249 | |||
250 | @BelongsTo(() => ActorImageModel, { | ||
251 | foreignKey: { | ||
252 | name: 'bannerId', | ||
253 | allowNull: true | ||
254 | }, | ||
255 | as: 'Banner', | ||
256 | onDelete: 'set null', | ||
257 | hooks: true | ||
258 | }) | ||
259 | Banner: ActorImageModel | ||
228 | 260 | ||
229 | @HasMany(() => ActorFollowModel, { | 261 | @HasMany(() => ActorFollowModel, { |
230 | foreignKey: { | 262 | foreignKey: { |
@@ -496,7 +528,7 @@ export class ActorModel extends Model { | |||
496 | } | 528 | } |
497 | 529 | ||
498 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 530 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
499 | let avatar: Avatar = null | 531 | let avatar: ActorImage = null |
500 | if (this.Avatar) { | 532 | if (this.Avatar) { |
501 | avatar = this.Avatar.toFormattedJSON() | 533 | avatar = this.Avatar.toFormattedJSON() |
502 | } | 534 | } |
@@ -512,29 +544,51 @@ export class ActorModel extends Model { | |||
512 | toFormattedJSON (this: MActorFormattable) { | 544 | toFormattedJSON (this: MActorFormattable) { |
513 | const base = this.toFormattedSummaryJSON() | 545 | const base = this.toFormattedSummaryJSON() |
514 | 546 | ||
547 | let banner: ActorImage = null | ||
548 | if (this.bannerId) { | ||
549 | banner = this.Banner.toFormattedJSON() | ||
550 | } | ||
551 | |||
515 | return Object.assign(base, { | 552 | return Object.assign(base, { |
516 | id: this.id, | 553 | id: this.id, |
517 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 554 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
518 | followingCount: this.followingCount, | 555 | followingCount: this.followingCount, |
519 | followersCount: this.followersCount, | 556 | followersCount: this.followersCount, |
557 | banner, | ||
520 | createdAt: this.createdAt, | 558 | createdAt: this.createdAt, |
521 | updatedAt: this.updatedAt | 559 | updatedAt: this.updatedAt |
522 | }) | 560 | }) |
523 | } | 561 | } |
524 | 562 | ||
525 | toActivityPubObject (this: MActorAP, name: string) { | 563 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
526 | let icon: ActivityIconObject | 564 | let icon: ActivityIconObject |
565 | let image: ActivityIconObject | ||
527 | 566 | ||
528 | if (this.avatarId) { | 567 | if (this.avatarId) { |
529 | const extension = extname(this.Avatar.filename) | 568 | const extension = extname(this.Avatar.filename) |
530 | 569 | ||
531 | icon = { | 570 | icon = { |
532 | type: 'Image', | 571 | type: 'Image', |
533 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', | 572 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
573 | height: this.Avatar.height, | ||
574 | width: this.Avatar.width, | ||
534 | url: this.getAvatarUrl() | 575 | url: this.getAvatarUrl() |
535 | } | 576 | } |
536 | } | 577 | } |
537 | 578 | ||
579 | if (this.bannerId) { | ||
580 | const banner = (this as MActorAPChannel).Banner | ||
581 | const extension = extname(banner.filename) | ||
582 | |||
583 | image = { | ||
584 | type: 'Image', | ||
585 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
586 | height: banner.height, | ||
587 | width: banner.width, | ||
588 | url: this.getBannerUrl() | ||
589 | } | ||
590 | } | ||
591 | |||
538 | const json = { | 592 | const json = { |
539 | type: this.type, | 593 | type: this.type, |
540 | id: this.url, | 594 | id: this.url, |
@@ -554,7 +608,8 @@ export class ActorModel extends Model { | |||
554 | owner: this.url, | 608 | owner: this.url, |
555 | publicKeyPem: this.publicKey | 609 | publicKeyPem: this.publicKey |
556 | }, | 610 | }, |
557 | icon | 611 | icon, |
612 | image | ||
558 | } | 613 | } |
559 | 614 | ||
560 | return activityPubContextify(json) | 615 | return activityPubContextify(json) |
@@ -624,6 +679,12 @@ export class ActorModel extends Model { | |||
624 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 679 | return WEBSERVER.URL + this.Avatar.getStaticPath() |
625 | } | 680 | } |
626 | 681 | ||
682 | getBannerUrl () { | ||
683 | if (!this.bannerId) return undefined | ||
684 | |||
685 | return WEBSERVER.URL + this.Banner.getStaticPath() | ||
686 | } | ||
687 | |||
627 | isOutdated () { | 688 | isOutdated () { |
628 | if (this.isOwned()) return false | 689 | if (this.isOwned()) return false |
629 | 690 | ||
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/avatar/avatar.ts b/server/models/avatar/avatar.ts deleted file mode 100644 index 0d246a144..000000000 --- a/server/models/avatar/avatar.ts +++ /dev/null | |||
@@ -1,81 +0,0 @@ | |||
1 | import { join } from 'path' | ||
2 | import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | ||
4 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { remove } from 'fs-extra' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { throwIfNotValid } from '../utils' | ||
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
10 | import { MAvatarFormattable } from '@server/types/models' | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'avatar', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: [ 'filename' ], | ||
17 | unique: true | ||
18 | } | ||
19 | ] | ||
20 | }) | ||
21 | export class AvatarModel extends Model { | ||
22 | |||
23 | @AllowNull(false) | ||
24 | @Column | ||
25 | filename: string | ||
26 | |||
27 | @AllowNull(true) | ||
28 | @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
29 | @Column | ||
30 | fileUrl: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | onDisk: boolean | ||
35 | |||
36 | @CreatedAt | ||
37 | createdAt: Date | ||
38 | |||
39 | @UpdatedAt | ||
40 | updatedAt: Date | ||
41 | |||
42 | @AfterDestroy | ||
43 | static removeFilesAndSendDelete (instance: AvatarModel) { | ||
44 | logger.info('Removing avatar file %s.', instance.filename) | ||
45 | |||
46 | // Don't block the transaction | ||
47 | instance.removeAvatar() | ||
48 | .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err)) | ||
49 | } | ||
50 | |||
51 | static loadByName (filename: string) { | ||
52 | const query = { | ||
53 | where: { | ||
54 | filename | ||
55 | } | ||
56 | } | ||
57 | |||
58 | return AvatarModel.findOne(query) | ||
59 | } | ||
60 | |||
61 | toFormattedJSON (this: MAvatarFormattable): Avatar { | ||
62 | return { | ||
63 | path: this.getStaticPath(), | ||
64 | createdAt: this.createdAt, | ||
65 | updatedAt: this.updatedAt | ||
66 | } | ||
67 | } | ||
68 | |||
69 | getStaticPath () { | ||
70 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
71 | } | ||
72 | |||
73 | getPath () { | ||
74 | return join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
75 | } | ||
76 | |||
77 | removeAvatar () { | ||
78 | const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
79 | return remove(avatarPath) | ||
80 | } | ||
81 | } | ||
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' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
16 | import { MUserAccountId } from '@server/types/models' | ||
15 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
16 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
17 | import { clearCacheByToken } from '../../lib/oauth-model' | ||
18 | import { AccountModel } from '../account/account' | 19 | import { AccountModel } from '../account/account' |
19 | import { UserModel } from '../account/user' | 20 | import { UserModel } from '../account/user' |
20 | import { ActorModel } from '../activitypub/actor' | 21 | import { 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/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 53293df37..53ebadeaf 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -32,6 +32,7 @@ import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | |||
32 | import { ActorModel } from '../activitypub/actor' | 32 | import { ActorModel } from '../activitypub/actor' |
33 | import { ServerModel } from '../server/server' | 33 | import { ServerModel } from '../server/server' |
34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
35 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | ||
35 | import { VideoModel } from '../video/video' | 36 | import { VideoModel } from '../video/video' |
36 | import { VideoChannelModel } from '../video/video-channel' | 37 | import { VideoChannelModel } from '../video/video-channel' |
37 | import { VideoFileModel } from '../video/video-file' | 38 | import { VideoFileModel } from '../video/video-file' |
@@ -374,7 +375,13 @@ export class VideoRedundancyModel extends Model { | |||
374 | ...this.buildVideoIdsForDuplication(peertubeActor) | 375 | ...this.buildVideoIdsForDuplication(peertubeActor) |
375 | }, | 376 | }, |
376 | include: [ | 377 | include: [ |
377 | VideoRedundancyModel.buildServerRedundancyInclude() | 378 | VideoRedundancyModel.buildServerRedundancyInclude(), |
379 | |||
380 | // Required by publishedAt sort | ||
381 | { | ||
382 | model: ScheduleVideoUpdateModel.unscoped(), | ||
383 | required: false | ||
384 | } | ||
378 | ] | 385 | ] |
379 | } | 386 | } |
380 | 387 | ||
diff --git a/server/models/utils.ts b/server/models/utils.ts index 5337ae75d..ec51c66bf 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -56,6 +56,14 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or | |||
56 | 56 | ||
57 | lastSort | 57 | lastSort |
58 | ] | 58 | ] |
59 | } else if (field === 'publishedAt') { | ||
60 | return [ | ||
61 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
62 | |||
63 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
64 | |||
65 | lastSort | ||
66 | ] | ||
59 | } | 67 | } |
60 | 68 | ||
61 | let finalField: string | Col | 69 | let finalField: string | Col |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 178878c55..d2a055f5b 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -28,17 +28,16 @@ import { | |||
28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
29 | import { sendDeleteActor } from '../../lib/activitypub/send' | 29 | import { sendDeleteActor } from '../../lib/activitypub/send' |
30 | import { | 30 | import { |
31 | MChannelAccountDefault, | ||
32 | MChannelActor, | 31 | MChannelActor, |
33 | MChannelActorAccountDefaultVideos, | ||
34 | MChannelAP, | 32 | MChannelAP, |
33 | MChannelBannerAccountDefault, | ||
35 | MChannelFormattable, | 34 | MChannelFormattable, |
36 | MChannelSummaryFormattable | 35 | MChannelSummaryFormattable |
37 | } from '../../types/models/video' | 36 | } from '../../types/models/video' |
38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 37 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
38 | import { ActorImageModel } from '../account/actor-image' | ||
39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
40 | import { ActorFollowModel } from '../activitypub/actor-follow' | 40 | import { ActorFollowModel } from '../activitypub/actor-follow' |
41 | import { AvatarModel } from '../avatar/avatar' | ||
42 | import { ServerModel } from '../server/server' | 41 | import { ServerModel } from '../server/server' |
43 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 42 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
44 | import { VideoModel } from './video' | 43 | import { VideoModel } from './video' |
@@ -49,6 +48,7 @@ export enum ScopeNames { | |||
49 | SUMMARY = 'SUMMARY', | 48 | SUMMARY = 'SUMMARY', |
50 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
51 | WITH_ACTOR = 'WITH_ACTOR', | 50 | WITH_ACTOR = 'WITH_ACTOR', |
51 | WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER', | ||
52 | WITH_VIDEOS = 'WITH_VIDEOS', | 52 | WITH_VIDEOS = 'WITH_VIDEOS', |
53 | WITH_STATS = 'WITH_STATS' | 53 | WITH_STATS = 'WITH_STATS' |
54 | } | 54 | } |
@@ -99,7 +99,14 @@ export type SummaryOptions = { | |||
99 | } | 99 | } |
100 | } | 100 | } |
101 | ] | 101 | ] |
102 | } | 102 | }, |
103 | include: [ | ||
104 | { | ||
105 | model: ActorImageModel, | ||
106 | as: 'Banner', | ||
107 | required: false | ||
108 | } | ||
109 | ] | ||
103 | }, | 110 | }, |
104 | { | 111 | { |
105 | model: AccountModel, | 112 | model: AccountModel, |
@@ -130,7 +137,8 @@ export type SummaryOptions = { | |||
130 | required: false | 137 | required: false |
131 | }, | 138 | }, |
132 | { | 139 | { |
133 | model: AvatarModel.unscoped(), | 140 | model: ActorImageModel.unscoped(), |
141 | as: 'Avatar', | ||
134 | required: false | 142 | required: false |
135 | } | 143 | } |
136 | ] | 144 | ] |
@@ -167,6 +175,20 @@ export type SummaryOptions = { | |||
167 | ActorModel | 175 | ActorModel |
168 | ] | 176 | ] |
169 | }, | 177 | }, |
178 | [ScopeNames.WITH_ACTOR_BANNER]: { | ||
179 | include: [ | ||
180 | { | ||
181 | model: ActorModel, | ||
182 | include: [ | ||
183 | { | ||
184 | model: ActorImageModel, | ||
185 | required: false, | ||
186 | as: 'Banner' | ||
187 | } | ||
188 | ] | ||
189 | } | ||
190 | ] | ||
191 | }, | ||
170 | [ScopeNames.WITH_VIDEOS]: { | 192 | [ScopeNames.WITH_VIDEOS]: { |
171 | include: [ | 193 | include: [ |
172 | VideoModel | 194 | VideoModel |
@@ -441,7 +463,7 @@ export class VideoChannelModel extends Model { | |||
441 | where | 463 | where |
442 | } | 464 | } |
443 | 465 | ||
444 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | 466 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
445 | 467 | ||
446 | if (options.withStats === true) { | 468 | if (options.withStats === true) { |
447 | scopes.push({ | 469 | scopes.push({ |
@@ -457,32 +479,13 @@ export class VideoChannelModel extends Model { | |||
457 | }) | 479 | }) |
458 | } | 480 | } |
459 | 481 | ||
460 | static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | 482 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { |
461 | return VideoChannelModel.unscoped() | 483 | return VideoChannelModel.unscoped() |
462 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 484 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
463 | .findByPk(id) | 485 | .findByPk(id) |
464 | } | 486 | } |
465 | 487 | ||
466 | static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { | 488 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
467 | const query = { | ||
468 | where: { | ||
469 | id, | ||
470 | accountId | ||
471 | } | ||
472 | } | ||
473 | |||
474 | return VideoChannelModel.unscoped() | ||
475 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
476 | .findOne(query) | ||
477 | } | ||
478 | |||
479 | static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | ||
480 | return VideoChannelModel.unscoped() | ||
481 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
482 | .findByPk(id) | ||
483 | } | ||
484 | |||
485 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> { | ||
486 | const query = { | 489 | const query = { |
487 | include: [ | 490 | include: [ |
488 | { | 491 | { |
@@ -490,7 +493,14 @@ export class VideoChannelModel extends Model { | |||
490 | required: true, | 493 | required: true, |
491 | where: { | 494 | where: { |
492 | url | 495 | url |
493 | } | 496 | }, |
497 | include: [ | ||
498 | { | ||
499 | model: ActorImageModel, | ||
500 | required: false, | ||
501 | as: 'Banner' | ||
502 | } | ||
503 | ] | ||
494 | } | 504 | } |
495 | ] | 505 | ] |
496 | } | 506 | } |
@@ -508,7 +518,7 @@ export class VideoChannelModel extends Model { | |||
508 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | 518 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) |
509 | } | 519 | } |
510 | 520 | ||
511 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { | 521 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> { |
512 | const query = { | 522 | const query = { |
513 | include: [ | 523 | include: [ |
514 | { | 524 | { |
@@ -517,17 +527,24 @@ export class VideoChannelModel extends Model { | |||
517 | where: { | 527 | where: { |
518 | preferredUsername: name, | 528 | preferredUsername: name, |
519 | serverId: null | 529 | serverId: null |
520 | } | 530 | }, |
531 | include: [ | ||
532 | { | ||
533 | model: ActorImageModel, | ||
534 | required: false, | ||
535 | as: 'Banner' | ||
536 | } | ||
537 | ] | ||
521 | } | 538 | } |
522 | ] | 539 | ] |
523 | } | 540 | } |
524 | 541 | ||
525 | return VideoChannelModel.unscoped() | 542 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 543 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
527 | .findOne(query) | 544 | .findOne(query) |
528 | } | 545 | } |
529 | 546 | ||
530 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { | 547 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> { |
531 | const query = { | 548 | const query = { |
532 | include: [ | 549 | include: [ |
533 | { | 550 | { |
@@ -541,6 +558,11 @@ export class VideoChannelModel extends Model { | |||
541 | model: ServerModel, | 558 | model: ServerModel, |
542 | required: true, | 559 | required: true, |
543 | where: { host } | 560 | where: { host } |
561 | }, | ||
562 | { | ||
563 | model: ActorImageModel, | ||
564 | required: false, | ||
565 | as: 'Banner' | ||
544 | } | 566 | } |
545 | ] | 567 | ] |
546 | } | 568 | } |
@@ -548,22 +570,10 @@ export class VideoChannelModel extends Model { | |||
548 | } | 570 | } |
549 | 571 | ||
550 | return VideoChannelModel.unscoped() | 572 | return VideoChannelModel.unscoped() |
551 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 573 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
552 | .findOne(query) | 574 | .findOne(query) |
553 | } | 575 | } |
554 | 576 | ||
555 | static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> { | ||
556 | const options = { | ||
557 | include: [ | ||
558 | VideoModel | ||
559 | ] | ||
560 | } | ||
561 | |||
562 | return VideoChannelModel.unscoped() | ||
563 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]) | ||
564 | .findByPk(id, options) | ||
565 | } | ||
566 | |||
567 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { | 577 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { |
568 | const actor = this.Actor.toFormattedSummaryJSON() | 578 | const actor = this.Actor.toFormattedSummaryJSON() |
569 | 579 | ||
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 96df0a7f8..4d95ddee2 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build | |||
490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', | 490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', |
491 | 491 | ||
492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', | 492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', |
493 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | 493 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + |
494 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | ||
494 | 495 | ||
495 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | 496 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + |
496 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', | 497 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', |
497 | 498 | ||
498 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + | 499 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + |
499 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', | 500 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', |
500 | 501 | ||
501 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' | 502 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 3c4f3d3df..e9afb2c18 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -24,7 +24,6 @@ import { | |||
24 | Table, | 24 | Table, |
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { v4 as uuidv4 } from 'uuid' | ||
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 28 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live-manager' | 29 | import { LiveManager } from '@server/lib/live-manager' |
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models | |||
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 99 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 100 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 101 | import { AccountVideoRateModel } from '../account/account-video-rate' |
102 | import { ActorImageModel } from '../account/actor-image' | ||
103 | import { UserModel } from '../account/user' | 103 | import { UserModel } from '../account/user' |
104 | import { UserVideoHistoryModel } from '../account/user-video-history' | 104 | import { UserVideoHistoryModel } from '../account/user-video-history' |
105 | import { ActorModel } from '../activitypub/actor' | 105 | import { ActorModel } from '../activitypub/actor' |
106 | import { AvatarModel } from '../avatar/avatar' | ||
107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 106 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
108 | import { ServerModel } from '../server/server' | 107 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | 108 | import { TrackerModel } from '../server/tracker' |
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = { | |||
286 | required: false | 285 | required: false |
287 | }, | 286 | }, |
288 | { | 287 | { |
289 | model: AvatarModel.unscoped(), | 288 | model: ActorImageModel.unscoped(), |
289 | as: 'Avatar', | ||
290 | required: false | 290 | required: false |
291 | } | 291 | } |
292 | ] | 292 | ] |
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = { | |||
308 | required: false | 308 | required: false |
309 | }, | 309 | }, |
310 | { | 310 | { |
311 | model: AvatarModel.unscoped(), | 311 | model: ActorImageModel.unscoped(), |
312 | as: 'Avatar', | ||
312 | required: false | 313 | required: false |
313 | } | 314 | } |
314 | ] | 315 | ] |
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model { | |||
1703 | 1704 | ||
1704 | function buildActor (rowActor: any) { | 1705 | function buildActor (rowActor: any) { |
1705 | const avatarModel = rowActor.Avatar.id !== null | 1706 | const avatarModel = rowActor.Avatar.id !== null |
1706 | ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) | 1707 | ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) |
1707 | : null | 1708 | : null |
1708 | 1709 | ||
1709 | const serverModel = rowActor.Server.id !== null | 1710 | const serverModel = rowActor.Server.id !== null |
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model { | |||
1869 | this.Thumbnails.push(savedThumbnail) | 1870 | this.Thumbnails.push(savedThumbnail) |
1870 | } | 1871 | } |
1871 | 1872 | ||
1872 | generateThumbnailName () { | ||
1873 | return uuidv4() + '.jpg' | ||
1874 | } | ||
1875 | |||
1876 | getMiniature () { | 1873 | getMiniature () { |
1877 | if (Array.isArray(this.Thumbnails) === false) return undefined | 1874 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1878 | 1875 | ||
1879 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) | 1876 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) |
1880 | } | 1877 | } |
1881 | 1878 | ||
1882 | generatePreviewName () { | ||
1883 | return uuidv4() + '.jpg' | ||
1884 | } | ||
1885 | |||
1886 | hasPreview () { | 1879 | hasPreview () { |
1887 | return !!this.getPreview() | 1880 | return !!this.getPreview() |
1888 | } | 1881 | } |
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 | |||
20 | const expect = chai.expect | 22 | const expect = chai.expect |
21 | 23 | ||
22 | function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { | 24 | function 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 | ||
29 | function getAnnounceWithoutContext (server2: ServerInfo) { | 33 | function 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 | |||
42 | function 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/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 0dd436426..bc2e6192e 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -234,7 +234,8 @@ describe('Test video channels API validator', function () { | |||
234 | }) | 234 | }) |
235 | }) | 235 | }) |
236 | 236 | ||
237 | describe('When updating video channel avatar', function () { | 237 | describe('When updating video channel avatar/banner', function () { |
238 | const types = [ 'avatar', 'banner' ] | ||
238 | let path: string | 239 | let path: string |
239 | 240 | ||
240 | before(async function () { | 241 | before(async function () { |
@@ -242,48 +243,57 @@ describe('Test video channels API validator', function () { | |||
242 | }) | 243 | }) |
243 | 244 | ||
244 | it('Should fail with an incorrect input file', async function () { | 245 | it('Should fail with an incorrect input file', async function () { |
245 | const fields = {} | 246 | for (const type of types) { |
246 | const attaches = { | 247 | const fields = {} |
247 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 248 | const attaches = { |
249 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | ||
250 | } | ||
251 | |||
252 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
248 | } | 253 | } |
249 | await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) | ||
250 | }) | 254 | }) |
251 | 255 | ||
252 | it('Should fail with a big file', async function () { | 256 | it('Should fail with a big file', async function () { |
253 | const fields = {} | 257 | for (const type of types) { |
254 | const attaches = { | 258 | const fields = {} |
255 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | 259 | const attaches = { |
260 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | ||
261 | } | ||
262 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | ||
256 | } | 263 | } |
257 | await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) | ||
258 | }) | 264 | }) |
259 | 265 | ||
260 | it('Should fail with an unauthenticated user', async function () { | 266 | it('Should fail with an unauthenticated user', async function () { |
261 | const fields = {} | 267 | for (const type of types) { |
262 | const attaches = { | 268 | const fields = {} |
263 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 269 | const attaches = { |
270 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
271 | } | ||
272 | await makeUploadRequest({ | ||
273 | url: server.url, | ||
274 | path: `${path}/${type}/pick`, | ||
275 | fields, | ||
276 | attaches, | ||
277 | statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 | ||
278 | }) | ||
264 | } | 279 | } |
265 | await makeUploadRequest({ | ||
266 | url: server.url, | ||
267 | path: path + '/avatar/pick', | ||
268 | fields, | ||
269 | attaches, | ||
270 | statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 | ||
271 | }) | ||
272 | }) | 280 | }) |
273 | 281 | ||
274 | it('Should succeed with the correct params', async function () { | 282 | it('Should succeed with the correct params', async function () { |
275 | const fields = {} | 283 | for (const type of types) { |
276 | const attaches = { | 284 | const fields = {} |
277 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 285 | const attaches = { |
286 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | ||
287 | } | ||
288 | await makeUploadRequest({ | ||
289 | url: server.url, | ||
290 | path: `${path}/${type}/pick`, | ||
291 | token: server.accessToken, | ||
292 | fields, | ||
293 | attaches, | ||
294 | statusCodeExpected: HttpStatusCode.OK_200 | ||
295 | }) | ||
278 | } | 296 | } |
279 | await makeUploadRequest({ | ||
280 | url: server.url, | ||
281 | path: path + '/avatar/pick', | ||
282 | token: server.accessToken, | ||
283 | fields, | ||
284 | attaches, | ||
285 | statusCodeExpected: HttpStatusCode.OK_200 | ||
286 | }) | ||
287 | }) | 297 | }) |
288 | }) | 298 | }) |
289 | 299 | ||
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 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions' | ||
6 | import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils' | ||
7 | import { ServerInfo } from '../../../../shared/extra-utils/index' | ||
8 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | ||
9 | import { | ||
10 | CheckerBaseParams, | ||
11 | checkNewPeerTubeVersion, | ||
12 | checkNewPluginVersion, | ||
13 | prepareNotificationsTest | ||
14 | } from '../../../../shared/extra-utils/users/user-notifications' | ||
15 | import { UserNotification, UserNotificationType } from '../../../../shared/models/users' | ||
16 | import { PluginType } from '@shared/models' | ||
17 | |||
18 | describe('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 @@ | |||
1 | import './admin-notifications' | ||
1 | import './comments-notifications' | 2 | import './comments-notifications' |
2 | import './moderation-notifications' | 3 | import './moderation-notifications' |
3 | import './notifications-api' | 4 | import './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/server/services.ts b/server/tests/api/server/services.ts index df910c111..f0fa91674 100644 --- a/server/tests/api/server/services.ts +++ b/server/tests/api/server/services.ts | |||
@@ -20,6 +20,7 @@ const expect = chai.expect | |||
20 | describe('Test services', function () { | 20 | describe('Test services', function () { |
21 | let server: ServerInfo = null | 21 | let server: ServerInfo = null |
22 | let playlistUUID: string | 22 | let playlistUUID: string |
23 | let playlistDisplayName: string | ||
23 | let video: Video | 24 | let video: Video |
24 | 25 | ||
25 | before(async function () { | 26 | before(async function () { |
@@ -52,6 +53,7 @@ describe('Test services', function () { | |||
52 | }) | 53 | }) |
53 | 54 | ||
54 | playlistUUID = res.body.videoPlaylist.uuid | 55 | playlistUUID = res.body.videoPlaylist.uuid |
56 | playlistDisplayName = 'The Life and Times of Scrooge McDuck' | ||
55 | 57 | ||
56 | await addVideoInPlaylist({ | 58 | await addVideoInPlaylist({ |
57 | url: server.url, | 59 | url: server.url, |
@@ -69,7 +71,7 @@ describe('Test services', function () { | |||
69 | 71 | ||
70 | const res = await getOEmbed(server.url, oembedUrl) | 72 | const res = await getOEmbed(server.url, oembedUrl) |
71 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 73 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
72 | `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 74 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
73 | 'frameborder="0" allowfullscreen></iframe>' | 75 | 'frameborder="0" allowfullscreen></iframe>' |
74 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath | 76 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath |
75 | 77 | ||
@@ -88,7 +90,7 @@ describe('Test services', function () { | |||
88 | 90 | ||
89 | const res = await getOEmbed(server.url, oembedUrl) | 91 | const res = await getOEmbed(server.url, oembedUrl) |
90 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 92 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
91 | `src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + | 93 | `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + |
92 | 'frameborder="0" allowfullscreen></iframe>' | 94 | 'frameborder="0" allowfullscreen></iframe>' |
93 | 95 | ||
94 | expect(res.body.html).to.equal(expectedHtml) | 96 | expect(res.body.html).to.equal(expectedHtml) |
@@ -97,8 +99,8 @@ describe('Test services', function () { | |||
97 | expect(res.body.width).to.equal(560) | 99 | expect(res.body.width).to.equal(560) |
98 | expect(res.body.height).to.equal(315) | 100 | expect(res.body.height).to.equal(315) |
99 | expect(res.body.thumbnail_url).exist | 101 | expect(res.body.thumbnail_url).exist |
100 | expect(res.body.thumbnail_width).to.equal(223) | 102 | expect(res.body.thumbnail_width).to.equal(280) |
101 | expect(res.body.thumbnail_height).to.equal(122) | 103 | expect(res.body.thumbnail_height).to.equal(157) |
102 | }) | 104 | }) |
103 | 105 | ||
104 | it('Should have a valid oEmbed response with small max height query', async function () { | 106 | it('Should have a valid oEmbed response with small max height query', async function () { |
@@ -109,7 +111,7 @@ describe('Test services', function () { | |||
109 | 111 | ||
110 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) | 112 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) |
111 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + | 113 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + |
112 | `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 114 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
113 | 'frameborder="0" allowfullscreen></iframe>' | 115 | 'frameborder="0" allowfullscreen></iframe>' |
114 | 116 | ||
115 | expect(res.body.html).to.equal(expectedHtml) | 117 | expect(res.body.html).to.equal(expectedHtml) |
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' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' |
6 | import { CustomConfig } from '@shared/models/server' | 6 | import { CustomConfig } from '@shared/models/server' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { | 8 | import { |
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' |
46 | import { follow } from '../../../../shared/extra-utils/server/follows' | 51 | import { follow } from '../../../../shared/extra-utils/server/follows' |
47 | import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | 52 | import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' |
48 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' | 53 | import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' |
49 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 54 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
50 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
51 | 55 | ||
52 | const expect = chai.expect | 56 | const 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/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 367f99fdd..d12d58e75 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -2,16 +2,20 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { basename } from 'path' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createUser, | 8 | createUser, |
9 | deleteVideoChannelImage, | ||
8 | doubleFollow, | 10 | doubleFollow, |
9 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
12 | getActorImage, | ||
10 | getVideo, | 13 | getVideo, |
14 | getVideoChannel, | ||
11 | getVideoChannelVideos, | 15 | getVideoChannelVideos, |
12 | testImage, | 16 | testImage, |
13 | updateVideo, | 17 | updateVideo, |
14 | updateVideoChannelAvatar, | 18 | updateVideoChannelImage, |
15 | uploadVideo, | 19 | uploadVideo, |
16 | userLogin, | 20 | userLogin, |
17 | wait | 21 | wait |
@@ -21,7 +25,6 @@ import { | |||
21 | deleteVideoChannel, | 25 | deleteVideoChannel, |
22 | getAccountVideoChannelsList, | 26 | getAccountVideoChannelsList, |
23 | getMyUserInformation, | 27 | getMyUserInformation, |
24 | getVideoChannel, | ||
25 | getVideoChannelsList, | 28 | getVideoChannelsList, |
26 | ServerInfo, | 29 | ServerInfo, |
27 | setAccessTokensToServers, | 30 | setAccessTokensToServers, |
@@ -30,9 +33,17 @@ import { | |||
30 | } from '../../../../shared/extra-utils/index' | 33 | } from '../../../../shared/extra-utils/index' |
31 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 34 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
32 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' | 35 | import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' |
36 | import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants' | ||
33 | 37 | ||
34 | const expect = chai.expect | 38 | const expect = chai.expect |
35 | 39 | ||
40 | async function findChannel (server: ServerInfo, channelId: number) { | ||
41 | const res = await getVideoChannelsList(server.url, 0, 5, '-name') | ||
42 | const videoChannel = res.body.data.find(c => c.id === channelId) | ||
43 | |||
44 | return videoChannel as VideoChannel | ||
45 | } | ||
46 | |||
36 | describe('Test video channels', function () { | 47 | describe('Test video channels', function () { |
37 | let servers: ServerInfo[] | 48 | let servers: ServerInfo[] |
38 | let userInfo: User | 49 | let userInfo: User |
@@ -262,38 +273,94 @@ describe('Test video channels', function () { | |||
262 | }) | 273 | }) |
263 | 274 | ||
264 | it('Should update video channel avatar', async function () { | 275 | it('Should update video channel avatar', async function () { |
265 | this.timeout(5000) | 276 | this.timeout(15000) |
266 | 277 | ||
267 | const fixture = 'avatar.png' | 278 | const fixture = 'avatar.png' |
268 | 279 | ||
269 | await updateVideoChannelAvatar({ | 280 | await updateVideoChannelImage({ |
270 | url: servers[0].url, | 281 | url: servers[0].url, |
271 | accessToken: servers[0].accessToken, | 282 | accessToken: servers[0].accessToken, |
272 | videoChannelName: 'second_video_channel', | 283 | videoChannelName: 'second_video_channel', |
273 | fixture | 284 | fixture, |
285 | type: 'avatar' | ||
274 | }) | 286 | }) |
275 | 287 | ||
276 | await waitJobs(servers) | 288 | await waitJobs(servers) |
289 | |||
290 | for (const server of servers) { | ||
291 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
292 | |||
293 | await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') | ||
294 | |||
295 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path)) | ||
296 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) | ||
297 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) | ||
298 | } | ||
277 | }) | 299 | }) |
278 | 300 | ||
279 | it('Should have video channel avatar updated', async function () { | 301 | it('Should update video channel banner', async function () { |
302 | this.timeout(15000) | ||
303 | |||
304 | const fixture = 'banner.jpg' | ||
305 | |||
306 | await updateVideoChannelImage({ | ||
307 | url: servers[0].url, | ||
308 | accessToken: servers[0].accessToken, | ||
309 | videoChannelName: 'second_video_channel', | ||
310 | fixture, | ||
311 | type: 'banner' | ||
312 | }) | ||
313 | |||
314 | await waitJobs(servers) | ||
315 | |||
280 | for (const server of servers) { | 316 | for (const server of servers) { |
281 | const res = await getVideoChannelsList(server.url, 0, 1, '-name') | 317 | const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host) |
318 | const videoChannel = res.body | ||
282 | 319 | ||
283 | const videoChannel = res.body.data.find(c => c.id === secondVideoChannelId) | 320 | await testImage(server.url, 'banner-resized', videoChannel.banner.path) |
284 | 321 | ||
285 | await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') | 322 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path)) |
323 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) | ||
324 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | it('Should delete the video channel avatar', async function () { | ||
329 | this.timeout(15000) | ||
330 | |||
331 | await deleteVideoChannelImage({ | ||
332 | url: servers[0].url, | ||
333 | accessToken: servers[0].accessToken, | ||
334 | videoChannelName: 'second_video_channel', | ||
335 | type: 'avatar' | ||
336 | }) | ||
337 | |||
338 | await waitJobs(servers) | ||
339 | |||
340 | for (const server of servers) { | ||
341 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
342 | |||
343 | expect(videoChannel.avatar).to.be.null | ||
286 | } | 344 | } |
287 | }) | 345 | }) |
288 | 346 | ||
289 | it('Should get video channel', async function () { | 347 | it('Should delete the video channel banner', async function () { |
290 | const res = await getVideoChannel(servers[0].url, 'second_video_channel') | 348 | this.timeout(15000) |
349 | |||
350 | await deleteVideoChannelImage({ | ||
351 | url: servers[0].url, | ||
352 | accessToken: servers[0].accessToken, | ||
353 | videoChannelName: 'second_video_channel', | ||
354 | type: 'banner' | ||
355 | }) | ||
356 | |||
357 | await waitJobs(servers) | ||
358 | |||
359 | for (const server of servers) { | ||
360 | const videoChannel = await findChannel(server, secondVideoChannelId) | ||
291 | 361 | ||
292 | const videoChannel = res.body | 362 | expect(videoChannel.banner).to.be.null |
293 | expect(videoChannel.name).to.equal('second_video_channel') | 363 | } |
294 | expect(videoChannel.displayName).to.equal('video channel updated') | ||
295 | expect(videoChannel.description).to.equal('video channel description updated') | ||
296 | expect(videoChannel.support).to.equal('video channel support text updated') | ||
297 | }) | 364 | }) |
298 | 365 | ||
299 | it('Should list the second video channel videos', async function () { | 366 | it('Should list the second video channel videos', async function () { |
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' | |||
6 | import './plugins' | 6 | import './plugins' |
7 | import './print-transcode-command' | 7 | import './print-transcode-command' |
8 | import './prune-storage' | 8 | import './prune-storage' |
9 | import './regenerate-thumbnails' | ||
9 | import './reset-password' | 10 | import './reset-password' |
10 | import './update-host' | 11 | import './update-host' |
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts new file mode 100644 index 000000000..8acb9f263 --- /dev/null +++ b/server/tests/cli/regenerate-thumbnails.ts | |||
@@ -0,0 +1,124 @@ | |||
1 | import 'mocha' | ||
2 | import { expect } from 'chai' | ||
3 | import { writeFile } from 'fs-extra' | ||
4 | import { basename, join } from 'path' | ||
5 | import { Video, VideoDetails } from '@shared/models' | ||
6 | import { | ||
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' | ||
20 | import { HttpStatusCode } from '@shared/core-utils' | ||
21 | |||
22 | async function testThumbnail (server: ServerInfo, videoId: number | string) { | ||
23 | const res = await getVideo(server.url, videoId) | ||
24 | const video: VideoDetails = res.body | ||
25 | |||
26 | const res1 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200) | ||
27 | expect(res1.body).to.not.have.lengthOf(0) | ||
28 | |||
29 | const res2 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200) | ||
30 | expect(res2.body).to.not.have.lengthOf(0) | ||
31 | } | ||
32 | |||
33 | describe('Test regenerate thumbnails script', function () { | ||
34 | let servers: ServerInfo[] | ||
35 | |||
36 | let video1: Video | ||
37 | let video2: Video | ||
38 | let remoteVideo: Video | ||
39 | |||
40 | let thumbnail1Path: string | ||
41 | let thumbnailRemotePath: string | ||
42 | |||
43 | before(async function () { | ||
44 | this.timeout(60000) | ||
45 | |||
46 | servers = await flushAndRunMultipleServers(2) | ||
47 | await setAccessTokensToServers(servers) | ||
48 | |||
49 | await doubleFollow(servers[0], servers[1]) | ||
50 | |||
51 | { | ||
52 | const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid | ||
53 | video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body)) | ||
54 | |||
55 | thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath)) | ||
56 | |||
57 | const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid | ||
58 | video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body)) | ||
59 | } | ||
60 | |||
61 | { | ||
62 | const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid | ||
63 | await waitJobs(servers) | ||
64 | |||
65 | remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body)) | ||
66 | |||
67 | thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath)) | ||
68 | } | ||
69 | |||
70 | await writeFile(thumbnail1Path, '') | ||
71 | await writeFile(thumbnailRemotePath, '') | ||
72 | }) | ||
73 | |||
74 | it('Should have empty thumbnails', async function () { | ||
75 | { | ||
76 | const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200) | ||
77 | expect(res.body).to.have.lengthOf(0) | ||
78 | } | ||
79 | |||
80 | { | ||
81 | const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200) | ||
82 | expect(res.body).to.not.have.lengthOf(0) | ||
83 | } | ||
84 | |||
85 | { | ||
86 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | ||
87 | expect(res.body).to.have.lengthOf(0) | ||
88 | } | ||
89 | }) | ||
90 | |||
91 | it('Should regenerate local thumbnails from the CLI', async function () { | ||
92 | this.timeout(15000) | ||
93 | |||
94 | const env = getEnvCli(servers[0]) | ||
95 | await execCLI(`${env} npm run regenerate-thumbnails`) | ||
96 | }) | ||
97 | |||
98 | it('Should have generated new thumbnail files', async function () { | ||
99 | await testThumbnail(servers[0], video1.uuid) | ||
100 | await testThumbnail(servers[0], video2.uuid) | ||
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 | it('Should have deleted old thumbnail files', async function () { | ||
107 | { | ||
108 | await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | ||
109 | } | ||
110 | |||
111 | { | ||
112 | await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.NOT_FOUND_404) | ||
113 | } | ||
114 | |||
115 | { | ||
116 | const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200) | ||
117 | expect(res.body).to.have.lengthOf(0) | ||
118 | } | ||
119 | }) | ||
120 | |||
121 | after(async function () { | ||
122 | await cleanupTests(servers) | ||
123 | }) | ||
124 | }) | ||
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index f1055ea44..7bad81751 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import * as libxmljs from 'libxmljs' | 5 | import * as xmlParser from 'fast-xml-parser' |
6 | import { | 6 | import { |
7 | addAccountToAccountBlocklist, | 7 | addAccountToAccountBlocklist, |
8 | addAccountToServerBlocklist, | 8 | addAccountToServerBlocklist, |
@@ -139,12 +139,15 @@ describe('Test syndication feeds', () => { | |||
139 | it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () { | 139 | it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () { |
140 | for (const server of servers) { | 140 | for (const server of servers) { |
141 | const rss = await getXMLfeed(server.url, 'videos') | 141 | const rss = await getXMLfeed(server.url, 'videos') |
142 | const xmlDoc = libxmljs.parseXmlString(rss.text) | 142 | expect(xmlParser.validate(rss.text)).to.be.true |
143 | const xmlEnclosure = xmlDoc.get('/rss/channel/item/enclosure') | 143 | |
144 | expect(xmlEnclosure).to.exist | 144 | const xmlDoc = xmlParser.parse(rss.text, { parseAttributeValue: true, ignoreAttributes: false }) |
145 | expect(xmlEnclosure.attr('type').value()).to.be.equal('application/x-bittorrent') | 145 | |
146 | expect(xmlEnclosure.attr('length').value()).to.be.equal('218910') | 146 | const enclosure = xmlDoc.rss.channel.item[0].enclosure |
147 | expect(xmlEnclosure.attr('url').value()).to.contain('720.torrent') | 147 | expect(enclosure).to.exist |
148 | expect(enclosure['@_type']).to.equal('application/x-bittorrent') | ||
149 | expect(enclosure['@_length']).to.equal(218910) | ||
150 | expect(enclosure['@_url']).to.contain('720.torrent') | ||
148 | } | 151 | } |
149 | }) | 152 | }) |
150 | 153 | ||
diff --git a/server/tests/fixtures/banner-resized.jpg b/server/tests/fixtures/banner-resized.jpg new file mode 100644 index 000000000..13ea422cb --- /dev/null +++ b/server/tests/fixtures/banner-resized.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/banner.jpg b/server/tests/fixtures/banner.jpg new file mode 100644 index 000000000..e5f284f59 --- /dev/null +++ b/server/tests/fixtures/banner.jpg | |||
Binary files differ | |||
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 | ||
189 | async function unregister () { | 259 | async function unregister () { |
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg index 19db4f18c..62cd77435 100644 --- a/server/tests/fixtures/thumbnail-playlist.jpg +++ b/server/tests/fixtures/thumbnail-playlist.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg index fcc50b75f..9ee1bc382 100644 --- a/server/tests/fixtures/video_import_thumbnail.jpg +++ b/server/tests/fixtures/video_import_thumbnail.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.mp4.jpg b/server/tests/fixtures/video_short.mp4.jpg index 48790ffec..62cd77435 100644 --- a/server/tests/fixtures/video_short.mp4.jpg +++ b/server/tests/fixtures/video_short.mp4.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.ogv.jpg b/server/tests/fixtures/video_short.ogv.jpg index c4c1d00e5..62cd77435 100644 --- a/server/tests/fixtures/video_short.ogv.jpg +++ b/server/tests/fixtures/video_short.ogv.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short.webm.jpg b/server/tests/fixtures/video_short.webm.jpg index 7f8047516..62cd77435 100644 --- a/server/tests/fixtures/video_short.webm.jpg +++ b/server/tests/fixtures/video_short.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short1.webm.jpg b/server/tests/fixtures/video_short1.webm.jpg index 582eb9ea3..615cb2a5d 100644 --- a/server/tests/fixtures/video_short1.webm.jpg +++ b/server/tests/fixtures/video_short1.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short2.webm.jpg b/server/tests/fixtures/video_short2.webm.jpg index b331aba3b..aa3126381 100644 --- a/server/tests/fixtures/video_short2.webm.jpg +++ b/server/tests/fixtures/video_short2.webm.jpg | |||
Binary files differ | |||
diff --git a/server/tests/fixtures/video_short3.webm.jpg b/server/tests/fixtures/video_short3.webm.jpg index ec8652167..62cd77435 100644 --- a/server/tests/fixtures/video_short3.webm.jpg +++ b/server/tests/fixtures/video_short3.webm.jpg | |||
Binary files differ | |||
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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | ||
5 | import { get4KFileUrl, root, wait } from '../../../shared/extra-utils' | ||
6 | import { join } from 'path' | ||
7 | import { pathExists, remove } from 'fs-extra' | ||
8 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { pathExists, remove } from 'fs-extra' | ||
6 | import { join } from 'path' | ||
7 | import { get4KFileUrl, root, wait } from '../../../shared/extra-utils' | ||
8 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | ||
9 | 9 | ||
10 | describe('Request helpers', function () { | 10 | describe('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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels' | ||
5 | import { ServerConfig } from '@shared/models' | 6 | import { ServerConfig } from '@shared/models' |
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { | 8 | import { |
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' |
31 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' | 38 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' |
32 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' | 39 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' |
33 | import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos' | 40 | import { |
41 | VideoDetails, | ||
42 | VideoImport, | ||
43 | VideoImportState, | ||
44 | VideoPlaylist, | ||
45 | VideoPlaylistPrivacy, | ||
46 | VideoPrivacy | ||
47 | } from '../../../shared/models/videos' | ||
34 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' | 48 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' |
35 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
36 | 49 | ||
37 | const expect = chai.expect | 50 | const 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/account/account.ts b/server/types/models/account/account.ts index d2add9810..9513acad8 100644 --- a/server/types/models/account/account.ts +++ b/server/types/models/account/account.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
1 | import { AccountModel } from '../../../models/account/account' | 2 | import { AccountModel } from '../../../models/account/account' |
3 | import { MChannelDefault } from '../video/video-channels' | ||
4 | import { MAccountBlocklistId } from './account-blocklist' | ||
2 | import { | 5 | import { |
3 | MActor, | 6 | MActor, |
4 | MActorAP, | 7 | MActorAPAccount, |
5 | MActorAPI, | 8 | MActorAPI, |
6 | MActorAudience, | 9 | MActorAudience, |
7 | MActorDefault, | 10 | MActorDefault, |
@@ -13,9 +16,6 @@ import { | |||
13 | MActorSummaryFormattable, | 16 | MActorSummaryFormattable, |
14 | MActorUrl | 17 | MActorUrl |
15 | } from './actor' | 18 | } from './actor' |
16 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
17 | import { MAccountBlocklistId } from './account-blocklist' | ||
18 | import { MChannelDefault } from '../video/video-channels' | ||
19 | 19 | ||
20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> | 20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> |
21 | 21 | ||
@@ -106,4 +106,4 @@ export type MAccountFormattable = | |||
106 | 106 | ||
107 | export type MAccountAP = | 107 | export type MAccountAP = |
108 | Pick<MAccount, 'name' | 'description'> & | 108 | Pick<MAccount, 'name' | 'description'> & |
109 | Use<'Actor', MActorAP> | 109 | Use<'Actor', MActorAPAccount> |
diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/account/actor-follow.ts index 8c213d09c..8e19c6140 100644 --- a/server/types/models/account/actor-follow.ts +++ b/server/types/models/account/actor-follow.ts | |||
@@ -1,16 +1,15 @@ | |||
1 | import { PickWith } from '@shared/core-utils' | ||
1 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
2 | import { | 3 | import { |
3 | MActor, | 4 | MActor, |
4 | MActorChannelAccountActor, | 5 | MActorChannelAccountActor, |
5 | MActorDefault, | 6 | MActorDefault, |
6 | MActorDefaultAccountChannel, | 7 | MActorDefaultAccountChannel, |
8 | MActorDefaultChannelId, | ||
7 | MActorFormattable, | 9 | MActorFormattable, |
8 | MActorHost, | 10 | MActorHost, |
9 | MActorUsername | 11 | MActorUsername |
10 | } from './actor' | 12 | } from './actor' |
11 | import { PickWith } from '@shared/core-utils' | ||
12 | import { ActorModel } from '@server/models/activitypub/actor' | ||
13 | import { MChannelDefault } from '../video/video-channels' | ||
14 | 13 | ||
15 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> | 14 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> |
16 | 15 | ||
@@ -47,14 +46,10 @@ export type MActorFollowFull = | |||
47 | 46 | ||
48 | // For subscriptions | 47 | // For subscriptions |
49 | 48 | ||
50 | type SubscriptionFollowing = | ||
51 | MActorDefault & | ||
52 | PickWith<ActorModel, 'VideoChannel', MChannelDefault> | ||
53 | |||
54 | export type MActorFollowActorsDefaultSubscription = | 49 | export type MActorFollowActorsDefaultSubscription = |
55 | MActorFollow & | 50 | MActorFollow & |
56 | Use<'ActorFollower', MActorDefault> & | 51 | Use<'ActorFollower', MActorDefault> & |
57 | Use<'ActorFollowing', SubscriptionFollowing> | 52 | Use<'ActorFollowing', MActorDefaultChannelId> |
58 | 53 | ||
59 | export type MActorFollowSubscriptions = | 54 | export type MActorFollowSubscriptions = |
60 | MActorFollow & | 55 | MActorFollow & |
diff --git a/server/types/models/account/actor-image.ts b/server/types/models/account/actor-image.ts new file mode 100644 index 000000000..e59f8b141 --- /dev/null +++ b/server/types/models/account/actor-image.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { ActorImageModel } from '../../../models/account/actor-image' | ||
2 | import { FunctionProperties } from '@shared/core-utils' | ||
3 | |||
4 | export type MActorImage = ActorImageModel | ||
5 | |||
6 | // ############################################################################ | ||
7 | |||
8 | // Format for API or AP object | ||
9 | |||
10 | export type MActorImageFormattable = | ||
11 | FunctionProperties<MActorImage> & | ||
12 | Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'> | ||
diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts index ee0d05f4e..8f3f30074 100644 --- a/server/types/models/account/actor.ts +++ b/server/types/models/account/actor.ts | |||
@@ -1,15 +1,17 @@ | |||
1 | import { ActorModel } from '../../../models/activitypub/actor' | 1 | |
2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' | 2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' |
3 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' | 3 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' | 4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' |
5 | import { MAvatar, MAvatarFormattable } from './avatar' | ||
6 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' | 5 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' |
6 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' | ||
7 | import { MActorImage, MActorImageFormattable } from './actor-image' | ||
7 | 8 | ||
8 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> | 9 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> |
10 | type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M> | ||
9 | 11 | ||
10 | // ############################################################################ | 12 | // ############################################################################ |
11 | 13 | ||
12 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'> | 14 | export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'> |
13 | 15 | ||
14 | // ############################################################################ | 16 | // ############################################################################ |
15 | 17 | ||
@@ -34,7 +36,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ | |||
34 | export type MActorDefaultLight = | 36 | export type MActorDefaultLight = |
35 | MActorLight & | 37 | MActorLight & |
36 | Use<'Server', MServerHost> & | 38 | Use<'Server', MServerHost> & |
37 | Use<'Avatar', MAvatar> | 39 | Use<'Avatar', MActorImage> |
38 | 40 | ||
39 | export type MActorAccountId = | 41 | export type MActorAccountId = |
40 | MActor & | 42 | MActor & |
@@ -75,10 +77,25 @@ export type MActorServer = | |||
75 | 77 | ||
76 | // Complex actor associations | 78 | // Complex actor associations |
77 | 79 | ||
80 | export type MActorImages = | ||
81 | MActor & | ||
82 | Use<'Avatar', MActorImage> & | ||
83 | UseOpt<'Banner', MActorImage> | ||
84 | |||
78 | export type MActorDefault = | 85 | export type MActorDefault = |
79 | MActor & | 86 | MActor & |
80 | Use<'Server', MServer> & | 87 | Use<'Server', MServer> & |
81 | Use<'Avatar', MAvatar> | 88 | Use<'Avatar', MActorImage> |
89 | |||
90 | export type MActorDefaultChannelId = | ||
91 | MActorDefault & | ||
92 | Use<'VideoChannel', MChannelId> | ||
93 | |||
94 | export type MActorDefaultBanner = | ||
95 | MActor & | ||
96 | Use<'Server', MServer> & | ||
97 | Use<'Avatar', MActorImage> & | ||
98 | Use<'Banner', MActorImage> | ||
82 | 99 | ||
83 | // Actor with channel that is associated to an account and its actor | 100 | // Actor with channel that is associated to an account and its actor |
84 | // Actor -> VideoChannel -> Account -> Actor | 101 | // Actor -> VideoChannel -> Account -> Actor |
@@ -89,7 +106,8 @@ export type MActorChannelAccountActor = | |||
89 | export type MActorFull = | 106 | export type MActorFull = |
90 | MActor & | 107 | MActor & |
91 | Use<'Server', MServer> & | 108 | Use<'Server', MServer> & |
92 | Use<'Avatar', MAvatar> & | 109 | Use<'Avatar', MActorImage> & |
110 | Use<'Banner', MActorImage> & | ||
93 | Use<'Account', MAccount> & | 111 | Use<'Account', MAccount> & |
94 | Use<'VideoChannel', MChannelAccountActor> | 112 | Use<'VideoChannel', MChannelAccountActor> |
95 | 113 | ||
@@ -97,7 +115,8 @@ export type MActorFull = | |||
97 | export type MActorFullActor = | 115 | export type MActorFullActor = |
98 | MActor & | 116 | MActor & |
99 | Use<'Server', MServer> & | 117 | Use<'Server', MServer> & |
100 | Use<'Avatar', MAvatar> & | 118 | Use<'Avatar', MActorImage> & |
119 | Use<'Banner', MActorImage> & | ||
101 | Use<'Account', MAccountDefault> & | 120 | Use<'Account', MAccountDefault> & |
102 | Use<'VideoChannel', MChannelAccountDefault> | 121 | Use<'VideoChannel', MChannelAccountDefault> |
103 | 122 | ||
@@ -109,7 +128,7 @@ export type MActorSummary = | |||
109 | FunctionProperties<MActor> & | 128 | FunctionProperties<MActor> & |
110 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & | 129 | Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & |
111 | Use<'Server', MServerHost> & | 130 | Use<'Server', MServerHost> & |
112 | Use<'Avatar', MAvatar> | 131 | Use<'Avatar', MActorImage> |
113 | 132 | ||
114 | export type MActorSummaryBlocks = | 133 | export type MActorSummaryBlocks = |
115 | MActorSummary & | 134 | MActorSummary & |
@@ -127,13 +146,21 @@ export type MActorSummaryFormattable = | |||
127 | FunctionProperties<MActor> & | 146 | FunctionProperties<MActor> & |
128 | Pick<MActor, 'url' | 'preferredUsername'> & | 147 | Pick<MActor, 'url' | 'preferredUsername'> & |
129 | Use<'Server', MServerHost> & | 148 | Use<'Server', MServerHost> & |
130 | Use<'Avatar', MAvatarFormattable> | 149 | Use<'Avatar', MActorImageFormattable> |
131 | 150 | ||
132 | export type MActorFormattable = | 151 | export type MActorFormattable = |
133 | MActorSummaryFormattable & | 152 | MActorSummaryFormattable & |
134 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> & | 153 | Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> & |
135 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> | 154 | Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> & |
155 | UseOpt<'Banner', MActorImageFormattable> | ||
136 | 156 | ||
137 | export type MActorAP = | 157 | type MActorAPBase = |
138 | MActor & | 158 | MActor & |
139 | Use<'Avatar', MAvatar> | 159 | Use<'Avatar', MActorImage> |
160 | |||
161 | export type MActorAPAccount = | ||
162 | MActorAPBase | ||
163 | |||
164 | export type MActorAPChannel = | ||
165 | MActorAPBase & | ||
166 | Use<'Banner', MActorImage> | ||
diff --git a/server/types/models/account/avatar.ts b/server/types/models/account/avatar.ts deleted file mode 100644 index 0489a8599..000000000 --- a/server/types/models/account/avatar.ts +++ /dev/null | |||
@@ -1,12 +0,0 @@ | |||
1 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
2 | import { FunctionProperties } from '@shared/core-utils' | ||
3 | |||
4 | export type MAvatar = AvatarModel | ||
5 | |||
6 | // ############################################################################ | ||
7 | |||
8 | // Format for API or AP object | ||
9 | |||
10 | export type MAvatarFormattable = | ||
11 | FunctionProperties<MAvatar> & | ||
12 | Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'> | ||
diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts index 513c09c40..e3fc00f94 100644 --- a/server/types/models/account/index.ts +++ b/server/types/models/account/index.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './account-blocklist' | 2 | export * from './account-blocklist' |
3 | export * from './actor' | ||
4 | export * from './actor-follow' | 3 | export * from './actor-follow' |
5 | export * from './avatar' | 4 | export * from './actor-image' |
5 | export * from './actor' | ||
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 @@ | |||
1 | import { ApplicationModel } from '@server/models/application/application' | ||
2 | |||
3 | // ############################################################################ | ||
4 | |||
5 | export 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 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './application' | ||
2 | export * from './moderation' | 3 | export * from './moderation' |
3 | export * from './oauth' | 4 | export * from './oauth' |
4 | export * from './server' | 5 | export * from './server' |
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 58764a748..7ebb0485d 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -1,12 +1,14 @@ | |||
1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | 1 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' |
2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | 2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' |
3 | import { ApplicationModel } from '@server/models/application/application' | ||
4 | import { PluginModel } from '@server/models/server/plugin' | ||
3 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 5 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { AbuseModel } from '../../../models/abuse/abuse' | 6 | import { AbuseModel } from '../../../models/abuse/abuse' |
5 | import { AccountModel } from '../../../models/account/account' | 7 | import { AccountModel } from '../../../models/account/account' |
8 | import { ActorImageModel } from '../../../models/account/actor-image' | ||
6 | import { UserNotificationModel } from '../../../models/account/user-notification' | 9 | import { UserNotificationModel } from '../../../models/account/user-notification' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
9 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
10 | import { ServerModel } from '../../../models/server/server' | 12 | import { ServerModel } from '../../../models/server/server' |
11 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
12 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 14 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
@@ -27,7 +29,7 @@ export module UserNotificationIncludes { | |||
27 | 29 | ||
28 | export type ActorInclude = | 30 | export type ActorInclude = |
29 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 31 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
30 | PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & | 32 | PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> & |
31 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | 33 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> |
32 | 34 | ||
33 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> | 35 | export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> |
@@ -73,7 +75,7 @@ export module UserNotificationIncludes { | |||
73 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 75 | Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
74 | PickWith<ActorModel, 'Account', AccountInclude> & | 76 | PickWith<ActorModel, 'Account', AccountInclude> & |
75 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & | 77 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & |
76 | PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> | 78 | PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> |
77 | 79 | ||
78 | export type ActorFollowing = | 80 | export type ActorFollowing = |
79 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & | 81 | Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & |
@@ -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 | ||
92 | export type MUserNotification = | 100 | export 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/types/models/user/user.ts b/server/types/models/user/user.ts index 12a68accf..fa7de9c52 100644 --- a/server/types/models/user/user.ts +++ b/server/types/models/user/user.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { UserModel } from '../../../models/account/user' | 1 | import { AccountModel } from '@server/models/account/account' |
2 | import { MVideoPlaylist } from '@server/types/models' | ||
2 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 3 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { UserModel } from '../../../models/account/user' | ||
3 | import { | 5 | import { |
4 | MAccount, | 6 | MAccount, |
5 | MAccountDefault, | 7 | MAccountDefault, |
@@ -9,10 +11,8 @@ import { | |||
9 | MAccountIdActorId, | 11 | MAccountIdActorId, |
10 | MAccountUrl | 12 | MAccountUrl |
11 | } from '../account' | 13 | } from '../account' |
12 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' | ||
13 | import { AccountModel } from '@server/models/account/account' | ||
14 | import { MChannelFormattable } from '../video/video-channels' | 14 | import { MChannelFormattable } from '../video/video-channels' |
15 | import { MVideoPlaylist } from '@server/types/models' | 15 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' |
16 | 16 | ||
17 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> | 17 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> |
18 | 18 | ||
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts index 77790daa4..f577807ca 100644 --- a/server/types/models/video/video-channels.ts +++ b/server/types/models/video/video-channels.ts | |||
@@ -12,15 +12,17 @@ import { | |||
12 | MAccountUserId, | 12 | MAccountUserId, |
13 | MActor, | 13 | MActor, |
14 | MActorAccountChannelId, | 14 | MActorAccountChannelId, |
15 | MActorAP, | 15 | MActorAPChannel, |
16 | MActorAPI, | 16 | MActorAPI, |
17 | MActorDefault, | 17 | MActorDefault, |
18 | MActorDefaultBanner, | ||
18 | MActorDefaultLight, | 19 | MActorDefaultLight, |
19 | MActorFormattable, | 20 | MActorFormattable, |
20 | MActorHost, | 21 | MActorHost, |
21 | MActorLight, | 22 | MActorLight, |
22 | MActorSummary, | 23 | MActorSummary, |
23 | MActorSummaryFormattable, MActorUrl | 24 | MActorSummaryFormattable, |
25 | MActorUrl | ||
24 | } from '../account' | 26 | } from '../account' |
25 | import { MVideo } from './video' | 27 | import { MVideo } from './video' |
26 | 28 | ||
@@ -55,14 +57,14 @@ export type MChannelDefault = | |||
55 | MChannel & | 57 | MChannel & |
56 | Use<'Actor', MActorDefault> | 58 | Use<'Actor', MActorDefault> |
57 | 59 | ||
60 | export type MChannelBannerDefault = | ||
61 | MChannel & | ||
62 | Use<'Actor', MActorDefaultBanner> | ||
63 | |||
58 | // ############################################################################ | 64 | // ############################################################################ |
59 | 65 | ||
60 | // Not all association attributes | 66 | // Not all association attributes |
61 | 67 | ||
62 | export type MChannelLight = | ||
63 | MChannel & | ||
64 | Use<'Actor', MActorDefaultLight> | ||
65 | |||
66 | export type MChannelActorLight = | 68 | export type MChannelActorLight = |
67 | MChannel & | 69 | MChannel & |
68 | Use<'Actor', MActorLight> | 70 | Use<'Actor', MActorLight> |
@@ -84,29 +86,23 @@ export type MChannelAccountActor = | |||
84 | MChannel & | 86 | MChannel & |
85 | Use<'Account', MAccountActor> | 87 | Use<'Account', MAccountActor> |
86 | 88 | ||
87 | export type MChannelAccountDefault = | 89 | export type MChannelBannerAccountDefault = |
88 | MChannel & | 90 | MChannel & |
89 | Use<'Actor', MActorDefault> & | 91 | Use<'Actor', MActorDefaultBanner> & |
90 | Use<'Account', MAccountDefault> | 92 | Use<'Account', MAccountDefault> |
91 | 93 | ||
92 | export type MChannelActorAccountActor = | 94 | export type MChannelAccountDefault = |
93 | MChannel & | 95 | MChannel & |
94 | Use<'Account', MAccountActor> & | 96 | Use<'Actor', MActorDefault> & |
95 | Use<'Actor', MActor> | 97 | Use<'Account', MAccountDefault> |
96 | 98 | ||
97 | // ############################################################################ | 99 | // ############################################################################ |
98 | 100 | ||
99 | // Videos associations | 101 | // Videos associations |
100 | export type MChannelVideos = | 102 | export type MChannelVideos = |
101 | MChannel & | 103 | MChannel & |
102 | Use<'Videos', MVideo[]> | 104 | Use<'Videos', MVideo[]> |
103 | 105 | ||
104 | export type MChannelActorAccountDefaultVideos = | ||
105 | MChannel & | ||
106 | Use<'Actor', MActorDefault> & | ||
107 | Use<'Account', MAccountDefault> & | ||
108 | Use<'Videos', MVideo[]> | ||
109 | |||
110 | // ############################################################################ | 106 | // ############################################################################ |
111 | 107 | ||
112 | // For API | 108 | // For API |
@@ -146,5 +142,5 @@ export type MChannelFormattable = | |||
146 | 142 | ||
147 | export type MChannelAP = | 143 | export type MChannelAP = |
148 | Pick<MChannel, 'name' | 'description' | 'support'> & | 144 | Pick<MChannel, 'name' | 'description' | 'support'> & |
149 | Use<'Actor', MActorAP> & | 145 | Use<'Actor', MActorAPChannel> & |
150 | Use<'Account', MAccountUrl> | 146 | Use<'Account', MAccountUrl> |
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 66acfb3f5..cf3e7ae34 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -3,7 +3,9 @@ import { | |||
3 | MAbuseMessage, | 3 | MAbuseMessage, |
4 | MAbuseReporter, | 4 | MAbuseReporter, |
5 | MAccountBlocklist, | 5 | MAccountBlocklist, |
6 | MActorFollowActorsDefault, | ||
6 | MActorUrl, | 7 | MActorUrl, |
8 | MChannelBannerAccountDefault, | ||
7 | MStreamingPlaylist, | 9 | MStreamingPlaylist, |
8 | MVideoChangeOwnershipFull, | 10 | MVideoChangeOwnershipFull, |
9 | MVideoFile, | 11 | MVideoFile, |
@@ -17,15 +19,12 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server' | |||
17 | import { MVideoImportDefault } from '@server/types/models/video/video-import' | 19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' |
18 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
19 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
20 | import { UserRole } from '@shared/models' | ||
21 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 22 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
22 | import { | 23 | import { |
23 | MAccountDefault, | 24 | MAccountDefault, |
24 | MActorAccountChannelId, | 25 | MActorAccountChannelId, |
25 | MActorFollowActorsDefault, | ||
26 | MActorFollowActorsDefaultSubscription, | 26 | MActorFollowActorsDefaultSubscription, |
27 | MActorFull, | 27 | MActorFull, |
28 | MChannelAccountDefault, | ||
29 | MComment, | 28 | MComment, |
30 | MCommentOwnerVideoReply, | 29 | MCommentOwnerVideoReply, |
31 | MUserDefault, | 30 | MUserDefault, |
@@ -49,22 +48,6 @@ declare module 'express' { | |||
49 | } | 48 | } |
50 | 49 | ||
51 | interface PeerTubeLocals { | 50 | interface 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 |
@@ -88,7 +71,7 @@ interface PeerTubeLocals { | |||
88 | 71 | ||
89 | videoStreamingPlaylist?: MStreamingPlaylist | 72 | videoStreamingPlaylist?: MStreamingPlaylist |
90 | 73 | ||
91 | videoChannel?: MChannelAccountDefault | 74 | videoChannel?: MChannelBannerAccountDefault |
92 | 75 | ||
93 | videoPlaylistFull?: MVideoPlaylistFull | 76 | videoPlaylistFull?: MVideoPlaylistFull |
94 | videoPlaylistSummary?: MVideoPlaylistFullSummary | 77 | videoPlaylistSummary?: MVideoPlaylistFullSummary |