diff options
Diffstat (limited to 'server')
187 files changed, 4405 insertions, 1990 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index fb108ca1c..2ddb73519 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,24 +1,17 @@ | |||
1 | import { Hooks } from '@server/lib/plugins/hooks' | ||
2 | import * as express from 'express' | 1 | import * as express from 'express' |
3 | import { remove, writeJSON } from 'fs-extra' | 2 | import { remove, writeJSON } from 'fs-extra' |
4 | import { snakeCase } from 'lodash' | 3 | import { snakeCase } from 'lodash' |
5 | import validator from 'validator' | 4 | import validator from 'validator' |
6 | import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared' | 5 | import { getServerConfig } from '@server/lib/config' |
6 | import { UserRight } from '../../../shared' | ||
7 | import { About } from '../../../shared/models/server/about.model' | 7 | import { About } from '../../../shared/models/server/about.model' |
8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
9 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' | 9 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' |
10 | import { objectConverter } from '../../helpers/core-utils' | 10 | import { objectConverter } from '../../helpers/core-utils' |
11 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' | 11 | import { CONFIG, reloadConfig } from '../../initializers/config' |
12 | import { getServerCommit } from '../../helpers/utils' | ||
13 | import { getEnabledResolutions } from '../../lib/video-transcoding' | ||
14 | import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config' | ||
15 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants' | ||
16 | import { ClientHtml } from '../../lib/client-html' | 12 | import { ClientHtml } from '../../lib/client-html' |
17 | import { PluginManager } from '../../lib/plugins/plugin-manager' | ||
18 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | ||
19 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | 13 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' |
20 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' | 14 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' |
21 | import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles' | ||
22 | 15 | ||
23 | const configRouter = express.Router() | 16 | const configRouter = express.Router() |
24 | 17 | ||
@@ -46,174 +39,8 @@ configRouter.delete('/custom', | |||
46 | asyncMiddleware(deleteCustomConfig) | 39 | asyncMiddleware(deleteCustomConfig) |
47 | ) | 40 | ) |
48 | 41 | ||
49 | let serverCommit: string | ||
50 | |||
51 | async function getConfig (req: express.Request, res: express.Response) { | 42 | async function getConfig (req: express.Request, res: express.Response) { |
52 | const { allowed } = await Hooks.wrapPromiseFun( | 43 | const json = await getServerConfig(req.ip) |
53 | isSignupAllowed, | ||
54 | { | ||
55 | ip: req.ip | ||
56 | }, | ||
57 | 'filter:api.user.signup.allowed.result' | ||
58 | ) | ||
59 | |||
60 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) | ||
61 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) | ||
62 | |||
63 | if (serverCommit === undefined) serverCommit = await getServerCommit() | ||
64 | |||
65 | const json: ServerConfig = { | ||
66 | instance: { | ||
67 | name: CONFIG.INSTANCE.NAME, | ||
68 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | ||
69 | isNSFW: CONFIG.INSTANCE.IS_NSFW, | ||
70 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
71 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | ||
72 | customizations: { | ||
73 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, | ||
74 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | ||
75 | } | ||
76 | }, | ||
77 | search: { | ||
78 | remoteUri: { | ||
79 | users: CONFIG.SEARCH.REMOTE_URI.USERS, | ||
80 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | ||
81 | }, | ||
82 | searchIndex: { | ||
83 | enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED, | ||
84 | url: CONFIG.SEARCH.SEARCH_INDEX.URL, | ||
85 | disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, | ||
86 | isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH | ||
87 | } | ||
88 | }, | ||
89 | plugin: { | ||
90 | registered: getRegisteredPlugins(), | ||
91 | registeredExternalAuths: getExternalAuthsPlugins(), | ||
92 | registeredIdAndPassAuths: getIdAndPassAuthPlugins() | ||
93 | }, | ||
94 | theme: { | ||
95 | registered: getRegisteredThemes(), | ||
96 | default: defaultTheme | ||
97 | }, | ||
98 | email: { | ||
99 | enabled: isEmailEnabled() | ||
100 | }, | ||
101 | contactForm: { | ||
102 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
103 | }, | ||
104 | serverVersion: PEERTUBE_VERSION, | ||
105 | serverCommit, | ||
106 | signup: { | ||
107 | allowed, | ||
108 | allowedForCurrentIP, | ||
109 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
110 | }, | ||
111 | transcoding: { | ||
112 | hls: { | ||
113 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
114 | }, | ||
115 | webtorrent: { | ||
116 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
117 | }, | ||
118 | enabledResolutions: getEnabledResolutions('vod'), | ||
119 | profile: CONFIG.TRANSCODING.PROFILE, | ||
120 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod') | ||
121 | }, | ||
122 | live: { | ||
123 | enabled: CONFIG.LIVE.ENABLED, | ||
124 | |||
125 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | ||
126 | maxDuration: CONFIG.LIVE.MAX_DURATION, | ||
127 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | ||
128 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | ||
129 | |||
130 | transcoding: { | ||
131 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | ||
132 | enabledResolutions: getEnabledResolutions('live'), | ||
133 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
134 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live') | ||
135 | }, | ||
136 | |||
137 | rtmp: { | ||
138 | port: CONFIG.LIVE.RTMP.PORT | ||
139 | } | ||
140 | }, | ||
141 | import: { | ||
142 | videos: { | ||
143 | http: { | ||
144 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
145 | }, | ||
146 | torrent: { | ||
147 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED | ||
148 | } | ||
149 | } | ||
150 | }, | ||
151 | autoBlacklist: { | ||
152 | videos: { | ||
153 | ofUsers: { | ||
154 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED | ||
155 | } | ||
156 | } | ||
157 | }, | ||
158 | avatar: { | ||
159 | file: { | ||
160 | size: { | ||
161 | max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max | ||
162 | }, | ||
163 | extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | ||
164 | } | ||
165 | }, | ||
166 | video: { | ||
167 | image: { | ||
168 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, | ||
169 | size: { | ||
170 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max | ||
171 | } | ||
172 | }, | ||
173 | file: { | ||
174 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME | ||
175 | } | ||
176 | }, | ||
177 | videoCaption: { | ||
178 | file: { | ||
179 | size: { | ||
180 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | ||
181 | }, | ||
182 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
183 | } | ||
184 | }, | ||
185 | user: { | ||
186 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
187 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
188 | }, | ||
189 | trending: { | ||
190 | videos: { | ||
191 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS, | ||
192 | algorithms: { | ||
193 | enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED, | ||
194 | default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT | ||
195 | } | ||
196 | } | ||
197 | }, | ||
198 | tracker: { | ||
199 | enabled: CONFIG.TRACKER.ENABLED | ||
200 | }, | ||
201 | |||
202 | followings: { | ||
203 | instance: { | ||
204 | autoFollowIndex: { | ||
205 | indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL | ||
206 | } | ||
207 | } | ||
208 | }, | ||
209 | |||
210 | broadcastMessage: { | ||
211 | enabled: CONFIG.BROADCAST_MESSAGE.ENABLED, | ||
212 | message: CONFIG.BROADCAST_MESSAGE.MESSAGE, | ||
213 | level: CONFIG.BROADCAST_MESSAGE.LEVEL, | ||
214 | dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE | ||
215 | } | ||
216 | } | ||
217 | 44 | ||
218 | return res.json(json) | 45 | return res.json(json) |
219 | } | 46 | } |
@@ -284,69 +111,10 @@ async function updateCustomConfig (req: express.Request, res: express.Response) | |||
284 | return res.json(data) | 111 | return res.json(data) |
285 | } | 112 | } |
286 | 113 | ||
287 | function getRegisteredThemes () { | ||
288 | return PluginManager.Instance.getRegisteredThemes() | ||
289 | .map(t => ({ | ||
290 | name: t.name, | ||
291 | version: t.version, | ||
292 | description: t.description, | ||
293 | css: t.css, | ||
294 | clientScripts: t.clientScripts | ||
295 | })) | ||
296 | } | ||
297 | |||
298 | function getRegisteredPlugins () { | ||
299 | return PluginManager.Instance.getRegisteredPlugins() | ||
300 | .map(p => ({ | ||
301 | name: p.name, | ||
302 | version: p.version, | ||
303 | description: p.description, | ||
304 | clientScripts: p.clientScripts | ||
305 | })) | ||
306 | } | ||
307 | |||
308 | function getIdAndPassAuthPlugins () { | ||
309 | const result: RegisteredIdAndPassAuthConfig[] = [] | ||
310 | |||
311 | for (const p of PluginManager.Instance.getIdAndPassAuths()) { | ||
312 | for (const auth of p.idAndPassAuths) { | ||
313 | result.push({ | ||
314 | npmName: p.npmName, | ||
315 | name: p.name, | ||
316 | version: p.version, | ||
317 | authName: auth.authName, | ||
318 | weight: auth.getWeight() | ||
319 | }) | ||
320 | } | ||
321 | } | ||
322 | |||
323 | return result | ||
324 | } | ||
325 | |||
326 | function getExternalAuthsPlugins () { | ||
327 | const result: RegisteredExternalAuthConfig[] = [] | ||
328 | |||
329 | for (const p of PluginManager.Instance.getExternalAuths()) { | ||
330 | for (const auth of p.externalAuths) { | ||
331 | result.push({ | ||
332 | npmName: p.npmName, | ||
333 | name: p.name, | ||
334 | version: p.version, | ||
335 | authName: auth.authName, | ||
336 | authDisplayName: auth.authDisplayName() | ||
337 | }) | ||
338 | } | ||
339 | } | ||
340 | |||
341 | return result | ||
342 | } | ||
343 | |||
344 | // --------------------------------------------------------------------------- | 114 | // --------------------------------------------------------------------------- |
345 | 115 | ||
346 | export { | 116 | export { |
347 | configRouter, | 117 | configRouter |
348 | getRegisteredPlugins, | ||
349 | getRegisteredThemes | ||
350 | } | 118 | } |
351 | 119 | ||
352 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
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..a186de010 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -151,7 +151,7 @@ async function updatePlugin (req: express.Request, res: express.Response) { | |||
151 | const fromDisk = !!body.path | 151 | const fromDisk = !!body.path |
152 | const toUpdate = body.npmName || body.path | 152 | const toUpdate = body.npmName || body.path |
153 | try { | 153 | try { |
154 | const plugin = await PluginManager.Instance.update(toUpdate, undefined, fromDisk) | 154 | const plugin = await PluginManager.Instance.update(toUpdate, fromDisk) |
155 | 155 | ||
156 | return res.json(plugin.toFormattedJSON()) | 156 | return res.json(plugin.toFormattedJSON()) |
157 | } catch (err) { | 157 | } catch (err) { |
@@ -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..8d9003a3e 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,8 +1,8 @@ | |||
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 { join } from 'path' | 3 | import { join } from 'path' |
4 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config' | ||
5 | import { serveIndexHTML } from '@server/lib/client-html' | 4 | import { serveIndexHTML } from '@server/lib/client-html' |
5 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/lib/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 { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' | 7 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' |
8 | import { root } from '../helpers/core-utils' | 8 | import { root } from '../helpers/core-utils' |
@@ -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..75297df8f 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -3,12 +3,13 @@ import * as ffmpeg from 'fluent-ffmpeg' | |||
3 | import { readFile, remove, writeFile } from 'fs-extra' | 3 | import { readFile, remove, writeFile } from 'fs-extra' |
4 | import { dirname, join } from 'path' | 4 | 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, EncoderOptions, 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' |
12 | import { FilterSpecification } from 'fluent-ffmpeg' | ||
12 | 13 | ||
13 | /** | 14 | /** |
14 | * | 15 | * |
@@ -226,21 +227,14 @@ async function getLiveTranscodingCommand (options: { | |||
226 | 227 | ||
227 | const varStreamMap: string[] = [] | 228 | const varStreamMap: string[] = [] |
228 | 229 | ||
229 | command.complexFilter([ | 230 | const complexFilter: FilterSpecification[] = [ |
230 | { | 231 | { |
231 | inputs: '[v:0]', | 232 | inputs: '[v:0]', |
232 | filter: 'split', | 233 | filter: 'split', |
233 | options: resolutions.length, | 234 | options: resolutions.length, |
234 | outputs: resolutions.map(r => `vtemp${r}`) | 235 | outputs: resolutions.map(r => `vtemp${r}`) |
235 | }, | 236 | } |
236 | 237 | ] | |
237 | ...resolutions.map(r => ({ | ||
238 | inputs: `vtemp${r}`, | ||
239 | filter: 'scale', | ||
240 | options: `w=-2:h=${r}`, | ||
241 | outputs: `vout${r}` | ||
242 | })) | ||
243 | ]) | ||
244 | 238 | ||
245 | command.outputOption('-preset superfast') | 239 | command.outputOption('-preset superfast') |
246 | command.outputOption('-sc_threshold 0') | 240 | command.outputOption('-sc_threshold 0') |
@@ -277,7 +271,14 @@ async function getLiveTranscodingCommand (options: { | |||
277 | logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult) | 271 | logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult) |
278 | 272 | ||
279 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) | 273 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) |
280 | command.addOutputOptions(builderResult.result.outputOptions) | 274 | applyEncoderOptions(command, builderResult.result) |
275 | |||
276 | complexFilter.push({ | ||
277 | inputs: `vtemp${resolution}`, | ||
278 | filter: getScaleFilter(builderResult.result), | ||
279 | options: `w=-2:h=${resolution}`, | ||
280 | outputs: `vout${resolution}` | ||
281 | }) | ||
281 | } | 282 | } |
282 | 283 | ||
283 | { | 284 | { |
@@ -294,12 +295,14 @@ async function getLiveTranscodingCommand (options: { | |||
294 | logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult) | 295 | logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult) |
295 | 296 | ||
296 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) | 297 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) |
297 | command.addOutputOptions(builderResult.result.outputOptions) | 298 | applyEncoderOptions(command, builderResult.result) |
298 | } | 299 | } |
299 | 300 | ||
300 | varStreamMap.push(`v:${i},a:${i}`) | 301 | varStreamMap.push(`v:${i},a:${i}`) |
301 | } | 302 | } |
302 | 303 | ||
304 | command.complexFilter(complexFilter) | ||
305 | |||
303 | addDefaultLiveHLSParams(command, outPath) | 306 | addDefaultLiveHLSParams(command, outPath) |
304 | 307 | ||
305 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | 308 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) |
@@ -389,29 +392,29 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran | |||
389 | let fps = await getVideoFileFPS(options.inputPath) | 392 | let fps = await getVideoFileFPS(options.inputPath) |
390 | fps = computeFPS(fps, options.resolution) | 393 | fps = computeFPS(fps, options.resolution) |
391 | 394 | ||
392 | command = await presetVideo(command, options.inputPath, options, fps) | 395 | let scaleFilterValue: string |
393 | 396 | ||
394 | if (options.resolution !== undefined) { | 397 | if (options.resolution !== undefined) { |
395 | // '?x720' or '720x?' for example | 398 | scaleFilterValue = options.isPortraitMode === true |
396 | const size = options.isPortraitMode === true | 399 | ? `w=${options.resolution}:h=-2` |
397 | ? `${options.resolution}x?` | 400 | : `w=-2:h=${options.resolution}` |
398 | : `?x${options.resolution}` | ||
399 | |||
400 | command = command.size(size) | ||
401 | } | 401 | } |
402 | 402 | ||
403 | command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue }) | ||
404 | |||
403 | return command | 405 | return command |
404 | } | 406 | } |
405 | 407 | ||
406 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { | 408 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { |
407 | command = command.loop(undefined) | 409 | command = command.loop(undefined) |
408 | 410 | ||
409 | command = await presetVideo(command, options.audioPath, options) | 411 | // Avoid "height not divisible by 2" error |
412 | const scaleFilterValue = 'trunc(iw/2)*2:trunc(ih/2)*2' | ||
413 | command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue }) | ||
410 | 414 | ||
411 | command.outputOption('-preset:v veryfast') | 415 | command.outputOption('-preset:v veryfast') |
412 | 416 | ||
413 | command = command.input(options.audioPath) | 417 | command = command.input(options.audioPath) |
414 | .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error | ||
415 | .outputOption('-tune stillimage') | 418 | .outputOption('-tune stillimage') |
416 | .outputOption('-shortest') | 419 | .outputOption('-shortest') |
417 | 420 | ||
@@ -555,12 +558,15 @@ async function getEncoderBuilderResult (options: { | |||
555 | return null | 558 | return null |
556 | } | 559 | } |
557 | 560 | ||
558 | async function presetVideo ( | 561 | async function presetVideo (options: { |
559 | command: ffmpeg.FfmpegCommand, | 562 | command: ffmpeg.FfmpegCommand |
560 | input: string, | 563 | input: string |
561 | transcodeOptions: TranscodeOptions, | 564 | transcodeOptions: TranscodeOptions |
562 | fps?: number | 565 | fps?: number |
563 | ) { | 566 | scaleFilterValue?: string |
567 | }) { | ||
568 | const { command, input, transcodeOptions, fps, scaleFilterValue } = options | ||
569 | |||
564 | let localCommand = command | 570 | let localCommand = command |
565 | .format('mp4') | 571 | .format('mp4') |
566 | .outputOption('-movflags faststart') | 572 | .outputOption('-movflags faststart') |
@@ -601,11 +607,15 @@ async function presetVideo ( | |||
601 | 607 | ||
602 | if (streamType === 'video') { | 608 | if (streamType === 'video') { |
603 | localCommand.videoCodec(builderResult.encoder) | 609 | localCommand.videoCodec(builderResult.encoder) |
610 | |||
611 | if (scaleFilterValue) { | ||
612 | localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`) | ||
613 | } | ||
604 | } else if (streamType === 'audio') { | 614 | } else if (streamType === 'audio') { |
605 | localCommand.audioCodec(builderResult.encoder) | 615 | localCommand.audioCodec(builderResult.encoder) |
606 | } | 616 | } |
607 | 617 | ||
608 | command.addOutputOptions(builderResult.result.outputOptions) | 618 | applyEncoderOptions(localCommand, builderResult.result) |
609 | addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) | 619 | addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) |
610 | } | 620 | } |
611 | 621 | ||
@@ -626,6 +636,18 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { | |||
626 | .noVideo() | 636 | .noVideo() |
627 | } | 637 | } |
628 | 638 | ||
639 | function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand { | ||
640 | return command | ||
641 | .inputOptions(options.inputOptions ?? []) | ||
642 | .outputOptions(options.outputOptions ?? []) | ||
643 | } | ||
644 | |||
645 | function getScaleFilter (options: EncoderOptions): string { | ||
646 | if (options.scaleFilter) return options.scaleFilter.name | ||
647 | |||
648 | return 'scale' | ||
649 | } | ||
650 | |||
629 | // --------------------------------------------------------------------------- | 651 | // --------------------------------------------------------------------------- |
630 | // Utils | 652 | // Utils |
631 | // --------------------------------------------------------------------------- | 653 | // --------------------------------------------------------------------------- |
@@ -649,6 +671,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') { | |||
649 | return command | 671 | return command |
650 | } | 672 | } |
651 | 673 | ||
674 | function getFFmpegVersion () { | ||
675 | return new Promise<string>((res, rej) => { | ||
676 | (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => { | ||
677 | if (err) return rej(err) | ||
678 | if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path')) | ||
679 | |||
680 | return execPromise(`${ffmpegPath} -version`) | ||
681 | .then(stdout => { | ||
682 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+\.\d+)/) | ||
683 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | ||
684 | |||
685 | return res(parsed[1]) | ||
686 | }) | ||
687 | .catch(err => rej(err)) | ||
688 | }) | ||
689 | }) | ||
690 | } | ||
691 | |||
652 | async function runCommand (options: { | 692 | async function runCommand (options: { |
653 | command: ffmpeg.FfmpegCommand | 693 | command: ffmpeg.FfmpegCommand |
654 | silent?: boolean // false | 694 | silent?: boolean // false |
@@ -695,6 +735,7 @@ export { | |||
695 | TranscodeOptionsType, | 735 | TranscodeOptionsType, |
696 | transcode, | 736 | transcode, |
697 | runCommand, | 737 | runCommand, |
738 | getFFmpegVersion, | ||
698 | 739 | ||
699 | resetSupportedEncoders, | 740 | resetSupportedEncoders, |
700 | 741 | ||
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/markdown.ts b/server/helpers/markdown.ts new file mode 100644 index 000000000..2126bb752 --- /dev/null +++ b/server/helpers/markdown.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | ||
2 | |||
3 | const sanitizeHtml = require('sanitize-html') | ||
4 | const markdownItEmoji = require('markdown-it-emoji/light') | ||
5 | const MarkdownItClass = require('markdown-it') | ||
6 | const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true }) | ||
7 | |||
8 | markdownIt.enable(TEXT_WITH_HTML_RULES) | ||
9 | markdownIt.use(markdownItEmoji) | ||
10 | |||
11 | const toSafeHtml = text => { | ||
12 | if (!text) return '' | ||
13 | |||
14 | // Restore line feed | ||
15 | const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n') | ||
16 | |||
17 | // Convert possible markdown (emojis, emphasis and lists) to html | ||
18 | const html = markdownIt.render(textWithLineFeed) | ||
19 | |||
20 | // Convert to safe Html | ||
21 | return sanitizeHtml(html, SANITIZE_OPTIONS) | ||
22 | } | ||
23 | |||
24 | const mdToPlainText = text => { | ||
25 | if (!text) return '' | ||
26 | |||
27 | // Convert possible markdown (emojis, emphasis and lists) to html | ||
28 | const html = markdownIt.render(text) | ||
29 | |||
30 | // Convert to safe Html | ||
31 | const safeHtml = sanitizeHtml(html, SANITIZE_OPTIONS) | ||
32 | |||
33 | return safeHtml.replace(/<[^>]+>/g, '') | ||
34 | .replace(/\n$/, '') | ||
35 | .replace('\n', ', ') | ||
36 | } | ||
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | export { | ||
41 | toSafeHtml, | ||
42 | mdToPlainText | ||
43 | } | ||
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/signup.ts b/server/helpers/signup.ts index d34ff2db5..ed872539b 100644 --- a/server/helpers/signup.ts +++ b/server/helpers/signup.ts | |||
@@ -20,6 +20,8 @@ async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: st | |||
20 | } | 20 | } |
21 | 21 | ||
22 | function isSignupAllowedForCurrentIP (ip: string) { | 22 | function isSignupAllowedForCurrentIP (ip: string) { |
23 | if (!ip) return false | ||
24 | |||
23 | const addr = ipaddr.parse(ip) | 25 | const addr = ipaddr.parse(ip) |
24 | const excludeList = [ 'blacklist' ] | 26 | const excludeList = [ 'blacklist' ] |
25 | let matched = '' | 27 | let matched = '' |
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..2864b0287 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! |
@@ -17,6 +17,7 @@ function checkMissedConfig () { | |||
17 | 'log.level', | 17 | 'log.level', |
18 | 'user.video_quota', 'user.video_quota_daily', | 18 | 'user.video_quota', 'user.video_quota_daily', |
19 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', | 19 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', |
20 | 'security.frameguard.enabled', | ||
20 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', | 21 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', |
21 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', | 22 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', |
22 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 23 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
@@ -37,6 +38,7 @@ function checkMissedConfig () { | |||
37 | 'theme.default', | 38 | 'theme.default', |
38 | 'remote_redundancy.videos.accept_from', | 39 | 'remote_redundancy.videos.accept_from', |
39 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', | 40 | 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', |
41 | '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', | 42 | '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', | 43 | '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', | 44 | 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', |
@@ -102,8 +104,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { | |||
102 | 104 | ||
103 | function checkNodeVersion () { | 105 | function checkNodeVersion () { |
104 | const v = process.version | 106 | const v = process.version |
105 | const majorString = v.split('.')[0].replace('v', '') | 107 | const { major } = parseSemVersion(v) |
106 | const major = parseInt(majorString, 10) | ||
107 | 108 | ||
108 | logger.debug('Checking NodeJS version %s.', v) | 109 | logger.debug('Checking NodeJS version %s.', v) |
109 | 110 | ||
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c16b63c33..5281d3a66 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import * as bytes from 'bytes' | ||
1 | import { IConfig } from 'config' | 2 | import { IConfig } from 'config' |
3 | import decache from 'decache' | ||
2 | import { dirname, join } from 'path' | 4 | import { dirname, join } from 'path' |
5 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | ||
6 | import { BroadcastMessageLevel } from '@shared/models/server' | ||
3 | import { VideosRedundancyStrategy } from '../../shared/models' | 7 | import { VideosRedundancyStrategy } from '../../shared/models' |
8 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | ||
4 | // Do not use barrels, remain constants as independent as possible | 9 | // Do not use barrels, remain constants as independent as possible |
5 | import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' | 10 | import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' |
6 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | ||
7 | import * as bytes from 'bytes' | ||
8 | import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' | ||
9 | import { BroadcastMessageLevel } from '@shared/models/server' | ||
10 | 11 | ||
11 | // Use a variable to reload the configuration if we need | 12 | // Use a variable to reload the configuration if we need |
12 | let config: IConfig = require('config') | 13 | let config: IConfig = require('config') |
@@ -59,7 +60,7 @@ const CONFIG = { | |||
59 | }, | 60 | }, |
60 | STORAGE: { | 61 | STORAGE: { |
61 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | 62 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), |
62 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 63 | ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')), |
63 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 64 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
64 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 65 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
65 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), | 66 | STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), |
@@ -133,6 +134,11 @@ const CONFIG = { | |||
133 | REPORT_ONLY: config.get<boolean>('csp.report_only'), | 134 | REPORT_ONLY: config.get<boolean>('csp.report_only'), |
134 | REPORT_URI: config.get<string>('csp.report_uri') | 135 | REPORT_URI: config.get<string>('csp.report_uri') |
135 | }, | 136 | }, |
137 | SECURITY: { | ||
138 | FRAMEGUARD: { | ||
139 | ENABLED: config.get<boolean>('security.frameguard.enabled') | ||
140 | } | ||
141 | }, | ||
136 | TRACKER: { | 142 | TRACKER: { |
137 | ENABLED: config.get<boolean>('tracker.enabled'), | 143 | ENABLED: config.get<boolean>('tracker.enabled'), |
138 | PRIVATE: config.get<boolean>('tracker.private'), | 144 | PRIVATE: config.get<boolean>('tracker.private'), |
@@ -163,6 +169,12 @@ const CONFIG = { | |||
163 | CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') | 169 | CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') |
164 | } | 170 | } |
165 | }, | 171 | }, |
172 | PEERTUBE: { | ||
173 | CHECK_LATEST_VERSION: { | ||
174 | ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'), | ||
175 | URL: config.get<string>('peertube.check_latest_version.url') | ||
176 | } | ||
177 | }, | ||
166 | ADMIN: { | 178 | ADMIN: { |
167 | get EMAIL () { return config.get<string>('admin.email') } | 179 | get EMAIL () { return config.get<string>('admin.email') } |
168 | }, | 180 | }, |
@@ -404,7 +416,7 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] { | |||
404 | 416 | ||
405 | export function reloadConfig () { | 417 | export function reloadConfig () { |
406 | 418 | ||
407 | function directory () { | 419 | function getConfigDirectory () { |
408 | if (process.env.NODE_CONFIG_DIR) { | 420 | if (process.env.NODE_CONFIG_DIR) { |
409 | return process.env.NODE_CONFIG_DIR | 421 | return process.env.NODE_CONFIG_DIR |
410 | } | 422 | } |
@@ -413,15 +425,17 @@ export function reloadConfig () { | |||
413 | } | 425 | } |
414 | 426 | ||
415 | function purge () { | 427 | function purge () { |
428 | const directory = getConfigDirectory() | ||
429 | |||
416 | for (const fileName in require.cache) { | 430 | for (const fileName in require.cache) { |
417 | if (fileName.includes(directory()) === false) { | 431 | if (fileName.includes(directory) === false) { |
418 | continue | 432 | continue |
419 | } | 433 | } |
420 | 434 | ||
421 | delete require.cache[fileName] | 435 | delete require.cache[fileName] |
422 | } | 436 | } |
423 | 437 | ||
424 | delete require.cache[require.resolve('config')] | 438 | decache('config') |
425 | } | 439 | } |
426 | 440 | ||
427 | purge() | 441 | purge() |
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 243795e60..edf12bc41 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' |
@@ -85,7 +85,7 @@ const sequelizeTypescript = new SequelizeTypescript({ | |||
85 | newMessage += ' in ' + benchmark + 'ms' | 85 | newMessage += ' in ' + benchmark + 'ms' |
86 | } | 86 | } |
87 | 87 | ||
88 | logger.debug(newMessage, { sql: message }) | 88 | logger.debug(newMessage, { sql: message, tags: [ 'sql' ] }) |
89 | } | 89 | } |
90 | }) | 90 | }) |
91 | 91 | ||
@@ -104,7 +104,7 @@ async function initDatabaseModels (silent: boolean) { | |||
104 | ApplicationModel, | 104 | ApplicationModel, |
105 | ActorModel, | 105 | ActorModel, |
106 | ActorFollowModel, | 106 | ActorFollowModel, |
107 | AvatarModel, | 107 | ActorImageModel, |
108 | AccountModel, | 108 | AccountModel, |
109 | OAuthClientModel, | 109 | OAuthClientModel, |
110 | OAuthTokenModel, | 110 | 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..88a968318 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -7,7 +7,15 @@ 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 { |
11 | MAccountActor, | ||
12 | MActor, | ||
13 | MActorFull, | ||
14 | MActorSignature, | ||
15 | MChannelAccountActor, | ||
16 | MChannelActor, | ||
17 | MCommentOwnerVideo | ||
18 | } from '../../../types/models' | ||
11 | import { markCommentAsDeleted } from '../../video-comment' | 19 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 20 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | 21 | ||
@@ -30,9 +38,8 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete | |||
30 | } else if (byActorFull.type === 'Group') { | 38 | } 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.') | 39 | if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') |
32 | 40 | ||
33 | const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor | 41 | const channelToDelete = byActorFull.VideoChannel as MChannelAccountActor & { Actor: MActorFull } |
34 | channelToDelete.Actor = byActorFull | 42 | channelToDelete.Actor = byActorFull |
35 | |||
36 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) | 43 | return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) |
37 | } | 44 | } |
38 | } | 45 | } |
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..506204674 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) |
@@ -558,13 +539,21 @@ async function refreshVideoIfNeeded (options: { | |||
558 | account: channelActor.VideoChannel.Account, | 539 | account: channelActor.VideoChannel.Account, |
559 | channel: channelActor.VideoChannel | 540 | channel: channelActor.VideoChannel |
560 | } | 541 | } |
561 | await retryTransactionWrapper(updateVideoFromAP, updateOptions) | 542 | await updateVideoFromAP(updateOptions) |
562 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | 543 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) |
563 | 544 | ||
564 | ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId) | 545 | ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId) |
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..203bd3893 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, |
@@ -23,6 +24,7 @@ import { VideoChannelModel } from '../models/video/video-channel' | |||
23 | import { getActivityStreamDuration } from '../models/video/video-format-utils' | 24 | import { getActivityStreamDuration } from '../models/video/video-format-utils' |
24 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 25 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
25 | import { MAccountActor, MChannelActor } from '../types/models' | 26 | import { MAccountActor, MChannelActor } from '../types/models' |
27 | import { mdToPlainText } from '../helpers/markdown' | ||
26 | 28 | ||
27 | type Tags = { | 29 | type Tags = { |
28 | ogType: string | 30 | ogType: string |
@@ -93,13 +95,13 @@ class ClientHtml { | |||
93 | } | 95 | } |
94 | 96 | ||
95 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) | 97 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) |
96 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description)) | 98 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(video.description)) |
97 | 99 | ||
98 | const url = WEBSERVER.URL + video.getWatchStaticPath() | 100 | const url = WEBSERVER.URL + video.getWatchStaticPath() |
99 | const originUrl = video.url | 101 | const originUrl = video.url |
100 | const title = escapeHTML(video.name) | 102 | const title = escapeHTML(video.name) |
101 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 103 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) |
102 | const description = escapeHTML(video.description) | 104 | const description = mdToPlainText(video.description) |
103 | 105 | ||
104 | const image = { | 106 | const image = { |
105 | url: WEBSERVER.URL + video.getPreviewStaticPath() | 107 | url: WEBSERVER.URL + video.getPreviewStaticPath() |
@@ -151,13 +153,13 @@ class ClientHtml { | |||
151 | } | 153 | } |
152 | 154 | ||
153 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name)) | 155 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name)) |
154 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(videoPlaylist.description)) | 156 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(videoPlaylist.description)) |
155 | 157 | ||
156 | const url = videoPlaylist.getWatchUrl() | 158 | const url = videoPlaylist.getWatchUrl() |
157 | const originUrl = videoPlaylist.url | 159 | const originUrl = videoPlaylist.url |
158 | const title = escapeHTML(videoPlaylist.name) | 160 | const title = escapeHTML(videoPlaylist.name) |
159 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 161 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) |
160 | const description = escapeHTML(videoPlaylist.description) | 162 | const description = mdToPlainText(videoPlaylist.description) |
161 | 163 | ||
162 | const image = { | 164 | const image = { |
163 | url: videoPlaylist.getThumbnailUrl() | 165 | url: videoPlaylist.getThumbnailUrl() |
@@ -235,18 +237,18 @@ class ClientHtml { | |||
235 | } | 237 | } |
236 | 238 | ||
237 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName())) | 239 | let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName())) |
238 | customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(entity.description)) | 240 | customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(entity.description)) |
239 | 241 | ||
240 | const url = entity.getLocalUrl() | 242 | const url = entity.getLocalUrl() |
241 | const originUrl = entity.Actor.url | 243 | const originUrl = entity.Actor.url |
242 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) | 244 | const siteName = escapeHTML(CONFIG.INSTANCE.NAME) |
243 | const title = escapeHTML(entity.getDisplayName()) | 245 | const title = escapeHTML(entity.getDisplayName()) |
244 | const description = escapeHTML(entity.description) | 246 | const description = mdToPlainText(entity.description) |
245 | 247 | ||
246 | const image = { | 248 | const image = { |
247 | url: entity.Actor.getAvatarUrl(), | 249 | url: entity.Actor.getAvatarUrl(), |
248 | width: AVATARS_SIZE.width, | 250 | width: ACTOR_IMAGES_SIZE.AVATARS.width, |
249 | height: AVATARS_SIZE.height | 251 | height: ACTOR_IMAGES_SIZE.AVATARS.height |
250 | } | 252 | } |
251 | 253 | ||
252 | const ogType = 'website' | 254 | const ogType = 'website' |
@@ -377,7 +379,7 @@ class ClientHtml { | |||
377 | } | 379 | } |
378 | 380 | ||
379 | metaTags['og:url'] = tags.url | 381 | metaTags['og:url'] = tags.url |
380 | metaTags['og:description'] = tags.description | 382 | metaTags['og:description'] = mdToPlainText(tags.description) |
381 | 383 | ||
382 | if (tags.embed) { | 384 | if (tags.embed) { |
383 | metaTags['og:video:url'] = tags.embed.url | 385 | metaTags['og:video:url'] = tags.embed.url |
@@ -393,7 +395,7 @@ class ClientHtml { | |||
393 | private static generateStandardMetaTags (tags: Tags) { | 395 | private static generateStandardMetaTags (tags: Tags) { |
394 | return { | 396 | return { |
395 | name: tags.title, | 397 | name: tags.title, |
396 | description: tags.description, | 398 | description: mdToPlainText(tags.description), |
397 | image: tags.image.url | 399 | image: tags.image.url |
398 | } | 400 | } |
399 | } | 401 | } |
diff --git a/server/lib/config.ts b/server/lib/config.ts new file mode 100644 index 000000000..b4c4c9299 --- /dev/null +++ b/server/lib/config.ts | |||
@@ -0,0 +1,255 @@ | |||
1 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/signup' | ||
2 | import { getServerCommit } from '@server/helpers/utils' | ||
3 | import { CONFIG, isEmailEnabled } from '@server/initializers/config' | ||
4 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants' | ||
5 | import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models' | ||
6 | import { Hooks } from './plugins/hooks' | ||
7 | import { PluginManager } from './plugins/plugin-manager' | ||
8 | import { getThemeOrDefault } from './plugins/theme-utils' | ||
9 | import { getEnabledResolutions } from './video-transcoding' | ||
10 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | ||
11 | |||
12 | let serverCommit: string | ||
13 | |||
14 | async function getServerConfig (ip?: string): Promise<ServerConfig> { | ||
15 | if (serverCommit === undefined) serverCommit = await getServerCommit() | ||
16 | |||
17 | const { allowed } = await Hooks.wrapPromiseFun( | ||
18 | isSignupAllowed, | ||
19 | { | ||
20 | ip | ||
21 | }, | ||
22 | 'filter:api.user.signup.allowed.result' | ||
23 | ) | ||
24 | |||
25 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) | ||
26 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) | ||
27 | |||
28 | return { | ||
29 | instance: { | ||
30 | name: CONFIG.INSTANCE.NAME, | ||
31 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | ||
32 | isNSFW: CONFIG.INSTANCE.IS_NSFW, | ||
33 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
34 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | ||
35 | customizations: { | ||
36 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, | ||
37 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | ||
38 | } | ||
39 | }, | ||
40 | search: { | ||
41 | remoteUri: { | ||
42 | users: CONFIG.SEARCH.REMOTE_URI.USERS, | ||
43 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | ||
44 | }, | ||
45 | searchIndex: { | ||
46 | enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED, | ||
47 | url: CONFIG.SEARCH.SEARCH_INDEX.URL, | ||
48 | disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, | ||
49 | isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH | ||
50 | } | ||
51 | }, | ||
52 | plugin: { | ||
53 | registered: getRegisteredPlugins(), | ||
54 | registeredExternalAuths: getExternalAuthsPlugins(), | ||
55 | registeredIdAndPassAuths: getIdAndPassAuthPlugins() | ||
56 | }, | ||
57 | theme: { | ||
58 | registered: getRegisteredThemes(), | ||
59 | default: defaultTheme | ||
60 | }, | ||
61 | email: { | ||
62 | enabled: isEmailEnabled() | ||
63 | }, | ||
64 | contactForm: { | ||
65 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
66 | }, | ||
67 | serverVersion: PEERTUBE_VERSION, | ||
68 | serverCommit, | ||
69 | signup: { | ||
70 | allowed, | ||
71 | allowedForCurrentIP, | ||
72 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
73 | }, | ||
74 | transcoding: { | ||
75 | hls: { | ||
76 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
77 | }, | ||
78 | webtorrent: { | ||
79 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
80 | }, | ||
81 | enabledResolutions: getEnabledResolutions('vod'), | ||
82 | profile: CONFIG.TRANSCODING.PROFILE, | ||
83 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod') | ||
84 | }, | ||
85 | live: { | ||
86 | enabled: CONFIG.LIVE.ENABLED, | ||
87 | |||
88 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | ||
89 | maxDuration: CONFIG.LIVE.MAX_DURATION, | ||
90 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | ||
91 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | ||
92 | |||
93 | transcoding: { | ||
94 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | ||
95 | enabledResolutions: getEnabledResolutions('live'), | ||
96 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
97 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live') | ||
98 | }, | ||
99 | |||
100 | rtmp: { | ||
101 | port: CONFIG.LIVE.RTMP.PORT | ||
102 | } | ||
103 | }, | ||
104 | import: { | ||
105 | videos: { | ||
106 | http: { | ||
107 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
108 | }, | ||
109 | torrent: { | ||
110 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED | ||
111 | } | ||
112 | } | ||
113 | }, | ||
114 | autoBlacklist: { | ||
115 | videos: { | ||
116 | ofUsers: { | ||
117 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED | ||
118 | } | ||
119 | } | ||
120 | }, | ||
121 | avatar: { | ||
122 | file: { | ||
123 | size: { | ||
124 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
125 | }, | ||
126 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
127 | } | ||
128 | }, | ||
129 | banner: { | ||
130 | file: { | ||
131 | size: { | ||
132 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
133 | }, | ||
134 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
135 | } | ||
136 | }, | ||
137 | video: { | ||
138 | image: { | ||
139 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, | ||
140 | size: { | ||
141 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max | ||
142 | } | ||
143 | }, | ||
144 | file: { | ||
145 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME | ||
146 | } | ||
147 | }, | ||
148 | videoCaption: { | ||
149 | file: { | ||
150 | size: { | ||
151 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | ||
152 | }, | ||
153 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
154 | } | ||
155 | }, | ||
156 | user: { | ||
157 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
158 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
159 | }, | ||
160 | trending: { | ||
161 | videos: { | ||
162 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS, | ||
163 | algorithms: { | ||
164 | enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED, | ||
165 | default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT | ||
166 | } | ||
167 | } | ||
168 | }, | ||
169 | tracker: { | ||
170 | enabled: CONFIG.TRACKER.ENABLED | ||
171 | }, | ||
172 | |||
173 | followings: { | ||
174 | instance: { | ||
175 | autoFollowIndex: { | ||
176 | indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL | ||
177 | } | ||
178 | } | ||
179 | }, | ||
180 | |||
181 | broadcastMessage: { | ||
182 | enabled: CONFIG.BROADCAST_MESSAGE.ENABLED, | ||
183 | message: CONFIG.BROADCAST_MESSAGE.MESSAGE, | ||
184 | level: CONFIG.BROADCAST_MESSAGE.LEVEL, | ||
185 | dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | |||
190 | function getRegisteredThemes () { | ||
191 | return PluginManager.Instance.getRegisteredThemes() | ||
192 | .map(t => ({ | ||
193 | name: t.name, | ||
194 | version: t.version, | ||
195 | description: t.description, | ||
196 | css: t.css, | ||
197 | clientScripts: t.clientScripts | ||
198 | })) | ||
199 | } | ||
200 | |||
201 | function getRegisteredPlugins () { | ||
202 | return PluginManager.Instance.getRegisteredPlugins() | ||
203 | .map(p => ({ | ||
204 | name: p.name, | ||
205 | version: p.version, | ||
206 | description: p.description, | ||
207 | clientScripts: p.clientScripts | ||
208 | })) | ||
209 | } | ||
210 | |||
211 | // --------------------------------------------------------------------------- | ||
212 | |||
213 | export { | ||
214 | getServerConfig, | ||
215 | getRegisteredThemes, | ||
216 | getRegisteredPlugins | ||
217 | } | ||
218 | |||
219 | // --------------------------------------------------------------------------- | ||
220 | |||
221 | function getIdAndPassAuthPlugins () { | ||
222 | const result: RegisteredIdAndPassAuthConfig[] = [] | ||
223 | |||
224 | for (const p of PluginManager.Instance.getIdAndPassAuths()) { | ||
225 | for (const auth of p.idAndPassAuths) { | ||
226 | result.push({ | ||
227 | npmName: p.npmName, | ||
228 | name: p.name, | ||
229 | version: p.version, | ||
230 | authName: auth.authName, | ||
231 | weight: auth.getWeight() | ||
232 | }) | ||
233 | } | ||
234 | } | ||
235 | |||
236 | return result | ||
237 | } | ||
238 | |||
239 | function getExternalAuthsPlugins () { | ||
240 | const result: RegisteredExternalAuthConfig[] = [] | ||
241 | |||
242 | for (const p of PluginManager.Instance.getExternalAuths()) { | ||
243 | for (const auth of p.externalAuths) { | ||
244 | result.push({ | ||
245 | npmName: p.npmName, | ||
246 | name: p.name, | ||
247 | version: p.version, | ||
248 | authName: auth.authName, | ||
249 | authDisplayName: auth.authDisplayName() | ||
250 | }) | ||
251 | } | ||
252 | } | ||
253 | |||
254 | return result | ||
255 | } | ||
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 969eae77b..2fad82bcc 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -5,36 +5,16 @@ import { join } from 'path' | |||
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | 5 | import { VideoChannelModel } from '@server/models/video/video-channel' |
6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | 6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' |
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' | ||
9 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | 8 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' |
10 | import { SendEmailOptions } from '../../shared/models/server/emailer.model' | 9 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
11 | import { isTestInstance, root } from '../helpers/core-utils' | 10 | import { isTestInstance, root } from '../helpers/core-utils' |
12 | import { bunyanLogger, logger } from '../helpers/logger' | 11 | import { bunyanLogger, logger } from '../helpers/logger' |
13 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 12 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
14 | import { WEBSERVER } from '../initializers/constants' | 13 | import { WEBSERVER } from '../initializers/constants' |
15 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' | 14 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' |
16 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | 15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' |
17 | import { JobQueue } from './job-queue' | 16 | import { JobQueue } from './job-queue' |
18 | 17 | import { toSafeHtml } from '../helpers/markdown' | |
19 | const sanitizeHtml = require('sanitize-html') | ||
20 | const markdownItEmoji = require('markdown-it-emoji/light') | ||
21 | const MarkdownItClass = require('markdown-it') | ||
22 | const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true }) | ||
23 | |||
24 | markdownIt.enable(TEXT_WITH_HTML_RULES) | ||
25 | |||
26 | markdownIt.use(markdownItEmoji) | ||
27 | |||
28 | const toSafeHtml = text => { | ||
29 | // Restore line feed | ||
30 | const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n') | ||
31 | |||
32 | // Convert possible markdown (emojis, emphasis and lists) to html | ||
33 | const html = markdownIt.render(textWithLineFeed) | ||
34 | |||
35 | // Convert to safe Html | ||
36 | return sanitizeHtml(html, SANITIZE_OPTIONS) | ||
37 | } | ||
38 | 18 | ||
39 | const Email = require('email-templates') | 19 | const Email = require('email-templates') |
40 | 20 | ||
@@ -403,9 +383,9 @@ class Emailer { | |||
403 | } | 383 | } |
404 | 384 | ||
405 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { | 385 | async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
406 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 386 | const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
407 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() | 387 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
408 | const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() | 388 | const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() |
409 | 389 | ||
410 | const emailPayload: EmailPayload = { | 390 | const emailPayload: EmailPayload = { |
411 | template: 'video-auto-blacklist-new', | 391 | template: 'video-auto-blacklist-new', |
@@ -417,7 +397,7 @@ class Emailer { | |||
417 | videoName: videoBlacklist.Video.name, | 397 | videoName: videoBlacklist.Video.name, |
418 | action: { | 398 | action: { |
419 | text: 'Review autoblacklist', | 399 | text: 'Review autoblacklist', |
420 | url: VIDEO_AUTO_BLACKLIST_URL | 400 | url: videoAutoBlacklistUrl |
421 | } | 401 | } |
422 | } | 402 | } |
423 | } | 403 | } |
@@ -472,6 +452,36 @@ class Emailer { | |||
472 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 452 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
473 | } | 453 | } |
474 | 454 | ||
455 | addNewPeerTubeVersionNotification (to: string[], latestVersion: string) { | ||
456 | const emailPayload: EmailPayload = { | ||
457 | to, | ||
458 | template: 'peertube-version-new', | ||
459 | subject: `A new PeerTube version is available: ${latestVersion}`, | ||
460 | locals: { | ||
461 | latestVersion | ||
462 | } | ||
463 | } | ||
464 | |||
465 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
466 | } | ||
467 | |||
468 | addNewPlugionVersionNotification (to: string[], plugin: MPlugin) { | ||
469 | const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type | ||
470 | |||
471 | const emailPayload: EmailPayload = { | ||
472 | to, | ||
473 | template: 'plugin-version-new', | ||
474 | subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`, | ||
475 | locals: { | ||
476 | pluginName: plugin.name, | ||
477 | latestVersion: plugin.latestVersion, | ||
478 | pluginUrl | ||
479 | } | ||
480 | } | ||
481 | |||
482 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
483 | } | ||
484 | |||
475 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { | 485 | addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { |
476 | const emailPayload: EmailPayload = { | 486 | const emailPayload: EmailPayload = { |
477 | template: 'password-reset', | 487 | template: 'password-reset', |
@@ -569,26 +579,27 @@ class Emailer { | |||
569 | }) | 579 | }) |
570 | 580 | ||
571 | for (const to of options.to) { | 581 | for (const to of options.to) { |
572 | await email | 582 | const baseOptions: SendEmailDefaultOptions = { |
573 | .send(merge( | 583 | template: 'common', |
574 | { | 584 | message: { |
575 | template: 'common', | 585 | to, |
576 | message: { | 586 | from: options.from, |
577 | to, | 587 | subject: options.subject, |
578 | from: options.from, | 588 | replyTo: options.replyTo |
579 | subject: options.subject, | 589 | }, |
580 | replyTo: options.replyTo | 590 | locals: { // default variables available in all templates |
581 | }, | 591 | WEBSERVER, |
582 | locals: { // default variables available in all templates | 592 | EMAIL: CONFIG.EMAIL, |
583 | WEBSERVER, | 593 | instanceName: CONFIG.INSTANCE.NAME, |
584 | EMAIL: CONFIG.EMAIL, | 594 | text: options.text, |
585 | instanceName: CONFIG.INSTANCE.NAME, | 595 | subject: options.subject |
586 | text: options.text, | 596 | } |
587 | subject: options.subject | 597 | } |
588 | } | 598 | |
589 | }, | 599 | // overriden/new variables given for a specific template in the payload |
590 | options // overriden/new variables given for a specific template in the payload | 600 | const sendOptions = merge(baseOptions, options) |
591 | ) as SendEmailOptions) | 601 | |
602 | await email.send(sendOptions) | ||
592 | .then(res => logger.debug('Sent email.', { res })) | 603 | .then(res => logger.debug('Sent email.', { res })) |
593 | .catch(err => logger.error('Error in email sender.', { err })) | 604 | .catch(err => logger.error('Error in email sender.', { err })) |
594 | } | 605 | } |
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/peertube-socket.ts b/server/lib/peertube-socket.ts index c91407a59..0740e378e 100644 --- a/server/lib/peertube-socket.ts +++ b/server/lib/peertube-socket.ts | |||
@@ -42,12 +42,14 @@ class PeerTubeSocket { | |||
42 | socket.on('subscribe', ({ videoId }) => { | 42 | socket.on('subscribe', ({ videoId }) => { |
43 | if (!isIdValid(videoId)) return | 43 | if (!isIdValid(videoId)) return |
44 | 44 | ||
45 | /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
45 | socket.join(videoId) | 46 | socket.join(videoId) |
46 | }) | 47 | }) |
47 | 48 | ||
48 | socket.on('unsubscribe', ({ videoId }) => { | 49 | socket.on('unsubscribe', ({ videoId }) => { |
49 | if (!isIdValid(videoId)) return | 50 | if (!isIdValid(videoId)) return |
50 | 51 | ||
52 | /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
51 | socket.leave(videoId) | 53 | socket.leave(videoId) |
52 | }) | 54 | }) |
53 | }) | 55 | }) |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index dac6b3185..cbd849742 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -12,8 +12,10 @@ import { VideoBlacklistCreate } from '@shared/models' | |||
12 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' | 12 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' |
13 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' | 13 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' |
14 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | 14 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' |
15 | import { getServerConfig } from '../config' | ||
16 | import { MPlugin } from '@server/types/models' | ||
15 | 17 | ||
16 | function buildPluginHelpers (npmName: string): PeerTubeHelpers { | 18 | function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { |
17 | const logger = buildPluginLogger(npmName) | 19 | const logger = buildPluginLogger(npmName) |
18 | 20 | ||
19 | const database = buildDatabaseHelpers() | 21 | const database = buildDatabaseHelpers() |
@@ -25,12 +27,15 @@ function buildPluginHelpers (npmName: string): PeerTubeHelpers { | |||
25 | 27 | ||
26 | const moderation = buildModerationHelpers() | 28 | const moderation = buildModerationHelpers() |
27 | 29 | ||
30 | const plugin = buildPluginRelatedHelpers(pluginModel) | ||
31 | |||
28 | return { | 32 | return { |
29 | logger, | 33 | logger, |
30 | database, | 34 | database, |
31 | videos, | 35 | videos, |
32 | config, | 36 | config, |
33 | moderation, | 37 | moderation, |
38 | plugin, | ||
34 | server | 39 | server |
35 | } | 40 | } |
36 | } | 41 | } |
@@ -132,6 +137,16 @@ function buildConfigHelpers () { | |||
132 | return { | 137 | return { |
133 | getWebserverUrl () { | 138 | getWebserverUrl () { |
134 | return WEBSERVER.URL | 139 | return WEBSERVER.URL |
140 | }, | ||
141 | |||
142 | getServerConfig () { | ||
143 | return getServerConfig() | ||
135 | } | 144 | } |
136 | } | 145 | } |
137 | } | 146 | } |
147 | |||
148 | function buildPluginRelatedHelpers (plugin: MPlugin) { | ||
149 | return { | ||
150 | getBaseStaticRoute: () => `/plugins/${plugin.name}/${plugin.version}/static/` | ||
151 | } | ||
152 | } | ||
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 7bcb6ed4c..165bc91b3 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,12 +58,28 @@ 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 | } |
65 | 69 | ||
70 | async function getLatestPluginVersion (npmName: string) { | ||
71 | const results = await getLatestPluginsVersion([ npmName ]) | ||
72 | |||
73 | if (Array.isArray(results) === false || results.length !== 1) { | ||
74 | logger.warn('Cannot get latest supported plugin version of %s.', npmName) | ||
75 | return undefined | ||
76 | } | ||
77 | |||
78 | return results[0].latestVersion | ||
79 | } | ||
80 | |||
66 | export { | 81 | export { |
67 | listAvailablePluginsFromIndex, | 82 | listAvailablePluginsFromIndex, |
83 | getLatestPluginVersion, | ||
68 | getLatestPluginsVersion | 84 | getLatestPluginsVersion |
69 | } | 85 | } |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index c19b40135..03ea48416 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import decache from 'decache' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
2 | import { createReadStream, createWriteStream } from 'fs' | 3 | import { createReadStream, createWriteStream } from 'fs' |
3 | import { outputFile, readJSON } from 'fs-extra' | 4 | import { outputFile, readJSON } from 'fs-extra' |
@@ -327,11 +328,18 @@ export class PluginManager implements ServerHook { | |||
327 | return plugin | 328 | return plugin |
328 | } | 329 | } |
329 | 330 | ||
330 | async update (toUpdate: string, version?: string, fromDisk = false) { | 331 | async update (toUpdate: string, fromDisk = false) { |
331 | const npmName = fromDisk ? basename(toUpdate) : toUpdate | 332 | const npmName = fromDisk ? basename(toUpdate) : toUpdate |
332 | 333 | ||
333 | logger.info('Updating plugin %s.', npmName) | 334 | logger.info('Updating plugin %s.', npmName) |
334 | 335 | ||
336 | // Use the latest version from DB, to not upgrade to a version that does not support our PeerTube version | ||
337 | let version: string | ||
338 | if (!fromDisk) { | ||
339 | const plugin = await PluginModel.loadByNpmName(toUpdate) | ||
340 | version = plugin.latestVersion | ||
341 | } | ||
342 | |||
335 | // Unregister old hooks | 343 | // Unregister old hooks |
336 | await this.unregister(npmName) | 344 | await this.unregister(npmName) |
337 | 345 | ||
@@ -411,7 +419,7 @@ export class PluginManager implements ServerHook { | |||
411 | 419 | ||
412 | // Delete cache if needed | 420 | // Delete cache if needed |
413 | const modulePath = join(pluginPath, packageJSON.library) | 421 | const modulePath = join(pluginPath, packageJSON.library) |
414 | delete require.cache[modulePath] | 422 | decache(modulePath) |
415 | const library: PluginLibrary = require(modulePath) | 423 | const library: PluginLibrary = require(modulePath) |
416 | 424 | ||
417 | if (!isLibraryCodeValid(library)) { | 425 | if (!isLibraryCodeValid(library)) { |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index 1f2a88c27..c018e54a8 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, |
@@ -109,7 +109,7 @@ export class RegisterHelpers { | |||
109 | const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() | 109 | const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth() |
110 | const unregisterExternalAuth = this.buildUnregisterExternalAuth() | 110 | const unregisterExternalAuth = this.buildUnregisterExternalAuth() |
111 | 111 | ||
112 | const peertubeHelpers = buildPluginHelpers(this.npmName) | 112 | const peertubeHelpers = buildPluginHelpers(this.plugin, this.npmName) |
113 | 113 | ||
114 | return { | 114 | return { |
115 | registerHook, | 115 | registerHook, |
diff --git a/server/lib/plugins/yarn.ts b/server/lib/plugins/yarn.ts index e40351b6e..3f45681d3 100644 --- a/server/lib/plugins/yarn.ts +++ b/server/lib/plugins/yarn.ts | |||
@@ -1,14 +1,17 @@ | |||
1 | import { outputJSON, pathExists } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
1 | import { execShell } from '../../helpers/core-utils' | 3 | import { execShell } from '../../helpers/core-utils' |
2 | import { logger } from '../../helpers/logger' | ||
3 | import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | 4 | import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' |
5 | import { logger } from '../../helpers/logger' | ||
4 | import { CONFIG } from '../../initializers/config' | 6 | import { CONFIG } from '../../initializers/config' |
5 | import { outputJSON, pathExists } from 'fs-extra' | 7 | import { getLatestPluginVersion } from './plugin-index' |
6 | import { join } from 'path' | ||
7 | 8 | ||
8 | async function installNpmPlugin (npmName: string, version?: string) { | 9 | async function installNpmPlugin (npmName: string, versionArg?: string) { |
9 | // Security check | 10 | // Security check |
10 | checkNpmPluginNameOrThrow(npmName) | 11 | checkNpmPluginNameOrThrow(npmName) |
11 | if (version) checkPluginVersionOrThrow(version) | 12 | if (versionArg) checkPluginVersionOrThrow(versionArg) |
13 | |||
14 | const version = versionArg || await getLatestPluginVersion(npmName) | ||
12 | 15 | ||
13 | let toInstall = npmName | 16 | let toInstall = npmName |
14 | if (version) toInstall += `@${version}` | 17 | if (version) toInstall += `@${version}` |
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/stat-manager.ts b/server/lib/stat-manager.ts index 547d7a56b..09ba208bd 100644 --- a/server/lib/stat-manager.ts +++ b/server/lib/stat-manager.ts | |||
@@ -3,8 +3,10 @@ import { UserModel } from '@server/models/account/user' | |||
3 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | 4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' |
5 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
6 | import { VideoCommentModel } from '@server/models/video/video-comment' | 7 | import { VideoCommentModel } from '@server/models/video/video-comment' |
7 | import { VideoFileModel } from '@server/models/video/video-file' | 8 | import { VideoFileModel } from '@server/models/video/video-file' |
9 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
8 | import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' | 10 | import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' |
9 | 11 | ||
10 | class StatsManager { | 12 | class StatsManager { |
@@ -46,21 +48,36 @@ class StatsManager { | |||
46 | const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats() | 48 | const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats() |
47 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 49 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
48 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | 50 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() |
51 | const { | ||
52 | totalLocalVideoChannels, | ||
53 | totalLocalDailyActiveVideoChannels, | ||
54 | totalLocalWeeklyActiveVideoChannels, | ||
55 | totalLocalMonthlyActiveVideoChannels | ||
56 | } = await VideoChannelModel.getStats() | ||
57 | const { totalLocalPlaylists } = await VideoPlaylistModel.getStats() | ||
49 | 58 | ||
50 | const videosRedundancyStats = await this.buildRedundancyStats() | 59 | const videosRedundancyStats = await this.buildRedundancyStats() |
51 | 60 | ||
52 | const data: ServerStats = { | 61 | const data: ServerStats = { |
62 | totalUsers, | ||
63 | totalDailyActiveUsers, | ||
64 | totalWeeklyActiveUsers, | ||
65 | totalMonthlyActiveUsers, | ||
66 | |||
53 | totalLocalVideos, | 67 | totalLocalVideos, |
54 | totalLocalVideoViews, | 68 | totalLocalVideoViews, |
55 | totalLocalVideoFilesSize, | ||
56 | totalLocalVideoComments, | 69 | totalLocalVideoComments, |
70 | totalLocalVideoFilesSize, | ||
71 | |||
57 | totalVideos, | 72 | totalVideos, |
58 | totalVideoComments, | 73 | totalVideoComments, |
59 | 74 | ||
60 | totalUsers, | 75 | totalLocalVideoChannels, |
61 | totalDailyActiveUsers, | 76 | totalLocalDailyActiveVideoChannels, |
62 | totalWeeklyActiveUsers, | 77 | totalLocalWeeklyActiveVideoChannels, |
63 | totalMonthlyActiveUsers, | 78 | totalLocalMonthlyActiveVideoChannels, |
79 | |||
80 | totalLocalPlaylists, | ||
64 | 81 | ||
65 | totalInstanceFollowers, | 82 | totalInstanceFollowers, |
66 | totalInstanceFollowing, | 83 | totalInstanceFollowing, |
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/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts index b7f9178c4..81f5e1962 100644 --- a/server/lib/video-transcoding-profiles.ts +++ b/server/lib/video-transcoding-profiles.ts | |||
@@ -55,7 +55,7 @@ const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNu | |||
55 | 55 | ||
56 | if (await canDoQuickAudioTranscode(input, probe)) { | 56 | if (await canDoQuickAudioTranscode(input, probe)) { |
57 | logger.debug('Copy audio stream %s by AAC encoder.', input) | 57 | logger.debug('Copy audio stream %s by AAC encoder.', input) |
58 | return { copy: true, outputOptions: [] } | 58 | return { copy: true, outputOptions: [ ] } |
59 | } | 59 | } |
60 | 60 | ||
61 | const parsedAudio = await getAudioStream(input, probe) | 61 | const parsedAudio = await getAudioStream(input, probe) |
diff --git a/server/middlewares/async.ts b/server/middlewares/async.ts index 3a1bdabb8..3d6e38809 100644 --- a/server/middlewares/async.ts +++ b/server/middlewares/async.ts | |||
@@ -13,7 +13,7 @@ function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]) | |||
13 | return (req: Request, res: Response, next: NextFunction) => { | 13 | return (req: Request, res: Response, next: NextFunction) => { |
14 | if (Array.isArray(fun) === true) { | 14 | if (Array.isArray(fun) === true) { |
15 | return eachSeries(fun as RequestHandler[], (f, cb) => { | 15 | return eachSeries(fun as RequestHandler[], (f, cb) => { |
16 | Promise.resolve(f(req, res, cb)) | 16 | Promise.resolve(f(req, res, err => cb(err))) |
17 | .catch(err => next(err)) | 17 | .catch(err => next(err)) |
18 | }, next) | 18 | }, next) |
19 | } | 19 | } |
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/plugins.ts b/server/middlewares/validators/plugins.ts index 1083e0afa..ab87fe720 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -109,7 +109,6 @@ const installOrUpdatePluginValidator = [ | |||
109 | if (!body.path && !body.npmName) { | 109 | if (!body.path && !body.npmName) { |
110 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 110 | return res.status(HttpStatusCode.BAD_REQUEST_400) |
111 | .json({ error: 'Should have either a npmName or a path' }) | 111 | .json({ error: 'Should have either a npmName or a path' }) |
112 | .end() | ||
113 | } | 112 | } |
114 | 113 | ||
115 | return next() | 114 | return next() |
@@ -140,7 +139,6 @@ const existingPluginValidator = [ | |||
140 | if (!plugin) { | 139 | if (!plugin) { |
141 | return res.status(HttpStatusCode.NOT_FOUND_404) | 140 | return res.status(HttpStatusCode.NOT_FOUND_404) |
142 | .json({ error: 'Plugin not found' }) | 141 | .json({ error: 'Plugin not found' }) |
143 | .end() | ||
144 | } | 142 | } |
145 | 143 | ||
146 | res.locals.plugin = plugin | 144 | res.locals.plugin = plugin |
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..19f3f7e04 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.Banner) { | ||
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..b7ffbd3b1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -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 |
@@ -316,6 +338,47 @@ export class VideoChannelModel extends Model { | |||
316 | return VideoChannelModel.count(query) | 338 | return VideoChannelModel.count(query) |
317 | } | 339 | } |
318 | 340 | ||
341 | static async getStats () { | ||
342 | |||
343 | function getActiveVideoChannels (days: number) { | ||
344 | const options = { | ||
345 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
346 | raw: true | ||
347 | } | ||
348 | |||
349 | const query = ` | ||
350 | SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count" | ||
351 | FROM "videoChannel" AS "VideoChannelModel" | ||
352 | INNER JOIN "video" AS "Videos" | ||
353 | ON "VideoChannelModel"."id" = "Videos"."channelId" | ||
354 | AND ("Videos"."publishedAt" > Now() - interval '${days}d') | ||
355 | INNER JOIN "account" AS "Account" | ||
356 | ON "VideoChannelModel"."accountId" = "Account"."id" | ||
357 | INNER JOIN "actor" AS "Account->Actor" | ||
358 | ON "Account"."actorId" = "Account->Actor"."id" | ||
359 | AND "Account->Actor"."serverId" IS NULL | ||
360 | LEFT OUTER JOIN "server" AS "Account->Actor->Server" | ||
361 | ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | ||
362 | |||
363 | return VideoChannelModel.sequelize.query<{ count: string }>(query, options) | ||
364 | .then(r => parseInt(r[0].count, 10)) | ||
365 | } | ||
366 | |||
367 | const totalLocalVideoChannels = await VideoChannelModel.count() | ||
368 | const totalLocalDailyActiveVideoChannels = await getActiveVideoChannels(1) | ||
369 | const totalLocalWeeklyActiveVideoChannels = await getActiveVideoChannels(7) | ||
370 | const totalLocalMonthlyActiveVideoChannels = await getActiveVideoChannels(30) | ||
371 | const totalHalfYearActiveVideoChannels = await getActiveVideoChannels(180) | ||
372 | |||
373 | return { | ||
374 | totalLocalVideoChannels, | ||
375 | totalLocalDailyActiveVideoChannels, | ||
376 | totalLocalWeeklyActiveVideoChannels, | ||
377 | totalLocalMonthlyActiveVideoChannels, | ||
378 | totalHalfYearActiveVideoChannels | ||
379 | } | ||
380 | } | ||
381 | |||
319 | static listForApi (parameters: { | 382 | static listForApi (parameters: { |
320 | actorId: number | 383 | actorId: number |
321 | start: number | 384 | start: number |
@@ -441,7 +504,7 @@ export class VideoChannelModel extends Model { | |||
441 | where | 504 | where |
442 | } | 505 | } |
443 | 506 | ||
444 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | 507 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
445 | 508 | ||
446 | if (options.withStats === true) { | 509 | if (options.withStats === true) { |
447 | scopes.push({ | 510 | scopes.push({ |
@@ -457,32 +520,13 @@ export class VideoChannelModel extends Model { | |||
457 | }) | 520 | }) |
458 | } | 521 | } |
459 | 522 | ||
460 | static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | 523 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { |
461 | return VideoChannelModel.unscoped() | 524 | return VideoChannelModel.unscoped() |
462 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 525 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
463 | .findByPk(id) | 526 | .findByPk(id) |
464 | } | 527 | } |
465 | 528 | ||
466 | static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { | 529 | 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 = { | 530 | const query = { |
487 | include: [ | 531 | include: [ |
488 | { | 532 | { |
@@ -490,7 +534,14 @@ export class VideoChannelModel extends Model { | |||
490 | required: true, | 534 | required: true, |
491 | where: { | 535 | where: { |
492 | url | 536 | url |
493 | } | 537 | }, |
538 | include: [ | ||
539 | { | ||
540 | model: ActorImageModel, | ||
541 | required: false, | ||
542 | as: 'Banner' | ||
543 | } | ||
544 | ] | ||
494 | } | 545 | } |
495 | ] | 546 | ] |
496 | } | 547 | } |
@@ -508,7 +559,7 @@ export class VideoChannelModel extends Model { | |||
508 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | 559 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) |
509 | } | 560 | } |
510 | 561 | ||
511 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { | 562 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> { |
512 | const query = { | 563 | const query = { |
513 | include: [ | 564 | include: [ |
514 | { | 565 | { |
@@ -517,17 +568,24 @@ export class VideoChannelModel extends Model { | |||
517 | where: { | 568 | where: { |
518 | preferredUsername: name, | 569 | preferredUsername: name, |
519 | serverId: null | 570 | serverId: null |
520 | } | 571 | }, |
572 | include: [ | ||
573 | { | ||
574 | model: ActorImageModel, | ||
575 | required: false, | ||
576 | as: 'Banner' | ||
577 | } | ||
578 | ] | ||
521 | } | 579 | } |
522 | ] | 580 | ] |
523 | } | 581 | } |
524 | 582 | ||
525 | return VideoChannelModel.unscoped() | 583 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 584 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
527 | .findOne(query) | 585 | .findOne(query) |
528 | } | 586 | } |
529 | 587 | ||
530 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { | 588 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> { |
531 | const query = { | 589 | const query = { |
532 | include: [ | 590 | include: [ |
533 | { | 591 | { |
@@ -541,6 +599,11 @@ export class VideoChannelModel extends Model { | |||
541 | model: ServerModel, | 599 | model: ServerModel, |
542 | required: true, | 600 | required: true, |
543 | where: { host } | 601 | where: { host } |
602 | }, | ||
603 | { | ||
604 | model: ActorImageModel, | ||
605 | required: false, | ||
606 | as: 'Banner' | ||
544 | } | 607 | } |
545 | ] | 608 | ] |
546 | } | 609 | } |
@@ -548,22 +611,10 @@ export class VideoChannelModel extends Model { | |||
548 | } | 611 | } |
549 | 612 | ||
550 | return VideoChannelModel.unscoped() | 613 | return VideoChannelModel.unscoped() |
551 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 614 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
552 | .findOne(query) | 615 | .findOne(query) |
553 | } | 616 | } |
554 | 617 | ||
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 { | 618 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { |
568 | const actor = this.Actor.toFormattedSummaryJSON() | 619 | const actor = this.Actor.toFormattedSummaryJSON() |
569 | 620 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 49a406608..efe5be36d 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -54,6 +54,7 @@ import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdat | |||
54 | import { ThumbnailModel } from './thumbnail' | 54 | import { ThumbnailModel } from './thumbnail' |
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | 56 | import { VideoPlaylistElementModel } from './video-playlist-element' |
57 | import { ActorModel } from '../activitypub/actor' | ||
57 | 58 | ||
58 | enum ScopeNames { | 59 | enum ScopeNames { |
59 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 60 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
@@ -65,7 +66,7 @@ enum ScopeNames { | |||
65 | } | 66 | } |
66 | 67 | ||
67 | type AvailableForListOptions = { | 68 | type AvailableForListOptions = { |
68 | followerActorId: number | 69 | followerActorId?: number |
69 | type?: VideoPlaylistType | 70 | type?: VideoPlaylistType |
70 | accountId?: number | 71 | accountId?: number |
71 | videoChannelId?: number | 72 | videoChannelId?: number |
@@ -134,20 +135,26 @@ type AvailableForListOptions = { | |||
134 | privacy: VideoPlaylistPrivacy.PUBLIC | 135 | privacy: VideoPlaylistPrivacy.PUBLIC |
135 | }) | 136 | }) |
136 | 137 | ||
137 | // Only list local playlists OR playlists that are on an instance followed by actorId | 138 | // Only list local playlists |
138 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 139 | const whereActorOr: WhereOptions[] = [ |
140 | { | ||
141 | serverId: null | ||
142 | } | ||
143 | ] | ||
139 | 144 | ||
140 | whereActor = { | 145 | // … OR playlists that are on an instance followed by actorId |
141 | [Op.or]: [ | 146 | if (options.followerActorId) { |
142 | { | 147 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
143 | serverId: null | 148 | |
144 | }, | 149 | whereActorOr.push({ |
145 | { | 150 | serverId: { |
146 | serverId: { | 151 | [Op.in]: literal(inQueryInstanceFollow) |
147 | [Op.in]: literal(inQueryInstanceFollow) | ||
148 | } | ||
149 | } | 152 | } |
150 | ] | 153 | }) |
154 | } | ||
155 | |||
156 | whereActor = { | ||
157 | [Op.or]: whereActorOr | ||
151 | } | 158 | } |
152 | } | 159 | } |
153 | 160 | ||
@@ -495,6 +502,33 @@ export class VideoPlaylistModel extends Model { | |||
495 | return '/video-playlists/embed/' + this.uuid | 502 | return '/video-playlists/embed/' + this.uuid |
496 | } | 503 | } |
497 | 504 | ||
505 | static async getStats () { | ||
506 | const totalLocalPlaylists = await VideoPlaylistModel.count({ | ||
507 | include: [ | ||
508 | { | ||
509 | model: AccountModel, | ||
510 | required: true, | ||
511 | include: [ | ||
512 | { | ||
513 | model: ActorModel, | ||
514 | required: true, | ||
515 | where: { | ||
516 | serverId: null | ||
517 | } | ||
518 | } | ||
519 | ] | ||
520 | } | ||
521 | ], | ||
522 | where: { | ||
523 | privacy: VideoPlaylistPrivacy.PUBLIC | ||
524 | } | ||
525 | }) | ||
526 | |||
527 | return { | ||
528 | totalLocalPlaylists | ||
529 | } | ||
530 | } | ||
531 | |||
498 | setAsRefreshed () { | 532 | setAsRefreshed () { |
499 | this.changed('updatedAt', true) | 533 | this.changed('updatedAt', true) |
500 | 534 | ||
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..422bf6deb 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 | } |
@@ -2034,9 +2027,11 @@ export class VideoModel extends Model { | |||
2034 | } | 2027 | } |
2035 | 2028 | ||
2036 | setAsRefreshed () { | 2029 | setAsRefreshed () { |
2037 | this.changed('updatedAt', true) | 2030 | const options = { |
2031 | where: { id: this.id } | ||
2032 | } | ||
2038 | 2033 | ||
2039 | return this.save() | 2034 | return VideoModel.update({ updatedAt: new Date() }, options) |
2040 | } | 2035 | } |
2041 | 2036 | ||
2042 | requiresAuth () { | 2037 | requiresAuth () { |
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/live/live.ts b/server/tests/api/live/live.ts index 0831f91f0..d48e2a8ee 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -255,7 +255,7 @@ describe('Test live', function () { | |||
255 | } | 255 | } |
256 | 256 | ||
257 | it('Should not allow a stream without the appropriate path', async function () { | 257 | it('Should not allow a stream without the appropriate path', async function () { |
258 | this.timeout(30000) | 258 | this.timeout(60000) |
259 | 259 | ||
260 | liveVideo = await createLiveWrapper() | 260 | liveVideo = await createLiveWrapper() |
261 | 261 | ||
@@ -264,14 +264,14 @@ describe('Test live', function () { | |||
264 | }) | 264 | }) |
265 | 265 | ||
266 | it('Should not allow a stream without the appropriate stream key', async function () { | 266 | it('Should not allow a stream without the appropriate stream key', async function () { |
267 | this.timeout(30000) | 267 | this.timeout(60000) |
268 | 268 | ||
269 | const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key') | 269 | const command = sendRTMPStream(rtmpUrl + '/live', 'bad-stream-key') |
270 | await testFfmpegStreamError(command, true) | 270 | await testFfmpegStreamError(command, true) |
271 | }) | 271 | }) |
272 | 272 | ||
273 | it('Should succeed with the correct params', async function () { | 273 | it('Should succeed with the correct params', async function () { |
274 | this.timeout(30000) | 274 | this.timeout(60000) |
275 | 275 | ||
276 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) | 276 | const command = sendRTMPStream(rtmpUrl + '/live', liveVideo.streamKey) |
277 | await testFfmpegStreamError(command, false) | 277 | await testFfmpegStreamError(command, false) |
@@ -292,7 +292,7 @@ describe('Test live', function () { | |||
292 | }) | 292 | }) |
293 | 293 | ||
294 | it('Should not allow a stream on a live that was blacklisted', async function () { | 294 | it('Should not allow a stream on a live that was blacklisted', async function () { |
295 | this.timeout(30000) | 295 | this.timeout(60000) |
296 | 296 | ||
297 | liveVideo = await createLiveWrapper() | 297 | liveVideo = await createLiveWrapper() |
298 | 298 | ||
@@ -303,7 +303,7 @@ describe('Test live', function () { | |||
303 | }) | 303 | }) |
304 | 304 | ||
305 | it('Should not allow a stream on a live that was deleted', async function () { | 305 | it('Should not allow a stream on a live that was deleted', async function () { |
306 | this.timeout(30000) | 306 | this.timeout(60000) |
307 | 307 | ||
308 | liveVideo = await createLiveWrapper() | 308 | liveVideo = await createLiveWrapper() |
309 | 309 | ||
diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts new file mode 100644 index 000000000..cfe0bd2bb --- /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 { PluginType } from '@shared/models' | ||
7 | import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils' | ||
8 | import { ServerInfo } from '../../../../shared/extra-utils/index' | ||
9 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | ||
10 | import { | ||
11 | CheckerBaseParams, | ||
12 | checkNewPeerTubeVersion, | ||
13 | checkNewPluginVersion, | ||
14 | prepareNotificationsTest | ||
15 | } from '../../../../shared/extra-utils/users/user-notifications' | ||
16 | import { UserNotification, UserNotificationType } from '../../../../shared/models/users' | ||
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 | joinPeerTubeServer = new MockJoinPeerTubeVersions() | ||
30 | const port = await joinPeerTubeServer.initialize() | ||
31 | |||
32 | const config = { | ||
33 | peertube: { | ||
34 | check_latest_version: { | ||
35 | enabled: true, | ||
36 | url: `http://localhost:${port}/versions.json` | ||
37 | } | ||
38 | }, | ||
39 | plugins: { | ||
40 | index: { | ||
41 | enabled: true, | ||
42 | check_latest_versions_interval: '5 seconds' | ||
43 | } | ||
44 | } | ||
45 | } | ||
46 | |||
47 | const res = await prepareNotificationsTest(1, config) | ||
48 | emails = res.emails | ||
49 | server = res.servers[0] | ||
50 | |||
51 | userNotifications = res.userNotifications | ||
52 | adminNotifications = res.adminNotifications | ||
53 | |||
54 | baseParams = { | ||
55 | server: server, | ||
56 | emails, | ||
57 | socketNotifications: adminNotifications, | ||
58 | token: server.accessToken | ||
59 | } | ||
60 | |||
61 | await installPlugin({ | ||
62 | url: server.url, | ||
63 | accessToken: server.accessToken, | ||
64 | npmName: 'peertube-plugin-hello-world' | ||
65 | }) | ||
66 | |||
67 | await installPlugin({ | ||
68 | url: server.url, | ||
69 | accessToken: server.accessToken, | ||
70 | npmName: 'peertube-theme-background-red' | ||
71 | }) | ||
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/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index 4c00d97f8..4ce6675b6 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts | |||
@@ -2,8 +2,9 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { v4 as uuidv4 } from 'uuid' | 4 | import { v4 as uuidv4 } from 'uuid' |
5 | 5 | import { AbuseState } from '@shared/models' | |
6 | import { | 6 | import { |
7 | addAbuseMessage, | ||
7 | addVideoCommentThread, | 8 | addVideoCommentThread, |
8 | addVideoToBlacklist, | 9 | addVideoToBlacklist, |
9 | cleanupTests, | 10 | cleanupTests, |
@@ -20,18 +21,19 @@ import { | |||
20 | removeVideoFromBlacklist, | 21 | removeVideoFromBlacklist, |
21 | reportAbuse, | 22 | reportAbuse, |
22 | unfollow, | 23 | unfollow, |
24 | updateAbuse, | ||
23 | updateCustomConfig, | 25 | updateCustomConfig, |
24 | updateCustomSubConfig, | 26 | updateCustomSubConfig, |
25 | wait, | 27 | wait |
26 | updateAbuse, | ||
27 | addAbuseMessage | ||
28 | } from '../../../../shared/extra-utils' | 28 | } from '../../../../shared/extra-utils' |
29 | import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' | 29 | import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' |
30 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | 30 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' |
31 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 31 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
32 | import { | 32 | import { |
33 | checkAbuseStateChange, | ||
33 | checkAutoInstanceFollowing, | 34 | checkAutoInstanceFollowing, |
34 | CheckerBaseParams, | 35 | CheckerBaseParams, |
36 | checkNewAbuseMessage, | ||
35 | checkNewAccountAbuseForModerators, | 37 | checkNewAccountAbuseForModerators, |
36 | checkNewBlacklistOnMyVideo, | 38 | checkNewBlacklistOnMyVideo, |
37 | checkNewCommentAbuseForModerators, | 39 | checkNewCommentAbuseForModerators, |
@@ -41,15 +43,12 @@ import { | |||
41 | checkUserRegistered, | 43 | checkUserRegistered, |
42 | checkVideoAutoBlacklistForModerators, | 44 | checkVideoAutoBlacklistForModerators, |
43 | checkVideoIsPublished, | 45 | checkVideoIsPublished, |
44 | prepareNotificationsTest, | 46 | prepareNotificationsTest |
45 | checkAbuseStateChange, | ||
46 | checkNewAbuseMessage | ||
47 | } from '../../../../shared/extra-utils/users/user-notifications' | 47 | } from '../../../../shared/extra-utils/users/user-notifications' |
48 | import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' | 48 | import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' |
49 | import { CustomConfig } from '../../../../shared/models/server' | 49 | import { CustomConfig } from '../../../../shared/models/server' |
50 | import { UserNotification } from '../../../../shared/models/users' | 50 | import { UserNotification } from '../../../../shared/models/users' |
51 | import { VideoPrivacy } from '../../../../shared/models/videos' | 51 | import { VideoPrivacy } from '../../../../shared/models/videos' |
52 | import { AbuseState } from '@shared/models' | ||
53 | 52 | ||
54 | describe('Test moderation notifications', function () { | 53 | describe('Test moderation notifications', function () { |
55 | let servers: ServerInfo[] = [] | 54 | let servers: ServerInfo[] = [] |
@@ -364,16 +363,7 @@ describe('Test moderation notifications', function () { | |||
364 | 363 | ||
365 | describe('New instance follows', function () { | 364 | describe('New instance follows', function () { |
366 | const instanceIndexServer = new MockInstancesIndex() | 365 | const instanceIndexServer = new MockInstancesIndex() |
367 | const config = { | 366 | let config: any |
368 | followings: { | ||
369 | instance: { | ||
370 | autoFollowIndex: { | ||
371 | indexUrl: 'http://localhost:42101/api/v1/instances/hosts', | ||
372 | enabled: true | ||
373 | } | ||
374 | } | ||
375 | } | ||
376 | } | ||
377 | let baseParams: CheckerBaseParams | 367 | let baseParams: CheckerBaseParams |
378 | 368 | ||
379 | before(async () => { | 369 | before(async () => { |
@@ -384,8 +374,19 @@ describe('Test moderation notifications', function () { | |||
384 | token: servers[0].accessToken | 374 | token: servers[0].accessToken |
385 | } | 375 | } |
386 | 376 | ||
387 | await instanceIndexServer.initialize() | 377 | const port = await instanceIndexServer.initialize() |
388 | instanceIndexServer.addInstance(servers[1].host) | 378 | instanceIndexServer.addInstance(servers[1].host) |
379 | |||
380 | config = { | ||
381 | followings: { | ||
382 | instance: { | ||
383 | autoFollowIndex: { | ||
384 | indexUrl: `http://localhost:${port}/api/v1/instances/hosts`, | ||
385 | enabled: true | ||
386 | } | ||
387 | } | ||
388 | } | ||
389 | } | ||
389 | }) | 390 | }) |
390 | 391 | ||
391 | it('Should send a notification only to admin when there is a new instance follower', async function () { | 392 | it('Should send a notification only to admin when there is a new instance follower', async function () { |
diff --git a/server/tests/api/server/auto-follows.ts b/server/tests/api/server/auto-follows.ts index e04d70af4..1519b263f 100644 --- a/server/tests/api/server/auto-follows.ts +++ b/server/tests/api/server/auto-follows.ts | |||
@@ -1,7 +1,7 @@ | |||
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 * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { | 5 | import { |
6 | acceptFollower, | 6 | acceptFollower, |
7 | cleanupTests, | 7 | cleanupTests, |
@@ -153,9 +153,10 @@ describe('Test auto follows', function () { | |||
153 | 153 | ||
154 | describe('Auto follow index', function () { | 154 | describe('Auto follow index', function () { |
155 | const instanceIndexServer = new MockInstancesIndex() | 155 | const instanceIndexServer = new MockInstancesIndex() |
156 | let port: number | ||
156 | 157 | ||
157 | before(async () => { | 158 | before(async () => { |
158 | await instanceIndexServer.initialize() | 159 | port = await instanceIndexServer.initialize() |
159 | }) | 160 | }) |
160 | 161 | ||
161 | it('Should not auto follow index if the option is not enabled', async function () { | 162 | it('Should not auto follow index if the option is not enabled', async function () { |
@@ -177,7 +178,7 @@ describe('Test auto follows', function () { | |||
177 | followings: { | 178 | followings: { |
178 | instance: { | 179 | instance: { |
179 | autoFollowIndex: { | 180 | autoFollowIndex: { |
180 | indexUrl: 'http://localhost:42101/api/v1/instances/hosts', | 181 | indexUrl: `http://localhost:${port}/api/v1/instances/hosts`, |
181 | enabled: true | 182 | enabled: true |
182 | } | 183 | } |
183 | } | 184 | } |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 0b0f48d22..1d9ea31df 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | getConfig, | 12 | getConfig, |
13 | getCustomConfig, | 13 | getCustomConfig, |
14 | killallServers, | 14 | killallServers, |
15 | makeGetRequest, | ||
15 | parallelTests, | 16 | parallelTests, |
16 | registerUser, | 17 | registerUser, |
17 | reRunServer, | 18 | reRunServer, |
@@ -508,6 +509,39 @@ describe('Test config', function () { | |||
508 | checkInitialConfig(server, data) | 509 | checkInitialConfig(server, data) |
509 | }) | 510 | }) |
510 | 511 | ||
512 | it('Should enable frameguard', async function () { | ||
513 | this.timeout(25000) | ||
514 | |||
515 | { | ||
516 | const res = await makeGetRequest({ | ||
517 | url: server.url, | ||
518 | path: '/api/v1/config', | ||
519 | statusCodeExpected: 200 | ||
520 | }) | ||
521 | |||
522 | expect(res.headers['x-frame-options']).to.exist | ||
523 | } | ||
524 | |||
525 | killallServers([ server ]) | ||
526 | |||
527 | const config = { | ||
528 | security: { | ||
529 | frameguard: { enabled: false } | ||
530 | } | ||
531 | } | ||
532 | server = await reRunServer(server, config) | ||
533 | |||
534 | { | ||
535 | const res = await makeGetRequest({ | ||
536 | url: server.url, | ||
537 | path: '/api/v1/config', | ||
538 | statusCodeExpected: 200 | ||
539 | }) | ||
540 | |||
541 | expect(res.headers['x-frame-options']).to.not.exist | ||
542 | } | ||
543 | }) | ||
544 | |||
511 | after(async function () { | 545 | after(async function () { |
512 | await cleanupTests([ server ]) | 546 | await cleanupTests([ server ]) |
513 | }) | 547 | }) |
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/server/stats.ts b/server/tests/api/server/stats.ts index eb474c1f5..304181a6d 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -3,8 +3,10 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { | 5 | import { |
6 | addVideoChannel, | ||
6 | cleanupTests, | 7 | cleanupTests, |
7 | createUser, | 8 | createUser, |
9 | createVideoPlaylist, | ||
8 | doubleFollow, | 10 | doubleFollow, |
9 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
10 | follow, | 12 | follow, |
@@ -21,12 +23,14 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | |||
21 | import { getStats } from '../../../../shared/extra-utils/server/stats' | 23 | import { getStats } from '../../../../shared/extra-utils/server/stats' |
22 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' | 24 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' |
23 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | 25 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' |
26 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
24 | import { ActivityType } from '@shared/models' | 27 | import { ActivityType } from '@shared/models' |
25 | 28 | ||
26 | const expect = chai.expect | 29 | const expect = chai.expect |
27 | 30 | ||
28 | describe('Test stats (excluding redundancy)', function () { | 31 | describe('Test stats (excluding redundancy)', function () { |
29 | let servers: ServerInfo[] = [] | 32 | let servers: ServerInfo[] = [] |
33 | let channelId | ||
30 | const user = { | 34 | const user = { |
31 | username: 'user1', | 35 | username: 'user1', |
32 | password: 'super_password' | 36 | password: 'super_password' |
@@ -70,6 +74,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
70 | expect(data.totalVideos).to.equal(1) | 74 | expect(data.totalVideos).to.equal(1) |
71 | expect(data.totalInstanceFollowers).to.equal(2) | 75 | expect(data.totalInstanceFollowers).to.equal(2) |
72 | expect(data.totalInstanceFollowing).to.equal(1) | 76 | expect(data.totalInstanceFollowing).to.equal(1) |
77 | expect(data.totalLocalPlaylists).to.equal(0) | ||
73 | }) | 78 | }) |
74 | 79 | ||
75 | it('Should have the correct stats on instance 2', async function () { | 80 | it('Should have the correct stats on instance 2', async function () { |
@@ -85,6 +90,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
85 | expect(data.totalVideos).to.equal(1) | 90 | expect(data.totalVideos).to.equal(1) |
86 | expect(data.totalInstanceFollowers).to.equal(1) | 91 | expect(data.totalInstanceFollowers).to.equal(1) |
87 | expect(data.totalInstanceFollowing).to.equal(1) | 92 | expect(data.totalInstanceFollowing).to.equal(1) |
93 | expect(data.totalLocalPlaylists).to.equal(0) | ||
88 | }) | 94 | }) |
89 | 95 | ||
90 | it('Should have the correct stats on instance 3', async function () { | 96 | it('Should have the correct stats on instance 3', async function () { |
@@ -99,6 +105,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
99 | expect(data.totalVideos).to.equal(1) | 105 | expect(data.totalVideos).to.equal(1) |
100 | expect(data.totalInstanceFollowing).to.equal(1) | 106 | expect(data.totalInstanceFollowing).to.equal(1) |
101 | expect(data.totalInstanceFollowers).to.equal(0) | 107 | expect(data.totalInstanceFollowers).to.equal(0) |
108 | expect(data.totalLocalPlaylists).to.equal(0) | ||
102 | }) | 109 | }) |
103 | 110 | ||
104 | it('Should have the correct total videos stats after an unfollow', async function () { | 111 | it('Should have the correct total videos stats after an unfollow', async function () { |
@@ -113,7 +120,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
113 | expect(data.totalVideos).to.equal(0) | 120 | expect(data.totalVideos).to.equal(0) |
114 | }) | 121 | }) |
115 | 122 | ||
116 | it('Should have the correct active users stats', async function () { | 123 | it('Should have the correct active user stats', async function () { |
117 | const server = servers[0] | 124 | const server = servers[0] |
118 | 125 | ||
119 | { | 126 | { |
@@ -135,6 +142,69 @@ describe('Test stats (excluding redundancy)', function () { | |||
135 | } | 142 | } |
136 | }) | 143 | }) |
137 | 144 | ||
145 | it('Should have the correct active channel stats', async function () { | ||
146 | const server = servers[0] | ||
147 | |||
148 | { | ||
149 | const res = await getStats(server.url) | ||
150 | const data: ServerStats = res.body | ||
151 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
152 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
153 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const channelAttributes = { | ||
158 | name: 'stats_channel', | ||
159 | displayName: 'My stats channel' | ||
160 | } | ||
161 | const resChannel = await addVideoChannel(server.url, server.accessToken, channelAttributes) | ||
162 | channelId = resChannel.body.videoChannel.id | ||
163 | |||
164 | const res = await getStats(server.url) | ||
165 | const data: ServerStats = res.body | ||
166 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
167 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
168 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
169 | } | ||
170 | |||
171 | { | ||
172 | await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.webm', channelId }) | ||
173 | |||
174 | const res = await getStats(server.url) | ||
175 | const data: ServerStats = res.body | ||
176 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(2) | ||
177 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2) | ||
178 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2) | ||
179 | } | ||
180 | }) | ||
181 | |||
182 | it('Should have the correct playlist stats', async function () { | ||
183 | const server = servers[0] | ||
184 | |||
185 | { | ||
186 | const resStats = await getStats(server.url) | ||
187 | const dataStats: ServerStats = resStats.body | ||
188 | expect(dataStats.totalLocalPlaylists).to.equal(0) | ||
189 | } | ||
190 | |||
191 | { | ||
192 | await createVideoPlaylist({ | ||
193 | url: server.url, | ||
194 | token: server.accessToken, | ||
195 | playlistAttrs: { | ||
196 | displayName: 'playlist for count', | ||
197 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
198 | videoChannelId: channelId | ||
199 | } | ||
200 | }) | ||
201 | |||
202 | const resStats = await getStats(server.url) | ||
203 | const dataStats: ServerStats = resStats.body | ||
204 | expect(dataStats.totalLocalPlaylists).to.equal(1) | ||
205 | } | ||
206 | }) | ||
207 | |||
138 | it('Should correctly count video file sizes if transcoding is enabled', async function () { | 208 | it('Should correctly count video file sizes if transcoding is enabled', async function () { |
139 | this.timeout(60000) | 209 | this.timeout(60000) |
140 | 210 | ||
@@ -173,8 +243,8 @@ describe('Test stats (excluding redundancy)', function () { | |||
173 | { | 243 | { |
174 | const res = await getStats(servers[0].url) | 244 | const res = await getStats(servers[0].url) |
175 | const data: ServerStats = res.body | 245 | const data: ServerStats = res.body |
176 | expect(data.totalLocalVideoFilesSize).to.be.greaterThan(300000) | 246 | expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000) |
177 | expect(data.totalLocalVideoFilesSize).to.be.lessThan(400000) | 247 | expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000) |
178 | } | 248 | } |
179 | }) | 249 | }) |
180 | 250 | ||
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/print-transcode-command.ts b/server/tests/cli/print-transcode-command.ts index 4a7988d4d..2d7255db7 100644 --- a/server/tests/cli/print-transcode-command.ts +++ b/server/tests/cli/print-transcode-command.ts | |||
@@ -22,7 +22,8 @@ describe('Test create transcoding jobs', function () { | |||
22 | const command = await execCLI(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`) | 22 | const command = await execCLI(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`) |
23 | const targetBitrate = Math.min(getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS), bitrate) | 23 | const targetBitrate = Math.min(getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS), bitrate) |
24 | 24 | ||
25 | expect(command).to.includes(`-y -acodec aac -vcodec libx264 -filter:v scale=w=trunc(oh*a/2)*2:h=${resolution}`) | 25 | expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`) |
26 | expect(command).to.includes(`-y -acodec aac -vcodec libx264`) | ||
26 | expect(command).to.includes('-f mp4') | 27 | expect(command).to.includes('-f mp4') |
27 | expect(command).to.includes('-movflags faststart') | 28 | expect(command).to.includes('-movflags faststart') |
28 | expect(command).to.includes('-b:a 256k') | 29 | expect(command).to.includes('-b:a 256k') |
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/client.ts b/server/tests/client.ts index d608764ee..3c99bcd1f 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts | |||
@@ -39,7 +39,8 @@ describe('Test a client controllers', function () { | |||
39 | let account: Account | 39 | let account: Account |
40 | 40 | ||
41 | const videoName = 'my super name for server 1' | 41 | const videoName = 'my super name for server 1' |
42 | const videoDescription = 'my super description for server 1' | 42 | const videoDescription = 'my<br> super __description__ for *server* 1<p></p>' |
43 | const videoDescriptionPlainText = 'my super description for server 1' | ||
43 | 44 | ||
44 | const playlistName = 'super playlist name' | 45 | const playlistName = 'super playlist name' |
45 | const playlistDescription = 'super playlist description' | 46 | const playlistDescription = 'super playlist description' |
@@ -169,7 +170,7 @@ describe('Test a client controllers', function () { | |||
169 | .expect(HttpStatusCode.OK_200) | 170 | .expect(HttpStatusCode.OK_200) |
170 | 171 | ||
171 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) | 172 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) |
172 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescription}" />`) | 173 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) |
173 | expect(res.text).to.contain('<meta property="og:type" content="video" />') | 174 | expect(res.text).to.contain('<meta property="og:type" content="video" />') |
174 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | 175 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) |
175 | }) | 176 | }) |
@@ -181,7 +182,7 @@ describe('Test a client controllers', function () { | |||
181 | .expect(HttpStatusCode.OK_200) | 182 | .expect(HttpStatusCode.OK_200) |
182 | 183 | ||
183 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) | 184 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) |
184 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescription}" />`) | 185 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) |
185 | expect(res.text).to.contain('<meta property="og:type" content="video" />') | 186 | expect(res.text).to.contain('<meta property="og:type" content="video" />') |
186 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | 187 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) |
187 | }) | 188 | }) |
@@ -210,7 +211,7 @@ describe('Test a client controllers', function () { | |||
210 | expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />') | 211 | expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />') |
211 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | 212 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') |
212 | expect(res.text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) | 213 | expect(res.text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) |
213 | expect(res.text).to.contain(`<meta property="twitter:description" content="${videoDescription}" />`) | 214 | expect(res.text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`) |
214 | }) | 215 | }) |
215 | 216 | ||
216 | it('Should have valid twitter card on the watch playlist page', async function () { | 217 | it('Should have valid twitter card on the watch playlist page', async function () { |
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-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js index 8df456c8a..ea0599997 100644 --- a/server/tests/fixtures/peertube-plugin-test-four/main.js +++ b/server/tests/fixtures/peertube-plugin-test-four/main.js | |||
@@ -69,7 +69,20 @@ async function register ({ | |||
69 | res.sendStatus(500) | 69 | res.sendStatus(500) |
70 | } | 70 | } |
71 | }) | 71 | }) |
72 | |||
73 | router.get('/server-config', async (req, res) => { | ||
74 | const serverConfig = await peertubeHelpers.config.getServerConfig() | ||
75 | |||
76 | return res.json({ serverConfig }) | ||
77 | }) | ||
78 | |||
79 | router.get('/static-route', async (req, res) => { | ||
80 | const staticRoute = await peertubeHelpers.plugin.getBaseStaticRoute() | ||
81 | |||
82 | return res.json({ staticRoute }) | ||
83 | }) | ||
72 | } | 84 | } |
85 | |||
73 | } | 86 | } |
74 | 87 | ||
75 | async function unregister () { | 88 | async function unregister () { |
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js index 5990ce1ce..59b136947 100644 --- a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js +++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js | |||
@@ -1,30 +1,88 @@ | |||
1 | async function register ({ transcodingManager }) { | 1 | async function register ({ transcodingManager }) { |
2 | 2 | ||
3 | // Output options | ||
3 | { | 4 | { |
4 | const builder = () => { | 5 | { |
5 | return { | 6 | const builder = () => { |
6 | outputOptions: [ | 7 | return { |
7 | '-r 10' | 8 | outputOptions: [ |
8 | ] | 9 | '-r 10' |
10 | ] | ||
11 | } | ||
9 | } | 12 | } |
13 | |||
14 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) | ||
10 | } | 15 | } |
11 | 16 | ||
12 | transcodingManager.addVODProfile('libx264', 'low-vod', builder) | 17 | { |
18 | const builder = (options) => { | ||
19 | return { | ||
20 | outputOptions: [ | ||
21 | '-r:' + options.streamNum + ' 5' | ||
22 | ] | ||
23 | } | ||
24 | } | ||
25 | |||
26 | transcodingManager.addLiveProfile('libx264', 'low-live', builder) | ||
27 | } | ||
13 | } | 28 | } |
14 | 29 | ||
30 | // Input options | ||
15 | { | 31 | { |
16 | const builder = (options) => { | 32 | { |
17 | return { | 33 | const builder = () => { |
18 | outputOptions: [ | 34 | return { |
19 | '-r:' + options.streamNum + ' 5' | 35 | inputOptions: [ |
20 | ] | 36 | '-r 5' |
37 | ] | ||
38 | } | ||
21 | } | 39 | } |
40 | |||
41 | transcodingManager.addVODProfile('libx264', 'input-options-vod', builder) | ||
22 | } | 42 | } |
23 | 43 | ||
24 | transcodingManager.addLiveProfile('libx264', 'low-live', builder) | 44 | { |
45 | const builder = () => { | ||
46 | return { | ||
47 | inputOptions: [ | ||
48 | '-r 5' | ||
49 | ] | ||
50 | } | ||
51 | } | ||
52 | |||
53 | transcodingManager.addLiveProfile('libx264', 'input-options-live', builder) | ||
54 | } | ||
55 | } | ||
56 | |||
57 | // Scale filters | ||
58 | { | ||
59 | { | ||
60 | const builder = () => { | ||
61 | return { | ||
62 | scaleFilter: { | ||
63 | name: 'Glomgold' | ||
64 | } | ||
65 | } | ||
66 | } | ||
67 | |||
68 | transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder) | ||
69 | } | ||
70 | |||
71 | { | ||
72 | const builder = () => { | ||
73 | return { | ||
74 | scaleFilter: { | ||
75 | name: 'Flintheart' | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | |||
80 | transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder) | ||
81 | } | ||
25 | } | 82 | } |
26 | } | 83 | } |
27 | 84 | ||
85 | |||
28 | async function unregister () { | 86 | async function unregister () { |
29 | return | 87 | return |
30 | } | 88 | } |
diff --git a/server/tests/fixtures/peertube-plugin-test-unloading/lib.js b/server/tests/fixtures/peertube-plugin-test-unloading/lib.js new file mode 100644 index 000000000..f57e7cb01 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-unloading/lib.js | |||
@@ -0,0 +1,2 @@ | |||
1 | const d = new Date() | ||
2 | exports.value = d.getTime() | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-unloading/main.js b/server/tests/fixtures/peertube-plugin-test-unloading/main.js new file mode 100644 index 000000000..5c8457cef --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-unloading/main.js | |||
@@ -0,0 +1,14 @@ | |||
1 | const lib = require('./lib') | ||
2 | |||
3 | async function register ({ getRouter }) { | ||
4 | const router = getRouter() | ||
5 | router.get('/get', (req, res) => res.json({ message: lib.value })) | ||
6 | } | ||
7 | |||
8 | async function unregister () { | ||
9 | } | ||
10 | |||
11 | module.exports = { | ||
12 | register, | ||
13 | unregister | ||
14 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-unloading/package.json b/server/tests/fixtures/peertube-plugin-test-unloading/package.json new file mode 100644 index 000000000..7076d4b6f --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-unloading/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-unloading", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test (modules unloading)", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
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/tests/plugins/index.ts b/server/tests/plugins/index.ts index fd7116efd..4534120fd 100644 --- a/server/tests/plugins/index.ts +++ b/server/tests/plugins/index.ts | |||
@@ -7,5 +7,6 @@ import './plugin-helpers' | |||
7 | import './plugin-router' | 7 | import './plugin-router' |
8 | import './plugin-storage' | 8 | import './plugin-storage' |
9 | import './plugin-transcoding' | 9 | import './plugin-transcoding' |
10 | import './plugin-unloading' | ||
10 | import './translations' | 11 | import './translations' |
11 | import './video-constants' | 12 | import './video-constants' |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index a585e3020..325d20e84 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -12,7 +12,8 @@ import { | |||
12 | uploadVideoAndGetId, | 12 | uploadVideoAndGetId, |
13 | viewVideo, | 13 | viewVideo, |
14 | getVideosList, | 14 | getVideosList, |
15 | waitJobs | 15 | waitJobs, |
16 | makeGetRequest | ||
16 | } from '../../../shared/extra-utils' | 17 | } from '../../../shared/extra-utils' |
17 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' | 18 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' |
18 | import { expect } from 'chai' | 19 | import { expect } from 'chai' |
@@ -68,6 +69,17 @@ describe('Test plugin helpers', function () { | |||
68 | it('Should have the correct webserver url', async function () { | 69 | it('Should have the correct webserver url', async function () { |
69 | await waitUntilLog(servers[0], `server url is http://localhost:${servers[0].port}`) | 70 | await waitUntilLog(servers[0], `server url is http://localhost:${servers[0].port}`) |
70 | }) | 71 | }) |
72 | |||
73 | it('Should have the correct config', async function () { | ||
74 | const res = await makeGetRequest({ | ||
75 | url: servers[0].url, | ||
76 | path: '/plugins/test-four/router/server-config', | ||
77 | statusCodeExpected: HttpStatusCode.OK_200 | ||
78 | }) | ||
79 | |||
80 | expect(res.body.serverConfig).to.exist | ||
81 | expect(res.body.serverConfig.instance.name).to.equal('PeerTube') | ||
82 | }) | ||
71 | }) | 83 | }) |
72 | 84 | ||
73 | describe('Server', function () { | 85 | describe('Server', function () { |
@@ -77,6 +89,19 @@ describe('Test plugin helpers', function () { | |||
77 | }) | 89 | }) |
78 | }) | 90 | }) |
79 | 91 | ||
92 | describe('Plugin', function () { | ||
93 | |||
94 | it('Should get the base static route', async function () { | ||
95 | const res = await makeGetRequest({ | ||
96 | url: servers[0].url, | ||
97 | path: '/plugins/test-four/router/static-route', | ||
98 | statusCodeExpected: HttpStatusCode.OK_200 | ||
99 | }) | ||
100 | |||
101 | expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/') | ||
102 | }) | ||
103 | }) | ||
104 | |||
80 | describe('Moderation', function () { | 105 | describe('Moderation', function () { |
81 | let videoUUIDServer1: string | 106 | let videoUUIDServer1: string |
82 | 107 | ||
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts index ecea21e69..c834b6985 100644 --- a/server/tests/plugins/plugin-transcoding.ts +++ b/server/tests/plugins/plugin-transcoding.ts | |||
@@ -15,6 +15,7 @@ import { | |||
15 | sendRTMPStreamInVideo, | 15 | sendRTMPStreamInVideo, |
16 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | setDefaultVideoChannel, | 17 | setDefaultVideoChannel, |
18 | testFfmpegStreamError, | ||
18 | uninstallPlugin, | 19 | uninstallPlugin, |
19 | updateCustomSubConfig, | 20 | updateCustomSubConfig, |
20 | uploadVideoAndGetId, | 21 | uploadVideoAndGetId, |
@@ -119,8 +120,8 @@ describe('Test transcoding plugins', function () { | |||
119 | const res = await getConfig(server.url) | 120 | const res = await getConfig(server.url) |
120 | const config = res.body as ServerConfig | 121 | const config = res.body as ServerConfig |
121 | 122 | ||
122 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod' ]) | 123 | expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ]) |
123 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live' ]) | 124 | expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live', 'bad-scale-live' ]) |
124 | }) | 125 | }) |
125 | 126 | ||
126 | it('Should not use the plugin profile if not chosen by the admin', async function () { | 127 | it('Should not use the plugin profile if not chosen by the admin', async function () { |
@@ -143,6 +144,33 @@ describe('Test transcoding plugins', function () { | |||
143 | await checkVideoFPS(videoUUID, 'below', 12) | 144 | await checkVideoFPS(videoUUID, 'below', 12) |
144 | }) | 145 | }) |
145 | 146 | ||
147 | it('Should apply input options in vod profile', async function () { | ||
148 | this.timeout(120000) | ||
149 | |||
150 | await updateConf(server, 'input-options-vod', 'default') | ||
151 | |||
152 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
153 | await waitJobs([ server ]) | ||
154 | |||
155 | await checkVideoFPS(videoUUID, 'below', 6) | ||
156 | }) | ||
157 | |||
158 | it('Should apply the scale filter in vod profile', async function () { | ||
159 | this.timeout(120000) | ||
160 | |||
161 | await updateConf(server, 'bad-scale-vod', 'default') | ||
162 | |||
163 | const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid | ||
164 | await waitJobs([ server ]) | ||
165 | |||
166 | // Transcoding failed | ||
167 | const res = await getVideo(server.url, videoUUID) | ||
168 | const video: VideoDetails = res.body | ||
169 | |||
170 | expect(video.files).to.have.lengthOf(1) | ||
171 | expect(video.streamingPlaylists).to.have.lengthOf(0) | ||
172 | }) | ||
173 | |||
146 | it('Should not use the plugin profile if not chosen by the admin', async function () { | 174 | it('Should not use the plugin profile if not chosen by the admin', async function () { |
147 | this.timeout(120000) | 175 | this.timeout(120000) |
148 | 176 | ||
@@ -169,6 +197,31 @@ describe('Test transcoding plugins', function () { | |||
169 | await checkLiveFPS(liveVideoId, 'below', 12) | 197 | await checkLiveFPS(liveVideoId, 'below', 12) |
170 | }) | 198 | }) |
171 | 199 | ||
200 | it('Should apply the input options on live profile', async function () { | ||
201 | this.timeout(120000) | ||
202 | |||
203 | await updateConf(server, 'low-vod', 'input-options-live') | ||
204 | |||
205 | const liveVideoId = await createLiveWrapper(server) | ||
206 | |||
207 | await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
208 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoId) | ||
209 | await waitJobs([ server ]) | ||
210 | |||
211 | await checkLiveFPS(liveVideoId, 'below', 6) | ||
212 | }) | ||
213 | |||
214 | it('Should apply the scale filter name on live profile', async function () { | ||
215 | this.timeout(120000) | ||
216 | |||
217 | await updateConf(server, 'low-vod', 'bad-scale-live') | ||
218 | |||
219 | const liveVideoId = await createLiveWrapper(server) | ||
220 | |||
221 | const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm') | ||
222 | await testFfmpegStreamError(command, true) | ||
223 | }) | ||
224 | |||
172 | it('Should default to the default profile if the specified profile does not exist', async function () { | 225 | it('Should default to the default profile if the specified profile does not exist', async function () { |
173 | this.timeout(120000) | 226 | this.timeout(120000) |
174 | 227 | ||
diff --git a/server/tests/plugins/plugin-unloading.ts b/server/tests/plugins/plugin-unloading.ts new file mode 100644 index 000000000..74ca82e2f --- /dev/null +++ b/server/tests/plugins/plugin-unloading.ts | |||
@@ -0,0 +1,89 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | flushAndRunServer, | ||
7 | getPluginTestPath, | ||
8 | makeGetRequest, | ||
9 | installPlugin, | ||
10 | uninstallPlugin, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers | ||
13 | } from '../../../shared/extra-utils' | ||
14 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
15 | import { expect } from 'chai' | ||
16 | |||
17 | describe('Test plugins module unloading', function () { | ||
18 | let server: ServerInfo = null | ||
19 | const requestPath = '/plugins/test-unloading/router/get' | ||
20 | let value: string = null | ||
21 | |||
22 | before(async function () { | ||
23 | this.timeout(30000) | ||
24 | |||
25 | server = await flushAndRunServer(1) | ||
26 | await setAccessTokensToServers([ server ]) | ||
27 | |||
28 | await installPlugin({ | ||
29 | url: server.url, | ||
30 | accessToken: server.accessToken, | ||
31 | path: getPluginTestPath('-unloading') | ||
32 | }) | ||
33 | }) | ||
34 | |||
35 | it('Should return a numeric value', async function () { | ||
36 | const res = await makeGetRequest({ | ||
37 | url: server.url, | ||
38 | path: requestPath, | ||
39 | statusCodeExpected: HttpStatusCode.OK_200 | ||
40 | }) | ||
41 | |||
42 | expect(res.body.message).to.match(/^\d+$/) | ||
43 | value = res.body.message | ||
44 | }) | ||
45 | |||
46 | it('Should return the same value the second time', async function () { | ||
47 | const res = await makeGetRequest({ | ||
48 | url: server.url, | ||
49 | path: requestPath, | ||
50 | statusCodeExpected: HttpStatusCode.OK_200 | ||
51 | }) | ||
52 | |||
53 | expect(res.body.message).to.be.equal(value) | ||
54 | }) | ||
55 | |||
56 | it('Should uninstall the plugin and free the route', async function () { | ||
57 | await uninstallPlugin({ | ||
58 | url: server.url, | ||
59 | accessToken: server.accessToken, | ||
60 | npmName: 'peertube-plugin-test-unloading' | ||
61 | }) | ||
62 | |||
63 | await makeGetRequest({ | ||
64 | url: server.url, | ||
65 | path: requestPath, | ||
66 | statusCodeExpected: HttpStatusCode.NOT_FOUND_404 | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | it('Should return a different numeric value', async function () { | ||
71 | await installPlugin({ | ||
72 | url: server.url, | ||
73 | accessToken: server.accessToken, | ||
74 | path: getPluginTestPath('-unloading') | ||
75 | }) | ||
76 | const res = await makeGetRequest({ | ||
77 | url: server.url, | ||
78 | path: requestPath, | ||
79 | statusCodeExpected: HttpStatusCode.OK_200 | ||
80 | }) | ||
81 | |||
82 | expect(res.body.message).to.match(/^\d+$/) | ||
83 | expect(res.body.message).to.be.not.equal(value) | ||
84 | }) | ||
85 | |||
86 | after(async function () { | ||
87 | await cleanupTests([ server ]) | ||
88 | }) | ||
89 | }) | ||
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/tools/yarn.lock b/server/tools/yarn.lock index 168b93bd6..065e32e3d 100644 --- a/server/tools/yarn.lock +++ b/server/tools/yarn.lock | |||
@@ -15,9 +15,9 @@ | |||
15 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== | 15 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== |
16 | 16 | ||
17 | "@babel/highlight@^7.12.13": | 17 | "@babel/highlight@^7.12.13": |
18 | version "7.12.13" | 18 | version "7.13.10" |
19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" | 19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" |
20 | integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== | 20 | integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== |
21 | dependencies: | 21 | dependencies: |
22 | "@babel/helper-validator-identifier" "^7.12.11" | 22 | "@babel/helper-validator-identifier" "^7.12.11" |
23 | chalk "^2.0.0" | 23 | chalk "^2.0.0" |
@@ -82,9 +82,9 @@ | |||
82 | integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== | 82 | integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== |
83 | 83 | ||
84 | "@types/node@^13.7.0": | 84 | "@types/node@^13.7.0": |
85 | version "13.13.40" | 85 | version "13.13.48" |
86 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.40.tgz#f655ef327362cc83912f2e69336ddc62a24a9f88" | 86 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.48.tgz#46a3df718aed5217277f2395a682e055a487e341" |
87 | integrity sha512-eKaRo87lu1yAXrzEJl0zcJxfUMDT5/mZalFyOkT44rnQps41eS2pfWzbaulSPpQLFNy29bFqn+Y5lOTL8ATlEQ== | 87 | integrity sha512-z8wvSsgWQzkr4sVuMEEOvwMdOQjiRY2Y/ZW4fDfjfe3+TfQrZqFKOthBgk2RnVEmtOKrkwdZ7uTvsxTBLjKGDQ== |
88 | 88 | ||
89 | abbrev@1: | 89 | abbrev@1: |
90 | version "1.1.1" | 90 | version "1.1.1" |
@@ -154,9 +154,9 @@ are-we-there-yet@~1.1.2: | |||
154 | readable-stream "^2.0.6" | 154 | readable-stream "^2.0.6" |
155 | 155 | ||
156 | balanced-match@^1.0.0: | 156 | balanced-match@^1.0.0: |
157 | version "1.0.0" | 157 | version "1.0.2" |
158 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" | 158 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" |
159 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= | 159 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== |
160 | 160 | ||
161 | base64-js@^1.3.1: | 161 | base64-js@^1.3.1: |
162 | version "1.5.1" | 162 | version "1.5.1" |
@@ -214,22 +214,22 @@ bittorrent-peerid@^1.3.2: | |||
214 | integrity sha512-tSh9HdQgwyEAfo1jzoGEis6o/zs4CcdRTchG93XVl5jct+DCAN90M5MVUV76k2vJ9Xg3GAzLB5NLsY/vnVTh6w== | 214 | integrity sha512-tSh9HdQgwyEAfo1jzoGEis6o/zs4CcdRTchG93XVl5jct+DCAN90M5MVUV76k2vJ9Xg3GAzLB5NLsY/vnVTh6w== |
215 | 215 | ||
216 | bittorrent-protocol@^3.2.0: | 216 | bittorrent-protocol@^3.2.0: |
217 | version "3.2.0" | 217 | version "3.3.1" |
218 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.2.0.tgz#fe423e31b4752def5da0ffaf321102a7b163268b" | 218 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.3.1.tgz#b7a8e66babc423c1eb8e379c1cf7ded26a400a73" |
219 | integrity sha512-8xgCgK8xUg7MXZBxhqJapZy9hexHwN2TCD+b8m4yWg56/ZFfdkULxhxQkzuhpXyUfvKIKxeRzGmLT43li7FDKg== | 219 | integrity sha512-DJy0/jjqJD62PPJY79duCccmPMihp3KPowlmd7BLEU8FTtnDsYjso6BAx+pWwCKOeDORdc9RiJ7L72x3taCh6g== |
220 | dependencies: | 220 | dependencies: |
221 | bencode "^2.0.1" | 221 | bencode "^2.0.1" |
222 | bitfield "^4.0.0" | 222 | bitfield "^4.0.0" |
223 | debug "^4.2.0" | 223 | debug "^4.3.1" |
224 | randombytes "^2.1.0" | 224 | randombytes "^2.1.0" |
225 | readable-stream "^3.6.0" | 225 | readable-stream "^3.6.0" |
226 | speedometer "^1.1.0" | 226 | speedometer "^1.1.0" |
227 | unordered-array-remove "^1.0.2" | 227 | unordered-array-remove "^1.0.2" |
228 | 228 | ||
229 | bittorrent-tracker@^9.0.0: | 229 | bittorrent-tracker@^9.0.0: |
230 | version "9.16.0" | 230 | version "9.17.0" |
231 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.16.0.tgz#88a4dffa28875f08cecebbcedf1f830e10544005" | 231 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.0.tgz#8b4b6f6a49efa9023267c3ca22e1a5f63216fc1f" |
232 | integrity sha512-6VEaW1rYA6f8H2VATlrXrTtgqB4WF7+BMIuVMsoLPgdK43d5VDPdI/ky9gpeRHcY9qmVJv+kHfkwH8G16sNbNw== | 232 | integrity sha512-ErpOx8AAUW8eLwxnEHp15vs0LDJECLADHISEBM+HXclG3J2/9kMBJ31IjwlB8kUNigknSwm8odAThjJEeyL1yA== |
233 | dependencies: | 233 | dependencies: |
234 | bencode "^2.0.1" | 234 | bencode "^2.0.1" |
235 | bittorrent-peerid "^1.3.2" | 235 | bittorrent-peerid "^1.3.2" |
@@ -241,6 +241,7 @@ bittorrent-tracker@^9.0.0: | |||
241 | lru "^3.1.0" | 241 | lru "^3.1.0" |
242 | minimist "^1.2.5" | 242 | minimist "^1.2.5" |
243 | once "^1.4.0" | 243 | once "^1.4.0" |
244 | queue-microtask "^1.2.2" | ||
244 | random-iterate "^1.0.1" | 245 | random-iterate "^1.0.1" |
245 | randombytes "^2.1.0" | 246 | randombytes "^2.1.0" |
246 | run-parallel "^1.1.9" | 247 | run-parallel "^1.1.9" |
@@ -260,17 +261,17 @@ blob-to-buffer@^1.2.6, blob-to-buffer@^1.2.9: | |||
260 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" | 261 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" |
261 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== | 262 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== |
262 | 263 | ||
263 | block-stream2@^2.0.0: | 264 | block-stream2@^2.0.0, block-stream2@^2.1.0: |
264 | version "2.0.0" | 265 | version "2.1.0" |
265 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-2.0.0.tgz#680b9d357ca8b9d5637f4ec8a41fb5968029108f" | 266 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-2.1.0.tgz#ac0c5ef4298b3857796e05be8ebed72196fa054b" |
266 | integrity sha512-1oI+RHHUEo64xomy1ozLgVJetFlHkIfQfJzTBQrj6xWnEMEPooeo2fZoqFjp0yzfHMBrgxwgh70tKp6T17+i3g== | 267 | integrity sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg== |
267 | dependencies: | 268 | dependencies: |
268 | readable-stream "^3.4.0" | 269 | readable-stream "^3.4.0" |
269 | 270 | ||
270 | bn.js@^5.1.1: | 271 | bn.js@^5.1.1: |
271 | version "5.1.3" | 272 | version "5.2.0" |
272 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" | 273 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" |
273 | integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== | 274 | integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== |
274 | 275 | ||
275 | brace-expansion@^1.1.7: | 276 | brace-expansion@^1.1.7: |
276 | version "1.1.11" | 277 | version "1.1.11" |
@@ -313,7 +314,7 @@ buffer-indexof@^1.0.0: | |||
313 | resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" | 314 | resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" |
314 | integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== | 315 | integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== |
315 | 316 | ||
316 | buffer@^6.0.2: | 317 | buffer@^6.0.3: |
317 | version "6.0.3" | 318 | version "6.0.3" |
318 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" | 319 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" |
319 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== | 320 | integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== |
@@ -401,10 +402,10 @@ chromecasts@^1.9.1: | |||
401 | optionalDependencies: | 402 | optionalDependencies: |
402 | node-ssdp "^2.2.0" | 403 | node-ssdp "^2.2.0" |
403 | 404 | ||
404 | chunk-store-stream@^4.1.1: | 405 | chunk-store-stream@^4.2.0: |
405 | version "4.1.1" | 406 | version "4.3.0" |
406 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.1.1.tgz#7c86a50f789c90b3eb3bb752f67eea1d16738b8e" | 407 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" |
407 | integrity sha512-2nVS+VLMCHNW+S1Y3bOlCtUL4ABPA4zeAcP8E/m15AAc+8fw1elEKIhs/Wg5EZNSfqIk24kUd9rvewmGNPehGQ== | 408 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== |
408 | dependencies: | 409 | dependencies: |
409 | block-stream2 "^2.0.0" | 410 | block-stream2 "^2.0.0" |
410 | readable-stream "^3.6.0" | 411 | readable-stream "^3.6.0" |
@@ -483,13 +484,18 @@ core-util-is@~1.0.0: | |||
483 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" | 484 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" |
484 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= | 485 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= |
485 | 486 | ||
486 | create-torrent@^4.4.2: | 487 | cpus@^1.0.3: |
487 | version "4.4.3" | 488 | version "1.0.3" |
488 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-4.4.3.tgz#b6159cf6dde8fce3fdeac34d3e78466b5fbddfe8" | 489 | resolved "https://registry.yarnpkg.com/cpus/-/cpus-1.0.3.tgz#4ef6deea461968d6329d07dd01205685df2934a2" |
489 | integrity sha512-balXwhJEpD8F0rt1vNKJuxhYPWOHo82LVm50wRzUFVzZ8APciwtZntzIzDEX6dNnD4NoLL0AeYgosF7e+FVQBA== | 490 | integrity sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA== |
491 | |||
492 | create-torrent@^4.4.2, create-torrent@^4.4.4: | ||
493 | version "4.7.0" | ||
494 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-4.7.0.tgz#ba5d52d41e7621d0d61c895c8026d3fb22aa4333" | ||
495 | integrity sha512-Pb3XjZNKdCs0Nk46yFKb82y+a3xRQeMvGi1AlJfIV40y/iwkgBqzS5EfqdnakEOvh2jzTOx3v8QxZpkz4hPzyw== | ||
490 | dependencies: | 496 | dependencies: |
491 | bencode "^2.0.1" | 497 | bencode "^2.0.1" |
492 | block-stream2 "^2.0.0" | 498 | block-stream2 "^2.1.0" |
493 | filestream "^5.0.0" | 499 | filestream "^5.0.0" |
494 | is-file "^1.0.0" | 500 | is-file "^1.0.0" |
495 | junk "^3.1.0" | 501 | junk "^3.1.0" |
@@ -497,6 +503,7 @@ create-torrent@^4.4.2: | |||
497 | multistream "^4.0.1" | 503 | multistream "^4.0.1" |
498 | once "^1.4.0" | 504 | once "^1.4.0" |
499 | piece-length "^2.0.1" | 505 | piece-length "^2.0.1" |
506 | queue-microtask "^1.2.2" | ||
500 | readable-stream "^3.6.0" | 507 | readable-stream "^3.6.0" |
501 | run-parallel "^1.1.10" | 508 | run-parallel "^1.1.10" |
502 | simple-sha1 "^3.0.1" | 509 | simple-sha1 "^3.0.1" |
@@ -639,24 +646,17 @@ emoji-regex@^8.0.0: | |||
639 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" | 646 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" |
640 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== | 647 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== |
641 | 648 | ||
642 | end-of-stream@1.4.1: | 649 | end-of-stream@^1.1.0, end-of-stream@^1.4.4: |
643 | version "1.4.1" | ||
644 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" | ||
645 | integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== | ||
646 | dependencies: | ||
647 | once "^1.4.0" | ||
648 | |||
649 | end-of-stream@^1.1.0: | ||
650 | version "1.4.4" | 650 | version "1.4.4" |
651 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" | 651 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" |
652 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== | 652 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== |
653 | dependencies: | 653 | dependencies: |
654 | once "^1.4.0" | 654 | once "^1.4.0" |
655 | 655 | ||
656 | err-code@^2.0.3: | 656 | err-code@^3.0.1: |
657 | version "2.0.3" | 657 | version "3.0.1" |
658 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" | 658 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" |
659 | integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== | 659 | integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== |
660 | 660 | ||
661 | error-ex@^1.3.1: | 661 | error-ex@^1.3.1: |
662 | version "1.3.2" | 662 | version "1.3.2" |
@@ -709,10 +709,11 @@ freelist@^1.0.3: | |||
709 | integrity sha1-AGd1UJ85NXAXhNPtL8nxLJ3xurI= | 709 | integrity sha1-AGd1UJ85NXAXhNPtL8nxLJ3xurI= |
710 | 710 | ||
711 | fs-chunk-store@^2.0.2: | 711 | fs-chunk-store@^2.0.2: |
712 | version "2.0.2" | 712 | version "2.0.3" |
713 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.2.tgz#8fa9b29dca98c782c5332949081ee29849747597" | 713 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.3.tgz#21e51f1833a84a07cb5e911d058dae084030375a" |
714 | integrity sha512-4nSngo6KyoGguscb5sTxjcxOvtRiMxuQl4vFK9SnYYMjmy08Nm095ACoM3lxj1KmCuxO4JMnuo4EebMWHlagjQ== | 714 | integrity sha512-qQi93nHX3880gtoQPt1hKQcuYBNVfCbMk8OVRDqR0cJ0riheELW25ry9yl7pII8E9gOAONTGKBD5N/zGHFSVQg== |
715 | dependencies: | 715 | dependencies: |
716 | queue-microtask "^1.2.2" | ||
716 | random-access-file "^2.0.1" | 717 | random-access-file "^2.0.1" |
717 | randombytes "^2.0.3" | 718 | randombytes "^2.0.3" |
718 | rimraf "^3.0.0" | 719 | rimraf "^3.0.0" |
@@ -745,7 +746,7 @@ gauge@~2.7.3: | |||
745 | strip-ansi "^3.0.1" | 746 | strip-ansi "^3.0.1" |
746 | wide-align "^1.1.0" | 747 | wide-align "^1.1.0" |
747 | 748 | ||
748 | get-browser-rtc@^1.0.2: | 749 | get-browser-rtc@^1.1.0: |
749 | version "1.1.0" | 750 | version "1.1.0" |
750 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" | 751 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" |
751 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== | 752 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== |
@@ -778,9 +779,9 @@ glob@^7.1.3: | |||
778 | path-is-absolute "^1.0.0" | 779 | path-is-absolute "^1.0.0" |
779 | 780 | ||
780 | graceful-fs@^4.1.15: | 781 | graceful-fs@^4.1.15: |
781 | version "4.2.4" | 782 | version "4.2.6" |
782 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" | 783 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" |
783 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== | 784 | integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== |
784 | 785 | ||
785 | has-flag@^3.0.0: | 786 | has-flag@^3.0.0: |
786 | version "3.0.0" | 787 | version "3.0.0" |
@@ -892,9 +893,9 @@ is-ascii@^1.0.0: | |||
892 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= | 893 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= |
893 | 894 | ||
894 | is-docker@^2.0.0: | 895 | is-docker@^2.0.0: |
895 | version "2.1.1" | 896 | version "2.2.1" |
896 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" | 897 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" |
897 | integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== | 898 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== |
898 | 899 | ||
899 | is-file@^1.0.0: | 900 | is-file@^1.0.0: |
900 | version "1.0.0" | 901 | version "1.0.0" |
@@ -966,11 +967,11 @@ junk@^3.1.0: | |||
966 | integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== | 967 | integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== |
967 | 968 | ||
968 | k-bucket@^5.0.0: | 969 | k-bucket@^5.0.0: |
969 | version "5.0.0" | 970 | version "5.1.0" |
970 | resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.0.0.tgz#ef7a401fcd4c37cd31dceaa6ae4440ca91055e01" | 971 | resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-5.1.0.tgz#db2c9e72bd168b432e3f3e8fc092e2ccb61bff89" |
971 | integrity sha512-r/q+wV/Kde62/tk+rqyttEJn6h0jR7x+incdMVSYTqK73zVxVrzJa70kJL49cIKen8XjIgUZKSvk8ktnrQbK4w== | 972 | integrity sha512-Fac7iINEovXIWU20GPnOMLUbjctiS+cnmyjC4zAUgvs3XPf1vo9akfCHkigftSic/jiKqKl+KA3a/vFcJbHyCg== |
972 | dependencies: | 973 | dependencies: |
973 | randombytes "^2.0.3" | 974 | randombytes "^2.1.0" |
974 | 975 | ||
975 | k-rpc-socket@^1.7.2: | 976 | k-rpc-socket@^1.7.2: |
976 | version "1.11.1" | 977 | version "1.11.1" |
@@ -1002,12 +1003,12 @@ lines-and-columns@^1.1.6: | |||
1002 | integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= | 1003 | integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= |
1003 | 1004 | ||
1004 | load-ip-set@^2.1.2: | 1005 | load-ip-set@^2.1.2: |
1005 | version "2.1.2" | 1006 | version "2.2.1" |
1006 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.1.2.tgz#6fdb827eae2862ba999868199acc86be987ea2e7" | 1007 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.2.1.tgz#9496ab8aa14ebf81aeb7c8bb38e7abdf50af3563" |
1007 | integrity sha512-AgO3AGwWPru+tJboq0l2+hIkFqbAE3tbG74z2SCpt+c3h0PFI3t2mHamS707OzSc3b+GBVBKjZxPFz97YZNisA== | 1008 | integrity sha512-G3hQXehU2LTOp52e+lPffpK4EvidfjwbvHaGqmFcp4ptiZagR4xFdL+D08kMX906dxeqZyWhfonEjdUxrWcldg== |
1008 | dependencies: | 1009 | dependencies: |
1009 | ip-set "^2.1.0" | 1010 | ip-set "^2.1.0" |
1010 | netmask "^1.0.6" | 1011 | netmask "^2.0.1" |
1011 | once "^1.4.0" | 1012 | once "^1.4.0" |
1012 | simple-get "^4.0.0" | 1013 | simple-get "^4.0.0" |
1013 | split "^1.0.1" | 1014 | split "^1.0.1" |
@@ -1083,20 +1084,22 @@ mediasource@^2.2.2, mediasource@^2.4.0: | |||
1083 | readable-stream "^3.6.0" | 1084 | readable-stream "^3.6.0" |
1084 | to-arraybuffer "^1.0.1" | 1085 | to-arraybuffer "^1.0.1" |
1085 | 1086 | ||
1086 | memory-chunk-store@^1.3.0: | 1087 | memory-chunk-store@^1.3.1: |
1087 | version "1.3.0" | 1088 | version "1.3.2" |
1088 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.0.tgz#ae99e7e3b58b52db43d49d94722930d39459d0c4" | 1089 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.2.tgz#3bde573c957c0260d8116e6e2c0ce62ff2032894" |
1089 | integrity sha512-6LsOpHKKhxYrLhHmOJdBCUtSO7op5rUs1pag0fhjHo0QiXRyna0bwYf4EmQuL7InUeF2J7dUMPr6VMogRyf9NA== | 1090 | integrity sha512-EBcbwpdQlzT5aNV0FTT+RAfh1cGEssjiCcRGcTk57mKsnZlRMOtH4Cfk/AqQnkz8xP2dUF+/lgpmErSGwwE1FA== |
1091 | dependencies: | ||
1092 | queue-microtask "^1.2.2" | ||
1090 | 1093 | ||
1091 | mime@^1.3.4: | 1094 | mime@^1.3.4: |
1092 | version "1.6.0" | 1095 | version "1.6.0" |
1093 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 1096 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
1094 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 1097 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
1095 | 1098 | ||
1096 | mime@^2.4.1, mime@^2.4.6: | 1099 | mime@^2.4.1, mime@^2.4.6, mime@^2.5.0: |
1097 | version "2.5.0" | 1100 | version "2.5.2" |
1098 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" | 1101 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" |
1099 | integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== | 1102 | integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== |
1100 | 1103 | ||
1101 | mimic-response@^1.0.0: | 1104 | mimic-response@^1.0.0: |
1102 | version "1.0.1" | 1105 | version "1.0.1" |
@@ -1165,12 +1168,13 @@ mp4-box-encoding@^1.3.0: | |||
1165 | uint64be "^2.0.2" | 1168 | uint64be "^2.0.2" |
1166 | 1169 | ||
1167 | mp4-stream@^3.0.0: | 1170 | mp4-stream@^3.0.0: |
1168 | version "3.1.0" | 1171 | version "3.1.3" |
1169 | resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-3.1.0.tgz#7a0800b50759b28fa4757cdb4ab6a49517543cd7" | 1172 | resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-3.1.3.tgz#79b8a19900337203a9bd607a02eccc64419a379c" |
1170 | integrity sha512-ZQQjf0VEiqPucwRvmT3e0pfZfMSE3nc5ngGUiN1+2VMxCtrInrlAjZ2K6jpNmxSZ/roiQne/ovYJYTeOvZDXPw== | 1173 | integrity sha512-DUT8f0x2jHbZjNMdqe9h6lZdt6RENWTTdGn8z3TXa4uEsoltuNY9lCCij84mdm0q7xcV0E2W25WRxlKBMo4hSw== |
1171 | dependencies: | 1174 | dependencies: |
1172 | mp4-box-encoding "^1.3.0" | 1175 | mp4-box-encoding "^1.3.0" |
1173 | next-event "^1.0.0" | 1176 | next-event "^1.0.0" |
1177 | queue-microtask "^1.2.2" | ||
1174 | readable-stream "^3.0.6" | 1178 | readable-stream "^3.0.6" |
1175 | 1179 | ||
1176 | ms@2.0.0: | 1180 | ms@2.0.0: |
@@ -1196,11 +1200,12 @@ multicast-dns@^6.0.1: | |||
1196 | dns-packet "^1.3.1" | 1200 | dns-packet "^1.3.1" |
1197 | thunky "^1.0.2" | 1201 | thunky "^1.0.2" |
1198 | 1202 | ||
1199 | multistream@^4.0.1: | 1203 | multistream@^4.0.1, multistream@^4.1.0: |
1200 | version "4.0.1" | 1204 | version "4.1.0" |
1201 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.0.1.tgz#6c188a40b66ab0205c9e9f560a5348f13fe1f82a" | 1205 | resolved "https://registry.yarnpkg.com/multistream/-/multistream-4.1.0.tgz#7bf00dfd119556fbc153cff3de4c6d477909f5a8" |
1202 | integrity sha512-LNPIR/LD0JUw2beGlSv4sgTSnGbZp16d/PG2rnIrYjkeCaepNmBTobuiaNQATCPiYgn+BBuQTm70UlvwRfLZ3Q== | 1206 | integrity sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw== |
1203 | dependencies: | 1207 | dependencies: |
1208 | once "^1.4.0" | ||
1204 | readable-stream "^3.6.0" | 1209 | readable-stream "^3.6.0" |
1205 | 1210 | ||
1206 | napi-macros@^2.0.0: | 1211 | napi-macros@^2.0.0: |
@@ -1217,10 +1222,10 @@ needle@^2.2.1: | |||
1217 | iconv-lite "^0.4.4" | 1222 | iconv-lite "^0.4.4" |
1218 | sax "^1.2.4" | 1223 | sax "^1.2.4" |
1219 | 1224 | ||
1220 | netmask@^1.0.6: | 1225 | netmask@^2.0.1: |
1221 | version "1.0.6" | 1226 | version "2.0.2" |
1222 | resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" | 1227 | resolved "https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7" |
1223 | integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU= | 1228 | integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg== |
1224 | 1229 | ||
1225 | netrc-parser@^3.1.6: | 1230 | netrc-parser@^3.1.6: |
1226 | version "3.1.6" | 1231 | version "3.1.6" |
@@ -1352,9 +1357,9 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: | |||
1352 | wrappy "1" | 1357 | wrappy "1" |
1353 | 1358 | ||
1354 | open@^7.1.0: | 1359 | open@^7.1.0: |
1355 | version "7.4.0" | 1360 | version "7.4.2" |
1356 | resolved "https://registry.yarnpkg.com/open/-/open-7.4.0.tgz#ad95b98f871d9acb0ec8fecc557082cc9986626b" | 1361 | resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" |
1357 | integrity sha512-PGoBCX/lclIWlpS/R2PQuIR4NJoXh6X5AwVzE7WXnWRGvHg7+4TBCgsujUgiPpm0K1y4qvQeWnCWVTpTKZBtvA== | 1362 | integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== |
1358 | dependencies: | 1363 | dependencies: |
1359 | is-docker "^2.0.0" | 1364 | is-docker "^2.0.0" |
1360 | is-wsl "^2.1.1" | 1365 | is-wsl "^2.1.1" |
@@ -1411,15 +1416,16 @@ parse-torrent@^7.1.3: | |||
1411 | simple-get "^3.0.1" | 1416 | simple-get "^3.0.1" |
1412 | simple-sha1 "^3.0.0" | 1417 | simple-sha1 "^3.0.0" |
1413 | 1418 | ||
1414 | parse-torrent@^9.1.0: | 1419 | parse-torrent@^9.1.1: |
1415 | version "9.1.1" | 1420 | version "9.1.3" |
1416 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.1.tgz#01bbaba8151412b25d3fc0c9b10512cf2dda7e4d" | 1421 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.3.tgz#9b4bc8dca243b356bf449938d6d38a259a2a707c" |
1417 | integrity sha512-QQePPIN0cX6bEjwAT6x2o5fUgOsz2SwXlZqzz2uvt12+uiCrgU4WYAsy3wEthQWLZQpn1utMbSbHCRDvjAnhMA== | 1422 | integrity sha512-/Yr951CvJM8S6TjMaqrsmMxeQEAjDeCX+MZ3hGXXc7DG2wqzp/rzOsHtDzIVqN6NsFRCqy6wYLF/W7Sgvq7bXw== |
1418 | dependencies: | 1423 | dependencies: |
1419 | bencode "^2.0.1" | 1424 | bencode "^2.0.1" |
1420 | blob-to-buffer "^1.2.9" | 1425 | blob-to-buffer "^1.2.9" |
1421 | get-stdin "^8.0.0" | 1426 | get-stdin "^8.0.0" |
1422 | magnet-uri "^6.0.0" | 1427 | magnet-uri "^6.0.0" |
1428 | queue-microtask "^1.2.2" | ||
1423 | simple-get "^4.0.0" | 1429 | simple-get "^4.0.0" |
1424 | simple-sha1 "^3.0.1" | 1430 | simple-sha1 "^3.0.1" |
1425 | 1431 | ||
@@ -1493,15 +1499,15 @@ qap@^3.1.2: | |||
1493 | resolved "https://registry.yarnpkg.com/qap/-/qap-3.3.1.tgz#11f9e8fa8890fe7cb99210c0f44d0613b7372cac" | 1499 | resolved "https://registry.yarnpkg.com/qap/-/qap-3.3.1.tgz#11f9e8fa8890fe7cb99210c0f44d0613b7372cac" |
1494 | integrity sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw= | 1500 | integrity sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw= |
1495 | 1501 | ||
1496 | queue-microtask@^1.1.2, queue-microtask@^1.2.0, queue-microtask@^1.2.2: | 1502 | queue-microtask@^1.2.0, queue-microtask@^1.2.2, queue-microtask@^1.2.3: |
1497 | version "1.2.2" | 1503 | version "1.2.3" |
1498 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" | 1504 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
1499 | integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== | 1505 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
1500 | 1506 | ||
1501 | random-access-file@^2.0.1: | 1507 | random-access-file@^2.0.1: |
1502 | version "2.1.5" | 1508 | version "2.2.0" |
1503 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.5.tgz#27af6115b920a9adabb44559e29ea9944bb35bfe" | 1509 | resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.0.tgz#b49b999efefb374afb7587f219071fec5ce66546" |
1504 | integrity sha512-lqmUGgF9X+LD0XSeWSHcs7U2nSLYp+RQvkDDqKWoxW8jcd13tZ00G6PHV32OZqDIHmS9ewoEUEa6jcvyB7UCvg== | 1510 | integrity sha512-B744003Mj7v3EcuPl9hCiB2Ot4aZjgtU2mV6yFY1THiWU/XfGf1uSadR+SlQdJcwHgAWeG7Lbos0aUqjtj8FQg== |
1505 | dependencies: | 1511 | dependencies: |
1506 | mkdirp-classic "^0.5.2" | 1512 | mkdirp-classic "^0.5.2" |
1507 | random-access-storage "^1.1.1" | 1513 | random-access-storage "^1.1.1" |
@@ -1600,21 +1606,25 @@ rimraf@^3.0.0: | |||
1600 | glob "^7.1.3" | 1606 | glob "^7.1.3" |
1601 | 1607 | ||
1602 | run-parallel-limit@^1.0.6: | 1608 | run-parallel-limit@^1.0.6: |
1603 | version "1.0.6" | 1609 | version "1.1.0" |
1604 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.0.6.tgz#0982a893d825b050cbaff1a35414832b195541b6" | 1610 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" |
1605 | integrity sha512-yFFs4Q2kECi5mWXyyZj3UlAZ5OFq5E07opABC+EmhZdjEkrxXaUwFqOaaNF4tbayMnBxrsbujpeCYTVjGufZGQ== | 1611 | integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== |
1612 | dependencies: | ||
1613 | queue-microtask "^1.2.2" | ||
1606 | 1614 | ||
1607 | run-parallel@^1.1.10, run-parallel@^1.1.2, run-parallel@^1.1.6, run-parallel@^1.1.9: | 1615 | run-parallel@^1.1.10, run-parallel@^1.1.2, run-parallel@^1.1.6, run-parallel@^1.1.9: |
1608 | version "1.1.10" | 1616 | version "1.2.0" |
1609 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" | 1617 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" |
1610 | integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== | 1618 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== |
1619 | dependencies: | ||
1620 | queue-microtask "^1.2.2" | ||
1611 | 1621 | ||
1612 | run-series@^1.1.8, run-series@^1.1.9: | 1622 | run-series@^1.1.8, run-series@^1.1.9: |
1613 | version "1.1.9" | 1623 | version "1.1.9" |
1614 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" | 1624 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" |
1615 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== | 1625 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== |
1616 | 1626 | ||
1617 | rusha@^0.8.1: | 1627 | rusha@^0.8.13: |
1618 | version "0.8.13" | 1628 | version "0.8.13" |
1619 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" | 1629 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" |
1620 | integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= | 1630 | integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= |
@@ -1714,25 +1724,25 @@ simple-get@^4.0.0: | |||
1714 | simple-concat "^1.0.0" | 1724 | simple-concat "^1.0.0" |
1715 | 1725 | ||
1716 | simple-peer@^9.7.1, simple-peer@^9.9.3: | 1726 | simple-peer@^9.7.1, simple-peer@^9.9.3: |
1717 | version "9.9.3" | 1727 | version "9.11.0" |
1718 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.9.3.tgz#b52c39d1173620d06c8b29ada7ee2ad3384bb469" | 1728 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571" |
1719 | integrity sha512-T3wuv0UqBpDTV0x0pJPPsz4thy0tC0fTOHE4g9+AF43RUxxT+MWeXVtdQcK5Xuzv/XTVrB2NrGzdfO1IFBqOkw== | 1729 | integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg== |
1720 | dependencies: | 1730 | dependencies: |
1721 | buffer "^6.0.2" | 1731 | buffer "^6.0.3" |
1722 | debug "^4.2.0" | 1732 | debug "^4.3.1" |
1723 | err-code "^2.0.3" | 1733 | err-code "^3.0.1" |
1724 | get-browser-rtc "^1.0.2" | 1734 | get-browser-rtc "^1.1.0" |
1725 | queue-microtask "^1.2.0" | 1735 | queue-microtask "^1.2.3" |
1726 | randombytes "^2.1.0" | 1736 | randombytes "^2.1.0" |
1727 | readable-stream "^3.6.0" | 1737 | readable-stream "^3.6.0" |
1728 | 1738 | ||
1729 | simple-sha1@^3.0.0, simple-sha1@^3.0.1: | 1739 | simple-sha1@^3.0.0, simple-sha1@^3.0.1: |
1730 | version "3.0.1" | 1740 | version "3.1.0" |
1731 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.0.1.tgz#b34c3c978d74ac4baf99b6555c1e6736e0d6e700" | 1741 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" |
1732 | integrity sha512-q7ehqWfHc1VhOm7sW099YDZ4I0yYX7rqyhqqhHV1IYeUTjPOhHyD3mXvv8k2P+rO7+7c8R4/D+8ffzC9BE7Cqg== | 1742 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== |
1733 | dependencies: | 1743 | dependencies: |
1734 | queue-microtask "^1.1.2" | 1744 | queue-microtask "^1.2.2" |
1735 | rusha "^0.8.1" | 1745 | rusha "^0.8.13" |
1736 | 1746 | ||
1737 | simple-websocket@^9.0.0: | 1747 | simple-websocket@^9.0.0: |
1738 | version "9.1.0" | 1748 | version "9.1.0" |
@@ -1801,9 +1811,9 @@ string-width@^1.0.1: | |||
1801 | strip-ansi "^4.0.0" | 1811 | strip-ansi "^4.0.0" |
1802 | 1812 | ||
1803 | string-width@^4.2.0: | 1813 | string-width@^4.2.0: |
1804 | version "4.2.0" | 1814 | version "4.2.2" |
1805 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" | 1815 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" |
1806 | integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== | 1816 | integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== |
1807 | dependencies: | 1817 | dependencies: |
1808 | emoji-regex "^8.0.0" | 1818 | emoji-regex "^8.0.0" |
1809 | is-fullwidth-code-point "^3.0.0" | 1819 | is-fullwidth-code-point "^3.0.0" |
@@ -1929,9 +1939,9 @@ torrent-discovery@^9.4.0: | |||
1929 | run-parallel "^1.1.2" | 1939 | run-parallel "^1.1.2" |
1930 | 1940 | ||
1931 | torrent-piece@^2.0.0: | 1941 | torrent-piece@^2.0.0: |
1932 | version "2.0.0" | 1942 | version "2.0.1" |
1933 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.0.tgz#6598ae67d93699e887f178db267ba16d89d7ec9b" | 1943 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.1.tgz#a1a50fffa589d9bf9560e38837230708bc3afdc6" |
1934 | integrity sha512-H/Z/yCuvZJj1vl1IQHI8dvF2QrUuXRJoptT5DW5967/dsLpXlCg+uyhFR5lfNj5mNaYePUbKtnL+qKWZGXv4Nw== | 1944 | integrity sha512-JLSOyvQVLI6JTWqioY4vFL0JkEUKQcaHQsU3loxkCvPTSttw8ePs2tFwsP4XIjw99Fz8EdOzt/4faykcbnPbCQ== |
1935 | 1945 | ||
1936 | type-fest@^0.6.0: | 1946 | type-fest@^0.6.0: |
1937 | version "0.6.0" | 1947 | version "0.6.0" |
@@ -2022,10 +2032,10 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: | |||
2022 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" | 2032 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |
2023 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= | 2033 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= |
2024 | 2034 | ||
2025 | utp-native@^2.2.1: | 2035 | utp-native@^2.3.0: |
2026 | version "2.3.0" | 2036 | version "2.4.0" |
2027 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.3.0.tgz#4e97f45cef97c6d201b9307ba6d15c1eb0a9dc5e" | 2037 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.4.0.tgz#7010de2134e9d767be0ec34e817c3300592befc0" |
2028 | integrity sha512-5dV711teCP21FIRndcq44ETDPL00TnRVmEnINu3jxyg0tq//ptxOyq7oNO6TcMmuFfeFZRSYOqDmTUl8MmPauQ== | 2038 | integrity sha512-jKwpFiEaDUuNH5S4vVk/+waAX+yA6f3Lw4flqOROH1ZE/jcT4mh0/hjIGSuPP9j9RbQcsBG6Fu6LaFk4ojXFxw== |
2029 | dependencies: | 2039 | dependencies: |
2030 | napi-macros "^2.0.0" | 2040 | napi-macros "^2.0.0" |
2031 | node-gyp-build "^4.2.0" | 2041 | node-gyp-build "^4.2.0" |
@@ -2095,29 +2105,30 @@ webtorrent-hybrid@^4.0.3: | |||
2095 | wrtc "^0.4.6" | 2105 | wrtc "^0.4.6" |
2096 | 2106 | ||
2097 | webtorrent@>=0.108.6, webtorrent@>=0.111.0: | 2107 | webtorrent@>=0.108.6, webtorrent@>=0.111.0: |
2098 | version "0.112.3" | 2108 | version "0.116.2" |
2099 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.112.3.tgz#f41225e36ee2e6905dc529237fcc187aa5092323" | 2109 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.116.2.tgz#5f7a851443947cf72ea09c4bca68371ea442a952" |
2100 | integrity sha512-CKe3udf2psctyd/GBNfZgz9bnXugvFTiU5GJCkcg4MKdd63qf0d8CS8bOYxwyir+EqvmgQQgJRO56b4PftI+PA== | 2110 | integrity sha512-u6ctyPEwUvbFKZsT9HRU1Q+SSqKWoNMlXWbaPTUlGsPrNZ3mCCeHtn8Hcf61jr1e4hna5oQBtVjg2N5/2V8d9g== |
2101 | dependencies: | 2111 | dependencies: |
2102 | addr-to-ip-port "^1.5.1" | 2112 | addr-to-ip-port "^1.5.1" |
2103 | bitfield "^4.0.0" | 2113 | bitfield "^4.0.0" |
2104 | bittorrent-dht "^10.0.0" | 2114 | bittorrent-dht "^10.0.0" |
2105 | bittorrent-protocol "^3.2.0" | 2115 | bittorrent-protocol "^3.2.0" |
2106 | chrome-net "^3.3.4" | 2116 | chrome-net "^3.3.4" |
2107 | chunk-store-stream "^4.1.1" | 2117 | chunk-store-stream "^4.2.0" |
2108 | create-torrent "^4.4.2" | 2118 | cpus "^1.0.3" |
2119 | create-torrent "^4.4.4" | ||
2109 | debug "^4.3.1" | 2120 | debug "^4.3.1" |
2110 | end-of-stream "1.4.1" | 2121 | end-of-stream "^1.4.4" |
2111 | escape-html "^1.0.3" | 2122 | escape-html "^1.0.3" |
2112 | fs-chunk-store "^2.0.2" | 2123 | fs-chunk-store "^2.0.2" |
2113 | http-node "github:feross/http-node#webtorrent" | 2124 | http-node "github:feross/http-node#webtorrent" |
2114 | immediate-chunk-store "^2.1.1" | 2125 | immediate-chunk-store "^2.1.1" |
2115 | load-ip-set "^2.1.2" | 2126 | load-ip-set "^2.1.2" |
2116 | memory-chunk-store "^1.3.0" | 2127 | memory-chunk-store "^1.3.1" |
2117 | mime "^2.4.6" | 2128 | mime "^2.5.0" |
2118 | multistream "^4.0.1" | 2129 | multistream "^4.1.0" |
2119 | package-json-versionify "^1.0.4" | 2130 | package-json-versionify "^1.0.4" |
2120 | parse-torrent "^9.1.0" | 2131 | parse-torrent "^9.1.1" |
2121 | pump "^3.0.0" | 2132 | pump "^3.0.0" |
2122 | queue-microtask "^1.2.2" | 2133 | queue-microtask "^1.2.2" |
2123 | random-iterate "^1.0.1" | 2134 | random-iterate "^1.0.1" |
@@ -2140,7 +2151,7 @@ webtorrent@>=0.108.6, webtorrent@>=0.111.0: | |||
2140 | unordered-array-remove "^1.0.2" | 2151 | unordered-array-remove "^1.0.2" |
2141 | ut_metadata "^3.5.2" | 2152 | ut_metadata "^3.5.2" |
2142 | ut_pex "^2.0.1" | 2153 | ut_pex "^2.0.1" |
2143 | utp-native "^2.2.1" | 2154 | utp-native "^2.3.0" |
2144 | 2155 | ||
2145 | which@^1.2.9: | 2156 | which@^1.2.9: |
2146 | version "1.3.1" | 2157 | version "1.3.1" |
@@ -2198,9 +2209,9 @@ wrtc@^0.4.6: | |||
2198 | domexception "^1.0.1" | 2209 | domexception "^1.0.1" |
2199 | 2210 | ||
2200 | ws@^7.3.0, ws@^7.4.2: | 2211 | ws@^7.3.0, ws@^7.4.2: |
2201 | version "7.4.3" | 2212 | version "7.4.4" |
2202 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" | 2213 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" |
2203 | integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== | 2214 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== |
2204 | 2215 | ||
2205 | xml2js@^0.4.8: | 2216 | xml2js@^0.4.8: |
2206 | version "0.4.23" | 2217 | version "0.4.23" |
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/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 1ca17e4ab..391dcc3f9 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | PluginVideoPrivacyManager, | 12 | PluginVideoPrivacyManager, |
13 | RegisterServerHookOptions, | 13 | RegisterServerHookOptions, |
14 | RegisterServerSettingOptions, | 14 | RegisterServerSettingOptions, |
15 | ServerConfig, | ||
15 | VideoBlacklistCreate | 16 | VideoBlacklistCreate |
16 | } from '@shared/models' | 17 | } from '@shared/models' |
17 | import { MVideoThumbnail } from '../models' | 18 | import { MVideoThumbnail } from '../models' |
@@ -37,6 +38,8 @@ export type PeerTubeHelpers = { | |||
37 | 38 | ||
38 | config: { | 39 | config: { |
39 | getWebserverUrl: () => string | 40 | getWebserverUrl: () => string |
41 | |||
42 | getServerConfig: () => Promise<ServerConfig> | ||
40 | } | 43 | } |
41 | 44 | ||
42 | moderation: { | 45 | moderation: { |
@@ -52,6 +55,10 @@ export type PeerTubeHelpers = { | |||
52 | server: { | 55 | server: { |
53 | getServerActor: () => Promise<ActorModel> | 56 | getServerActor: () => Promise<ActorModel> |
54 | } | 57 | } |
58 | |||
59 | plugin: { | ||
60 | getBaseStaticRoute: () => string | ||
61 | } | ||
55 | } | 62 | } |
56 | 63 | ||
57 | export type RegisterServerOptions = { | 64 | export type RegisterServerOptions = { |
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 |