aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts242
-rw-r--r--server/controllers/api/jobs.ts4
-rw-r--r--server/controllers/api/plugins.ts3
-rw-r--r--server/controllers/api/search.ts43
-rw-r--r--server/controllers/api/users/index.ts8
-rw-r--r--server/controllers/api/users/me.ts10
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/controllers/api/users/my-subscriptions.ts12
-rw-r--r--server/controllers/api/users/token.ts72
-rw-r--r--server/controllers/api/video-channel.ts66
-rw-r--r--server/controllers/api/videos/index.ts11
-rw-r--r--server/controllers/api/videos/ownership.ts2
-rw-r--r--server/controllers/client.ts28
-rw-r--r--server/controllers/download.ts85
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/lazy-static.ts41
-rw-r--r--server/controllers/plugins.ts14
-rw-r--r--server/controllers/services.ts4
-rw-r--r--server/controllers/static.ts6
-rw-r--r--server/helpers/activitypub.ts5
-rw-r--r--server/helpers/core-utils.ts38
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts110
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts18
-rw-r--r--server/helpers/custom-validators/activitypub/share.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/view.ts13
-rw-r--r--server/helpers/custom-validators/actor-images.ts17
-rw-r--r--server/helpers/custom-validators/user-notifications.ts5
-rw-r--r--server/helpers/custom-validators/users.ts17
-rw-r--r--server/helpers/ffmpeg-utils.ts99
-rw-r--r--server/helpers/image-utils.ts6
-rw-r--r--server/helpers/logger.ts10
-rw-r--r--server/helpers/markdown.ts43
-rw-r--r--server/helpers/middlewares/video-channels.ts7
-rw-r--r--server/helpers/middlewares/videos.ts23
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/helpers/requests.ts199
-rw-r--r--server/helpers/signup.ts2
-rw-r--r--server/helpers/youtube-dl.ts71
-rw-r--r--server/initializers/checker-after-init.ts27
-rw-r--r--server/initializers/checker-before-init.ts7
-rw-r--r--server/initializers/config.ts30
-rw-r--r--server/initializers/constants.ts35
-rw-r--r--server/initializers/database.ts10
-rw-r--r--server/initializers/migrations/0610-views-index copy.ts (renamed from server/initializers/migrations/0610-views-index.ts)0
-rw-r--r--server/initializers/migrations/0615-latest-versions-notification-settings.ts44
-rw-r--r--server/initializers/migrations/0620-latest-versions-application.ts27
-rw-r--r--server/initializers/migrations/0625-latest-versions-notification.ts26
-rw-r--r--server/initializers/migrations/0630-banner.ts50
-rw-r--r--server/initializers/migrations/0635-actor-image-size.ts35
-rw-r--r--server/lib/activitypub/actor.ts212
-rw-r--r--server/lib/activitypub/crawl.ts25
-rw-r--r--server/lib/activitypub/playlist.ts69
-rw-r--r--server/lib/activitypub/process/process-delete.ts13
-rw-r--r--server/lib/activitypub/process/process-update.ts13
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/share.ts30
-rw-r--r--server/lib/activitypub/video-comments.ts22
-rw-r--r--server/lib/activitypub/video-rates.ts22
-rw-r--r--server/lib/activitypub/videos.ts83
-rw-r--r--server/lib/actor-image.ts97
-rw-r--r--server/lib/auth/external-auth.ts (renamed from server/lib/auth.ts)129
-rw-r--r--server/lib/auth/oauth-model.ts (renamed from server/lib/oauth-model.ts)137
-rw-r--r--server/lib/auth/oauth.ts180
-rw-r--r--server/lib/auth/tokens-cache.ts52
-rw-r--r--server/lib/avatar.ts85
-rw-r--r--server/lib/client-html.ts26
-rw-r--r--server/lib/config.ts255
-rw-r--r--server/lib/emailer.ts103
-rw-r--r--server/lib/emails/peertube-version-new/html.pug9
-rw-r--r--server/lib/emails/plugin-version-new/html.pug9
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts2
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/files-cache/videos-torrent-cache.ts15
-rw-r--r--server/lib/hls.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts63
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts5
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts15
-rw-r--r--server/lib/notifier.ts74
-rw-r--r--server/lib/peertube-socket.ts2
-rw-r--r--server/lib/plugins/plugin-helpers-builder.ts17
-rw-r--r--server/lib/plugins/plugin-index.ts38
-rw-r--r--server/lib/plugins/plugin-manager.ts12
-rw-r--r--server/lib/plugins/register-helpers.ts4
-rw-r--r--server/lib/plugins/yarn.ts13
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts8
-rw-r--r--server/lib/schedulers/peertube-version-check-scheduler.ts55
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts6
-rw-r--r--server/lib/stat-manager.ts27
-rw-r--r--server/lib/thumbnail.ts9
-rw-r--r--server/lib/user.ts4
-rw-r--r--server/lib/video-blacklist.ts6
-rw-r--r--server/lib/video-channel.ts16
-rw-r--r--server/lib/video-transcoding-profiles.ts2
-rw-r--r--server/middlewares/async.ts2
-rw-r--r--server/middlewares/auth.ts (renamed from server/middlewares/oauth.ts)24
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/validators/activitypub/signature.ts2
-rw-r--r--server/middlewares/validators/actor-image.ts30
-rw-r--r--server/middlewares/validators/avatar.ts26
-rw-r--r--server/middlewares/validators/follows.ts1
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/jobs.ts6
-rw-r--r--server/middlewares/validators/pagination.ts33
-rw-r--r--server/middlewares/validators/plugins.ts2
-rw-r--r--server/middlewares/validators/sort.ts2
-rw-r--r--server/middlewares/validators/utils.ts4
-rw-r--r--server/middlewares/validators/videos/video-channels.ts2
-rw-r--r--server/middlewares/validators/videos/video-comments.ts2
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts2
-rw-r--r--server/models/account/account.ts5
-rw-r--r--server/models/account/actor-image.ts100
-rw-r--r--server/models/account/user-notification-setting.ts26
-rw-r--r--server/models/account/user-notification.ts98
-rw-r--r--server/models/account/user.ts20
-rw-r--r--server/models/activitypub/actor-follow.ts7
-rw-r--r--server/models/activitypub/actor.ts87
-rw-r--r--server/models/application/application.ts4
-rw-r--r--server/models/avatar/avatar.ts81
-rw-r--r--server/models/oauth/oauth-token.ts11
-rw-r--r--server/models/redundancy/video-redundancy.ts9
-rw-r--r--server/models/utils.ts8
-rw-r--r--server/models/video/video-channel.ts145
-rw-r--r--server/models/video/video-playlist.ts60
-rw-r--r--server/models/video/video-query-builder.ts5
-rw-r--r--server/models/video/video.ts25
-rw-r--r--server/tests/api/activitypub/security.ts116
-rw-r--r--server/tests/api/check-params/user-notifications.ts4
-rw-r--r--server/tests/api/check-params/users.ts2
-rw-r--r--server/tests/api/check-params/video-channels.ts70
-rw-r--r--server/tests/api/live/live.ts10
-rw-r--r--server/tests/api/notifications/admin-notifications.ts165
-rw-r--r--server/tests/api/notifications/index.ts1
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts39
-rw-r--r--server/tests/api/server/auto-follows.ts7
-rw-r--r--server/tests/api/server/config.ts34
-rw-r--r--server/tests/api/server/handle-down.ts2
-rw-r--r--server/tests/api/server/services.ts12
-rw-r--r--server/tests/api/server/stats.ts76
-rw-r--r--server/tests/api/users/users.ts51
-rw-r--r--server/tests/api/videos/video-channels.ts99
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/cli/print-transcode-command.ts3
-rw-r--r--server/tests/cli/regenerate-thumbnails.ts124
-rw-r--r--server/tests/client.ts9
-rw-r--r--server/tests/feeds/feeds.ts17
-rw-r--r--server/tests/fixtures/banner-resized.jpgbin0 -> 88780 bytes
-rw-r--r--server/tests/fixtures/banner.jpgbin0 -> 31648 bytes
-rw-r--r--server/tests/fixtures/peertube-plugin-test-four/main.js13
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js82
-rw-r--r--server/tests/fixtures/peertube-plugin-test-unloading/lib.js2
-rw-r--r--server/tests/fixtures/peertube-plugin-test-unloading/main.js14
-rw-r--r--server/tests/fixtures/peertube-plugin-test-unloading/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test/main.js70
-rw-r--r--server/tests/fixtures/thumbnail-playlist.jpgbin2520 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_import_thumbnail.jpgbin5885 -> 10980 bytes
-rw-r--r--server/tests/fixtures/video_short.mp4.jpgbin2618 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short.ogv.jpgbin2618 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short.webm.jpgbin3598 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short1.webm.jpgbin4616 -> 6309 bytes
-rw-r--r--server/tests/fixtures/video_short2.webm.jpgbin4221 -> 5506 bytes
-rw-r--r--server/tests/fixtures/video_short3.webm.jpgbin3972 -> 4981 bytes
-rw-r--r--server/tests/helpers/request.ts16
-rw-r--r--server/tests/plugins/external-auth.ts2
-rw-r--r--server/tests/plugins/filter-hooks.ts178
-rw-r--r--server/tests/plugins/index.ts1
-rw-r--r--server/tests/plugins/plugin-helpers.ts27
-rw-r--r--server/tests/plugins/plugin-transcoding.ts57
-rw-r--r--server/tests/plugins/plugin-unloading.ts89
-rw-r--r--server/tools/peertube-import-videos.ts5
-rw-r--r--server/tools/yarn.lock291
-rw-r--r--server/types/models/account/account.ts10
-rw-r--r--server/types/models/account/actor-follow.ts11
-rw-r--r--server/types/models/account/actor-image.ts12
-rw-r--r--server/types/models/account/actor.ts55
-rw-r--r--server/types/models/account/avatar.ts12
-rw-r--r--server/types/models/account/index.ts4
-rw-r--r--server/types/models/application/application.ts5
-rw-r--r--server/types/models/application/index.ts1
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts18
-rw-r--r--server/types/models/user/user.ts8
-rw-r--r--server/types/models/video/video-channels.ts34
-rw-r--r--server/types/plugins/register-server-option.model.ts7
-rw-r--r--server/typings/express/index.d.ts23
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 @@
1import { Hooks } from '@server/lib/plugins/hooks'
2import * as express from 'express' 1import * as express from 'express'
3import { remove, writeJSON } from 'fs-extra' 2import { remove, writeJSON } from 'fs-extra'
4import { snakeCase } from 'lodash' 3import { snakeCase } from 'lodash'
5import validator from 'validator' 4import validator from 'validator'
6import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared' 5import { getServerConfig } from '@server/lib/config'
6import { UserRight } from '../../../shared'
7import { About } from '../../../shared/models/server/about.model' 7import { About } from '../../../shared/models/server/about.model'
8import { CustomConfig } from '../../../shared/models/server/custom-config.model' 8import { CustomConfig } from '../../../shared/models/server/custom-config.model'
9import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 9import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
10import { objectConverter } from '../../helpers/core-utils' 10import { objectConverter } from '../../helpers/core-utils'
11import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 11import { CONFIG, reloadConfig } from '../../initializers/config'
12import { getServerCommit } from '../../helpers/utils'
13import { getEnabledResolutions } from '../../lib/video-transcoding'
14import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
15import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
16import { ClientHtml } from '../../lib/client-html' 12import { ClientHtml } from '../../lib/client-html'
17import { PluginManager } from '../../lib/plugins/plugin-manager'
18import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
19import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' 13import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
20import { customConfigUpdateValidator } from '../../middlewares/validators/config' 14import { customConfigUpdateValidator } from '../../middlewares/validators/config'
21import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
22 15
23const configRouter = express.Router() 16const configRouter = express.Router()
24 17
@@ -46,174 +39,8 @@ configRouter.delete('/custom',
46 asyncMiddleware(deleteCustomConfig) 39 asyncMiddleware(deleteCustomConfig)
47) 40)
48 41
49let serverCommit: string
50
51async function getConfig (req: express.Request, res: express.Response) { 42async 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
287function 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
298function 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
308function 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
326function 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
346export { 116export {
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'
15import { paginationValidator } from '../../middlewares/validators'
16import { listJobsValidator } from '../../middlewares/validators/jobs' 16import { listJobsValidator } from '../../middlewares/validators/jobs'
17 17
18const jobsRouter = express.Router() 18const jobsRouter = express.Router()
@@ -20,7 +20,7 @@ const jobsRouter = express.Router()
20jobsRouter.get('/:state?', 20jobsRouter.get('/:state?',
21 authenticate, 21 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 22 ensureUserHasRight(UserRight.MANAGE_JOBS),
23 paginationValidator, 23 paginationValidatorBuilder([ 'jobs' ]),
24 jobsSortValidator, 24 jobsSortValidator,
25 setDefaultSort, 25 setDefaultSort,
26 setDefaultPagination, 26 setDefaultPagination,
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 1c0b5edb1..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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils' 2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doRequest } from '@server/helpers/requests' 3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' 5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
6import { Hooks } from '@server/lib/plugins/hooks'
6import { AccountBlocklistModel } from '@server/models/account/account-blocklist' 7import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
7import { getServerActor } from '@server/models/application/application' 8import { getServerActor } from '@server/models/application/application'
8import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
@@ -22,8 +23,8 @@ import {
22 paginationValidator, 23 paginationValidator,
23 setDefaultPagination, 24 setDefaultPagination,
24 setDefaultSearchSort, 25 setDefaultSearchSort,
25 videoChannelsSearchSortValidator,
26 videoChannelsListSearchValidator, 26 videoChannelsListSearchValidator,
27 videoChannelsSearchSortValidator,
27 videosSearchSortValidator, 28 videosSearchSortValidator,
28 videosSearchValidator 29 videosSearchValidator
29} from '../../middlewares' 30} from '../../middlewares'
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
87async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { 88async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
88 const result = await buildMutedForSearchIndex(res) 89 const result = await buildMutedForSearchIndex(res)
89 90
90 const body = Object.assign(query, result) 91 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
91 92
92 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' 93 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
93 94
94 try { 95 try {
95 logger.debug('Doing video channels search index request on %s.', url, { body }) 96 logger.debug('Doing video channels search index request on %s.', url, { body })
96 97
97 const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true }) 98 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
99 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
98 100
99 return res.json(searchIndexResult.body) 101 return res.json(jsonResult)
100 } catch (err) { 102 } catch (err) {
101 logger.warn('Cannot use search index to make video channels search.', { err }) 103 logger.warn('Cannot use search index to make video channels search.', { err })
102 104
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
107async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { 109async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
108 const serverActor = await getServerActor() 110 const serverActor = await getServerActor()
109 111
110 const options = { 112 const apiOptions = await Hooks.wrapObject({
111 actorId: serverActor.id, 113 actorId: serverActor.id,
112 search: query.search, 114 search: query.search,
113 start: query.start, 115 start: query.start,
114 count: query.count, 116 count: query.count,
115 sort: query.sort 117 sort: query.sort
116 } 118 }, 'filter:api.search.video-channels.local.list.params')
117 const resultList = await VideoChannelModel.searchForApi(options) 119
120 const resultList = await Hooks.wrapPromiseFun(
121 VideoChannelModel.searchForApi,
122 apiOptions,
123 'filter:api.search.video-channels.local.list.result'
124 )
118 125
119 return res.json(getFormattedObjects(resultList.data, resultList.total)) 126 return res.json(getFormattedObjects(resultList.data, resultList.total))
120} 127}
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) {
168async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { 175async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
169 const result = await buildMutedForSearchIndex(res) 176 const result = await buildMutedForSearchIndex(res)
170 177
171 const body: VideosSearchQuery = Object.assign(query, result) 178 let body: VideosSearchQuery = Object.assign(query, result)
172 179
173 // Use the default instance NSFW policy if not specified 180 // Use the default instance NSFW policy if not specified
174 if (!body.nsfw) { 181 if (!body.nsfw) {
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
181 : 'both' 188 : 'both'
182 } 189 }
183 190
191 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
192
184 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' 193 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
185 194
186 try { 195 try {
187 logger.debug('Doing videos search index request on %s.', url, { body }) 196 logger.debug('Doing videos search index request on %s.', url, { body })
188 197
189 const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true }) 198 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
199 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
190 200
191 return res.json(searchIndexResult.body) 201 return res.json(jsonResult)
192 } catch (err) { 202 } catch (err) {
193 logger.warn('Cannot use search index to make video search.', { err }) 203 logger.warn('Cannot use search index to make video search.', { err })
194 204
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
197} 207}
198 208
199async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 209async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
200 const options = Object.assign(query, { 210 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
201 includeLocalVideos: true, 211 includeLocalVideos: true,
202 nsfw: buildNSFWFilter(res, query.nsfw), 212 nsfw: buildNSFWFilter(res, query.nsfw),
203 filter: query.filter, 213 filter: query.filter,
204 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 214 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
205 }) 215 }), 'filter:api.search.videos.local.list.params')
206 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 216
217 const resultList = await Hooks.wrapPromiseFun(
218 VideoModel.searchAndPopulateAccountAndServer,
219 apiOptions,
220 'filter:api.search.videos.local.list.result'
221 )
207 222
208 return res.json(getFormattedObjects(resultList.data, resultList.total)) 223 return res.json(getFormattedObjects(resultList.data, resultList.total))
209} 224}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 3be1d55ae..e2b1ea7cd 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -2,8 +2,10 @@ import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 3import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
5import { MUser, MUserAccountDefault } from '@server/types/models' 6import { MUser, MUserAccountDefault } from '@server/types/models'
6import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 7import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 9import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
8import { UserRegister } from '../../../../shared/models/users/user-register.model' 10import { UserRegister } from '../../../../shared/models/users/user-register.model'
9import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 11import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
14import { sequelizeTypescript } from '../../../initializers/database' 16import { sequelizeTypescript } from '../../../initializers/database'
15import { Emailer } from '../../../lib/emailer' 17import { Emailer } from '../../../lib/emailer'
16import { Notifier } from '../../../lib/notifier' 18import { Notifier } from '../../../lib/notifier'
17import { deleteUserToken } from '../../../lib/oauth-model'
18import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
19import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 20import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
20import { 21import {
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
52import { myNotificationsRouter } from './my-notifications' 53import { myNotificationsRouter } from './my-notifications'
53import { mySubscriptionsRouter } from './my-subscriptions' 54import { mySubscriptionsRouter } from './my-subscriptions'
54import { myVideoPlaylistsRouter } from './my-video-playlists' 55import { myVideoPlaylistsRouter } from './my-video-playlists'
55import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
56 56
57const auditLogger = auditLoggerFactory('users') 57const auditLogger = auditLoggerFactory('users')
58 58
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
335 const user = await userToUpdate.save() 335 const user = await userToUpdate.save()
336 336
337 // Destroy user token to refresh rights 337 // Destroy user token to refresh rights
338 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) 338 if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
339 339
340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
341 341
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
395 user.blockedReason = reason || null 395 user.blockedReason = reason || null
396 396
397 await sequelizeTypescript.transaction(async t => { 397 await sequelizeTypescript.transaction(async t => {
398 await deleteUserToken(user.id, t) 398 await OAuthTokenModel.deleteUserToken(user.id, t)
399 399
400 await user.save({ transaction: t }) 400 await user.save({ transaction: t })
401 }) 401 })
diff --git a/server/controllers/api/users/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'
2import * as express from 'express' 2import * as express from 'express'
3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 5import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
8import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
@@ -25,7 +25,7 @@ import {
25 usersVideoRatingValidator 25 usersVideoRatingValidator
26} from '../../../middlewares' 26} from '../../../middlewares'
27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' 27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
28import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
29import { AccountModel } from '../../../models/account/account' 29import { AccountModel } from '../../../models/account/account'
30import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 30import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
31import { UserModel } from '../../../models/account/user' 31import { 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 @@
1import 'multer' 1import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { sendUndoFollow } from '@server/lib/activitypub/send'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 6import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
4import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
5import { getFormattedObjects } from '../../../helpers/utils' 8import { getFormattedObjects } from '../../../helpers/utils'
@@ -26,8 +29,6 @@ import {
26} from '../../../middlewares/validators' 29} from '../../../middlewares/validators'
27import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 30import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
28import { VideoModel } from '../../../models/video/video' 31import { VideoModel } from '../../../models/video/video'
29import { sendUndoFollow } from '@server/lib/activitypub/send'
30import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
31 32
32const mySubscriptionsRouter = express.Router() 33const mySubscriptionsRouter = express.Router()
33 34
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions',
66mySubscriptionsRouter.get('/me/subscriptions/:uri', 67mySubscriptionsRouter.get('/me/subscriptions/:uri',
67 authenticate, 68 authenticate,
68 userSubscriptionGetValidator, 69 userSubscriptionGetValidator,
69 getUserSubscription 70 asyncMiddleware(getUserSubscription)
70) 71)
71 72
72mySubscriptionsRouter.delete('/me/subscriptions/:uri', 73mySubscriptionsRouter.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
133function getUserSubscription (req: express.Request, res: express.Response) { 134async 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
139async function deleteUserSubscription (req: express.Request, res: express.Response) { 141async 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 @@
1import { handleLogin, handleTokenRevocation } from '@server/lib/auth' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { v4 as uuidv4 } from 'uuid'
4import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
4import * as express from 'express' 6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
7import { handleOAuthToken } from '@server/lib/auth/oauth'
8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
5import { Hooks } from '@server/lib/plugins/hooks' 9import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares' 10import { asyncMiddleware, authenticate } from '@server/middlewares'
7import { ScopedToken } from '@shared/models/users/user-scoped-token' 11import { ScopedToken } from '@shared/models/users/user-scoped-token'
8import { v4 as uuidv4 } from 'uuid'
9 12
10const tokensRouter = express.Router() 13const tokensRouter = express.Router()
11 14
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
16 19
17tokensRouter.post('/token', 20tokensRouter.post('/token',
18 loginRateLimiter, 21 loginRateLimiter,
19 handleLogin, 22 asyncMiddleware(handleToken)
20 tokenSuccess
21) 23)
22 24
23tokensRouter.post('/revoke-token', 25tokensRouter.post('/revoke-token',
@@ -42,10 +44,53 @@ export {
42} 44}
43// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
44 46
45function tokenSuccess (req: express.Request) { 47async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
46 const username = req.body.username 48 const grantType = req.body.grant_type
49
50 try {
51 const bypassLogin = await buildByPassLogin(req, grantType)
52
53 const refreshTokenAuthName = grantType === 'refresh_token'
54 ? await getAuthNameFromRefreshGrant(req.body.refresh_token)
55 : undefined
56
57 const options = {
58 refreshTokenAuthName,
59 bypassLogin
60 }
61
62 const token = await handleOAuthToken(req, options)
63
64 res.set('Cache-Control', 'no-store')
65 res.set('Pragma', 'no-cache')
66
67 Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
68
69 return res.json({
70 token_type: 'Bearer',
47 71
48 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) 72 access_token: token.accessToken,
73 refresh_token: token.refreshToken,
74
75 expires_in: token.accessTokenExpiresIn,
76 refresh_token_expires_in: token.refreshTokenExpiresIn
77 })
78 } catch (err) {
79 logger.warn('Login error', { err })
80
81 return res.status(err.code || 400).json({
82 code: err.name,
83 error: err.message
84 })
85 }
86}
87
88async function handleTokenRevocation (req: express.Request, res: express.Response) {
89 const token = res.locals.oauth.token
90
91 const result = await revokeToken(token, { req, explicitLogout: true })
92
93 return res.json(result)
49} 94}
50 95
51function getScopedTokens (req: express.Request, res: express.Response) { 96function getScopedTokens (req: express.Request, res: express.Response) {
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
66 feedToken: user.feedToken 111 feedToken: user.feedToken
67 } as ScopedToken) 112 } as ScopedToken)
68} 113}
114
115async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
116 if (grantType !== 'password') return undefined
117
118 if (req.body.externalAuthToken) {
119 // Consistency with the getBypassFromPasswordGrant promise
120 return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
121 }
122
123 return getBypassFromPasswordGrant(req.body.username, req.body.password)
124}
diff --git a/server/controllers/api/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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks' 2import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { MChannelAccountDefault } from '@server/types/models' 4import { MChannelBannerAccountDefault } from '@server/types/models'
5import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
8import { resetSequelizeInstance } from '../../helpers/database-utils' 8import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' 16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 17import { JobQueue } from '../../lib/job-queue'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
@@ -33,7 +33,7 @@ import {
33 videoPlaylistsSortValidator 33 videoPlaylistsSortValidator
34} from '../../middlewares' 34} from '../../middlewares'
35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' 35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
36import { updateAvatarValidator } from '../../middlewares/validators/avatar' 36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
38import { AccountModel } from '../../models/account/account' 38import { AccountModel } from '../../models/account/account'
39import { VideoModel } from '../../models/video/video' 39import { VideoModel } from '../../models/video/video'
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
42 42
43const auditLogger = auditLoggerFactory('channels') 43const auditLogger = auditLoggerFactory('channels')
44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
45const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR })
45 46
46const videoChannelRouter = express.Router() 47const videoChannelRouter = express.Router()
47 48
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
69 asyncMiddleware(updateVideoChannelAvatar) 70 asyncMiddleware(updateVideoChannelAvatar)
70) 71)
71 72
73videoChannelRouter.post('/:nameWithHost/banner/pick',
74 authenticate,
75 reqBannerFile,
76 // Check the rights
77 asyncMiddleware(videoChannelsUpdateValidator),
78 updateBannerValidator,
79 asyncMiddleware(updateVideoChannelBanner)
80)
81
72videoChannelRouter.delete('/:nameWithHost/avatar', 82videoChannelRouter.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
89videoChannelRouter.delete('/:nameWithHost/banner',
90 authenticate,
91 // Check the rights
92 asyncMiddleware(videoChannelsUpdateValidator),
93 asyncMiddleware(deleteVideoChannelBanner)
94)
95
79videoChannelRouter.put('/:nameWithHost', 96videoChannelRouter.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
154async 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}
137async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 165async 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
153async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { 177async 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
185async 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
183async function updateVideoChannel (req: express.Request, res: express.Response) { 215async 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
254async function getVideoChannel (req: express.Request, res: express.Response) { 286async 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
264async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { 296async 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 '../../../
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
20import { logger } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { getFormattedObjects } from '../../../helpers/utils' 21import { getFormattedObjects } from '../../../helpers/utils'
22import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
23import { 23import {
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership'
67import { rateVideoRouter } from './rate' 67import { rateVideoRouter } from './rate'
68import { watchingRouter } from './watching' 68import { watchingRouter } from './watching'
69 69
70const lTags = loggerTagsFactory('api', 'video')
70const auditLogger = auditLoggerFactory('videos') 71const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 72const videosRouter = express.Router()
72 73
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) {
257 }) 258 })
258 259
259 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 260 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 261 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
261 262
262 return { videoCreated } 263 return { videoCreated }
263 }) 264 })
264 265
265 // Create the torrent file in async way because it could be long 266 // Create the torrent file in async way because it could be long
266 createTorrentAndSetInfoHashAsync(video, videoFile) 267 createTorrentAndSetInfoHashAsync(video, videoFile)
267 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err })) 268 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
268 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) 269 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
269 .then(refreshedVideo => { 270 .then(refreshedVideo => {
270 if (!refreshedVideo) return 271 if (!refreshedVideo) return
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) {
276 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) 277 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
277 }) 278 })
278 }) 279 })
279 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err })) 280 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
280 281
281 if (video.state === VideoState.TO_TRANSCODE) { 282 if (video.state === VideoState.TO_TRANSCODE) {
282 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) 283 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
389 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), 390 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
390 oldVideoAuditView 391 oldVideoAuditView
391 ) 392 )
392 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 393 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
393 394
394 return videoInstanceUpdated 395 return videoInstanceUpdated
395 }) 396 })
diff --git a/server/controllers/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'
2import { constants, promises as fs } from 'fs' 2import { constants, promises as fs } from 'fs'
3import { readFile } from 'fs-extra' 3import { readFile } from 'fs-extra'
4import { join } from 'path' 4import { join } from 'path'
5import { logger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { Hooks } from '@server/lib/plugins/hooks'
6import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode } from '@shared/core-utils'
7import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' 9import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
8import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
@@ -27,6 +29,7 @@ const embedMiddlewares = [
27 ? embedCSP 29 ? embedCSP
28 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), 30 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
29 31
32 // Set headers
30 (req: express.Request, res: express.Response, next: express.NextFunction) => { 33 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 res.removeHeader('X-Frame-Options') 34 res.removeHeader('X-Frame-Options')
32 35
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
105} 108}
106 109
107async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { 110async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
111 const hookName = req.originalUrl.startsWith('/video-playlists/')
112 ? 'filter:html.embed.video-playlist.allowed.result'
113 : 'filter:html.embed.video.allowed.result'
114
115 const allowParameters = { req }
116
117 const allowedResult = await Hooks.wrapFun(
118 isEmbedAllowed,
119 allowParameters,
120 hookName
121 )
122
123 if (!allowedResult || allowedResult.allowed !== true) {
124 logger.info('Embed is not allowed.', { allowedResult })
125
126 return sendHTML(allowedResult?.html || '', res)
127 }
128
108 const html = await ClientHtml.getEmbedHTML() 129 const html = await ClientHtml.getEmbedHTML()
109 130
110 return sendHTML(html, res) 131 return sendHTML(html, res)
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) {
158 } 179 }
159 } 180 }
160} 181}
182
183type AllowedResult = { allowed: boolean, html?: string }
184function isEmbedAllowed (_object: {
185 req: express.Request
186}): AllowedResult {
187 return { allowed: true }
188}
diff --git a/server/controllers/download.ts b/server/controllers/download.ts
index 27caa1518..9a8194c5c 100644
--- a/server/controllers/download.ts
+++ b/server/controllers/download.ts
@@ -1,8 +1,10 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '@server/helpers/logger'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 4import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5import { Hooks } from '@server/lib/plugins/hooks'
4import { getVideoFilePath } from '@server/lib/video-paths' 6import { getVideoFilePath } from '@server/lib/video-paths'
5import { MVideoFile, MVideoFullLight } from '@server/types/models' 7import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { VideoStreamingPlaylistType } from '@shared/models' 9import { VideoStreamingPlaylistType } from '@shared/models'
8import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' 10import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@@ -14,19 +16,19 @@ downloadRouter.use(cors())
14 16
15downloadRouter.use( 17downloadRouter.use(
16 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', 18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
17 downloadTorrent 19 asyncMiddleware(downloadTorrent)
18) 20)
19 21
20downloadRouter.use( 22downloadRouter.use(
21 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
22 asyncMiddleware(videosDownloadValidator), 24 asyncMiddleware(videosDownloadValidator),
23 downloadVideoFile 25 asyncMiddleware(downloadVideoFile)
24) 26)
25 27
26downloadRouter.use( 28downloadRouter.use(
27 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', 29 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
28 asyncMiddleware(videosDownloadValidator), 30 asyncMiddleware(videosDownloadValidator),
29 downloadHLSVideoFile 31 asyncMiddleware(downloadHLSVideoFile)
30) 32)
31 33
32// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
41 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
42 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 44 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
43 45
46 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
47
48 const allowedResult = await Hooks.wrapFun(
49 isTorrentDownloadAllowed,
50 allowParameters,
51 'filter:api.download.torrent.allowed.result'
52 )
53
54 if (!checkAllowResult(res, allowParameters, allowedResult)) return
55
44 return res.download(result.path, result.downloadName) 56 return res.download(result.path, result.downloadName)
45} 57}
46 58
47function downloadVideoFile (req: express.Request, res: express.Response) { 59async function downloadVideoFile (req: express.Request, res: express.Response) {
48 const video = res.locals.videoAll 60 const video = res.locals.videoAll
49 61
50 const videoFile = getVideoFile(req, video.VideoFiles) 62 const videoFile = getVideoFile(req, video.VideoFiles)
51 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 63 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
52 64
65 const allowParameters = { video, videoFile }
66
67 const allowedResult = await Hooks.wrapFun(
68 isVideoDownloadAllowed,
69 allowParameters,
70 'filter:api.download.video.allowed.result'
71 )
72
73 if (!checkAllowResult(res, allowParameters, allowedResult)) return
74
53 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) 75 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
54} 76}
55 77
56function downloadHLSVideoFile (req: express.Request, res: express.Response) { 78async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
57 const video = res.locals.videoAll 79 const video = res.locals.videoAll
58 const playlist = getHLSPlaylist(video) 80 const streamingPlaylist = getHLSPlaylist(video)
59 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end 81 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
60 82
61 const videoFile = getVideoFile(req, playlist.VideoFiles) 83 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
62 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 84 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63 85
64 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` 86 const allowParameters = { video, streamingPlaylist, videoFile }
65 return res.download(getVideoFilePath(playlist, videoFile), filename) 87
88 const allowedResult = await Hooks.wrapFun(
89 isVideoDownloadAllowed,
90 allowParameters,
91 'filter:api.download.video.allowed.result'
92 )
93
94 if (!checkAllowResult(res, allowParameters, allowedResult)) return
95
96 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
97 return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
66} 98}
67 99
68function getVideoFile (req: express.Request, files: MVideoFile[]) { 100function getVideoFile (req: express.Request, files: MVideoFile[]) {
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
76 108
77 return Object.assign(playlist, { Video: video }) 109 return Object.assign(playlist, { Video: video })
78} 110}
111
112type AllowedResult = {
113 allowed: boolean
114 errorMessage?: string
115}
116
117function isTorrentDownloadAllowed (_object: {
118 torrentPath: string
119}): AllowedResult {
120 return { allowed: true }
121}
122
123function isVideoDownloadAllowed (_object: {
124 video: MVideo
125 videoFile: MVideoFile
126 streamingPlaylist?: MStreamingPlaylist
127}): AllowedResult {
128 return { allowed: true }
129}
130
131function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
132 if (!result || result.allowed !== true) {
133 logger.info('Download is not allowed.', { result, allowParameters })
134 res.status(HttpStatusCode.FORBIDDEN_403)
135 .json({ error: result?.errorMessage || 'Refused download' })
136
137 return false
138 }
139
140 return true
141}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index e29a8fe1d..921067e65 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Feed from 'pfeed' 2import * as Feed from 'pfeed'
3import { VideoFilter } from '../../shared/models/videos/video-query.type'
3import { buildNSFWFilter } from '../helpers/express-utils' 4import { buildNSFWFilter } from '../helpers/express-utils'
4import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
5import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' 6import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
6import { 7import {
7 asyncMiddleware, 8 asyncMiddleware,
8 commonVideosFiltersValidator, 9 commonVideosFiltersValidator,
@@ -17,7 +18,6 @@ import {
17import { cacheRoute } from '../middlewares/cache' 18import { cacheRoute } from '../middlewares/cache'
18import { VideoModel } from '../models/video/video' 19import { VideoModel } from '../models/video/video'
19import { VideoCommentModel } from '../models/video/video-comment' 20import { VideoCommentModel } from '../models/video/video-comment'
20import { VideoFilter } from '../../shared/models/videos/video-query.type'
21 21
22const feedsRouter = express.Router() 22const feedsRouter = express.Router()
23 23
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
318 }, 318 },
319 thumbnail: [ 319 thumbnail: [
320 { 320 {
321 url: WEBSERVER.URL + video.getMiniatureStaticPath(), 321 url: WEBSERVER.URL + video.getPreviewStaticPath(),
322 height: THUMBNAILS_SIZE.height, 322 height: PREVIEWS_SIZE.height,
323 width: THUMBNAILS_SIZE.width 323 width: PREVIEWS_SIZE.width
324 } 324 }
325 ] 325 ]
326 }) 326 })
diff --git a/server/controllers/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
4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' 7import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
9import { asyncMiddleware } from '../middlewares' 9import { asyncMiddleware } from '../middlewares'
10import { AvatarModel } from '../models/avatar/avatar' 10import { ActorImageModel } from '../models/account/actor-image'
11 11
12const lazyStaticRouter = express.Router() 12const lazyStaticRouter = express.Router()
13 13
@@ -15,7 +15,12 @@ lazyStaticRouter.use(cors())
15 15
16lazyStaticRouter.use( 16lazyStaticRouter.use(
17 LAZY_STATIC_PATHS.AVATARS + ':filename', 17 LAZY_STATIC_PATHS.AVATARS + ':filename',
18 asyncMiddleware(getAvatar) 18 asyncMiddleware(getActorImage)
19)
20
21lazyStaticRouter.use(
22 LAZY_STATIC_PATHS.BANNERS + ':filename',
23 asyncMiddleware(getActorImage)
19) 24)
20 25
21lazyStaticRouter.use( 26lazyStaticRouter.use(
@@ -43,36 +48,36 @@ export {
43 48
44// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
45 50
46async function getAvatar (req: express.Request, res: express.Response) { 51async 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 2import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 3import { logger } from '@server/helpers/logger'
5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' 4import { optionalAuthenticate } from '@server/middlewares/auth'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
8import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' 5import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
9import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
10import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
11import { logger } from '@server/helpers/logger' 9import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
12import { optionalAuthenticate } from '@server/middlewares/oauth' 10import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
11import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
12import { serveThemeCSSValidator } from '../middlewares/validators/themes'
13 13
14const sendFileOptions = { 14const sendFileOptions = {
15 maxAge: '30 days', 15 maxAge: '30 days',
diff --git a/server/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
3import { asyncMiddleware, oembedValidator } from '../middlewares' 3import { asyncMiddleware, oembedValidator } from '../middlewares'
4import { accountNameWithHostGetValidator } from '../middlewares/validators' 4import { accountNameWithHostGetValidator } from '../middlewares/validators'
5import { MChannelSummary } from '@server/types/models' 5import { MChannelSummary } from '@server/types/models'
6import { escapeHTML } from '@shared/core-utils/renderer'
6 7
7const servicesRouter = express.Router() 8const 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 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { join } from 'path' 3import { join } from 'path'
4import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
5import { serveIndexHTML } from '@server/lib/client-html' 4import { serveIndexHTML } from '@server/lib/client-html'
5import { getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' 7import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
8import { root } from '../helpers/core-utils' 8import { 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'
3import validator from 'validator' 3import validator from 'validator'
4import { ContextType } from '@shared/models/activitypub/context' 4import { ContextType } from '@shared/models/activitypub/context'
5import { ResultList } from '../../shared/models' 5import { ResultList } from '../../shared/models'
6import { Activity } from '../../shared/models/activitypub'
7import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' 6import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
8import { MActor, MVideoWithHost } from '../types/models' 7import { MActor, MVideoWithHost } from '../types/models'
9import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination (
182 181
183} 182}
184 183
185function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { 184function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) {
186 const activity = activityPubContextify(data, contextType) 185 const activity = activityPubContextify(data, contextType)
187 186
188 return signJsonLDObject(byActor, activity) as Promise<Activity> 187 return signJsonLDObject(byActor, activity)
189} 188}
190 189
191function getAPId (activity: string | { id: string }) { 190function getAPId (activity: string | { id: string }) {
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 935fd22d9..b93868c12 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import { basename, isAbsolute, join, resolve } from 'path' 11import { basename, isAbsolute, join, resolve } from 'path'
12import * as pem from 'pem' 12import * as pem from 'pem'
13import { pipeline } from 'stream'
13import { URL } from 'url' 14import { URL } from 'url'
15import { promisify } from 'util'
14 16
15const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { 17const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
16 if (!oldObject || typeof oldObject !== 'object') { 18 if (!oldObject || typeof oldObject !== 'object') {
@@ -152,24 +154,6 @@ function root () {
152 return rootPath 154 return rootPath
153} 155}
154 156
155// Thanks: https://stackoverflow.com/a/12034334
156function escapeHTML (stringParam) {
157 if (!stringParam) return ''
158
159 const entityMap = {
160 '&': '&amp;',
161 '<': '&lt;',
162 '>': '&gt;',
163 '"': '&quot;',
164 '\'': '&#39;',
165 '/': '&#x2F;',
166 '`': '&#x60;',
167 '=': '&#x3D;'
168 }
169
170 return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
171}
172
173function pageToStartAndCount (page: number, itemsPerPage: number) { 157function 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
236type SemVersion = { major: number, minor: number, patch: number }
237function 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
252const randomBytesPromise = promisify1<number, Buffer>(randomBytes) 247const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
253const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) 248const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
254const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) 249const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
255const execPromise2 = promisify2<string, any, string>(exec) 250const execPromise2 = promisify2<string, any, string>(exec)
256const execPromise = promisify1<string, string>(exec) 251const execPromise = promisify1<string, string>(exec)
252const 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 @@
1import validator from 'validator' 1import validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { isAbuseReasonValid } from '../abuses'
3import { exists } from '../misc' 4import { exists } from '../misc'
4import { sanitizeAndCheckActorObject } from './actor' 5import { sanitizeAndCheckActorObject } from './actor'
5import { isCacheFileObjectValid } from './cache-file' 6import { isCacheFileObjectValid } from './cache-file'
6import { isFlagActivityValid } from './flag'
7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' 7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
8import { isPlaylistObjectValid } from './playlist' 8import { isPlaylistObjectValid } from './playlist'
9import { isDislikeActivityValid, isLikeActivityValid } from './rate'
10import { isShareActivityValid } from './share'
11import { sanitizeAndCheckVideoCommentObject } from './video-comments' 9import { sanitizeAndCheckVideoCommentObject } from './video-comments'
12import { sanitizeAndCheckVideoTorrentObject } from './videos' 10import { sanitizeAndCheckVideoTorrentObject } from './videos'
13import { isViewActivityValid } from './view'
14 11
15function isRootActivityValid (activity: any) { 12function isRootActivityValid (activity: any) {
16 return isCollection(activity) || isActivity(activity) 13 return isCollection(activity) || isActivity(activity)
@@ -29,18 +26,18 @@ function isActivity (activity: any) {
29} 26}
30 27
31const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { 28const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
32 Create: checkCreateActivity, 29 Create: isCreateActivityValid,
33 Update: checkUpdateActivity, 30 Update: isUpdateActivityValid,
34 Delete: checkDeleteActivity, 31 Delete: isDeleteActivityValid,
35 Follow: checkFollowActivity, 32 Follow: isFollowActivityValid,
36 Accept: checkAcceptActivity, 33 Accept: isAcceptActivityValid,
37 Reject: checkRejectActivity, 34 Reject: isRejectActivityValid,
38 Announce: checkAnnounceActivity, 35 Announce: isAnnounceActivityValid,
39 Undo: checkUndoActivity, 36 Undo: isUndoActivityValid,
40 Like: checkLikeActivity, 37 Like: isLikeActivityValid,
41 View: checkViewActivity, 38 View: isViewActivityValid,
42 Flag: checkFlagActivity, 39 Flag: isFlagActivityValid,
43 Dislike: checkDislikeActivity 40 Dislike: isDislikeActivityValid
44} 41}
45 42
46function isActivityValid (activity: any) { 43function isActivityValid (activity: any) {
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) {
51 return checker(activity) 48 return checker(activity)
52} 49}
53 50
54// --------------------------------------------------------------------------- 51function isFlagActivityValid (activity: any) {
55 52 return isBaseActivityValid(activity, 'Flag') &&
56export { 53 isAbuseReasonValid(activity.content) &&
57 isRootActivityValid, 54 isActivityPubUrlValid(activity.object)
58 isActivityValid
59} 55}
60 56
61// --------------------------------------------------------------------------- 57function isLikeActivityValid (activity: any) {
62 58 return isBaseActivityValid(activity, 'Like') &&
63function checkViewActivity (activity: any) { 59 isObjectValid(activity.object)
64 return isBaseActivityValid(activity, 'View') &&
65 isViewActivityValid(activity)
66} 60}
67 61
68function checkFlagActivity (activity: any) { 62function isDislikeActivityValid (activity: any) {
69 return isBaseActivityValid(activity, 'Flag') && 63 return isBaseActivityValid(activity, 'Dislike') &&
70 isFlagActivityValid(activity) 64 isObjectValid(activity.object)
71} 65}
72 66
73function checkDislikeActivity (activity: any) { 67function isAnnounceActivityValid (activity: any) {
74 return isDislikeActivityValid(activity) 68 return isBaseActivityValid(activity, 'Announce') &&
69 isObjectValid(activity.object)
75} 70}
76 71
77function checkLikeActivity (activity: any) { 72function isViewActivityValid (activity: any) {
78 return isLikeActivityValid(activity) 73 return isBaseActivityValid(activity, 'View') &&
74 isActivityPubUrlValid(activity.actor) &&
75 isActivityPubUrlValid(activity.object)
79} 76}
80 77
81function checkCreateActivity (activity: any) { 78function isCreateActivityValid (activity: any) {
82 return isBaseActivityValid(activity, 'Create') && 79 return isBaseActivityValid(activity, 'Create') &&
83 ( 80 (
84 isViewActivityValid(activity.object) || 81 isViewActivityValid(activity.object) ||
@@ -92,7 +89,7 @@ function checkCreateActivity (activity: any) {
92 ) 89 )
93} 90}
94 91
95function checkUpdateActivity (activity: any) { 92function isUpdateActivityValid (activity: any) {
96 return isBaseActivityValid(activity, 'Update') && 93 return isBaseActivityValid(activity, 'Update') &&
97 ( 94 (
98 isCacheFileObjectValid(activity.object) || 95 isCacheFileObjectValid(activity.object) ||
@@ -102,36 +99,51 @@ function checkUpdateActivity (activity: any) {
102 ) 99 )
103} 100}
104 101
105function checkDeleteActivity (activity: any) { 102function isDeleteActivityValid (activity: any) {
106 // We don't really check objects 103 // We don't really check objects
107 return isBaseActivityValid(activity, 'Delete') && 104 return isBaseActivityValid(activity, 'Delete') &&
108 isObjectValid(activity.object) 105 isObjectValid(activity.object)
109} 106}
110 107
111function checkFollowActivity (activity: any) { 108function isFollowActivityValid (activity: any) {
112 return isBaseActivityValid(activity, 'Follow') && 109 return isBaseActivityValid(activity, 'Follow') &&
113 isObjectValid(activity.object) 110 isObjectValid(activity.object)
114} 111}
115 112
116function checkAcceptActivity (activity: any) { 113function isAcceptActivityValid (activity: any) {
117 return isBaseActivityValid(activity, 'Accept') 114 return isBaseActivityValid(activity, 'Accept')
118} 115}
119 116
120function checkRejectActivity (activity: any) { 117function isRejectActivityValid (activity: any) {
121 return isBaseActivityValid(activity, 'Reject') 118 return isBaseActivityValid(activity, 'Reject')
122} 119}
123 120
124function checkAnnounceActivity (activity: any) { 121function isUndoActivityValid (activity: any) {
125 return isShareActivityValid(activity)
126}
127
128function checkUndoActivity (activity: any) {
129 return isBaseActivityValid(activity, 'Undo') && 122 return isBaseActivityValid(activity, 'Undo') &&
130 ( 123 (
131 checkFollowActivity(activity.object) || 124 isFollowActivityValid(activity.object) ||
132 checkLikeActivity(activity.object) || 125 isLikeActivityValid(activity.object) ||
133 checkDislikeActivity(activity.object) || 126 isDislikeActivityValid(activity.object) ||
134 checkAnnounceActivity(activity.object) || 127 isAnnounceActivityValid(activity.object) ||
135 checkCreateActivity(activity.object) 128 isCreateActivityValid(activity.object)
136 ) 129 )
137} 130}
131
132// ---------------------------------------------------------------------------
133
134export {
135 isRootActivityValid,
136 isActivityValid,
137 isFlagActivityValid,
138 isLikeActivityValid,
139 isDislikeActivityValid,
140 isAnnounceActivityValid,
141 isViewActivityValid,
142 isCreateActivityValid,
143 isUpdateActivityValid,
144 isDeleteActivityValid,
145 isFollowActivityValid,
146 isAcceptActivityValid,
147 isRejectActivityValid,
148 isUndoActivityValid
149}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
deleted file mode 100644
index dc90b3667..000000000
--- a/server/helpers/custom-validators/activitypub/flag.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2import { isAbuseReasonValid } from '../abuses'
3
4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' &&
6 isAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isFlagActivityValid
14}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
deleted file mode 100644
index aafdda443..000000000
--- a/server/helpers/custom-validators/activitypub/rate.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isObjectValid(activity.object)
6}
7
8function isDislikeActivityValid (activity: any) {
9 return isBaseActivityValid(activity, 'Dislike') &&
10 isObjectValid(activity.object)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 isDislikeActivityValid,
17 isLikeActivityValid
18}
diff --git a/server/helpers/custom-validators/activitypub/share.ts b/server/helpers/custom-validators/activitypub/share.ts
deleted file mode 100644
index fb5e4c05e..000000000
--- a/server/helpers/custom-validators/activitypub/share.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isShareActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Announce') &&
5 isObjectValid(activity.object)
6}
7// ---------------------------------------------------------------------------
8
9export {
10 isShareActivityValid
11}
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts
deleted file mode 100644
index 41d16469f..000000000
--- a/server/helpers/custom-validators/activitypub/view.ts
+++ /dev/null
@@ -1,13 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2
3function isViewActivityValid (activity: any) {
4 return activity.type === 'View' &&
5 isActivityPubUrlValid(activity.actor) &&
6 isActivityPubUrlValid(activity.object)
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 isViewActivityValid
13}
diff --git a/server/helpers/custom-validators/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
2import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
3import { isFileValid } from './misc'
4
5const imageMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
6 .map(v => v.replace('.', ''))
7 .join('|')
8const imageMimeTypesRegex = `image/(${imageMimeTypes})`
9function 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
15export {
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 @@
1import { exists } from './misc'
2import validator from 'validator' 1import validator from 'validator'
3import { UserNotificationType } from '../../../shared/models/users'
4import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 2import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
3import { exists } from './misc'
5 4
6function isUserNotificationTypeValid (value: any) { 5function isUserNotificationTypeValid (value: any) {
7 return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined 6 return exists(value) && validator.isInt('' + value)
8} 7}
9 8
10function isUserNotificationSettingValid (value: any) { 9function isUserNotificationSettingValid (value: any) {
diff --git a/server/helpers/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 @@
1import { values } from 'lodash'
1import validator from 'validator' 2import validator from 'validator'
2import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
4import { exists, isArray, isBooleanValid, isFileValid } from './misc'
5import { values } from 'lodash'
6import { isEmailEnabled } from '../../initializers/config' 4import { isEmailEnabled } from '../../initializers/config'
5import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
6import { exists, isArray, isBooleanValid } from './misc'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const 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
100const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
101 .map(v => v.replace('.', ''))
102 .join('|')
103const avatarMimeTypesRegex = `image/(${avatarMimeTypes})`
104function 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
110export { 102export {
@@ -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'
3import { readFile, remove, writeFile } from 'fs-extra' 3import { readFile, remove, writeFile } from 'fs-extra'
4import { dirname, join } from 'path' 4import { dirname, join } from 'path'
5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' 5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' 6import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptions, EncoderProfile, VideoResolution } from '../../shared/models/videos'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { promisify0 } from './core-utils' 8import { execPromise, promisify0 } from './core-utils'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
12import { 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
406async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { 408async 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
558async function presetVideo ( 561async 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
639function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand {
640 return command
641 .inputOptions(options.inputOptions ?? [])
642 .outputOptions(options.outputOptions ?? [])
643}
644
645function 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
674function 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
652async function runCommand (options: { 692async 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 @@
1import { copy, readFile, remove, rename } from 'fs-extra' 1import { copy, readFile, remove, rename } from 'fs-extra'
2import * as Jimp from 'jimp' 2import * as Jimp from 'jimp'
3import { extname } from 'path' 3import { extname } from 'path'
4import { v4 as uuidv4 } from 'uuid'
4import { convertWebPToJPG, processGIF } from './ffmpeg-utils' 5import { convertWebPToJPG, processGIF } from './ffmpeg-utils'
5import { logger } from './logger' 6import { logger } from './logger'
6 7
8function generateImageFilename (extension = '.jpg') {
9 return uuidv4() + extension
10}
11
7async function processImage ( 12async 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
33export { 38export {
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
50const consoleLoggerFormat = winston.format.printf(info => { 50const consoleLoggerFormat = winston.format.printf(info => {
51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql' ] 51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ]
52 52
53 const obj = omit(info, ...toOmit) 53 const obj = omit(info, ...toOmit)
54 54
@@ -150,6 +150,13 @@ const bunyanLogger = {
150 error: bunyanLogFactory('error'), 150 error: bunyanLogFactory('error'),
151 fatal: bunyanLogFactory('error') 151 fatal: bunyanLogFactory('error')
152} 152}
153
154function loggerTagsFactory (...defaultTags: string[]) {
155 return (...tags: string[]) => {
156 return { tags: defaultTags.concat(tags) }
157 }
158}
159
153// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
154 161
155export { 162export {
@@ -159,5 +166,6 @@ export {
159 consoleLoggerFormat, 166 consoleLoggerFormat,
160 jsonLoggerFormat, 167 jsonLoggerFormat,
161 logger, 168 logger,
169 loggerTagsFactory,
162 bunyanLogger 170 bunyanLogger
163} 171}
diff --git a/server/helpers/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 @@
1import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
2
3const sanitizeHtml = require('sanitize-html')
4const markdownItEmoji = require('markdown-it-emoji/light')
5const MarkdownItClass = require('markdown-it')
6const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
7
8markdownIt.enable(TEXT_WITH_HTML_RULES)
9markdownIt.use(markdownItEmoji)
10
11const 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
24const 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
40export {
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoChannelModel } from '../../models/video/video-channel' 2import { MChannelBannerAccountDefault } from '@server/types/models'
3import { MChannelAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { VideoChannelModel } from '../../models/video/video-channel'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { 6async 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
32function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { 32function 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
68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 68async 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
87async function signJsonLDObject (byActor: MActor, data: any) { 87async function signJsonLDObject <T> (byActor: MActor, data: T) {
88 const signature = { 88 const signature = {
89 type: 'RsaSignature2017', 89 type: 'RsaSignature2017',
90 creator: byActor.url, 90 creator: byActor.url,
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index b556c392e..fd2a56f30 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,58 +1,141 @@
1import * as Bluebird from 'bluebird'
2import { createWriteStream, remove } from 'fs-extra' 1import { createWriteStream, remove } from 'fs-extra'
3import * as request from 'request' 2import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got'
3import { join } from 'path'
4import { CONFIG } from '../initializers/config'
4import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' 5import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants'
6import { pipelinePromise } from './core-utils'
5import { processImage } from './image-utils' 7import { processImage } from './image-utils'
6import { join } from 'path'
7import { logger } from './logger' 8import { logger } from './logger'
8import { CONFIG } from '../initializers/config'
9 9
10function doRequest <T> ( 10export interface PeerTubeRequestError extends Error {
11 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }, 11 statusCode?: number
12 bodyKBLimit = 1000 // 1MB 12 responseBody?: any
13): Bluebird<{ response: request.RequestResponse, body: T }> { 13}
14 if (!(requestOptions.headers)) requestOptions.headers = {}
15 requestOptions.headers['User-Agent'] = getUserAgent()
16 14
17 if (requestOptions.activityPub === true) { 15const httpSignature = require('http-signature')
18 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 16
17type PeerTubeRequestOptions = {
18 activityPub?: boolean
19 bodyKBLimit?: number // 1MB
20 httpSignature?: {
21 algorithm: string
22 authorizationHeaderName: string
23 keyId: string
24 key: string
25 headers: string[]
19 } 26 }
27 jsonResponse?: boolean
28} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
29
30const peertubeGot = got.extend({
31 headers: {
32 'user-agent': getUserAgent()
33 },
34
35 handlers: [
36 (options, next) => {
37 const promiseOrStream = next(options) as CancelableRequest<any>
38 const bodyKBLimit = options.context?.bodyKBLimit as number
39 if (!bodyKBLimit) throw new Error('No KB limit for this request')
40
41 const bodyLimit = bodyKBLimit * 1000
42
43 /* eslint-disable @typescript-eslint/no-floating-promises */
44 promiseOrStream.on('downloadProgress', progress => {
45 if (progress.transferred > bodyLimit && progress.percent !== 1) {
46 const message = `Exceeded the download limit of ${bodyLimit} B`
47 logger.warn(message)
48
49 // CancelableRequest
50 if (promiseOrStream.cancel) {
51 promiseOrStream.cancel()
52 return
53 }
54
55 // Stream
56 (promiseOrStream as any).destroy()
57 }
58 })
20 59
21 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { 60 return promiseOrStream
22 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 61 }
23 .on('data', onRequestDataLengthCheck(bodyKBLimit)) 62 ],
24 }) 63
64 hooks: {
65 beforeRequest: [
66 options => {
67 const headers = options.headers || {}
68 headers['host'] = options.url.host
69 },
70
71 options => {
72 const httpSignatureOptions = options.context?.httpSignature
73
74 if (httpSignatureOptions) {
75 const method = options.method ?? 'GET'
76 const path = options.path ?? options.url.pathname
77
78 if (!method || !path) {
79 throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
80 }
81
82 httpSignature.signRequest({
83 getHeader: function (header) {
84 return options.headers[header]
85 },
86
87 setHeader: function (header, value) {
88 options.headers[header] = value
89 },
90
91 method,
92 path
93 }, httpSignatureOptions)
94 }
95 }
96 ]
97 }
98})
99
100function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
101 const gotOptions = buildGotOptions(options)
102
103 return peertubeGot(url, gotOptions)
104 .catch(err => { throw buildRequestError(err) })
25} 105}
26 106
27function doRequestAndSaveToFile ( 107function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
28 requestOptions: request.CoreOptions & request.UriOptions, 108 const gotOptions = buildGotOptions(options)
109
110 return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
111 .catch(err => { throw buildRequestError(err) })
112}
113
114async function doRequestAndSaveToFile (
115 url: string,
29 destPath: string, 116 destPath: string,
30 bodyKBLimit = 10000 // 10MB 117 options: PeerTubeRequestOptions = {}
31) { 118) {
32 if (!requestOptions.headers) requestOptions.headers = {} 119 const gotOptions = buildGotOptions(options)
33 requestOptions.headers['User-Agent'] = getUserAgent()
34
35 return new Bluebird<void>((res, rej) => {
36 const file = createWriteStream(destPath)
37 file.on('finish', () => res())
38 120
39 request(requestOptions) 121 const outFile = createWriteStream(destPath)
40 .on('data', onRequestDataLengthCheck(bodyKBLimit))
41 .on('error', err => {
42 file.close()
43 122
44 remove(destPath) 123 try {
45 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) 124 await pipelinePromise(
125 peertubeGot.stream(url, gotOptions),
126 outFile
127 )
128 } catch (err) {
129 remove(destPath)
130 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
46 131
47 return rej(err) 132 throw buildRequestError(err)
48 }) 133 }
49 .pipe(file)
50 })
51} 134}
52 135
53async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { 136async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
54 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) 137 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
55 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 138 await doRequestAndSaveToFile(url, tmpPath)
56 139
57 const destPath = join(destDir, destName) 140 const destPath = join(destDir, destName)
58 141
@@ -73,24 +156,46 @@ function getUserAgent () {
73 156
74export { 157export {
75 doRequest, 158 doRequest,
159 doJSONRequest,
76 doRequestAndSaveToFile, 160 doRequestAndSaveToFile,
77 downloadImage 161 downloadImage
78} 162}
79 163
80// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
81 165
82// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3 166function buildGotOptions (options: PeerTubeRequestOptions) {
83function onRequestDataLengthCheck (bodyKBLimit: number) { 167 const { activityPub, bodyKBLimit = 1000 } = options
84 let bufferLength = 0
85 const bytesLimit = bodyKBLimit * 1000
86 168
87 return function (chunk) { 169 const context = { bodyKBLimit, httpSignature: options.httpSignature }
88 bufferLength += chunk.length
89 if (bufferLength > bytesLimit) {
90 this.abort()
91 170
92 const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`) 171 let headers = options.headers || {}
93 this.emit('error', error) 172
94 } 173 if (!headers.date) {
174 headers = { ...headers, date: new Date().toUTCString() }
175 }
176
177 if (activityPub && !headers.accept) {
178 headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
95 } 179 }
180
181 return {
182 method: options.method,
183 json: options.json,
184 searchParams: options.searchParams,
185 headers,
186 context
187 }
188}
189
190function buildRequestError (error: RequestError) {
191 const newError: PeerTubeRequestError = new Error(error.message)
192 newError.name = error.name
193 newError.stack = error.stack
194
195 if (error.response) {
196 newError.responseBody = error.response.body
197 newError.statusCode = error.response.statusCode
198 }
199
200 return newError
96} 201}
diff --git a/server/helpers/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
22function isSignupAllowedForCurrentIP (ip: string) { 22function 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 @@
1import { createWriteStream } from 'fs' 1import { createWriteStream } from 'fs'
2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' 2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
3import got from 'got'
3import { join } from 'path' 4import { join } from 'path'
4import * as request from 'request'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoResolution } from '../../shared/models/videos' 7import { VideoResolution } from '../../shared/models/videos'
8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' 8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
9import { getEnabledResolutions } from '../lib/video-transcoding' 9import { getEnabledResolutions } from '../lib/video-transcoding'
10import { peertubeTruncate, root } from './core-utils' 10import { peertubeTruncate, pipelinePromise, root } from './core-utils'
11import { isVideoFileExtnameValid } from './custom-validators/videos' 11import { isVideoFileExtnameValid } from './custom-validators/videos'
12import { logger } from './logger' 12import { logger } from './logger'
13import { generateVideoImportTmpPath } from './utils' 13import { generateVideoImportTmpPath } from './utils'
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () {
195 195
196 await ensureDir(binDirectory) 196 await ensureDir(binDirectory)
197 197
198 return new Promise<void>(res => { 198 try {
199 request.get(url, { followRedirect: false }, (err, result) => { 199 const result = await got(url, { followRedirect: false })
200 if (err) {
201 logger.error('Cannot update youtube-dl.', { err })
202 return res()
203 }
204
205 if (result.statusCode !== HttpStatusCode.FOUND_302) {
206 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
207 return res()
208 }
209
210 const url = result.headers.location
211 const downloadFile = request.get(url)
212 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
213
214 downloadFile.on('response', result => {
215 if (result.statusCode !== HttpStatusCode.OK_200) {
216 logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode)
217 return res()
218 }
219
220 const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => {
221 logger.error('youtube-dl update error in write stream', { err })
222 return res()
223 })
224 200
225 downloadFile.pipe(writeStream) 201 if (result.statusCode !== HttpStatusCode.FOUND_302) {
226 }) 202 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
203 return
204 }
227 205
228 downloadFile.on('error', err => { 206 const newUrl = result.headers.location
229 logger.error('youtube-dl update error.', { err }) 207 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
230 return res()
231 })
232 208
233 downloadFile.on('end', () => { 209 const downloadFileStream = got.stream(newUrl)
234 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) 210 const writeStream = createWriteStream(bin, { mode: 493 })
235 writeFile(detailsPath, details, { encoding: 'utf8' }, err => {
236 if (err) {
237 logger.error('youtube-dl update error: cannot write details.', { err })
238 return res()
239 }
240 211
241 logger.info('youtube-dl updated to version %s.', newVersion) 212 await pipelinePromise(
242 return res() 213 downloadFileStream,
243 }) 214 writeStream
244 }) 215 )
245 }) 216
246 }) 217 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
218 await writeFile(detailsPath, details, { encoding: 'utf8' })
219
220 logger.info('youtube-dl updated to version %s.', newVersion)
221 } catch (err) {
222 logger.error('Cannot update youtube-dl.', { err })
223 }
247} 224}
248 225
249async function safeGetYoutubeDL () { 226async function safeGetYoutubeDL () {
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 2b00e2047..a93c8b7fd 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -1,16 +1,17 @@
1import * as config from 'config' 1import * as config from 'config'
2import { isProdInstance, isTestInstance } from '../helpers/core-utils' 2import { uniq } from 'lodash'
3import { UserModel } from '../models/account/user'
4import { getServerActor, ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { URL } from 'url' 3import { URL } from 'url'
7import { CONFIG, isEmailEnabled } from './config' 4import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils'
8import { logger } from '../helpers/logger' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { RecentlyAddedStrategy } from '../../shared/models/redundancy' 6import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
7import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
10import { isArray } from '../helpers/custom-validators/misc' 8import { isArray } from '../helpers/custom-validators/misc'
11import { uniq } from 'lodash' 9import { logger } from '../helpers/logger'
10import { UserModel } from '../models/account/user'
11import { ApplicationModel, getServerActor } from '../models/application/application'
12import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { CONFIG, isEmailEnabled } from './config'
12import { WEBSERVER } from './constants' 14import { WEBSERVER } from './constants'
13import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
14 15
15async function checkActivityPubUrls () { 16async function checkActivityPubUrls () {
16 const actor = await getServerActor() 17 const actor = await getServerActor()
@@ -176,11 +177,21 @@ async function applicationExist () {
176 return totalApplication !== 0 177 return totalApplication !== 0
177} 178}
178 179
180async function checkFFmpegVersion () {
181 const version = await getFFmpegVersion()
182 const { major, minor } = parseSemVersion(version)
183
184 if (major < 4 || (major === 4 && minor < 1)) {
185 logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade.', version)
186 }
187}
188
179// --------------------------------------------------------------------------- 189// ---------------------------------------------------------------------------
180 190
181export { 191export {
182 checkConfig, 192 checkConfig,
183 clientsExist, 193 clientsExist,
194 checkFFmpegVersion,
184 usersExist, 195 usersExist,
185 applicationExist, 196 applicationExist,
186 checkActivityPubUrls 197 checkActivityPubUrls
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 565e0d1fa..2864b0287 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -1,5 +1,5 @@
1import * as config from 'config' 1import * as config from 'config'
2import { promisify0 } from '../helpers/core-utils' 2import { parseSemVersion, promisify0 } from '../helpers/core-utils'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4 4
5// ONLY USE CORE MODULES IN THIS FILE! 5// ONLY USE CORE MODULES IN THIS FILE!
@@ -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
103function checkNodeVersion () { 105function 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 @@
1import * as bytes from 'bytes'
1import { IConfig } from 'config' 2import { IConfig } from 'config'
3import decache from 'decache'
2import { dirname, join } from 'path' 4import { dirname, join } from 'path'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server'
3import { VideosRedundancyStrategy } from '../../shared/models' 7import { VideosRedundancyStrategy } from '../../shared/models'
8import { 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
5import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' 10import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils'
6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
7import * as bytes from 'bytes'
8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { 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
12let config: IConfig = require('config') 13let 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
405export function reloadConfig () { 417export 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
27const LAST_MIGRATION_VERSION = 612 27const LAST_MIGRATION_VERSION = 635
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31const API_VERSION = 'v1' 31const API_VERSION = 'v1'
32const PEERTUBE_VERSION = require(join(root(), 'package.json')).version 32const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version
33 33
34const PAGINATION = { 34const PAGINATION = {
35 GLOBAL: { 35 GLOBAL: {
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
207 updateVideos: 60000, // 1 minute 207 updateVideos: 60000, // 1 minute
208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day 208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, 209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
210 checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
210 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day 211 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
211 removeOldViews: 60000 * 60 * 24, // 1 day 212 removeOldViews: 60000 * 60 * 24, // 1 day
212 removeOldHistory: 60000 * 60 * 24, // 1 day 213 removeOldHistory: 60000 * 60 * 24, // 1 day
@@ -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}
581const LAZY_STATIC_PATHS = { 584const 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
596const THUMBNAILS_SIZE = { 600const THUMBNAILS_SIZE = {
597 width: 223, 601 width: 280,
598 height: 122, 602 height: 157,
599 minWidth: 150 603 minWidth: 150
600} 604}
601const PREVIEWS_SIZE = { 605const PREVIEWS_SIZE = {
@@ -603,9 +607,15 @@ const PREVIEWS_SIZE = {
603 height: 480, 607 height: 480,
604 minWidth: 400 608 minWidth: 400
605} 609}
606const AVATARS_SIZE = { 610const 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
611const EMBED_SIZE = { 621const 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
672const QUEUE_CONCURRENCY = { 682const QUEUE_CONCURRENCY = {
673 AVATAR_PROCESS_IMAGE: 3 683 ACTOR_PROCESS_IMAGE: 3
674} 684}
675 685
676const REDUNDANCY = { 686const 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 @@
1import { TrackerModel } from '@server/models/server/tracker'
2import { VideoTrackerModel } from '@server/models/server/video-tracker'
3import { QueryTypes, Transaction } from 'sequelize' 1import { QueryTypes, Transaction } from 'sequelize'
4import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' 2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { TrackerModel } from '@server/models/server/tracker'
4import { VideoTrackerModel } from '@server/models/server/video-tracker'
5import { isTestInstance } from '../helpers/core-utils' 5import { isTestInstance } from '../helpers/core-utils'
6import { logger } from '../helpers/logger' 6import { logger } from '../helpers/logger'
7import { AbuseModel } from '../models/abuse/abuse' 7import { AbuseModel } from '../models/abuse/abuse'
@@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
11import { AccountModel } from '../models/account/account' 11import { AccountModel } from '../models/account/account'
12import { AccountBlocklistModel } from '../models/account/account-blocklist' 12import { AccountBlocklistModel } from '../models/account/account-blocklist'
13import { AccountVideoRateModel } from '../models/account/account-video-rate' 13import { AccountVideoRateModel } from '../models/account/account-video-rate'
14import { ActorImageModel } from '../models/account/actor-image'
14import { UserModel } from '../models/account/user' 15import { UserModel } from '../models/account/user'
15import { UserNotificationModel } from '../models/account/user-notification' 16import { UserNotificationModel } from '../models/account/user-notification'
16import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 17import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history'
18import { ActorModel } from '../models/activitypub/actor' 19import { ActorModel } from '../models/activitypub/actor'
19import { ActorFollowModel } from '../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../models/activitypub/actor-follow'
20import { ApplicationModel } from '../models/application/application' 21import { ApplicationModel } from '../models/application/application'
21import { AvatarModel } from '../models/avatar/avatar'
22import { OAuthClientModel } from '../models/oauth/oauth-client' 22import { OAuthClientModel } from '../models/oauth/oauth-client'
23import { OAuthTokenModel } from '../models/oauth/oauth-token' 23import { OAuthTokenModel } from '../models/oauth/oauth-token'
24import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 24import { 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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
11
12 for (const column of notificationSettingColumns) {
13 const data = {
14 type: Sequelize.INTEGER,
15 defaultValue: null,
16 allowNull: true
17 }
18 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
19 }
20
21 {
22 const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
23 await utils.sequelize.query(query)
24 }
25
26 for (const column of notificationSettingColumns) {
27 const data = {
28 type: Sequelize.INTEGER,
29 defaultValue: null,
30 allowNull: false
31 }
32 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
33 }
34 }
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
new file mode 100644
index 000000000..a689b18fc
--- /dev/null
+++ b/server/initializers/migrations/0620-latest-versions-application.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
new file mode 100644
index 000000000..77f395ce4
--- /dev/null
+++ b/server/initializers/migrations/0625-latest-versions-notification.ts
@@ -0,0 +1,26 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`
12 ALTER TABLE "userNotification"
13 ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
15 `)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`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
43function down (options) {
44 throw new Error('Not implemented.')
45}
46
47export {
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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const 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
28function down (options) {
29 throw new Error('Not implemented.')
30}
31
32export {
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
2import { Op, Transaction } from 'sequelize' 3import { Op, Transaction } from 'sequelize'
3import { URL } from 'url' 4import { URL } from 'url'
4import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 13import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 14import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 15import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 17import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest } from '../../helpers/requests' 18import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 19import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 20import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
21import { sequelizeTypescript } from '../../initializers/database'
16import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
23import { ActorImageModel } from '../../models/account/actor-image'
17import { ActorModel } from '../../models/activitypub/actor' 24import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 25import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 26import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue'
22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23import { sequelizeTypescript } from '../../initializers/database'
24import { 27import {
25 MAccount, 28 MAccount,
26 MAccountDefault, 29 MAccountDefault,
@@ -28,15 +31,14 @@ import {
28 MActorAccountChannelId, 31 MActorAccountChannelId,
29 MActorAccountChannelIdActor, 32 MActorAccountChannelIdActor,
30 MActorAccountId, 33 MActorAccountId,
31 MActorDefault,
32 MActorFull, 34 MActorFull,
33 MActorFullActor, 35 MActorFullActor,
34 MActorId, 36 MActorId,
37 MActorImage,
38 MActorImages,
35 MChannel 39 MChannel
36} from '../../types/models' 40} from '../../types/models'
37import { extname } from 'path' 41import { JobQueue } from '../job-queue'
38import { getServerActor } from '@server/models/application/application'
39import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
40 42
41// Set account keys, this could be long so process after the account creation and do not block the client 43// Set account keys, this could be long so process after the account creation and do not block the client
42async function generateAndSaveActorKeys <T extends MActor> (actor: T) { 44async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
168 } 170 }
169} 171}
170 172
171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } 173type ImageInfo = {
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { 174 name: string
173 if (!info.name) return actor 175 fileUrl: string
176 height: number
177 width: number
178 onDisk?: boolean
179}
180async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
181 const oldImageModel = type === ActorImageType.AVATAR
182 ? actor.Avatar
183 : actor.Banner
174 184
175 if (actor.Avatar) { 185 if (oldImageModel) {
176 // Don't update the avatar if the file URL did not change 186 // Don't update the avatar if the file URL did not change
177 if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor 187 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
178 188
179 try { 189 try {
180 await actor.Avatar.destroy({ transaction: t }) 190 await oldImageModel.destroy({ transaction: t })
191
192 setActorImage(actor, type, null)
181 } catch (err) { 193 } catch (err) {
182 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 194 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
183 } 195 }
184 } 196 }
185 197
186 const avatar = await AvatarModel.create({ 198 if (imageInfo) {
187 filename: info.name, 199 const imageModel = await ActorImageModel.create({
188 onDisk: info.onDisk, 200 filename: imageInfo.name,
189 fileUrl: info.fileUrl 201 onDisk: imageInfo.onDisk ?? false,
190 }, { transaction: t }) 202 fileUrl: imageInfo.fileUrl,
191 203 height: imageInfo.height,
192 actor.avatarId = avatar.id 204 width: imageInfo.width,
193 actor.Avatar = avatar 205 type
206 }, { transaction: t })
207
208 setActorImage(actor, type, imageModel)
209 }
194 210
195 return actor 211 return actor
196} 212}
197 213
198async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { 214async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
199 try { 215 try {
200 await actor.Avatar.destroy({ transaction: t }) 216 if (type === ActorImageType.AVATAR) {
217 await actor.Avatar.destroy({ transaction: t })
218
219 actor.avatarId = null
220 actor.Avatar = null
221 } else {
222 await actor.Banner.destroy({ transaction: t })
223
224 actor.bannerId = null
225 actor.Banner = null
226 }
201 } catch (err) { 227 } catch (err) {
202 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 228 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
203 } 229 }
204 230
205 actor.avatarId = null
206 actor.Avatar = null
207
208 return actor 231 return actor
209} 232}
210 233
211async function fetchActorTotalItems (url: string) { 234async function fetchActorTotalItems (url: string) {
212 const options = {
213 uri: url,
214 method: 'GET',
215 json: true,
216 activityPub: true
217 }
218
219 try { 235 try {
220 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) 236 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
221 return body.totalItems ? body.totalItems : 0 237
238 return body.totalItems || 0
222 } catch (err) { 239 } catch (err) {
223 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 240 logger.warn('Cannot fetch remote actor count %s.', url, { err })
224 return 0 241 return 0
225 } 242 }
226} 243}
227 244
228function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 245function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
229 const mimetypes = MIMETYPES.IMAGE 246 const mimetypes = MIMETYPES.IMAGE
230 const icon = actorJSON.icon 247 const icon = type === ActorImageType.AVATAR
248 ? actorJSON.icon
249 : actorJSON.image
231 250
232 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 251 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
233 252
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
245 264
246 return { 265 return {
247 name: uuidv4() + extension, 266 name: uuidv4() + extension,
248 fileUrl: icon.url 267 fileUrl: icon.url,
268 height: icon.height,
269 width: icon.width,
270 type
249 } 271 }
250} 272}
251 273
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
285 actorUrl = actor.url 307 actorUrl = actor.url
286 } 308 }
287 309
288 const { result, statusCode } = await fetchRemoteActor(actorUrl) 310 const { result } = await fetchRemoteActor(actorUrl)
289
290 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
291 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
292 actor.Account
293 ? await actor.Account.destroy()
294 : await actor.VideoChannel.destroy()
295
296 return { actor: undefined, refreshed: false }
297 }
298 311
299 if (result === undefined) { 312 if (result === undefined) {
300 logger.warn('Cannot fetch remote actor in refresh actor.') 313 logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
304 return sequelizeTypescript.transaction(async t => { 317 return sequelizeTypescript.transaction(async t => {
305 updateInstanceWithAnother(actor, result.actor) 318 updateInstanceWithAnother(actor, result.actor)
306 319
307 if (result.avatar !== undefined) { 320 await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t)
308 const avatarInfo = { 321 await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t)
309 name: result.avatar.name,
310 fileUrl: result.avatar.fileUrl,
311 onDisk: false
312 }
313
314 await updateActorAvatarInstance(actor, avatarInfo, t)
315 }
316 322
317 // Force update 323 // Force update
318 actor.setDataValue('updatedAt', new Date()) 324 actor.setDataValue('updatedAt', new Date())
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
334 return { refreshed: true, actor } 340 return { refreshed: true, actor }
335 }) 341 })
336 } catch (err) { 342 } catch (err) {
343 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
344 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
345 actor.Account
346 ? await actor.Account.destroy()
347 : await actor.VideoChannel.destroy()
348
349 return { actor: undefined, refreshed: false }
350 }
351
337 logger.warn('Cannot refresh actor %s.', actor.url, { err }) 352 logger.warn('Cannot refresh actor %s.', actor.url, { err })
338 return { actor, refreshed: false } 353 return { actor, refreshed: false }
339 } 354 }
@@ -344,16 +359,32 @@ export {
344 buildActorInstance, 359 buildActorInstance,
345 generateAndSaveActorKeys, 360 generateAndSaveActorKeys,
346 fetchActorTotalItems, 361 fetchActorTotalItems,
347 getAvatarInfoIfExists, 362 getImageInfoIfExists,
348 updateActorInstance, 363 updateActorInstance,
349 deleteActorAvatarInstance, 364 deleteActorImageInstance,
350 refreshActorIfNeeded, 365 refreshActorIfNeeded,
351 updateActorAvatarInstance, 366 updateActorImageInstance,
352 addFetchOutboxJob 367 addFetchOutboxJob
353} 368}
354 369
355// --------------------------------------------------------------------------- 370// ---------------------------------------------------------------------------
356 371
372function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
373 const id = imageModel
374 ? imageModel.id
375 : null
376
377 if (type === ActorImageType.AVATAR) {
378 actorModel.avatarId = id
379 actorModel.Avatar = imageModel
380 } else {
381 actorModel.bannerId = id
382 actorModel.Banner = imageModel
383 }
384
385 return actorModel
386}
387
357function saveActorAndServerAndModelIfNotExist ( 388function saveActorAndServerAndModelIfNotExist (
358 result: FetchRemoteActorResult, 389 result: FetchRemoteActorResult,
359 ownerActor?: MActorFullActor, 390 ownerActor?: MActorFullActor,
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist (
384 415
385 // Avatar? 416 // Avatar?
386 if (result.avatar) { 417 if (result.avatar) {
387 const avatar = await AvatarModel.create({ 418 const avatar = await ActorImageModel.create({
388 filename: result.avatar.name, 419 filename: result.avatar.name,
389 fileUrl: result.avatar.fileUrl, 420 fileUrl: result.avatar.fileUrl,
390 onDisk: false 421 width: result.avatar.width,
422 height: result.avatar.height,
423 onDisk: false,
424 type: ActorImageType.AVATAR
391 }, { transaction: t }) 425 }, { transaction: t })
392 426
393 actor.avatarId = avatar.id 427 actor.avatarId = avatar.id
394 } 428 }
395 429
430 // Banner?
431 if (result.banner) {
432 const banner = await ActorImageModel.create({
433 filename: result.banner.name,
434 fileUrl: result.banner.fileUrl,
435 width: result.banner.width,
436 height: result.banner.height,
437 onDisk: false,
438 type: ActorImageType.BANNER
439 }, { transaction: t })
440
441 actor.bannerId = banner.id
442 }
443
396 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 444 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
397 // (which could be false in a retried query) 445 // (which could be false in a retried query)
398 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ 446 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist (
436 } 484 }
437} 485}
438 486
487type ImageResult = {
488 name: string
489 fileUrl: string
490 height: number
491 width: number
492}
493
439type FetchRemoteActorResult = { 494type FetchRemoteActorResult = {
440 actor: MActor 495 actor: MActor
441 name: string 496 name: string
442 summary: string 497 summary: string
443 support?: string 498 support?: string
444 playlists?: string 499 playlists?: string
445 avatar?: { 500 avatar?: ImageResult
446 name: string 501 banner?: ImageResult
447 fileUrl: string
448 }
449 attributedTo: ActivityPubAttributedTo[] 502 attributedTo: ActivityPubAttributedTo[]
450} 503}
451async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 504async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
452 const options = {
453 uri: actorUrl,
454 method: 'GET',
455 json: true,
456 activityPub: true
457 }
458
459 logger.info('Fetching remote actor %s.', actorUrl) 505 logger.info('Fetching remote actor %s.', actorUrl)
460 506
461 const requestResult = await doRequest<ActivityPubActor>(options) 507 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
462 const actorJSON = requestResult.body 508 const actorJSON = requestResult.body
463 509
464 if (sanitizeAndCheckActorObject(actorJSON) === false) { 510 if (sanitizeAndCheckActorObject(actorJSON) === false) {
465 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 511 logger.debug('Remote actor JSON is not valid.', { actorJSON })
466 return { result: undefined, statusCode: requestResult.response.statusCode } 512 return { result: undefined, statusCode: requestResult.statusCode }
467 } 513 }
468 514
469 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 515 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
470 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) 516 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
471 return { result: undefined, statusCode: requestResult.response.statusCode } 517 return { result: undefined, statusCode: requestResult.statusCode }
472 } 518 }
473 519
474 const followersCount = await fetchActorTotalItems(actorJSON.followers) 520 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
492 : null 538 : null
493 }) 539 })
494 540
495 const avatarInfo = await getAvatarInfoIfExists(actorJSON) 541 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
542 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
496 543
497 const name = actorJSON.name || actorJSON.preferredUsername 544 const name = actorJSON.name || actorJSON.preferredUsername
498 return { 545 return {
499 statusCode: requestResult.response.statusCode, 546 statusCode: requestResult.statusCode,
500 result: { 547 result: {
501 actor, 548 actor,
502 name, 549 name,
503 avatar: avatarInfo, 550 avatar: avatarInfo,
551 banner: bannerInfo,
504 summary: actorJSON.summary, 552 summary: actorJSON.summary,
505 support: actorJSON.support, 553 support: actorJSON.support,
506 playlists: actorJSON.playlists, 554 playlists: actorJSON.playlists,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1ed105bbe..278abf7de 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,27 +1,26 @@
1import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { URL } from 'url' 2import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
4import { logger } from '../../helpers/logger'
5import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
10 10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { 11async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
12 logger.info('Crawling ActivityPub data on %s.', uri) 12 let url = argUrl
13
14 logger.info('Crawling ActivityPub data on %s.', url)
13 15
14 const options = { 16 const options = {
15 method: 'GET',
16 uri,
17 json: true,
18 activityPub: true, 17 activityPub: true,
19 timeout: REQUEST_TIMEOUT 18 timeout: REQUEST_TIMEOUT
20 } 19 }
21 20
22 const startDate = new Date() 21 const startDate = new Date()
23 22
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 23 const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
25 const firstBody = response.body 24 const firstBody = response.body
26 25
27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 26 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
35 const remoteHost = new URL(nextLink).host 34 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 35 if (remoteHost === WEBSERVER.HOST) continue
37 36
38 options.uri = nextLink 37 url = nextLink
39 38
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options) 39 const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
41 body = res.body 40 body = res.body
42 } else { 41 } else {
43 // nextLink is already the object we want 42 // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
49 48
50 if (Array.isArray(body.orderedItems)) { 49 if (Array.isArray(body.orderedItems)) {
51 const items = body.orderedItems 50 const items = body.orderedItems
52 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) 51 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
53 52
54 await handler(items) 53 await handler(items)
55 } 54 }
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index d5a3ef7c8..7166c68a6 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,24 +1,24 @@
1import * as Bluebird from 'bluebird'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 5import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { checkUrlsSameHost } from '../../helpers/activitypub'
7import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
4import { isArray } from '../../helpers/custom-validators/misc' 8import { isArray } from '../../helpers/custom-validators/misc'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
11import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { sequelizeTypescript } from '../../initializers/database'
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 13import { VideoPlaylistModel } from '../../models/video/video-playlist'
8import { doRequest } from '../../helpers/requests'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import * as Bluebird from 'bluebird'
11import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
12import { getOrCreateVideoAndAccountAndChannel } from './videos'
13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
16import { sequelizeTypescript } from '../../initializers/database'
17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
18import { FilteredModelAttributes } from '../../types/sequelize'
19import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' 15import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' 16import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
21import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 17import { FilteredModelAttributes } from '../../types/sequelize'
18import { createPlaylistMiniatureFromUrl } from '../thumbnail'
19import { getOrCreateActorAndServerAndModel } from './actor'
20import { crawlCollectionPage } from './crawl'
21import { getOrCreateVideoAndAccountAndChannel } from './videos'
22 22
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
56 if (exists === true) return 56 if (exists === true) return
57 57
58 // Fetch url 58 // Fetch url
59 const { body } = await doRequest<PlaylistObject>({ 59 const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
60 uri: playlistUrl,
61 json: true,
62 activityPub: true
63 })
64 60
65 if (!isPlaylistObjectValid(body)) { 61 if (!isPlaylistObjectValid(body)) {
66 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) 62 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
120 if (!videoPlaylist.isOutdated()) return videoPlaylist 116 if (!videoPlaylist.isOutdated()) return videoPlaylist
121 117
122 try { 118 try {
123 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) 119 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
124 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
125 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
126
127 await videoPlaylist.destroy()
128 return undefined
129 }
130 120
131 if (playlistObject === undefined) { 121 if (playlistObject === undefined) {
132 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) 122 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
140 130
141 return videoPlaylist 131 return videoPlaylist
142 } catch (err) { 132 } catch (err) {
133 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
134 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
135
136 await videoPlaylist.destroy()
137 return undefined
138 }
139
143 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) 140 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
144 141
145 await videoPlaylist.setAsRefreshed() 142 await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
164 161
165 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
166 try { 163 try {
167 // Fetch url 164 const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
168 const { body } = await doRequest<PlaylistElementObject>({
169 uri: elementUrl,
170 json: true,
171 activityPub: true
172 })
173 165
174 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) 166 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
175 167
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
199} 191}
200 192
201async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 193async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
202 const options = {
203 uri: playlistUrl,
204 method: 'GET',
205 json: true,
206 activityPub: true
207 }
208
209 logger.info('Fetching remote playlist %s.', playlistUrl) 194 logger.info('Fetching remote playlist %s.', playlistUrl)
210 195
211 const { response, body } = await doRequest<any>(options) 196 const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
212 197
213 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 198 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
214 logger.debug('Remote video playlist JSON is not valid.', { body }) 199 logger.debug('Remote video playlist JSON is not valid.', { body })
215 return { statusCode: response.statusCode, playlistObject: undefined } 200 return { statusCode, playlistObject: undefined }
216 } 201 }
217 202
218 return { statusCode: response.statusCode, playlistObject: body } 203 return { statusCode, playlistObject: body }
219} 204}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index a86def936..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'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model' 9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' 10import {
11 MAccountActor,
12 MActor,
13 MActorFull,
14 MActorSignature,
15 MChannelAccountActor,
16 MChannelActor,
17 MCommentOwnerVideo
18} from '../../../types/models'
11import { markCommentAsDeleted } from '../../video-comment' 19import { markCommentAsDeleted } from '../../video-comment'
12import { forwardVideoRelatedActivity } from '../send/utils' 20import { 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'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model' 17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../types/models' 18import { MActorSignature, MAccountIdActor } from '../../../types/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy' 19import { isRedundancyAccepted } from '@server/lib/redundancy'
20import { ActorImageType } from '@shared/models'
20 21
21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 22async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
22 const { activity, byActor } = options 23 const { activity, byActor } = options
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
119 let accountOrChannelFieldsSave: object 120 let accountOrChannelFieldsSave: object
120 121
121 // Fetch icon? 122 // Fetch icon?
122 const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) 123 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
124 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
123 125
124 try { 126 try {
125 await sequelizeTypescript.transaction(async t => { 127 await sequelizeTypescript.transaction(async t => {
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
132 134
133 await updateActorInstance(actor, actorAttributesToUpdate) 135 await updateActorInstance(actor, actorAttributesToUpdate)
134 136
135 if (avatarInfo !== undefined) { 137 await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t)
136 const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) 138 await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t)
137
138 await updateActorAvatarInstance(actor, avatarOptions, t)
139 }
140 139
141 await actor.save({ transaction: t }) 140 await actor.save({ transaction: t })
142 141
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9fb218224..baded642a 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
4import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { 9import {
10 MActorLight, 10 MActorLight,
@@ -18,10 +18,12 @@ import {
18import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context' 19import { ContextType } from '@shared/models/activitypub/context'
20 20
21const lTags = loggerTagsFactory('ap', 'create')
22
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 23async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 24 if (!video.hasPrivacyForFederation()) return undefined
23 25
24 logger.info('Creating job to send video creation of %s.', video.url) 26 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
25 27
26 const byActor = video.VideoChannel.Account.Actor 28 const byActor = video.VideoChannel.Account.Actor
27 const videoObject = video.toActivityPubObject() 29 const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
37 video: MVideoAccountLight, 39 video: MVideoAccountLight,
38 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 40 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
39) { 41) {
40 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 42 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
41 43
42 return sendVideoRelatedCreateActivity({ 44 return sendVideoRelatedCreateActivity({
43 byActor, 45 byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
51async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { 53async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
52 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 54 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
53 55
54 logger.info('Creating job to send create video playlist of %s.', playlist.url) 56 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
55 57
56 const byActor = playlist.OwnerAccount.Actor 58 const byActor = playlist.OwnerAccount.Actor
57 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 59 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1f8a8f3c4..c22fa0893 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,15 +1,17 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { logger, loggerTagsFactory } from '../../helpers/logger'
6import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
2import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor'
3import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
4import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
5import * as Bluebird from 'bluebird' 13
6import { doRequest } from '../../helpers/requests' 14const lTags = loggerTagsFactory('share')
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { logger } from '../../helpers/logger'
9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
12import { getServerActor } from '@server/models/application/application'
13 15
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 16async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 17 if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
25 oldVideoChannel: MChannelActorLight, 27 oldVideoChannel: MChannelActorLight,
26 t: Transaction 28 t: Transaction
27) { 29) {
28 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 30 logger.info(
31 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
32 lTags(video.uuid)
33 )
29 34
30 await undoShareByVideoChannel(video, oldVideoChannel, t) 35 await undoShareByVideoChannel(video, oldVideoChannel, t)
31 36
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
35async function addVideoShares (shareUrls: string[], video: MVideoId) { 40async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 41 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 42 try {
38 // Fetch url 43 const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
39 const { body } = await doRequest<any>({
40 uri: shareUrl,
41 json: true,
42 activityPub: true
43 })
44 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 44 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
45 45
46 const actorUrl = getAPId(body.actor) 46 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d025ed7f1..e23e0c0e7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird'
2import { checkUrlsSameHost } from '../../helpers/activitypub'
1import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
2import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 5import { doJSONRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
6import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
7import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
8import * as Bluebird from 'bluebird'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string 13 url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, async commentUrl => {
22 return resolveThread({ url: commentUrl, isVideo: false }) 22 try {
23 await resolveThread({ url: commentUrl, isVideo: false })
24 } catch (err) {
25 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
26 }
23 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 27 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
24} 28}
25 29
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
126 throw new Error('Recursion limit reached when resolving a thread') 130 throw new Error('Recursion limit reached when resolving a thread')
127 } 131 }
128 132
129 const { body } = await doRequest<any>({ 133 const { body } = await doJSONRequest<any>(url, { activityPub: true })
130 uri: url,
131 json: true,
132 activityPub: true
133 })
134 134
135 if (sanitizeAndCheckVideoCommentObject(body) === false) { 135 if (sanitizeAndCheckVideoCommentObject(body) === false) {
136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) 136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index e246b1313..f40c07fea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,26 +1,22 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
2import { sendLike, sendUndoDislike, sendUndoLike } from './send' 3import { doJSONRequest } from '@server/helpers/requests'
3import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
4import * as Bluebird from 'bluebird' 5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { AccountVideoRateModel } from '../../models/account/account-video-rate'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
9import { doRequest } from '../../helpers/requests' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 14
15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
16 await Bluebird.map(ratesUrl, async rateUrl => { 16 await Bluebird.map(ratesUrl, async rateUrl => {
17 try { 17 try {
18 // Fetch url 18 // Fetch url
19 const { body } = await doRequest<any>({ 19 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
20 uri: rateUrl,
21 json: true,
22 activityPub: true
23 })
24 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 20 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
25 21
26 const actorUrl = getAPId(body.actor) 22 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c02578aad..506204674 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,8 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename } from 'path'
5import * as request from 'request'
6import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -17,7 +16,7 @@ import {
17 ActivityUrlObject, 16 ActivityUrlObject,
18 ActivityVideoUrlObject 17 ActivityVideoUrlObject
19} from '../../../shared/index' 18} from '../../../shared/index'
20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
21import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc'
31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 30import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 31import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
33import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
34import { doRequest } from '../../helpers/requests' 33import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
38 LAZY_STATIC_PATHS,
39 MIMETYPES, 37 MIMETYPES,
40 P2P_MEDIA_LOADER_PEER_VERSION, 38 P2P_MEDIA_LOADER_PEER_VERSION,
41 PREVIEWS_SIZE, 39 PREVIEWS_SIZE,
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
115 } 113 }
116} 114}
117 115
118async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { 116async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
119 const options = {
120 uri: videoUrl,
121 method: 'GET',
122 json: true,
123 activityPub: true
124 }
125
126 logger.info('Fetching remote video %s.', videoUrl) 117 logger.info('Fetching remote video %s.', videoUrl)
127 118
128 const { response, body } = await doRequest<any>(options) 119 const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
129 120
130 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 121 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
131 logger.debug('Remote video JSON is not valid.', { body }) 122 logger.debug('Remote video JSON is not valid.', { body })
132 return { response, videoObject: undefined } 123 return { statusCode, videoObject: undefined }
133 } 124 }
134 125
135 return { response, videoObject: body } 126 return { statusCode, videoObject: body }
136} 127}
137 128
138async function fetchRemoteVideoDescription (video: MVideoAccountLight) { 129async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
139 const host = video.VideoChannel.Account.Actor.Server.host 130 const host = video.VideoChannel.Account.Actor.Server.host
140 const path = video.getDescriptionAPIPath() 131 const path = video.getDescriptionAPIPath()
141 const options = { 132 const url = REMOTE_SCHEME.HTTP + '://' + host + path
142 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
143 json: true
144 }
145 133
146 const { body } = await doRequest<any>(options) 134 const { body } = await doJSONRequest<any>(url)
147 return body.description ? body.description : '' 135 return body.description || ''
148} 136}
149 137
150function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { 138function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: {
378 366
379 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 367 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
380 368
381 if (videoUpdated.getPreview()) { 369 const previewIcon = getPreviewFromIcons(videoObject)
382 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) 370 if (videoUpdated.getPreview() && previewIcon) {
383 const previewModel = createPlaceholderThumbnail({ 371 const previewModel = createPlaceholderThumbnail({
384 fileUrl: previewUrl, 372 fileUrl: previewIcon.url,
385 video, 373 video,
386 type: ThumbnailType.PREVIEW, 374 type: ThumbnailType.PREVIEW,
387 size: PREVIEWS_SIZE 375 size: previewIcon
388 }) 376 })
389 await videoUpdated.addAndSaveThumbnail(previewModel, t) 377 await videoUpdated.addAndSaveThumbnail(previewModel, t)
390 } 378 }
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: {
534 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 522 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
535 523
536 try { 524 try {
537 const { response, videoObject } = await fetchRemoteVideo(video.url) 525 const { videoObject } = await fetchRemoteVideo(video.url)
538 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
539 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
540
541 // Video does not exist anymore
542 await video.destroy()
543 return undefined
544 }
545 526
546 if (videoObject === undefined) { 527 if (videoObject === undefined) {
547 logger.warn('Cannot refresh remote video %s: invalid body.', video.url) 528 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -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
909function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
910 return previewIcon
911 ? previewIcon.url
912 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
913}
914
915function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { 900function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
916 let wsFound = false 901 let wsFound = false
917 902
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts
new file mode 100644
index 000000000..f271f0b5b
--- /dev/null
+++ b/server/lib/actor-image.ts
@@ -0,0 +1,97 @@
1import 'multer'
2import { queue } from 'async'
3import * as LRUCache from 'lru-cache'
4import { extname, join } from 'path'
5import { v4 as uuidv4 } from 'uuid'
6import { ActorImageType } from '@shared/models'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { processImage } from '../helpers/image-utils'
9import { downloadImage } from '../helpers/requests'
10import { CONFIG } from '../initializers/config'
11import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
12import { sequelizeTypescript } from '../initializers/database'
13import { MAccountDefault, MChannelDefault } from '../types/models'
14import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor'
15import { sendUpdateActor } from './activitypub/send'
16
17async function updateLocalActorImageFile (
18 accountOrChannel: MAccountDefault | MChannelDefault,
19 imagePhysicalFile: Express.Multer.File,
20 type: ActorImageType
21) {
22 const imageSize = type === ActorImageType.AVATAR
23 ? ACTOR_IMAGES_SIZE.AVATARS
24 : ACTOR_IMAGES_SIZE.BANNERS
25
26 const extension = extname(imagePhysicalFile.filename)
27
28 const imageName = uuidv4() + extension
29 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
30 await processImage(imagePhysicalFile.path, destination, imageSize)
31
32 return retryTransactionWrapper(() => {
33 return sequelizeTypescript.transaction(async t => {
34 const actorImageInfo = {
35 name: imageName,
36 fileUrl: null,
37 height: imageSize.height,
38 width: imageSize.width,
39 onDisk: true
40 }
41
42 const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t)
43 await updatedActor.save({ transaction: t })
44
45 await sendUpdateActor(accountOrChannel, t)
46
47 return type === ActorImageType.AVATAR
48 ? updatedActor.Avatar
49 : updatedActor.Banner
50 })
51 })
52}
53
54async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
55 return retryTransactionWrapper(() => {
56 return sequelizeTypescript.transaction(async t => {
57 const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t)
58 await updatedActor.save({ transaction: t })
59
60 await sendUpdateActor(accountOrChannel, t)
61
62 return updatedActor.Avatar
63 })
64 })
65}
66
67type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
68
69const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
70 const size = task.type === ActorImageType.AVATAR
71 ? ACTOR_IMAGES_SIZE.AVATARS
72 : ACTOR_IMAGES_SIZE.BANNERS
73
74 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
75 .then(() => cb())
76 .catch(err => cb(err))
77}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
78
79function pushActorImageProcessInQueue (task: DownloadImageQueueTask) {
80 return new Promise<void>((res, rej) => {
81 downloadImageQueue.push(task, err => {
82 if (err) return rej(err)
83
84 return res()
85 })
86 })
87}
88
89// Unsafe so could returns paths that does not exist anymore
90const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE })
91
92export {
93 actorImagePathUnsafeCache,
94 updateLocalActorImageFile,
95 deleteLocalActorImageFile,
96 pushActorImageProcessInQueue
97}
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts
index dbd421a7b..80f5064b6 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth/external-auth.ts
@@ -1,28 +1,16 @@
1
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 2import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 4import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' 5import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import { 8import {
10 RegisterServerAuthenticatedResult, 9 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions, 10 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult 11 RegisterServerExternalAuthenticatedResult
13} from '@server/types/plugins/register-server-auth.model' 12} from '@server/types/plugins/register-server-auth.model'
14import * as express from 'express' 13import { UserRole } from '@shared/models'
15import * as OAuthServer from 'express-oauth-server'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17
18const oAuthServer = new OAuthServer({
19 useErrorHandler: true,
20 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
21 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
22 allowExtendedTokenAttributes: true,
23 continueMiddleware: true,
24 model: require('./oauth-model')
25})
26 14
27// Token is the key, expiration date is the value 15// Token is the key, expiration date is the value
28const authBypassTokens = new Map<string, { 16const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
37 npmName: string 25 npmName: string
38}>() 26}>()
39 27
40async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
41 const grantType = req.body.grant_type
42
43 if (grantType === 'password') {
44 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
45 else await proxifyPasswordGrant(req, res)
46 } else if (grantType === 'refresh_token') {
47 await proxifyRefreshGrant(req, res)
48 }
49
50 return forwardTokenReq(req, res, next)
51}
52
53async function handleTokenRevocation (req: express.Request, res: express.Response) {
54 const token = res.locals.oauth.token
55
56 res.locals.explicitLogout = true
57 const result = await revokeToken(token)
58
59 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
60 // oAuthServer.revoke(req, res, err => {
61 // if (err) {
62 // logger.warn('Error in revoke token handler.', { err })
63 //
64 // return res.status(err.status)
65 // .json({
66 // error: err.message,
67 // code: err.name
68 // })
69 // .end()
70 // }
71 // })
72
73 return res.json(result)
74}
75
76async function onExternalUserAuthenticated (options: { 28async function onExternalUserAuthenticated (options: {
77 npmName: string 29 npmName: string
78 authName: string 30 authName: string
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: {
107 authName 59 authName
108 }) 60 })
109 61
110 // Cleanup 62 // Cleanup expired tokens
111 const now = new Date() 63 const now = new Date()
112 for (const [ key, value ] of authBypassTokens) { 64 for (const [ key, value ] of authBypassTokens) {
113 if (value.expires.getTime() < now.getTime()) { 65 if (value.expires.getTime() < now.getTime()) {
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: {
118 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) 70 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
119} 71}
120 72
121// --------------------------------------------------------------------------- 73async function getAuthNameFromRefreshGrant (refreshToken?: string) {
122 74 if (!refreshToken) return undefined
123export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
124
125// ---------------------------------------------------------------------------
126
127function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
128 return oAuthServer.token()(req, res, err => {
129 if (err) {
130 logger.warn('Login error.', { err })
131
132 return res.status(err.status)
133 .json({
134 error: err.message,
135 code: err.name
136 })
137 }
138
139 if (next) return next()
140 })
141}
142
143async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
144 const refreshToken = req.body.refresh_token
145 if (!refreshToken) return
146 75
147 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) 76 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
148 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName 77
78 return tokenModel?.authName
149} 79}
150 80
151async function proxifyPasswordGrant (req: express.Request, res: express.Response) { 81async function getBypassFromPasswordGrant (username: string, password: string) {
152 const plugins = PluginManager.Instance.getIdAndPassAuths() 82 const plugins = PluginManager.Instance.getIdAndPassAuths()
153 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 83 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
154 84
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
174 }) 104 })
175 105
176 const loginOptions = { 106 const loginOptions = {
177 id: req.body.username, 107 id: username,
178 password: req.body.password 108 password
179 } 109 }
180 110
181 for (const pluginAuth of pluginAuths) { 111 for (const pluginAuth of pluginAuths) {
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
199 authName, npmName, loginOptions.id 129 authName, npmName, loginOptions.id
200 ) 130 )
201 131
202 res.locals.bypassLogin = { 132 return {
203 bypass: true, 133 bypass: true,
204 pluginName: pluginAuth.npmName, 134 pluginName: pluginAuth.npmName,
205 authName: authOptions.authName, 135 authName: authOptions.authName,
206 user: buildUserResult(loginResult) 136 user: buildUserResult(loginResult)
207 } 137 }
208
209 return
210 } catch (err) { 138 } catch (err) {
211 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) 139 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
212 } 140 }
213 } 141 }
142
143 return undefined
214} 144}
215 145
216function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { 146function getBypassFromExternalAuth (username: string, externalAuthToken: string) {
217 const obj = authBypassTokens.get(req.body.externalAuthToken) 147 const obj = authBypassTokens.get(externalAuthToken)
218 if (!obj) { 148 if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
219 logger.error('Cannot authenticate user with unknown bypass token')
220 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
221 }
222 149
223 const { expires, user, authName, npmName } = obj 150 const { expires, user, authName, npmName } = obj
224 151
225 const now = new Date() 152 const now = new Date()
226 if (now.getTime() > expires.getTime()) { 153 if (now.getTime() > expires.getTime()) {
227 logger.error('Cannot authenticate user with an expired external auth token') 154 throw new Error('Cannot authenticate user with an expired external auth token')
228 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
229 } 155 }
230 156
231 if (user.username !== req.body.username) { 157 if (user.username !== username) {
232 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) 158 throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`)
233 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
234 } 159 }
235 160
236 // Bypass oauth library validation
237 req.body.password = 'fake'
238
239 logger.info( 161 logger.info(
240 'Auth success with external auth method %s of plugin %s for %s.', 162 'Auth success with external auth method %s of plugin %s for %s.',
241 authName, npmName, user.email 163 authName, npmName, user.email
242 ) 164 )
243 165
244 res.locals.bypassLogin = { 166 return {
245 bypass: true, 167 bypass: true,
246 pluginName: npmName, 168 pluginName: npmName,
247 authName: authName, 169 authName: authName,
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
286 displayName: pluginResult.displayName || pluginResult.username 208 displayName: pluginResult.displayName || pluginResult.username
287 } 209 }
288} 210}
211
212// ---------------------------------------------------------------------------
213
214export {
215 onExternalUserAuthenticated,
216 getBypassFromExternalAuth,
217 getAuthNameFromRefreshGrant,
218 getBypassFromPasswordGrant
219}
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts
index a2c53a2c9..b9c69eb2d 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -1,49 +1,36 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as LRUCache from 'lru-cache'
3import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
4import { Transaction } from 'sequelize'
5import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
6import { ActorModel } from '@server/models/activitypub/actor' 4import { ActorModel } from '@server/models/activitypub/actor'
5import { MOAuthClient } from '@server/types/models'
7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
8import { MUser } from '@server/types/models/user/user' 7import { MUser } from '@server/types/models/user/user'
9import { UserAdminFlag } from '@shared/models/users/user-flag.model' 8import { UserAdminFlag } from '@shared/models/users/user-flag.model'
10import { UserRole } from '@shared/models/users/user-role' 9import { UserRole } from '@shared/models/users/user-role'
11import { logger } from '../helpers/logger' 10import { logger } from '../../helpers/logger'
12import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../../initializers/config'
13import { LRU_CACHE } from '../initializers/constants' 12import { UserModel } from '../../models/account/user'
14import { UserModel } from '../models/account/user' 13import { OAuthClientModel } from '../../models/oauth/oauth-client'
15import { OAuthClientModel } from '../models/oauth/oauth-client' 14import { OAuthTokenModel } from '../../models/oauth/oauth-token'
16import { OAuthTokenModel } from '../models/oauth/oauth-token' 15import { createUserAccountAndChannelAndPlaylist } from '../user'
17import { createUserAccountAndChannelAndPlaylist } from './user' 16import { TokensCache } from './tokens-cache'
18 17
19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = {
20 19 accessToken: string
21const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 20 refreshToken: string
22const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 21 accessTokenExpiresAt: Date
23 22 refreshTokenExpiresAt: Date
24// ---------------------------------------------------------------------------
25
26function deleteUserToken (userId: number, t?: Transaction) {
27 clearCacheByUserId(userId)
28
29 return OAuthTokenModel.deleteUserToken(userId, t)
30} 23}
31 24
32function clearCacheByUserId (userId: number) { 25export type BypassLogin = {
33 const token = userHavingToken.get(userId) 26 bypass: boolean
34 27 pluginName: string
35 if (token !== undefined) { 28 authName?: string
36 accessTokenCache.del(token) 29 user: {
37 userHavingToken.del(userId) 30 username: string
38 } 31 email: string
39} 32 displayName: string
40 33 role: UserRole
41function clearCacheByToken (token: string) {
42 const tokenModel = accessTokenCache.get(token)
43
44 if (tokenModel !== undefined) {
45 userHavingToken.del(tokenModel.userId)
46 accessTokenCache.del(token)
47 } 34 }
48} 35}
49 36
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) {
54 41
55 let tokenModel: MOAuthTokenUser 42 let tokenModel: MOAuthTokenUser
56 43
57 if (accessTokenCache.has(bearerToken)) { 44 if (TokensCache.Instance.hasToken(bearerToken)) {
58 tokenModel = accessTokenCache.get(bearerToken) 45 tokenModel = TokensCache.Instance.getByToken(bearerToken)
59 } else { 46 } else {
60 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 47 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
61 48
62 if (tokenModel) { 49 if (tokenModel) TokensCache.Instance.setToken(tokenModel)
63 accessTokenCache.set(bearerToken, tokenModel)
64 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
65 }
66 } 50 }
67 51
68 if (!tokenModel) return undefined 52 if (!tokenModel) return undefined
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) {
99 return tokenInfo 83 return tokenInfo
100} 84}
101 85
102async function getUser (usernameOrEmail?: string, password?: string) { 86async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) {
103 const res: express.Response = this.request.res
104
105 // Special treatment coming from a plugin 87 // Special treatment coming from a plugin
106 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { 88 if (bypassLogin && bypassLogin.bypass === true) {
107 const obj = res.locals.bypassLogin 89 logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
108 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
109 90
110 let user = await UserModel.loadByEmail(obj.user.email) 91 let user = await UserModel.loadByEmail(bypassLogin.user.email)
111 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) 92 if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
112 93
113 // Cannot create a user 94 // Cannot create a user
114 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') 95 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) {
117 // Then we just go through a regular login process 98 // Then we just go through a regular login process
118 if (user.pluginAuth !== null) { 99 if (user.pluginAuth !== null) {
119 // This user does not belong to this plugin, skip it 100 // This user does not belong to this plugin, skip it
120 if (user.pluginAuth !== obj.pluginName) return null 101 if (user.pluginAuth !== bypassLogin.pluginName) return null
121 102
122 checkUserValidityOrThrow(user) 103 checkUserValidityOrThrow(user)
123 104
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) {
143 return user 124 return user
144} 125}
145 126
146async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { 127async function revokeToken (
147 const res: express.Response = this.request.res 128 tokenInfo: { refreshToken: string },
129 options: {
130 req?: express.Request
131 explicitLogout?: boolean
132 } = {}
133): Promise<{ success: boolean, redirectUrl?: string }> {
134 const { req, explicitLogout } = options
135
148 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 136 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
149 137
150 if (token) { 138 if (token) {
151 let redirectUrl: string 139 let redirectUrl: string
152 140
153 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { 141 if (explicitLogout === true && token.User.pluginAuth && token.authName) {
154 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) 142 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req)
155 } 143 }
156 144
157 clearCacheByToken(token.accessToken) 145 TokensCache.Instance.clearCacheByToken(token.accessToken)
158 146
159 token.destroy() 147 token.destroy()
160 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 148 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ
165 return { success: false } 153 return { success: false }
166} 154}
167 155
168async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 156async function saveToken (
169 const res: express.Response = this.request.res 157 token: TokenInfo,
170 158 client: MOAuthClient,
159 user: MUser,
160 options: {
161 refreshTokenAuthName?: string
162 bypassLogin?: BypassLogin
163 } = {}
164) {
165 const { refreshTokenAuthName, bypassLogin } = options
171 let authName: string = null 166 let authName: string = null
172 if (res.locals.bypassLogin?.bypass === true) { 167
173 authName = res.locals.bypassLogin.authName 168 if (bypassLogin?.bypass === true) {
174 } else if (res.locals.refreshTokenAuthName) { 169 authName = bypassLogin.authName
175 authName = res.locals.refreshTokenAuthName 170 } else if (refreshTokenAuthName) {
171 authName = refreshTokenAuthName
176 } 172 }
177 173
178 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 174 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
199 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, 195 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt,
200 client, 196 client,
201 user, 197 user,
202 refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) 198 accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt),
199 refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt)
203 } 200 }
204} 201}
205 202
206// ---------------------------------------------------------------------------
207
208// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
209export { 203export {
210 deleteUserToken,
211 clearCacheByUserId,
212 clearCacheByToken,
213 getAccessToken, 204 getAccessToken,
214 getClient, 205 getClient,
215 getRefreshToken, 206 getRefreshToken,
@@ -218,6 +209,8 @@ export {
218 saveToken 209 saveToken
219} 210}
220 211
212// ---------------------------------------------------------------------------
213
221async function createUserFromExternal (pluginAuth: string, options: { 214async function createUserFromExternal (pluginAuth: string, options: {
222 username: string 215 username: string
223 email: string 216 email: string
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: {
252function checkUserValidityOrThrow (user: MUser) { 245function checkUserValidityOrThrow (user: MUser) {
253 if (user.blocked) throw new AccessDeniedError('User is blocked.') 246 if (user.blocked) throw new AccessDeniedError('User is blocked.')
254} 247}
248
249function buildExpiresIn (expiresAt: Date) {
250 return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000)
251}
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
new file mode 100644
index 000000000..5b6130d56
--- /dev/null
+++ b/server/lib/auth/oauth.ts
@@ -0,0 +1,180 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
15
16/**
17 *
18 * Reimplement some functions of OAuth2Server to inject external auth methods
19 *
20 */
21
22const oAuthServer = new (require('oauth2-server'))({
23 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
24 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
25
26 // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
27 model: require('./oauth-model')
28})
29
30// ---------------------------------------------------------------------------
31
32async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) {
33 const request = new Request(req)
34 const { refreshTokenAuthName, bypassLogin } = options
35
36 if (request.method !== 'POST') {
37 throw new InvalidRequestError('Invalid request: method must be POST')
38 }
39
40 if (!request.is([ 'application/x-www-form-urlencoded' ])) {
41 throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')
42 }
43
44 const clientId = request.body.client_id
45 const clientSecret = request.body.client_secret
46
47 if (!clientId || !clientSecret) {
48 throw new InvalidClientError('Invalid client: cannot retrieve client credentials')
49 }
50
51 const client = await getClient(clientId, clientSecret)
52 if (!client) {
53 throw new InvalidClientError('Invalid client: client is invalid')
54 }
55
56 const grantType = request.body.grant_type
57 if (!grantType) {
58 throw new InvalidRequestError('Missing parameter: `grant_type`')
59 }
60
61 if (![ 'password', 'refresh_token' ].includes(grantType)) {
62 throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid')
63 }
64
65 if (!client.grants.includes(grantType)) {
66 throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid')
67 }
68
69 if (grantType === 'password') {
70 return handlePasswordGrant({
71 request,
72 client,
73 bypassLogin
74 })
75 }
76
77 return handleRefreshGrant({
78 request,
79 client,
80 refreshTokenAuthName
81 })
82}
83
84async function handleOAuthAuthenticate (
85 req: express.Request,
86 res: express.Response,
87 authenticateInQuery = false
88) {
89 const options = authenticateInQuery
90 ? { allowBearerTokensInQueryString: true }
91 : {}
92
93 return oAuthServer.authenticate(new Request(req), new Response(res), options)
94}
95
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async function handlePasswordGrant (options: {
104 request: Request
105 client: MOAuthClient
106 bypassLogin?: BypassLogin
107}) {
108 const { request, client, bypassLogin } = options
109
110 if (!request.body.username) {
111 throw new InvalidRequestError('Missing parameter: `username`')
112 }
113
114 if (!bypassLogin && !request.body.password) {
115 throw new InvalidRequestError('Missing parameter: `password`')
116 }
117
118 const user = await getUser(request.body.username, request.body.password, bypassLogin)
119 if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid')
120
121 const token = await buildToken()
122
123 return saveToken(token, client, user, { bypassLogin })
124}
125
126async function handleRefreshGrant (options: {
127 request: Request
128 client: MOAuthClient
129 refreshTokenAuthName: string
130}) {
131 const { request, client, refreshTokenAuthName } = options
132
133 if (!request.body.refresh_token) {
134 throw new InvalidRequestError('Missing parameter: `refresh_token`')
135 }
136
137 const refreshToken = await getRefreshToken(request.body.refresh_token)
138
139 if (!refreshToken) {
140 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
141 }
142
143 if (refreshToken.client.id !== client.id) {
144 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
145 }
146
147 if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) {
148 throw new InvalidGrantError('Invalid grant: refresh token has expired')
149 }
150
151 await revokeToken({ refreshToken: refreshToken.refreshToken })
152
153 const token = await buildToken()
154
155 return saveToken(token, client, refreshToken.user, { refreshTokenAuthName })
156}
157
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function getTokenExpiresAt (type: 'access' | 'refresh') {
164 const lifetime = type === 'access'
165 ? OAUTH_LIFETIME.ACCESS_TOKEN
166 : OAUTH_LIFETIME.REFRESH_TOKEN
167
168 return new Date(Date.now() + lifetime * 1000)
169}
170
171async function buildToken () {
172 const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ])
173
174 return {
175 accessToken,
176 refreshToken,
177 accessTokenExpiresAt: getTokenExpiresAt('access'),
178 refreshTokenExpiresAt: getTokenExpiresAt('refresh')
179 }
180}
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts
new file mode 100644
index 000000000..b027ce69a
--- /dev/null
+++ b/server/lib/auth/tokens-cache.ts
@@ -0,0 +1,52 @@
1import * as LRUCache from 'lru-cache'
2import { MOAuthTokenUser } from '@server/types/models'
3import { LRU_CACHE } from '../../initializers/constants'
4
5export class TokensCache {
6
7 private static instance: TokensCache
8
9 private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
10 private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
11
12 private constructor () { }
13
14 static get Instance () {
15 return this.instance || (this.instance = new this())
16 }
17
18 hasToken (token: string) {
19 return this.accessTokenCache.has(token)
20 }
21
22 getByToken (token: string) {
23 return this.accessTokenCache.get(token)
24 }
25
26 setToken (token: MOAuthTokenUser) {
27 this.accessTokenCache.set(token.accessToken, token)
28 this.userHavingToken.set(token.userId, token.accessToken)
29 }
30
31 deleteUserToken (userId: number) {
32 this.clearCacheByUserId(userId)
33 }
34
35 clearCacheByUserId (userId: number) {
36 const token = this.userHavingToken.get(userId)
37
38 if (token !== undefined) {
39 this.accessTokenCache.del(token)
40 this.userHavingToken.del(userId)
41 }
42 }
43
44 clearCacheByToken (token: string) {
45 const tokenModel = this.accessTokenCache.get(token)
46
47 if (tokenModel !== undefined) {
48 this.userHavingToken.del(tokenModel.userId)
49 this.accessTokenCache.del(token)
50 }
51 }
52}
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
deleted file mode 100644
index 86f1e7bdb..000000000
--- a/server/lib/avatar.ts
+++ /dev/null
@@ -1,85 +0,0 @@
1import 'multer'
2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { v4 as uuidv4 } from 'uuid'
9import { CONFIG } from '../initializers/config'
10import { sequelizeTypescript } from '../initializers/database'
11import * as LRUCache from 'lru-cache'
12import { queue } from 'async'
13import { downloadImage } from '../helpers/requests'
14import { MAccountDefault, MChannelDefault } from '../types/models'
15
16async function updateLocalActorAvatarFile (
17 accountOrChannel: MAccountDefault | MChannelDefault,
18 avatarPhysicalFile: Express.Multer.File
19) {
20 const extension = extname(avatarPhysicalFile.filename)
21
22 const avatarName = uuidv4() + extension
23 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
24 await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
25
26 return retryTransactionWrapper(() => {
27 return sequelizeTypescript.transaction(async t => {
28 const avatarInfo = {
29 name: avatarName,
30 fileUrl: null,
31 onDisk: true
32 }
33
34 const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t)
35 await updatedActor.save({ transaction: t })
36
37 await sendUpdateActor(accountOrChannel, t)
38
39 return updatedActor.Avatar
40 })
41 })
42}
43
44async function deleteLocalActorAvatarFile (
45 accountOrChannel: MAccountDefault | MChannelDefault
46) {
47 return retryTransactionWrapper(() => {
48 return sequelizeTypescript.transaction(async t => {
49 const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t)
50 await updatedActor.save({ transaction: t })
51
52 await sendUpdateActor(accountOrChannel, t)
53
54 return updatedActor.Avatar
55 })
56 })
57}
58
59type DownloadImageQueueTask = { fileUrl: string, filename: string }
60
61const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
62 downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE)
63 .then(() => cb())
64 .catch(err => cb(err))
65}, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE)
66
67function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
68 return new Promise<void>((res, rej) => {
69 downloadImageQueue.push(task, err => {
70 if (err) return rej(err)
71
72 return res()
73 })
74 })
75}
76
77// Unsafe so could returns paths that does not exist anymore
78const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE })
79
80export {
81 avatarPathUnsafeCache,
82 updateLocalActorAvatarFile,
83 deleteLocalActorAvatarFile,
84 pushAvatarProcessInQueue
85}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index f19ec7df0..203bd3893 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -5,12 +5,13 @@ import validator from 'validator'
5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' 5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' 7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
8import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' 8import { isTestInstance, sha256 } from '../helpers/core-utils'
9import { escapeHTML } from '@shared/core-utils/renderer'
9import { logger } from '../helpers/logger' 10import { logger } from '../helpers/logger'
10import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
11import { 12import {
12 ACCEPT_HEADERS, 13 ACCEPT_HEADERS,
13 AVATARS_SIZE, 14 ACTOR_IMAGES_SIZE,
14 CUSTOM_HTML_TAG_COMMENTS, 15 CUSTOM_HTML_TAG_COMMENTS,
15 EMBED_SIZE, 16 EMBED_SIZE,
16 FILES_CONTENT_HASH, 17 FILES_CONTENT_HASH,
@@ -23,6 +24,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
23import { getActivityStreamDuration } from '../models/video/video-format-utils' 24import { getActivityStreamDuration } from '../models/video/video-format-utils'
24import { VideoPlaylistModel } from '../models/video/video-playlist' 25import { VideoPlaylistModel } from '../models/video/video-playlist'
25import { MAccountActor, MChannelActor } from '../types/models' 26import { MAccountActor, MChannelActor } from '../types/models'
27import { mdToPlainText } from '../helpers/markdown'
26 28
27type Tags = { 29type 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 @@
1import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/signup'
2import { getServerCommit } from '@server/helpers/utils'
3import { CONFIG, isEmailEnabled } from '@server/initializers/config'
4import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
5import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
6import { Hooks } from './plugins/hooks'
7import { PluginManager } from './plugins/plugin-manager'
8import { getThemeOrDefault } from './plugins/theme-utils'
9import { getEnabledResolutions } from './video-transcoding'
10import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
11
12let serverCommit: string
13
14async 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
190function 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
201function 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
213export {
214 getServerConfig,
215 getRegisteredThemes,
216 getRegisteredPlugins
217}
218
219// ---------------------------------------------------------------------------
220
221function 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
239function 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'
5import { VideoChannelModel } from '@server/models/video/video-channel' 5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' 6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' 7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' 8import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
10import { SendEmailOptions } from '../../shared/models/server/emailer.model' 9import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
11import { isTestInstance, root } from '../helpers/core-utils' 10import { isTestInstance, root } from '../helpers/core-utils'
12import { bunyanLogger, logger } from '../helpers/logger' 11import { bunyanLogger, logger } from '../helpers/logger'
13import { CONFIG, isEmailEnabled } from '../initializers/config' 12import { CONFIG, isEmailEnabled } from '../initializers/config'
14import { WEBSERVER } from '../initializers/constants' 13import { WEBSERVER } from '../initializers/constants'
15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 14import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue' 16import { JobQueue } from './job-queue'
18 17import { toSafeHtml } from '../helpers/markdown'
19const sanitizeHtml = require('sanitize-html')
20const markdownItEmoji = require('markdown-it-emoji/light')
21const MarkdownItClass = require('markdown-it')
22const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
23
24markdownIt.enable(TEXT_WITH_HTML_RULES)
25
26markdownIt.use(markdownItEmoji)
27
28const 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
39const Email = require('email-templates') 19const 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 @@
1extends ../common/greetings
2
3block title
4 | New PeerTube version available
5
6block content
7 p
8 | A new version of PeerTube is available: #{latestVersion}.
9 | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug
new file mode 100644
index 000000000..86d3d87e8
--- /dev/null
+++ b/server/lib/emails/plugin-version-new/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New plugin version available
5
6block content
7 p
8 | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
9 | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index ee0447010..58e2260b6 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = videoCaption.getFileUrl(video) 41 const remoteUrl = videoCaption.getFileUrl(video)
42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) 42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename)
43 43
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 44 await doRequestAndSaveToFile(remoteUrl, destPath)
45 45
46 return { isOwned: false, path: destPath } 46 return { isOwned: false, path: destPath }
47 } 47 }
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index ee72cd3f9..dd3a84aca 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) 39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
40 40
41 const remoteUrl = preview.getFileUrl(video) 41 const remoteUrl = preview.getFileUrl(video)
42 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 42 await doRequestAndSaveToFile(remoteUrl, destPath)
43 43
44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) 44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
45 45
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts
index ca0e1770d..23217f140 100644
--- a/server/lib/files-cache/videos-torrent-cache.ts
+++ b/server/lib/files-cache/videos-torrent-cache.ts
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
5import { FILES_CACHE } from '../../initializers/constants' 5import { FILES_CACHE } from '../../initializers/constants'
6import { VideoModel } from '../../models/video/video' 6import { VideoModel } from '../../models/video/video'
7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
8import { MVideo, MVideoFile } from '@server/types/models'
8 9
9class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { 10class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
10 11
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
22 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) 23 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
23 if (!file) return undefined 24 if (!file) return undefined
24 25
25 if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } 26 if (file.getVideo().isOwned()) {
27 const downloadName = this.buildDownloadName(file.getVideo(), file)
28
29 return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
30 }
26 31
27 return this.loadRemoteFile(filename) 32 return this.loadRemoteFile(filename)
28 } 33 }
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = file.getRemoteTorrentUrl(video) 46 const remoteUrl = file.getRemoteTorrentUrl(video)
42 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) 47 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
43 48
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 49 await doRequestAndSaveToFile(remoteUrl, destPath)
45 50
46 const downloadName = `${video.name}-${file.resolution}p.torrent` 51 const downloadName = this.buildDownloadName(video, file)
47 52
48 return { isOwned: false, path: destPath, downloadName } 53 return { isOwned: false, path: destPath, downloadName }
49 } 54 }
55
56 private buildDownloadName (video: MVideo, file: MVideoFile) {
57 return `${video.name}-${file.resolution}p.torrent`
58 }
50} 59}
51 60
52export { 61export {
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 04187668c..84539e2c1 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
135 const destPath = join(tmpDirectory, basename(fileUrl)) 135 const destPath = join(tmpDirectory, basename(fileUrl))
136 136
137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB 137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB
138 await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) 138 await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit })
139 } 139 }
140 140
141 clearTimeout(timer) 141 clearTimeout(timer)
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
156 } 156 }
157 157
158 async function fetchUniqUrls (playlistUrl: string) { 158 async function fetchUniqUrls (playlistUrl: string) {
159 const { body } = await doRequest<string>({ uri: playlistUrl }) 159 const { body } = await doRequest(playlistUrl)
160 160
161 if (!body) return [] 161 if (!body) return []
162 162
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts
index b58bbc983..1caca1dcc 100644
--- a/server/lib/job-queue/handlers/activitypub-cleaner.ts
+++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts
@@ -1,10 +1,13 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Bull from 'bull' 2import * as Bull from 'bull'
3import { checkUrlsSameHost } from '@server/helpers/activitypub' 3import { checkUrlsSameHost } from '@server/helpers/activitypub'
4import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' 4import {
5import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' 5 isAnnounceActivityValid,
6 isDislikeActivityValid,
7 isLikeActivityValid
8} from '@server/helpers/custom-validators/activitypub/activity'
6import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' 9import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
7import { doRequest } from '@server/helpers/requests' 10import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
8import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' 11import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
9import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
10import { VideoCommentModel } from '@server/models/video/video-comment' 13import { VideoCommentModel } from '@server/models/video/video-comment'
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> (
78 updater: (url: string, newUrl: string) => Promise<T>, 81 updater: (url: string, newUrl: string) => Promise<T>,
79 deleter: (url: string) => Promise<T> 82 deleter: (url: string) => Promise<T>
80): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { 83): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
81 // Fetch url 84 const on404OrTombstone = async () => {
82 const { response, body } = await doRequest<any>({
83 uri: url,
84 json: true,
85 activityPub: true
86 })
87
88 // Does not exist anymore, remove entry
89 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
90 logger.info('Removing remote AP object %s.', url) 85 logger.info('Removing remote AP object %s.', url)
91 const data = await deleter(url) 86 const data = await deleter(url)
92 87
93 return { status: 'deleted', data } 88 return { status: 'deleted' as 'deleted', data }
94 } 89 }
95 90
96 // If not same id, check same host and update 91 try {
97 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) 92 const { body } = await doJSONRequest<any>(url, { activityPub: true })
98 93
99 if (body.type === 'Tombstone') { 94 // If not same id, check same host and update
100 logger.info('Removing remote AP object %s.', url) 95 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
101 const data = await deleter(url)
102 96
103 return { status: 'deleted', data } 97 if (body.type === 'Tombstone') {
104 } 98 return on404OrTombstone()
99 }
105 100
106 const newUrl = body.id 101 const newUrl = body.id
107 if (newUrl !== url) { 102 if (newUrl !== url) {
108 if (checkUrlsSameHost(newUrl, url) !== true) { 103 if (checkUrlsSameHost(newUrl, url) !== true) {
109 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) 104 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
105 }
106
107 logger.info('Updating remote AP object %s.', url)
108 const data = await updater(url, newUrl)
109
110 return { status: 'updated', data }
110 } 111 }
111 112
112 logger.info('Updating remote AP object %s.', url) 113 return null
113 const data = await updater(url, newUrl) 114 } catch (err) {
115 // Does not exist anymore, remove entry
116 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
117 return on404OrTombstone()
118 }
114 119
115 return { status: 'updated', data } 120 throw err
116 } 121 }
117
118 return null
119} 122}
120 123
121function rateOptionsFactory () { 124function rateOptionsFactory () {
@@ -149,7 +152,7 @@ function rateOptionsFactory () {
149 152
150function shareOptionsFactory () { 153function shareOptionsFactory () {
151 return { 154 return {
152 bodyValidator: (body: any) => isShareActivityValid(body), 155 bodyValidator: (body: any) => isAnnounceActivityValid(body),
153 156
154 updater: async (url: string, newUrl: string) => { 157 updater: async (url: string, newUrl: string) => {
155 const share = await VideoShareModel.loadByUrl(url, undefined) 158 const share = await VideoShareModel.loadByUrl(url, undefined)
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 7174786d6..c69ff9e83 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri: '',
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
28 const goodUrls: string[] = [] 27 const goodUrls: string[] = []
29 28
30 await Bluebird.map(payload.uris, uri => { 29 await Bluebird.map(payload.uris, uri => {
31 return doRequest(Object.assign({}, options, { uri })) 30 return doRequest(uri, options)
32 .then(() => goodUrls.push(uri)) 31 .then(() => goodUrls.push(uri))
33 .catch(() => badUrls.push(uri)) 32 .catch(() => badUrls.push(uri))
34 }, { concurrency: BROADCAST_CONCURRENCY }) 33 }, { concurrency: BROADCAST_CONCURRENCY })
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index 74989d62e..585dad671 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri,
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
25 } 24 }
26 25
27 try { 26 try {
28 await doRequest(options) 27 await doRequest(uri, options)
29 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) 28 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], [])
30 } catch (err) { 29 } catch (err) {
31 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) 30 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ])
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index c030d31ef..e8a91450d 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto' 6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context' 7import { ContextType } from '@shared/models/activitypub/context'
8 8
9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } 9type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
10 10
11async function computeBody (payload: Payload) { 11async function computeBody <T> (
12 payload: Payload<T>
13): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> {
12 let body = payload.body 14 let body = payload.body
13 15
14 if (payload.signatureActorId) { 16 if (payload.signatureActorId) {
15 const actorSignature = await ActorModel.load(payload.signatureActorId) 17 const actorSignature = await ActorModel.load(payload.signatureActorId)
16 if (!actorSignature) throw new Error('Unknown signature actor id.') 18 if (!actorSignature) throw new Error('Unknown signature actor id.')
19
17 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) 20 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
18 } 21 }
19 22
20 return body 23 return body
21} 24}
22 25
23async function buildSignedRequestOptions (payload: Payload) { 26async function buildSignedRequestOptions (payload: Payload<any>) {
24 let actor: MActor | null 27 let actor: MActor | null
25 28
26 if (payload.signatureActorId) { 29 if (payload.signatureActorId) {
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) {
43 46
44function buildGlobalHeaders (body: any) { 47function buildGlobalHeaders (body: any) {
45 return { 48 return {
46 'Digest': buildDigest(body), 49 'digest': buildDigest(body),
47 'Content-Type': 'application/activity+json', 50 'content-type': 'application/activity+json',
48 'Accept': ACTIVITY_PUB.ACCEPT_HEADER 51 'accept': ACTIVITY_PUB.ACCEPT_HEADER
49 } 52 }
50} 53}
51 54
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 740c274d7..da7f7cc05 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
19import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
21import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { Emailer } from './emailer'
@@ -144,6 +144,20 @@ class Notifier {
144 }) 144 })
145 } 145 }
146 146
147 notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
148 this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
149 .catch(err => {
150 logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
151 })
152 }
153
154 notifyOfNewPluginVersion (plugin: MPlugin) {
155 this.notifyAdminsOfNewPluginVersion(plugin)
156 .catch(err => {
157 logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
158 })
159 }
160
147 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { 161 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
148 // List all followers that are users 162 // List all followers that are users
149 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 163 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -667,6 +681,64 @@ class Notifier {
667 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 681 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
668 } 682 }
669 683
684 private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
685 // Use the debug right to know who is an administrator
686 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
687 if (admins.length === 0) return
688
689 logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
690
691 function settingGetter (user: MUserWithNotificationSetting) {
692 return user.NotificationSetting.newPeerTubeVersion
693 }
694
695 async function notificationCreator (user: MUserWithNotificationSetting) {
696 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
697 type: UserNotificationType.NEW_PEERTUBE_VERSION,
698 userId: user.id,
699 applicationId: application.id
700 })
701 notification.Application = application
702
703 return notification
704 }
705
706 function emailSender (emails: string[]) {
707 return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
708 }
709
710 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
711 }
712
713 private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
714 // Use the debug right to know who is an administrator
715 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
716 if (admins.length === 0) return
717
718 logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
719
720 function settingGetter (user: MUserWithNotificationSetting) {
721 return user.NotificationSetting.newPluginVersion
722 }
723
724 async function notificationCreator (user: MUserWithNotificationSetting) {
725 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
726 type: UserNotificationType.NEW_PLUGIN_VERSION,
727 userId: user.id,
728 pluginId: plugin.id
729 })
730 notification.Plugin = plugin
731
732 return notification
733 }
734
735 function emailSender (emails: string[]) {
736 return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
737 }
738
739 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
740 }
741
670 private async notify<T extends MUserWithNotificationSetting> (options: { 742 private async notify<T extends MUserWithNotificationSetting> (options: {
671 users: T[] 743 users: T[]
672 notificationCreator: (user: T) => Promise<UserNotificationModelForApi> 744 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
diff --git a/server/lib/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'
12import { blacklistVideo, unblacklistVideo } from '../video-blacklist' 12import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
13import { VideoBlacklistModel } from '@server/models/video/video-blacklist' 13import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
14import { AccountBlocklistModel } from '@server/models/account/account-blocklist' 14import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
15import { getServerConfig } from '../config'
16import { MPlugin } from '@server/types/models'
15 17
16function buildPluginHelpers (npmName: string): PeerTubeHelpers { 18function 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
148function 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 @@
1import { doRequest } from '../../helpers/requests' 1import { sanitizeUrl } from '@server/helpers/core-utils'
2import { CONFIG } from '../../initializers/config' 2import { ResultList } from '../../../shared/models'
3import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
4import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
3import { 5import {
4 PeertubePluginLatestVersionRequest, 6 PeertubePluginLatestVersionRequest,
5 PeertubePluginLatestVersionResponse 7 PeertubePluginLatestVersionResponse
6} from '../../../shared/models/plugins/peertube-plugin-latest-version.model' 8} from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
7import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
8import { ResultList } from '../../../shared/models'
9import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
10import { PluginModel } from '../../models/server/plugin'
11import { PluginManager } from './plugin-manager'
12import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest } from '../../helpers/requests'
11import { CONFIG } from '../../initializers/config'
13import { PEERTUBE_VERSION } from '../../initializers/constants' 12import { PEERTUBE_VERSION } from '../../initializers/constants'
14import { sanitizeUrl } from '@server/helpers/core-utils' 13import { PluginModel } from '../../models/server/plugin'
14import { PluginManager } from './plugin-manager'
15 15
16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { 16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options 17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options
18 18
19 const qs: PeertubePluginIndexList = { 19 const searchParams: PeertubePluginIndexList & Record<string, string | number> = {
20 start, 20 start,
21 count, 21 count,
22 sort, 22 sort,
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
29 29
30 try { 30 try {
31 const { body } = await doRequest<any>({ uri, qs, json: true }) 31 const { body } = await doJSONRequest<any>(uri, { searchParams })
32 32
33 logger.debug('Got result from PeerTube index.', { body }) 33 logger.debug('Got result from PeerTube index.', { body })
34 34
@@ -58,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
70async 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
66export { 81export {
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 @@
1import decache from 'decache'
1import * as express from 'express' 2import * as express from 'express'
2import { createReadStream, createWriteStream } from 'fs' 3import { createReadStream, createWriteStream } from 'fs'
3import { outputFile, readJSON } from 'fs-extra' 4import { 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'
10import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
11import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
12import { 12import {
13 RegisterServerAuthExternalOptions, 13 RegisterServerAuthExternalOptions,
@@ -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 @@
1import { outputJSON, pathExists } from 'fs-extra'
2import { join } from 'path'
1import { execShell } from '../../helpers/core-utils' 3import { execShell } from '../../helpers/core-utils'
2import { logger } from '../../helpers/logger'
3import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 4import { isNpmPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
5import { logger } from '../../helpers/logger'
4import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
5import { outputJSON, pathExists } from 'fs-extra' 7import { getLatestPluginVersion } from './plugin-index'
6import { join } from 'path'
7 8
8async function installNpmPlugin (npmName: string, version?: string) { 9async 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 @@
1import { chunk } from 'lodash' 1import { chunk } from 'lodash'
2import { doRequest } from '@server/helpers/requests' 2import { doJSONRequest } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 4import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
34 try { 34 try {
35 const serverActor = await getServerActor() 35 const serverActor = await getServerActor()
36 36
37 const qs = { count: 1000 } 37 const searchParams = { count: 1000 }
38 if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) 38 if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() })
39 39
40 this.lastCheck = new Date() 40 this.lastCheck = new Date()
41 41
42 const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) 42 const { body } = await doJSONRequest<any>(indexUrl, { searchParams })
43 if (!body.data || Array.isArray(body.data) === false) { 43 if (!body.data || Array.isArray(body.data) === false) {
44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) 44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body })
45 return 45 return
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts
new file mode 100644
index 000000000..c8960465c
--- /dev/null
+++ b/server/lib/schedulers/peertube-version-check-scheduler.ts
@@ -0,0 +1,55 @@
1
2import { doJSONRequest } from '@server/helpers/requests'
3import { ApplicationModel } from '@server/models/application/application'
4import { compareSemVer } from '@shared/core-utils'
5import { JoinPeerTubeVersions } from '@shared/models'
6import { logger } from '../../helpers/logger'
7import { CONFIG } from '../../initializers/config'
8import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
9import { Notifier } from '../notifier'
10import { AbstractScheduler } from './abstract-scheduler'
11
12export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
13
14 private static instance: AbstractScheduler
15
16 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
17
18 private constructor () {
19 super()
20 }
21
22 protected async internalExecute () {
23 return this.checkLatestVersion()
24 }
25
26 private async checkLatestVersion () {
27 if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
28
29 logger.info('Checking latest PeerTube version.')
30
31 const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
32
33 if (!body?.peertube?.latestVersion) {
34 logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
35 return
36 }
37
38 const latestVersion = body.peertube.latestVersion
39 const application = await ApplicationModel.load()
40
41 // Already checked this version
42 if (application.latestPeerTubeVersion === latestVersion) return
43
44 if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
45 application.latestPeerTubeVersion = latestVersion
46 await application.save()
47
48 Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
49 }
50 }
51
52 static get Instance () {
53 return this.instance || (this.instance = new this())
54 }
55}
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 014993e94..9a1ae3ec5 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
6import { chunk } from 'lodash' 6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index' 7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' 8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier'
9 10
10export class PluginsCheckScheduler extends AbstractScheduler { 11export class PluginsCheckScheduler extends AbstractScheduler {
11 12
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
53 plugin.latestVersion = result.latestVersion 54 plugin.latestVersion = result.latestVersion
54 await plugin.save() 55 await plugin.save()
55 56
57 // Notify if there is an higher plugin version available
58 if (compareSemVer(plugin.version, result.latestVersion) < 0) {
59 Notifier.Instance.notifyOfNewPluginVersion(plugin)
60 }
61
56 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) 62 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
57 } 63 }
58 } 64 }
diff --git a/server/lib/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'
3import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 3import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
4import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' 4import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
5import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
6import { VideoChannelModel } from '@server/models/video/video-channel'
6import { VideoCommentModel } from '@server/models/video/video-comment' 7import { VideoCommentModel } from '@server/models/video/video-comment'
7import { VideoFileModel } from '@server/models/video/video-file' 8import { VideoFileModel } from '@server/models/video/video-file'
9import { VideoPlaylistModel } from '@server/models/video/video-playlist'
8import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' 10import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models'
9 11
10class StatsManager { 12class 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 @@
1import { join } from 'path' 1import { join } from 'path'
2
2import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
3import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
4import { processImage } from '../helpers/image-utils' 5import { generateImageFilename, processImage } from '../helpers/image-utils'
5import { downloadImage } from '../helpers/requests' 6import { downloadImage } from '../helpers/requests'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
11import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
12import { getVideoFilePath } from './video-paths' 13import { getVideoFilePath } from './video-paths'
13 14
14type ImageSize = { height: number, width: number } 15type ImageSize = { height?: number, width?: number }
15 16
16function createPlaylistMiniatureFromExisting (options: { 17function createPlaylistMiniatureFromExisting (options: {
17 inputPath: string 18 inputPath: string
@@ -200,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
200 : undefined 201 : undefined
201 202
202 if (type === ThumbnailType.MINIATURE) { 203 if (type === ThumbnailType.MINIATURE) {
203 const filename = video.generateThumbnailName() 204 const filename = generateImageFilename()
204 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR 205 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
205 206
206 return { 207 return {
@@ -214,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
214 } 215 }
215 216
216 if (type === ThumbnailType.PREVIEW) { 217 if (type === ThumbnailType.PREVIEW) {
217 const filename = video.generatePreviewName() 218 const filename = generateImageFilename()
218 const basePath = CONFIG.STORAGE.PREVIEWS_DIR 219 const basePath = CONFIG.STORAGE.PREVIEWS_DIR
219 220
220 return { 221 return {
diff --git a/server/lib/user.ts b/server/lib/user.ts
index e1892f22c..9b0a0a2f1 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
193 newInstanceFollower: UserNotificationSettingValue.WEB, 193 newInstanceFollower: UserNotificationSettingValue.WEB,
194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
196 autoInstanceFollowing: UserNotificationSettingValue.WEB 196 autoInstanceFollowing: UserNotificationSettingValue.WEB,
197 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
198 newPluginVersion: UserNotificationSettingValue.WEB
197 } 199 }
198 200
199 return UserNotificationSettingModel.create(values, { transaction: t }) 201 return UserNotificationSettingModel.create(values, { transaction: t })
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index dbb37e0b2..37c43c3b0 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -11,7 +11,7 @@ import {
11} from '@server/types/models' 11} from '@server/types/models'
12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' 12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
13import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 13import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
14import { logger } from '../helpers/logger' 14import { logger, loggerTagsFactory } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 15import { CONFIG } from '../initializers/config'
16import { VideoBlacklistModel } from '../models/video/video-blacklist' 16import { VideoBlacklistModel } from '../models/video/video-blacklist'
17import { sendDeleteVideo } from './activitypub/send' 17import { sendDeleteVideo } from './activitypub/send'
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager'
20import { Notifier } from './notifier' 20import { Notifier } from './notifier'
21import { Hooks } from './plugins/hooks' 21import { Hooks } from './plugins/hooks'
22 22
23const lTags = loggerTagsFactory('blacklist')
24
23async function autoBlacklistVideoIfNeeded (parameters: { 25async function autoBlacklistVideoIfNeeded (parameters: {
24 video: MVideoWithBlacklistLight 26 video: MVideoWithBlacklistLight
25 user?: MUser 27 user?: MUser
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
60 }) 62 })
61 } 63 }
62 64
63 logger.info('Video %s auto-blacklisted.', video.uuid) 65 logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid))
64 66
65 return true 67 return true
66} 68}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 49bdf4869..0476cb2d5 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoModel } from '../models/video/video' 4import { VideoModel } from '../models/video/video'
5import { VideoChannelModel } from '../models/video/video-channel' 5import { VideoChannelModel } from '../models/video/video-channel'
6import { MAccountId, MChannelDefault, MChannelId } from '../types/models' 6import { MAccountId, MChannelId } from '../types/models'
7import { buildActorInstance } from './activitypub/actor' 7import { buildActorInstance } from './activitypub/actor'
8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' 8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos' 9import { federateVideoIfNeeded } from './activitypub/videos'
10 10
11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } 11async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
12
13async function createLocalVideoChannel <T extends MAccountId> (
14 videoChannelInfo: VideoChannelCreate,
15 account: T,
16 t: Sequelize.Transaction
17): Promise<CustomVideoChannelModelAccount<T>> {
18 const uuid = uuidv4() 12 const uuid = uuidv4()
19 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) 13 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
20 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) 14 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> (
32 const videoChannel = new VideoChannelModel(videoChannelData) 26 const videoChannel = new VideoChannelModel(videoChannelData)
33 27
34 const options = { transaction: t } 28 const options = { transaction: t }
35 const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault 29 const videoChannelCreated = await videoChannel.save(options)
36 30
37 // Do not forget to add Account/Actor information to the created video channel
38 videoChannelCreated.Account = account
39 videoChannelCreated.Actor = actorInstanceCreated 31 videoChannelCreated.Actor = actorInstanceCreated
40 32
41 // No need to seed this empty video channel to followers 33 // No need to send this empty video channel to followers
42 return videoChannelCreated 34 return videoChannelCreated
43} 35}
44 36
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Socket } from 'socket.io' 2import { Socket } from 'socket.io'
3import { oAuthServer } from '@server/lib/auth' 3import { getAccessToken } from '@server/lib/auth/oauth-model'
4import { logger } from '../helpers/logger'
5import { getAccessToken } from '../lib/oauth-model'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger'
6import { handleOAuthAuthenticate } from '../lib/auth/oauth'
7 7
8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
9 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 9 handleOAuthAuthenticate(req, res, authenticateInQuery)
10 .then((token: any) => {
11 res.locals.oauth = { token }
12 res.locals.authenticated = true
10 13
11 oAuthServer.authenticate(options)(req, res, err => { 14 return next()
12 if (err) { 15 })
16 .catch(err => {
13 logger.warn('Cannot authenticate.', { err }) 17 logger.warn('Cannot authenticate.', { err })
14 18
15 return res.status(err.status) 19 return res.status(err.status)
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres
17 error: 'Token is invalid.', 21 error: 'Token is invalid.',
18 code: err.name 22 code: err.name
19 }) 23 })
20 .end() 24 })
21 }
22
23 res.locals.authenticated = true
24
25 return next()
26 })
27} 25}
28 26
29function authenticateSocket (socket: Socket, next: (err?: any) => void) { 27function authenticateSocket (socket: Socket, next: (err?: any) => void) {
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index b758a8586..3e280e16f 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -1,7 +1,7 @@
1export * from './validators' 1export * from './validators'
2export * from './activitypub' 2export * from './activitypub'
3export * from './async' 3export * from './async'
4export * from './oauth' 4export * from './auth'
5export * from './pagination' 5export * from './pagination'
6export * from './servers' 6export * from './servers'
7export * from './sort' 7export * from './sort'
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
index 02b191480..7c4e49463 100644
--- a/server/middlewares/validators/activitypub/signature.ts
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -23,7 +23,7 @@ const signatureValidator = [
23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), 23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
24 24
25 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) 26 logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
27 27
28 if (areValidationErrors(req, res)) return 28 if (areValidationErrors(req, res)) return
29 29
diff --git a/server/middlewares/validators/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 @@
1import * as express from 'express'
2import { body } from 'express-validator'
3import { isActorImageFile } from '@server/helpers/custom-validators/actor-images'
4import { cleanUpReqFiles } from '../../helpers/express-utils'
5import { logger } from '../../helpers/logger'
6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7import { areValidationErrors } from './utils'
8
9const 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
24const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
25const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
26
27export {
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 @@
1import * as express from 'express'
2import { body } from 'express-validator'
3import { isAvatarFile } from '../../helpers/custom-validators/users'
4import { areValidationErrors } from './utils'
5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { logger } from '../../helpers/logger'
7import { cleanUpReqFiles } from '../../helpers/express-utils'
8
9const 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
24export {
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 @@
1export * from './abuse' 1export * from './abuse'
2export * from './account' 2export * from './account'
3export * from './actor-image'
3export * from './blocklist' 4export * from './blocklist'
4export * from './oembed' 5export * from './oembed'
5export * from './activitypub' 6export * 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' 3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
4import { logger } from '../../helpers/logger' 4import { logger, loggerTagsFactory } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const lTags = loggerTagsFactory('validators', 'jobs')
8
7const listJobsValidator = [ 9const listJobsValidator = [
8 param('state') 10 param('state')
9 .optional() 11 .optional()
@@ -14,7 +16,7 @@ const listJobsValidator = [
14 .custom(isValidJobType).withMessage('Should have a valid job state'), 16 .custom(isValidJobType).withMessage('Should have a valid job state'),
15 17
16 (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params }) 19 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params, ...lTags() })
18 20
19 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
20 22
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts
index 1cae7848c..6b0a83d80 100644
--- a/server/middlewares/validators/pagination.ts
+++ b/server/middlewares/validators/pagination.ts
@@ -4,25 +4,30 @@ import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { PAGINATION } from '@server/initializers/constants' 5import { PAGINATION } from '@server/initializers/constants'
6 6
7const paginationValidator = [ 7const paginationValidator = paginationValidatorBuilder()
8 query('start')
9 .optional()
10 .isInt({ min: 0 }).withMessage('Should have a number start'),
11 query('count')
12 .optional()
13 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
14 8
15 (req: express.Request, res: express.Response, next: express.NextFunction) => { 9function paginationValidatorBuilder (tags: string[] = []) {
16 logger.debug('Checking pagination parameters', { parameters: req.query }) 10 return [
11 query('start')
12 .optional()
13 .isInt({ min: 0 }).withMessage('Should have a number start'),
14 query('count')
15 .optional()
16 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
17 17
18 if (areValidationErrors(req, res)) return 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking pagination parameters', { parameters: req.query, tags })
19 20
20 return next() 21 if (areValidationErrors(req, res)) return
21 } 22
22] 23 return next()
24 }
25 ]
26}
23 27
24// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
25 29
26export { 30export {
27 paginationValidator 31 paginationValidator,
32 paginationValidatorBuilder
28} 33}
diff --git a/server/middlewares/validators/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
29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ])
32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) 32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts
index 2899bed6f..4167f6d43 100644
--- a/server/middlewares/validators/utils.ts
+++ b/server/middlewares/validators/utils.ts
@@ -17,12 +17,12 @@ function areValidationErrors (req: express.Request, res: express.Response) {
17 return false 17 return false
18} 18}
19 19
20function checkSort (sortableColumns: string[]) { 20function checkSort (sortableColumns: string[], tags: string[] = []) {
21 return [ 21 return [
22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'), 22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'),
23 23
24 (req: express.Request, res: express.Response, next: express.NextFunction) => { 24 (req: express.Request, res: express.Response, next: express.NextFunction) => {
25 logger.debug('Checking sort parameters', { parameters: req.query }) 25 logger.debug('Checking sort parameters', { parameters: req.query, tags })
26 26
27 if (areValidationErrors(req, res)) return 27 if (areValidationErrors(req, res)) return
28 28
diff --git a/server/middlewares/validators/videos/video-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
29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
31import { MVideoPlaylist } from '../../../types/models/video/video-playlist' 31import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
32import { authenticatePromiseIfNeeded } from '../../oauth' 32import { authenticatePromiseIfNeeded } from '../../auth'
33import { areValidationErrors } from '../utils' 33import { areValidationErrors } from '../utils'
34 34
35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 37cc07b94..4d31d3dcb 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation'
54import { Hooks } from '../../../lib/plugins/hooks' 54import { Hooks } from '../../../lib/plugins/hooks'
55import { AccountModel } from '../../../models/account/account' 55import { AccountModel } from '../../../models/account/account'
56import { VideoModel } from '../../../models/video/video' 56import { VideoModel } from '../../../models/video/video'
57import { authenticatePromiseIfNeeded } from '../../oauth' 57import { authenticatePromiseIfNeeded } from '../../auth'
58import { areValidationErrors } from '../utils' 58import { areValidationErrors } from '../utils'
59 59
60const videosAddValidator = getCommonVideoEditAttributes().concat([ 60const videosAddValidator = getCommonVideoEditAttributes().concat([
diff --git a/server/models/account/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 {
33import { ActorModel } from '../activitypub/actor' 33import { ActorModel } from '../activitypub/actor'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { ApplicationModel } from '../application/application' 35import { ApplicationModel } from '../application/application'
36import { AvatarModel } from '../avatar/avatar' 36import { ActorImageModel } from './actor-image'
37import { ServerModel } from '../server/server' 37import { ServerModel } from '../server/server'
38import { ServerBlocklistModel } from '../server/server-blocklist' 38import { ServerBlocklistModel } from '../server/server-blocklist'
39import { getSort, throwIfNotValid } from '../utils' 39import { 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 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { MActorImageFormattable } from '@server/types/models'
5import { ActorImageType } from '@shared/models'
6import { ActorImage } from '../../../shared/models/actors/actor-image.model'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { logger } from '../../helpers/logger'
9import { CONFIG } from '../../initializers/config'
10import { LAZY_STATIC_PATHS } from '../../initializers/constants'
11import { throwIfNotValid } from '../utils'
12
13@Table({
14 tableName: 'actorImage',
15 indexes: [
16 {
17 fields: [ 'filename' ],
18 unique: true
19 }
20 ]
21})
22export 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'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
15import { MNotificationSettingFormattable } from '@server/types/models' 16import { MNotificationSettingFormattable } from '@server/types/models'
16import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 17import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' 18import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { clearCacheByUserId } from '../../lib/oauth-model'
19import { throwIfNotValid } from '../utils' 19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user' 20import { UserModel } from './user'
21 21
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
156 @Column 156 @Column
157 abuseNewMessage: UserNotificationSettingValue 157 abuseNewMessage: UserNotificationSettingValue
158 158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
159 @ForeignKey(() => UserModel) 177 @ForeignKey(() => UserModel)
160 @Column 178 @Column
161 userId: number 179 userId: number
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model {
177 @AfterUpdate 195 @AfterUpdate
178 @AfterDestroy 196 @AfterDestroy
179 static removeTokenCache (instance: UserNotificationSettingModel) { 197 static removeTokenCache (instance: UserNotificationSettingModel) {
180 return clearCacheByUserId(instance.userId) 198 return TokensCache.Instance.clearCacheByUserId(instance.userId)
181 } 199 }
182 200
183 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { 201 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
195 newInstanceFollower: this.newInstanceFollower, 213 newInstanceFollower: this.newInstanceFollower,
196 autoInstanceFollowing: this.autoInstanceFollowing, 214 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage, 215 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange 216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
199 } 219 }
200 } 220 }
201} 221}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index add129644..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'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
11import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
12import { AvatarModel } from '../avatar/avatar' 12import { ApplicationModel } from '../application/application'
13import { PluginModel } from '../server/plugin'
13import { ServerModel } from '../server/server' 14import { ServerModel } from '../server/server'
14import { getSort, throwIfNotValid } from '../utils' 15import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video' 16import { VideoModel } from '../video/video'
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment' 19import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import' 20import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account' 21import { AccountModel } from './account'
22import { ActorImageModel } from './actor-image'
21import { UserModel } from './user' 23import { UserModel } from './user'
22 24
23enum ScopeNames { 25enum 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'
24import { TokensCache } from '@server/lib/auth/tokens-cache'
24import { 25import {
25 MMyUserFormattable, 26 MMyUserFormattable,
26 MUser, 27 MUser,
@@ -58,7 +59,6 @@ import {
58} from '../../helpers/custom-validators/users' 59} from '../../helpers/custom-validators/users'
59import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 60import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
60import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 61import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
61import { clearCacheByUserId } from '../../lib/oauth-model'
62import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 62import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
63import { ActorModel } from '../activitypub/actor' 63import { ActorModel } from '../activitypub/actor'
64import { ActorFollowModel } from '../activitypub/actor-follow' 64import { ActorFollowModel } from '../activitypub/actor-follow'
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { AccountModel } from './account' 72import { AccountModel } from './account'
73import { UserNotificationSettingModel } from './user-notification-setting' 73import { UserNotificationSettingModel } from './user-notification-setting'
74import { ActorImageModel } from './actor-image'
74 75
75enum ScopeNames { 76enum 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'
20import { ModelCache } from '@server/models/model-cache' 20import { ModelCache } from '@server/models/model-cache'
21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' 21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
22import { Avatar } from '../../../shared/models/avatars/avatar.model' 22import { ActorImage } from '../../../shared/models/actors/actor-image.model'
23import { activityPubContextify } from '../../helpers/activitypub' 23import { activityPubContextify } from '../../helpers/activitypub'
24import { 24import {
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'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
33import { 40import {
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'
45import { AccountModel } from '../account/account' 53import { AccountModel } from '../account/account'
46import { AvatarModel } from '../avatar/avatar' 54import { ActorImageModel } from '../account/actor-image'
47import { ServerModel } from '../server/server' 55import { ServerModel } from '../server/server'
48import { isOutdated, throwIfNotValid } from '../utils' 56import { isOutdated, throwIfNotValid } from '../utils'
49import { VideoModel } from '../video/video' 57import { 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 @@
1import { join } from 'path'
2import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model'
4import { LAZY_STATIC_PATHS } from '../../initializers/constants'
5import { logger } from '../../helpers/logger'
6import { remove } from 'fs-extra'
7import { CONFIG } from '../../initializers/config'
8import { throwIfNotValid } from '../utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { MAvatarFormattable } from '@server/types/models'
11
12@Table({
13 tableName: 'avatar',
14 indexes: [
15 {
16 fields: [ 'filename' ],
17 unique: true
18 }
19 ]
20})
21export 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'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
16import { MUserAccountId } from '@server/types/models'
15import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 17import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
16import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
17import { clearCacheByToken } from '../../lib/oauth-model'
18import { AccountModel } from '../account/account' 19import { AccountModel } from '../account/account'
19import { UserModel } from '../account/user' 20import { UserModel } from '../account/user'
20import { ActorModel } from '../activitypub/actor' 21import { ActorModel } from '../activitypub/actor'
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = {
26 client: { 27 client: {
27 id: number 28 id: number
28 } 29 }
29 user: { 30 user: MUserAccountId
30 id: number
31 }
32 token: MOAuthTokenUser 31 token: MOAuthTokenUser
33} 32}
34 33
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model {
133 @AfterUpdate 132 @AfterUpdate
134 @AfterDestroy 133 @AfterDestroy
135 static removeTokenCache (token: OAuthTokenModel) { 134 static removeTokenCache (token: OAuthTokenModel) {
136 return clearCacheByToken(token.accessToken) 135 return TokensCache.Instance.clearCacheByToken(token.accessToken)
137 } 136 }
138 137
139 static loadByRefreshToken (refreshToken: string) { 138 static loadByRefreshToken (refreshToken: string) {
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model {
206 } 205 }
207 206
208 static deleteUserToken (userId: number, t?: Transaction) { 207 static deleteUserToken (userId: number, t?: Transaction) {
208 TokensCache.Instance.deleteUserToken(userId)
209
209 const query = { 210 const query = {
210 where: { 211 where: {
211 userId 212 userId
diff --git a/server/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'
32import { ActorModel } from '../activitypub/actor' 32import { ActorModel } from '../activitypub/actor'
33import { ServerModel } from '../server/server' 33import { ServerModel } from '../server/server'
34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' 34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
35import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
35import { VideoModel } from '../video/video' 36import { VideoModel } from '../video/video'
36import { VideoChannelModel } from '../video/video-channel' 37import { VideoChannelModel } from '../video/video-channel'
37import { VideoFileModel } from '../video/video-file' 38import { 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 @@
1import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BeforeDestroy, 4 BeforeDestroy,
@@ -28,17 +28,16 @@ import {
28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
29import { sendDeleteActor } from '../../lib/activitypub/send' 29import { sendDeleteActor } from '../../lib/activitypub/send'
30import { 30import {
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'
38import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' 37import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
38import { ActorImageModel } from '../account/actor-image'
39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
40import { ActorFollowModel } from '../activitypub/actor-follow' 40import { ActorFollowModel } from '../activitypub/actor-follow'
41import { AvatarModel } from '../avatar/avatar'
42import { ServerModel } from '../server/server' 41import { ServerModel } from '../server/server'
43import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 42import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
44import { VideoModel } from './video' 43import { 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 = `
350SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count"
351FROM "videoChannel" AS "VideoChannelModel"
352INNER JOIN "video" AS "Videos"
353ON "VideoChannelModel"."id" = "Videos"."channelId"
354AND ("Videos"."publishedAt" > Now() - interval '${days}d')
355INNER JOIN "account" AS "Account"
356ON "VideoChannelModel"."accountId" = "Account"."id"
357INNER JOIN "actor" AS "Account->Actor"
358ON "Account"."actorId" = "Account->Actor"."id"
359AND "Account->Actor"."serverId" IS NULL
360LEFT OUTER JOIN "server" AS "Account->Actor->Server"
361ON "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
54import { ThumbnailModel } from './thumbnail' 54import { ThumbnailModel } from './thumbnail'
55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
56import { VideoPlaylistElementModel } from './video-playlist-element' 56import { VideoPlaylistElementModel } from './video-playlist-element'
57import { ActorModel } from '../activitypub/actor'
57 58
58enum ScopeNames { 59enum 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
67type AvailableForListOptions = { 68type 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'
27import { v4 as uuidv4 } from 'uuid'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live-manager' 29import { LiveManager } from '@server/lib/live-manager'
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
100import { VideoAbuseModel } from '../abuse/video-abuse' 99import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account' 100import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate' 101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { ActorImageModel } from '../account/actor-image'
103import { UserModel } from '../account/user' 103import { UserModel } from '../account/user'
104import { UserVideoHistoryModel } from '../account/user-video-history' 104import { UserVideoHistoryModel } from '../account/user-video-history'
105import { ActorModel } from '../activitypub/actor' 105import { ActorModel } from '../activitypub/actor'
106import { AvatarModel } from '../avatar/avatar'
107import { VideoRedundancyModel } from '../redundancy/video-redundancy' 106import { VideoRedundancyModel } from '../redundancy/video-redundancy'
108import { ServerModel } from '../server/server' 107import { ServerModel } from '../server/server'
109import { TrackerModel } from '../server/tracker' 108import { 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
20const expect = chai.expect 22const expect = chai.expect
21 23
22function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { 24function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
25 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
26
23 return Promise.all([ 27 return Promise.all([
24 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey), 28 setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey),
25 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey) 29 setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey)
26 ]) 30 ])
27} 31}
28 32
29function getAnnounceWithoutContext (server2: ServerInfo) { 33function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) {
34 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
35
36 return Promise.all([
37 setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt),
38 setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt)
39 ])
40}
41
42function getAnnounceWithoutContext (server: ServerInfo) {
30 const json = require('./json/peertube/announce-without-context.json') 43 const json = require('./json/peertube/announce-without-context.json')
31 const result: typeof json = {} 44 const result: typeof json = {}
32 45
33 for (const key of Object.keys(json)) { 46 for (const key of Object.keys(json)) {
34 if (Array.isArray(json[key])) { 47 if (Array.isArray(json[key])) {
35 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`)) 48 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
36 } else { 49 } else {
37 result[key] = json[key].replace(':9002', `:${server2.port}`) 50 result[key] = json[key].replace(':9002', `:${server.port}`)
38 } 51 }
39 } 52 }
40 53
@@ -64,7 +77,8 @@ describe('Test ActivityPub security', function () {
64 77
65 url = servers[0].url + '/inbox' 78 url = servers[0].url + '/inbox'
66 79
67 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) 80 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
81 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
68 82
69 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' } 83 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
70 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey } 84 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
@@ -79,9 +93,12 @@ describe('Test ActivityPub security', function () {
79 Digest: buildDigest({ hello: 'coucou' }) 93 Digest: buildDigest({ hello: 'coucou' })
80 } 94 }
81 95
82 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 96 try {
83 97 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
84 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 98 expect(true, 'Did not throw').to.be.false
99 } catch (err) {
100 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
101 }
85 }) 102 })
86 103
87 it('Should fail with an invalid date', async function () { 104 it('Should fail with an invalid date', async function () {
@@ -89,9 +106,12 @@ describe('Test ActivityPub security', function () {
89 const headers = buildGlobalHeaders(body) 106 const headers = buildGlobalHeaders(body)
90 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' 107 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
91 108
92 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 109 try {
93 110 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
94 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 111 expect(true, 'Did not throw').to.be.false
112 } catch (err) {
113 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
114 }
95 }) 115 })
96 116
97 it('Should fail with bad keys', async function () { 117 it('Should fail with bad keys', async function () {
@@ -101,9 +121,12 @@ describe('Test ActivityPub security', function () {
101 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 121 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
102 const headers = buildGlobalHeaders(body) 122 const headers = buildGlobalHeaders(body)
103 123
104 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 124 try {
105 125 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
106 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 126 expect(true, 'Did not throw').to.be.false
127 } catch (err) {
128 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
129 }
107 }) 130 })
108 131
109 it('Should reject requests without appropriate signed headers', async function () { 132 it('Should reject requests without appropriate signed headers', async function () {
@@ -123,8 +146,12 @@ describe('Test ActivityPub security', function () {
123 for (const badHeaders of badHeadersMatrix) { 146 for (const badHeaders of badHeadersMatrix) {
124 signatureOptions.headers = badHeaders 147 signatureOptions.headers = badHeaders
125 148
126 const { response } = await makePOSTAPRequest(url, body, signatureOptions, headers) 149 try {
127 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 150 await makePOSTAPRequest(url, body, signatureOptions, headers)
151 expect(true, 'Did not throw').to.be.false
152 } catch (err) {
153 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
154 }
128 } 155 }
129 }) 156 })
130 157
@@ -132,27 +159,32 @@ describe('Test ActivityPub security', function () {
132 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 159 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
133 const headers = buildGlobalHeaders(body) 160 const headers = buildGlobalHeaders(body)
134 161
135 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 162 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
136 163 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
137 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
138 }) 164 })
139 165
140 it('Should refresh the actor keys', async function () { 166 it('Should refresh the actor keys', async function () {
141 this.timeout(20000) 167 this.timeout(20000)
142 168
143 // Wait refresh invalidation
144 await wait(10000)
145
146 // Update keys of server 2 to invalid keys 169 // Update keys of server 2 to invalid keys
147 // Server 1 should refresh the actor and fail 170 // Server 1 should refresh the actor and fail
148 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) 171 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
172 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
173
174 // Invalid peertube actor cache
175 killallServers([ servers[1] ])
176 await reRunServer(servers[1])
149 177
150 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 178 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
151 const headers = buildGlobalHeaders(body) 179 const headers = buildGlobalHeaders(body)
152 180
153 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 181 try {
154 182 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
155 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 183 expect(true, 'Did not throw').to.be.false
184 } catch (err) {
185 console.error(err)
186 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
187 }
156 }) 188 })
157 }) 189 })
158 190
@@ -183,9 +215,12 @@ describe('Test ActivityPub security', function () {
183 215
184 const headers = buildGlobalHeaders(signedBody) 216 const headers = buildGlobalHeaders(signedBody)
185 217
186 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 218 try {
187 219 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
188 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 220 expect(true, 'Did not throw').to.be.false
221 } catch (err) {
222 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
223 }
189 }) 224 })
190 225
191 it('Should fail with an altered body', async function () { 226 it('Should fail with an altered body', async function () {
@@ -204,9 +239,12 @@ describe('Test ActivityPub security', function () {
204 239
205 const headers = buildGlobalHeaders(signedBody) 240 const headers = buildGlobalHeaders(signedBody)
206 241
207 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 242 try {
208 243 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
209 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 244 expect(true, 'Did not throw').to.be.false
245 } catch (err) {
246 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
247 }
210 }) 248 })
211 249
212 it('Should succeed with a valid signature', async function () { 250 it('Should succeed with a valid signature', async function () {
@@ -220,9 +258,8 @@ describe('Test ActivityPub security', function () {
220 258
221 const headers = buildGlobalHeaders(signedBody) 259 const headers = buildGlobalHeaders(signedBody)
222 260
223 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 261 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
224 262 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
225 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
226 }) 263 })
227 264
228 it('Should refresh the actor keys', async function () { 265 it('Should refresh the actor keys', async function () {
@@ -243,9 +280,12 @@ describe('Test ActivityPub security', function () {
243 280
244 const headers = buildGlobalHeaders(signedBody) 281 const headers = buildGlobalHeaders(signedBody)
245 282
246 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 283 try {
247 284 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
248 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 285 expect(true, 'Did not throw').to.be.false
286 } catch (err) {
287 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
288 }
249 }) 289 })
250 }) 290 })
251 291
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 05a78b0ad..26d4423f9 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () {
176 newInstanceFollower: UserNotificationSettingValue.WEB, 176 newInstanceFollower: UserNotificationSettingValue.WEB,
177 autoInstanceFollowing: UserNotificationSettingValue.WEB, 177 autoInstanceFollowing: UserNotificationSettingValue.WEB,
178 abuseNewMessage: UserNotificationSettingValue.WEB, 178 abuseNewMessage: UserNotificationSettingValue.WEB,
179 abuseStateChange: UserNotificationSettingValue.WEB 179 abuseStateChange: UserNotificationSettingValue.WEB,
180 newPeerTubeVersion: UserNotificationSettingValue.WEB,
181 newPluginVersion: UserNotificationSettingValue.WEB
180 } 182 }
181 183
182 it('Should fail with missing fields', async function () { 184 it('Should fail with missing fields', async function () {
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 0a13f5b67..2b03fde2d 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -241,7 +241,7 @@ describe('Test users API validators', function () {
241 }) 241 })
242 242
243 it('Should succeed with no password on a server with smtp enabled', async function () { 243 it('Should succeed with no password on a server with smtp enabled', async function () {
244 this.timeout(10000) 244 this.timeout(20000)
245 245
246 killallServers([ server ]) 246 killallServers([ server ])
247 247
diff --git a/server/tests/api/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
3import 'mocha'
4import { expect } from 'chai'
5import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
6import { PluginType } from '@shared/models'
7import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
8import { ServerInfo } from '../../../../shared/extra-utils/index'
9import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
10import {
11 CheckerBaseParams,
12 checkNewPeerTubeVersion,
13 checkNewPluginVersion,
14 prepareNotificationsTest
15} from '../../../../shared/extra-utils/users/user-notifications'
16import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
17
18describe('Test admin notifications', function () {
19 let server: ServerInfo
20 let userNotifications: UserNotification[] = []
21 let adminNotifications: UserNotification[] = []
22 let emails: object[] = []
23 let baseParams: CheckerBaseParams
24 let joinPeerTubeServer: MockJoinPeerTubeVersions
25
26 before(async function () {
27 this.timeout(120000)
28
29 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 @@
1import './admin-notifications'
1import './comments-notifications' 2import './comments-notifications'
2import './moderation-notifications' 3import './moderation-notifications'
3import './notifications-api' 4import './notifications-api'
diff --git a/server/tests/api/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
3import 'mocha' 3import 'mocha'
4import { v4 as uuidv4 } from 'uuid' 4import { v4 as uuidv4 } from 'uuid'
5 5import { AbuseState } from '@shared/models'
6import { 6import {
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'
29import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' 29import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
30import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 30import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
31import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 31import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
32import { 32import {
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'
48import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions' 48import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
49import { CustomConfig } from '../../../../shared/models/server' 49import { CustomConfig } from '../../../../shared/models/server'
50import { UserNotification } from '../../../../shared/models/users' 50import { UserNotification } from '../../../../shared/models/users'
51import { VideoPrivacy } from '../../../../shared/models/videos' 51import { VideoPrivacy } from '../../../../shared/models/videos'
52import { AbuseState } from '@shared/models'
53 52
54describe('Test moderation notifications', function () { 53describe('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
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { 5import {
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
20describe('Test services', function () { 20describe('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 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { 5import {
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'
21import { getStats } from '../../../../shared/extra-utils/server/stats' 23import { getStats } from '../../../../shared/extra-utils/server/stats'
22import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' 24import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
23import { ServerStats } from '../../../../shared/models/server/server-stats.model' 25import { ServerStats } from '../../../../shared/models/server/server-stats.model'
26import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
24import { ActivityType } from '@shared/models' 27import { ActivityType } from '@shared/models'
25 28
26const expect = chai.expect 29const expect = chai.expect
27 30
28describe('Test stats (excluding redundancy)', function () { 31describe('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'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' 5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
6import { CustomConfig } from '@shared/models/server' 6import { CustomConfig } from '@shared/models/server'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { 8import {
8 addVideoCommentThread, 9 addVideoCommentThread,
9 blockUser, 10 blockUser,
10 cleanupTests, 11 cleanupTests,
12 closeAllSequelize,
11 createUser, 13 createUser,
12 deleteMe, 14 deleteMe,
13 flushAndRunServer, 15 flushAndRunServer,
@@ -24,6 +26,7 @@ import {
24 getVideoChannel, 26 getVideoChannel,
25 getVideosList, 27 getVideosList,
26 installPlugin, 28 installPlugin,
29 killallServers,
27 login, 30 login,
28 makePutBodyRequest, 31 makePutBodyRequest,
29 rateVideo, 32 rateVideo,
@@ -31,7 +34,9 @@ import {
31 removeUser, 34 removeUser,
32 removeVideo, 35 removeVideo,
33 reportAbuse, 36 reportAbuse,
37 reRunServer,
34 ServerInfo, 38 ServerInfo,
39 setTokenField,
35 testImage, 40 testImage,
36 unblockUser, 41 unblockUser,
37 updateAbuse, 42 updateAbuse,
@@ -44,10 +49,9 @@ import {
44 waitJobs 49 waitJobs
45} from '../../../../shared/extra-utils' 50} from '../../../../shared/extra-utils'
46import { follow } from '../../../../shared/extra-utils/server/follows' 51import { follow } from '../../../../shared/extra-utils/server/follows'
47import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 52import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
48import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 53import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 54import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
51 55
52const expect = chai.expect 56const expect = chai.expect
53 57
@@ -89,6 +93,7 @@ describe('Test users', function () {
89 const client = { id: 'client', secret: server.client.secret } 93 const client = { id: 'client', secret: server.client.secret }
90 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 94 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
91 95
96 expect(res.body.code).to.equal('invalid_client')
92 expect(res.body.error).to.contain('client is invalid') 97 expect(res.body.error).to.contain('client is invalid')
93 }) 98 })
94 99
@@ -96,6 +101,7 @@ describe('Test users', function () {
96 const client = { id: server.client.id, secret: 'coucou' } 101 const client = { id: server.client.id, secret: 'coucou' }
97 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 102 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
98 103
104 expect(res.body.code).to.equal('invalid_client')
99 expect(res.body.error).to.contain('client is invalid') 105 expect(res.body.error).to.contain('client is invalid')
100 }) 106 })
101 }) 107 })
@@ -106,6 +112,7 @@ describe('Test users', function () {
106 const user = { username: 'captain crochet', password: server.user.password } 112 const user = { username: 'captain crochet', password: server.user.password }
107 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 113 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
108 114
115 expect(res.body.code).to.equal('invalid_grant')
109 expect(res.body.error).to.contain('credentials are invalid') 116 expect(res.body.error).to.contain('credentials are invalid')
110 }) 117 })
111 118
@@ -113,6 +120,7 @@ describe('Test users', function () {
113 const user = { username: server.user.username, password: 'mew_three' } 120 const user = { username: server.user.username, password: 'mew_three' }
114 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 121 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
115 122
123 expect(res.body.code).to.equal('invalid_grant')
116 expect(res.body.error).to.contain('credentials are invalid') 124 expect(res.body.error).to.contain('credentials are invalid')
117 }) 125 })
118 126
@@ -245,12 +253,44 @@ describe('Test users', function () {
245 }) 253 })
246 254
247 it('Should be able to login again', async function () { 255 it('Should be able to login again', async function () {
248 server.accessToken = await serverLogin(server) 256 const res = await login(server.url, server.client, server.user)
257 server.accessToken = res.body.access_token
258 server.refreshToken = res.body.refresh_token
259 })
260
261 it('Should be able to get my user information again', async function () {
262 await getMyUserInformation(server.url, server.accessToken)
263 })
264
265 it('Should have an expired access token', async function () {
266 this.timeout(15000)
267
268 await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
269 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
270
271 killallServers([ server ])
272 await reRunServer(server)
273
274 await getMyUserInformation(server.url, server.accessToken, 401)
275 })
276
277 it('Should not be able to refresh an access token with an expired refresh token', async function () {
278 await refreshToken(server, server.refreshToken, 400)
249 }) 279 })
250 280
251 it('Should have an expired access token') 281 it('Should refresh the token', async function () {
282 this.timeout(15000)
283
284 const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
285 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate)
252 286
253 it('Should refresh the token') 287 killallServers([ server ])
288 await reRunServer(server)
289
290 const res = await refreshToken(server, server.refreshToken)
291 server.accessToken = res.body.access_token
292 server.refreshToken = res.body.refresh_token
293 })
254 294
255 it('Should be able to get my user information again', async function () { 295 it('Should be able to get my user information again', async function () {
256 await getMyUserInformation(server.url, server.accessToken) 296 await getMyUserInformation(server.url, server.accessToken)
@@ -976,6 +1016,7 @@ describe('Test users', function () {
976 }) 1016 })
977 1017
978 after(async function () { 1018 after(async function () {
1019 await closeAllSequelize([ server ])
979 await cleanupTests([ server ]) 1020 await cleanupTests([ server ])
980 }) 1021 })
981}) 1022})
diff --git a/server/tests/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
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { basename } from 'path'
5import { 6import {
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'
31import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 34import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
32import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' 35import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
36import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
33 37
34const expect = chai.expect 38const expect = chai.expect
35 39
40async 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
36describe('Test video channels', function () { 47describe('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'
6import './plugins' 6import './plugins'
7import './print-transcode-command' 7import './print-transcode-command'
8import './prune-storage' 8import './prune-storage'
9import './regenerate-thumbnails'
9import './reset-password' 10import './reset-password'
10import './update-host' 11import './update-host'
diff --git a/server/tests/cli/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 @@
1import 'mocha'
2import { expect } from 'chai'
3import { writeFile } from 'fs-extra'
4import { basename, join } from 'path'
5import { Video, VideoDetails } from '@shared/models'
6import {
7 buildServerDirectory,
8 cleanupTests,
9 doubleFollow,
10 execCLI,
11 flushAndRunMultipleServers,
12 getEnvCli,
13 getVideo,
14 makeRawRequest,
15 ServerInfo,
16 setAccessTokensToServers,
17 uploadVideoAndGetId,
18 waitJobs
19} from '../../../shared/extra-utils'
20import { HttpStatusCode } from '@shared/core-utils'
21
22async 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
33describe('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
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import * as libxmljs from 'libxmljs' 5import * as xmlParser from 'fast-xml-parser'
6import { 6import {
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
75async function unregister () { 88async 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 @@
1async function register ({ transcodingManager }) { 1async 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
28async function unregister () { 86async 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 @@
1const d = new Date()
2exports.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 @@
1const lib = require('./lib')
2
3async function register ({ getRouter }) {
4 const router = getRouter()
5 router.get('/get', (req, res) => res.json({ message: lib.value }))
6}
7
8async function unregister () {
9}
10
11module.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
189async function unregister () { 259async 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
3import 'mocha' 3import 'mocha'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
5import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
6import { join } from 'path'
7import { pathExists, remove } from 'fs-extra'
8import { expect } from 'chai' 4import { expect } from 'chai'
5import { pathExists, remove } from 'fs-extra'
6import { join } from 'path'
7import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
8import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
9 9
10describe('Request helpers', function () { 10describe('Request helpers', function () {
11 const destPath1 = join(root(), 'test-output-1.txt') 11 const destPath1 = join(root(), 'test-output-1.txt')
@@ -13,7 +13,7 @@ describe('Request helpers', function () {
13 13
14 it('Should throw an error when the bytes limit is exceeded for request', async function () { 14 it('Should throw an error when the bytes limit is exceeded for request', async function () {
15 try { 15 try {
16 await doRequest({ uri: get4KFileUrl() }, 3) 16 await doRequest(get4KFileUrl(), { bodyKBLimit: 3 })
17 } catch { 17 } catch {
18 return 18 return
19 } 19 }
@@ -23,7 +23,7 @@ describe('Request helpers', function () {
23 23
24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { 24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
25 try { 25 try {
26 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3) 26 await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 })
27 } catch { 27 } catch {
28 28
29 await wait(500) 29 await wait(500)
@@ -35,8 +35,8 @@ describe('Request helpers', function () {
35 }) 35 })
36 36
37 it('Should succeed if the file is below the limit', async function () { 37 it('Should succeed if the file is below the limit', async function () {
38 await doRequest({ uri: get4KFileUrl() }, 5) 38 await doRequest(get4KFileUrl(), { bodyKBLimit: 5 })
39 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5) 39 await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 })
40 40
41 expect(await pathExists(destPath2)).to.be.true 41 expect(await pathExists(destPath2)).to.be.true
42 }) 42 })
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index a1b5e8f5d..5addb45c7 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () {
137 137
138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) 138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
139 139
140 await waitUntilLog(server, 'expired external auth token') 140 await waitUntilLog(server, 'expired external auth token', 2)
141 }) 141 })
142 142
143 it('Should auto login Cyan, create the user and use the token', async function () { 143 it('Should auto login Cyan, create the user and use the token', async function () {
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts
index d88170201..ac958c5f5 100644
--- a/server/tests/plugins/filter-hooks.ts
+++ b/server/tests/plugins/filter-hooks.ts
@@ -2,11 +2,15 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
5import { ServerConfig } from '@shared/models' 6import { ServerConfig } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { 8import {
7 addVideoCommentReply, 9 addVideoCommentReply,
8 addVideoCommentThread, 10 addVideoCommentThread,
11 advancedVideosSearch,
9 createLive, 12 createLive,
13 createVideoPlaylist,
10 doubleFollow, 14 doubleFollow,
11 getAccountVideos, 15 getAccountVideos,
12 getConfig, 16 getConfig,
@@ -15,24 +19,33 @@ import {
15 getVideo, 19 getVideo,
16 getVideoChannelVideos, 20 getVideoChannelVideos,
17 getVideoCommentThreads, 21 getVideoCommentThreads,
22 getVideoPlaylist,
18 getVideosList, 23 getVideosList,
19 getVideosListPagination, 24 getVideosListPagination,
20 getVideoThreadComments, 25 getVideoThreadComments,
21 getVideoWithToken, 26 getVideoWithToken,
22 installPlugin, 27 installPlugin,
28 makeRawRequest,
23 registerUser, 29 registerUser,
24 setAccessTokensToServers, 30 setAccessTokensToServers,
25 setDefaultVideoChannel, 31 setDefaultVideoChannel,
26 updateCustomSubConfig, 32 updateCustomSubConfig,
27 updateVideo, 33 updateVideo,
28 uploadVideo, 34 uploadVideo,
35 uploadVideoAndGetId,
29 waitJobs 36 waitJobs
30} from '../../../shared/extra-utils' 37} from '../../../shared/extra-utils'
31import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' 38import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
32import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' 39import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
33import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos' 40import {
41 VideoDetails,
42 VideoImport,
43 VideoImportState,
44 VideoPlaylist,
45 VideoPlaylistPrivacy,
46 VideoPrivacy
47} from '../../../shared/models/videos'
34import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' 48import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
35import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
36 49
37const expect = chai.expect 50const expect = chai.expect
38 51
@@ -355,6 +368,165 @@ describe('Test plugin filter hooks', function () {
355 }) 368 })
356 }) 369 })
357 370
371 describe('Download hooks', function () {
372 const downloadVideos: VideoDetails[] = []
373
374 before(async function () {
375 this.timeout(60000)
376
377 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
378 transcoding: {
379 webtorrent: {
380 enabled: true
381 },
382 hls: {
383 enabled: true
384 }
385 }
386 })
387
388 const uuids: string[] = []
389
390 for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
391 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
392 uuids.push(uuid)
393 }
394
395 await waitJobs(servers)
396
397 for (const uuid of uuids) {
398 const res = await getVideo(servers[0].url, uuid)
399 downloadVideos.push(res.body)
400 }
401 })
402
403 it('Should run filter:api.download.torrent.allowed.result', async function () {
404 const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
405 expect(res.body.error).to.equal('Liu Bei')
406
407 await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
408 await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
409 })
410
411 it('Should run filter:api.download.video.allowed.result', async function () {
412 {
413 const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
414 expect(res.body.error).to.equal('Cao Cao')
415
416 await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
417 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
418 }
419
420 {
421 const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
422 expect(res.body.error).to.equal('Sun Jian')
423
424 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
425
426 await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
427 await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
428 }
429 })
430 })
431
432 describe('Embed filters', function () {
433 const embedVideos: VideoDetails[] = []
434 const embedPlaylists: VideoPlaylist[] = []
435
436 before(async function () {
437 this.timeout(60000)
438
439 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
440 transcoding: {
441 enabled: false
442 }
443 })
444
445 for (const name of [ 'bad embed', 'good embed' ]) {
446 {
447 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
448 const res = await getVideo(servers[0].url, uuid)
449 embedVideos.push(res.body)
450 }
451
452 {
453 const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
454 const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
455
456 const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id)
457 embedPlaylists.push(resPlaylist.body)
458 }
459 }
460 })
461
462 it('Should run filter:html.embed.video.allowed.result', async function () {
463 const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200)
464 expect(res.text).to.equal('Lu Bu')
465 })
466
467 it('Should run filter:html.embed.video-playlist.allowed.result', async function () {
468 const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200)
469 expect(res.text).to.equal('Diao Chan')
470 })
471 })
472
473 describe('Search filters', function () {
474
475 before(async function () {
476 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
477 search: {
478 searchIndex: {
479 enabled: true,
480 isDefaultSearch: false,
481 disableLocalSearch: false
482 }
483 }
484 })
485 })
486
487 it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
488 await advancedVideosSearch(servers[0].url, {
489 search: 'Sun Quan'
490 })
491
492 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
493 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
494 })
495
496 it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
497 await advancedVideosSearch(servers[0].url, {
498 search: 'Sun Quan',
499 searchTarget: 'search-index'
500 })
501
502 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
503 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
504 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1)
505 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1)
506 })
507
508 it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
509 await advancedVideoChannelSearch(servers[0].url, {
510 search: 'Sun Ce'
511 })
512
513 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
514 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
515 })
516
517 it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
518 await advancedVideoChannelSearch(servers[0].url, {
519 search: 'Sun Ce',
520 searchTarget: 'search-index'
521 })
522
523 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
524 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
525 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1)
526 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1)
527 })
528 })
529
358 after(async function () { 530 after(async function () {
359 await cleanupTests(servers) 531 await cleanupTests(servers)
360 }) 532 })
diff --git a/server/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'
7import './plugin-router' 7import './plugin-router'
8import './plugin-storage' 8import './plugin-storage'
9import './plugin-transcoding' 9import './plugin-transcoding'
10import './plugin-unloading'
10import './translations' 11import './translations'
11import './video-constants' 12import './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'
17import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' 18import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
18import { expect } from 'chai' 19import { 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
3import 'mocha'
4import {
5 cleanupTests,
6 flushAndRunServer,
7 getPluginTestPath,
8 makeGetRequest,
9 installPlugin,
10 uninstallPlugin,
11 ServerInfo,
12 setAccessTokensToServers
13} from '../../../shared/extra-utils'
14import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
15import { expect } from 'chai'
16
17describe('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
89abbrev@1: 89abbrev@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
156balanced-match@^1.0.0: 156balanced-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
161base64-js@^1.3.1: 161base64-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
216bittorrent-protocol@^3.2.0: 216bittorrent-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
229bittorrent-tracker@^9.0.0: 229bittorrent-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
263block-stream2@^2.0.0: 264block-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
270bn.js@^5.1.1: 271bn.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
275brace-expansion@^1.1.7: 276brace-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
316buffer@^6.0.2: 317buffer@^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
404chunk-store-stream@^4.1.1: 405chunk-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
486create-torrent@^4.4.2: 487cpus@^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
492create-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
642end-of-stream@1.4.1: 649end-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
649end-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
656err-code@^2.0.3: 656err-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
661error-ex@^1.3.1: 661error-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
711fs-chunk-store@^2.0.2: 711fs-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
748get-browser-rtc@^1.0.2: 749get-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
780graceful-fs@^4.1.15: 781graceful-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
785has-flag@^3.0.0: 786has-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
894is-docker@^2.0.0: 895is-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
899is-file@^1.0.0: 900is-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
968k-bucket@^5.0.0: 969k-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
975k-rpc-socket@^1.7.2: 976k-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
1004load-ip-set@^2.1.2: 1005load-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
1086memory-chunk-store@^1.3.0: 1087memory-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
1091mime@^1.3.4: 1094mime@^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
1096mime@^2.4.1, mime@^2.4.6: 1099mime@^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
1101mimic-response@^1.0.0: 1104mimic-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
1167mp4-stream@^3.0.0: 1170mp4-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
1176ms@2.0.0: 1180ms@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
1199multistream@^4.0.1: 1203multistream@^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
1206napi-macros@^2.0.0: 1211napi-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
1220netmask@^1.0.6: 1225netmask@^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
1225netrc-parser@^3.1.6: 1230netrc-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
1354open@^7.1.0: 1359open@^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
1414parse-torrent@^9.1.0: 1419parse-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
1496queue-microtask@^1.1.2, queue-microtask@^1.2.0, queue-microtask@^1.2.2: 1502queue-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
1501random-access-file@^2.0.1: 1507random-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
1602run-parallel-limit@^1.0.6: 1608run-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
1607run-parallel@^1.1.10, run-parallel@^1.1.2, run-parallel@^1.1.6, run-parallel@^1.1.9: 1615run-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
1612run-series@^1.1.8, run-series@^1.1.9: 1622run-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
1617rusha@^0.8.1: 1627rusha@^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
1716simple-peer@^9.7.1, simple-peer@^9.9.3: 1726simple-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
1729simple-sha1@^3.0.0, simple-sha1@^3.0.1: 1739simple-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
1737simple-websocket@^9.0.0: 1747simple-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
1803string-width@^4.2.0: 1813string-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
1931torrent-piece@^2.0.0: 1941torrent-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
1936type-fest@^0.6.0: 1946type-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
2025utp-native@^2.2.1: 2035utp-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
2097webtorrent@>=0.108.6, webtorrent@>=0.111.0: 2107webtorrent@>=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
2145which@^1.2.9: 2156which@^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
2200ws@^7.3.0, ws@^7.4.2: 2211ws@^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
2205xml2js@^0.4.8: 2216xml2js@^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 @@
1import { FunctionProperties, PickWith } from '@shared/core-utils'
1import { AccountModel } from '../../../models/account/account' 2import { AccountModel } from '../../../models/account/account'
3import { MChannelDefault } from '../video/video-channels'
4import { MAccountBlocklistId } from './account-blocklist'
2import { 5import {
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'
16import { FunctionProperties, PickWith } from '@shared/core-utils'
17import { MAccountBlocklistId } from './account-blocklist'
18import { MChannelDefault } from '../video/video-channels'
19 19
20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> 20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
21 21
@@ -106,4 +106,4 @@ export type MAccountFormattable =
106 106
107export type MAccountAP = 107export 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 @@
1import { PickWith } from '@shared/core-utils'
1import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 2import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
2import { 3import {
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'
11import { PickWith } from '@shared/core-utils'
12import { ActorModel } from '@server/models/activitypub/actor'
13import { MChannelDefault } from '../video/video-channels'
14 13
15type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> 14type 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
50type SubscriptionFollowing =
51 MActorDefault &
52 PickWith<ActorModel, 'VideoChannel', MChannelDefault>
53
54export type MActorFollowActorsDefaultSubscription = 49export type MActorFollowActorsDefaultSubscription =
55 MActorFollow & 50 MActorFollow &
56 Use<'ActorFollower', MActorDefault> & 51 Use<'ActorFollower', MActorDefault> &
57 Use<'ActorFollowing', SubscriptionFollowing> 52 Use<'ActorFollowing', MActorDefaultChannelId>
58 53
59export type MActorFollowSubscriptions = 54export 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 @@
1import { ActorImageModel } from '../../../models/account/actor-image'
2import { FunctionProperties } from '@shared/core-utils'
3
4export type MActorImage = ActorImageModel
5
6// ############################################################################
7
8// Format for API or AP object
9
10export 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 @@
1import { ActorModel } from '../../../models/activitypub/actor' 1
2import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' 2import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
3import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' 3import { ActorModel } from '../../../models/activitypub/actor'
4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' 4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
5import { MAvatar, MAvatarFormattable } from './avatar'
6import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' 5import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
6import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
7import { MActorImage, MActorImageFormattable } from './actor-image'
7 8
8type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> 9type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
10type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M>
9 11
10// ############################################################################ 12// ############################################################################
11 13
12export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'> 14export 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
34export type MActorDefaultLight = 36export type MActorDefaultLight =
35 MActorLight & 37 MActorLight &
36 Use<'Server', MServerHost> & 38 Use<'Server', MServerHost> &
37 Use<'Avatar', MAvatar> 39 Use<'Avatar', MActorImage>
38 40
39export type MActorAccountId = 41export 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
80export type MActorImages =
81 MActor &
82 Use<'Avatar', MActorImage> &
83 UseOpt<'Banner', MActorImage>
84
78export type MActorDefault = 85export type MActorDefault =
79 MActor & 86 MActor &
80 Use<'Server', MServer> & 87 Use<'Server', MServer> &
81 Use<'Avatar', MAvatar> 88 Use<'Avatar', MActorImage>
89
90export type MActorDefaultChannelId =
91 MActorDefault &
92 Use<'VideoChannel', MChannelId>
93
94export 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 =
89export type MActorFull = 106export 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 =
97export type MActorFullActor = 115export 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
114export type MActorSummaryBlocks = 133export 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
132export type MActorFormattable = 151export 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
137export type MActorAP = 157type MActorAPBase =
138 MActor & 158 MActor &
139 Use<'Avatar', MAvatar> 159 Use<'Avatar', MActorImage>
160
161export type MActorAPAccount =
162 MActorAPBase
163
164export 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 @@
1import { AvatarModel } from '../../../models/avatar/avatar'
2import { FunctionProperties } from '@shared/core-utils'
3
4export type MAvatar = AvatarModel
5
6// ############################################################################
7
8// Format for API or AP object
9
10export 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 @@
1export * from './account' 1export * from './account'
2export * from './account-blocklist' 2export * from './account-blocklist'
3export * from './actor'
4export * from './actor-follow' 3export * from './actor-follow'
5export * from './avatar' 4export * from './actor-image'
5export * 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 @@
1import { ApplicationModel } from '@server/models/application/application'
2
3// ############################################################################
4
5export type MApplication = Omit<ApplicationModel, 'Account'>
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts
new file mode 100644
index 000000000..26e4b031f
--- /dev/null
+++ b/server/types/models/application/index.ts
@@ -0,0 +1 @@
export * from './application'
diff --git a/server/types/models/index.ts b/server/types/models/index.ts
index affa17425..b4fdb1ff3 100644
--- a/server/types/models/index.ts
+++ b/server/types/models/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './application'
2export * from './moderation' 3export * from './moderation'
3export * from './oauth' 4export * from './oauth'
4export * from './server' 5export * from './server'
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index 58764a748..7ebb0485d 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -1,12 +1,14 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' 2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { ApplicationModel } from '@server/models/application/application'
4import { PluginModel } from '@server/models/server/plugin'
3import { PickWith, PickWithOpt } from '@shared/core-utils' 5import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse' 6import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { ActorImageModel } from '../../../models/account/actor-image'
6import { UserNotificationModel } from '../../../models/account/user-notification' 9import { UserNotificationModel } from '../../../models/account/user-notification'
7import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
8import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 11import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
9import { AvatarModel } from '../../../models/avatar/avatar'
10import { ServerModel } from '../../../models/server/server' 12import { ServerModel } from '../../../models/server/server'
11import { VideoModel } from '../../../models/video/video' 13import { VideoModel } from '../../../models/video/video'
12import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 14import { 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
92export type MUserNotification = 100export type MUserNotification =
93 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | 101 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
94 'VideoImport' | 'Account' | 'ActorFollow'> 102 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
95 103
96// ############################################################################ 104// ############################################################################
97 105
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
103 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & 111 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
104 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & 112 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
105 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & 113 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
114 Use<'Plugin', UserNotificationIncludes.PluginInclude> &
115 Use<'Application', UserNotificationIncludes.ApplicationInclude> &
106 Use<'Account', UserNotificationIncludes.AccountIncludeActor> 116 Use<'Account', UserNotificationIncludes.AccountIncludeActor>
diff --git a/server/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 @@
1import { UserModel } from '../../../models/account/user' 1import { AccountModel } from '@server/models/account/account'
2import { MVideoPlaylist } from '@server/types/models'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 3import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { UserModel } from '../../../models/account/user'
3import { 5import {
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'
12import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
13import { AccountModel } from '@server/models/account/account'
14import { MChannelFormattable } from '../video/video-channels' 14import { MChannelFormattable } from '../video/video-channels'
15import { MVideoPlaylist } from '@server/types/models' 15import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
16 16
17type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> 17type 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'
25import { MVideo } from './video' 27import { 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
60export 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
62export type MChannelLight =
63 MChannel &
64 Use<'Actor', MActorDefaultLight>
65
66export type MChannelActorLight = 68export 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
87export type MChannelAccountDefault = 89export type MChannelBannerAccountDefault =
88 MChannel & 90 MChannel &
89 Use<'Actor', MActorDefault> & 91 Use<'Actor', MActorDefaultBanner> &
90 Use<'Account', MAccountDefault> 92 Use<'Account', MAccountDefault>
91 93
92export type MChannelActorAccountActor = 94export 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
100export type MChannelVideos = 102export type MChannelVideos =
101 MChannel & 103 MChannel &
102 Use<'Videos', MVideo[]> 104 Use<'Videos', MVideo[]>
103 105
104export 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
147export type MChannelAP = 143export 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'
17import { MVideoThumbnail } from '../models' 18import { 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
57export type RegisterServerOptions = { 64export 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'
17import { MVideoImportDefault } from '@server/types/models/video/video-import' 19import { MVideoImportDefault } from '@server/types/models/video/video-import'
18import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' 20import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
19import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' 21import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
20import { UserRole } from '@shared/models'
21import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 22import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
22import { 23import {
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
51interface PeerTubeLocals { 50interface PeerTubeLocals {
52 bypassLogin?: {
53 bypass: boolean
54 pluginName: string
55 authName?: string
56 user: {
57 username: string
58 email: string
59 displayName: string
60 role: UserRole
61 }
62 }
63
64 refreshTokenAuthName?: string
65
66 explicitLogout?: boolean
67
68 videoAll?: MVideoFullLight 51 videoAll?: MVideoFullLight
69 onlyImmutableVideo?: MVideoImmutable 52 onlyImmutableVideo?: MVideoImmutable
70 onlyVideo?: MVideoThumbnail 53 onlyVideo?: MVideoThumbnail
@@ -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