aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <chocobozzz@cpy.re>2021-05-27 15:59:55 +0200
committerGitHub <noreply@github.com>2021-05-27 15:59:55 +0200
commit2539932e16129992a2c0889b4ff527c265a8e2c7 (patch)
treefb5048e63e02a2485eb96d27455f43e4b22e8ae0 /server
parenteb34ec30e0b57286fc6f85160490d2e973a3b0b1 (diff)
downloadPeerTube-2539932e16129992a2c0889b4ff527c265a8e2c7.tar.gz
PeerTube-2539932e16129992a2c0889b4ff527c265a8e2c7.tar.zst
PeerTube-2539932e16129992a2c0889b4ff527c265a8e2c7.zip
Instance homepage support (#4007)
* Prepare homepage parsers * Add ability to update instance hompage * Add ability to set homepage as landing page * Add homepage preview in admin * Dynamically update left menu for homepage * Inject home content in homepage * Add videos list and channel miniature custom markup * Remove unused elements in markup service
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts4
-rw-r--r--server/controllers/api/custom-page.ts42
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/videos/import.ts4
-rw-r--r--server/controllers/static.ts10
-rw-r--r--server/helpers/markdown.ts8
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/initializers/migrations/0650-actor-custom-pages.ts33
-rw-r--r--server/lib/client-html.ts6
-rw-r--r--server/lib/config.ts274
-rw-r--r--server/lib/job-queue/handlers/video-import.ts6
-rw-r--r--server/lib/plugins/plugin-helpers-builder.ts4
-rw-r--r--server/lib/server-config-manager.ts303
-rw-r--r--server/models/account/actor-custom-page.ts69
-rw-r--r--server/tests/api/check-params/custom-pages.ts81
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/server/homepage.ts85
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/types/models/account/actor-custom-page.ts4
-rw-r--r--server/types/models/account/index.ts1
21 files changed, 648 insertions, 296 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 5ce7adc35..c9b5c8047 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,8 +1,8 @@
1import { ServerConfigManager } from '@server/lib/server-config-manager'
1import * as express from 'express' 2import * as express from 'express'
2import { remove, writeJSON } from 'fs-extra' 3import { remove, writeJSON } from 'fs-extra'
3import { snakeCase } from 'lodash' 4import { snakeCase } from 'lodash'
4import validator from 'validator' 5import validator from 'validator'
5import { getServerConfig } from '@server/lib/config'
6import { UserRight } from '../../../shared' 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'
@@ -43,7 +43,7 @@ configRouter.delete('/custom',
43) 43)
44 44
45async function getConfig (req: express.Request, res: express.Response) { 45async function getConfig (req: express.Request, res: express.Response) {
46 const json = await getServerConfig(req.ip) 46 const json = await ServerConfigManager.Instance.getServerConfig(req.ip)
47 47
48 return res.json(json) 48 return res.json(json)
49} 49}
diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts
new file mode 100644
index 000000000..3c47f7b9a
--- /dev/null
+++ b/server/controllers/api/custom-page.ts
@@ -0,0 +1,42 @@
1import * as express from 'express'
2import { ServerConfigManager } from '@server/lib/server-config-manager'
3import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
4import { HttpStatusCode } from '@shared/core-utils'
5import { UserRight } from '@shared/models'
6import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
7
8const customPageRouter = express.Router()
9
10customPageRouter.get('/homepage/instance',
11 asyncMiddleware(getInstanceHomepage)
12)
13
14customPageRouter.put('/homepage/instance',
15 authenticate,
16 ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE),
17 asyncMiddleware(updateInstanceHomepage)
18)
19
20// ---------------------------------------------------------------------------
21
22export {
23 customPageRouter
24}
25
26// ---------------------------------------------------------------------------
27
28async function getInstanceHomepage (req: express.Request, res: express.Response) {
29 const page = await ActorCustomPageModel.loadInstanceHomepage()
30 if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
31
32 return res.json(page.toFormattedJSON())
33}
34
35async function updateInstanceHomepage (req: express.Request, res: express.Response) {
36 const content = req.body.content
37
38 await ActorCustomPageModel.updateInstanceHomepage(content)
39 ServerConfigManager.Instance.updateHomepageState(content)
40
41 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
42}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 7ade1df3a..28378654a 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -8,6 +8,7 @@ import { abuseRouter } from './abuse'
8import { accountsRouter } from './accounts' 8import { accountsRouter } from './accounts'
9import { bulkRouter } from './bulk' 9import { bulkRouter } from './bulk'
10import { configRouter } from './config' 10import { configRouter } from './config'
11import { customPageRouter } from './custom-page'
11import { jobsRouter } from './jobs' 12import { jobsRouter } from './jobs'
12import { oauthClientsRouter } from './oauth-clients' 13import { oauthClientsRouter } from './oauth-clients'
13import { overviewsRouter } from './overviews' 14import { overviewsRouter } from './overviews'
@@ -47,6 +48,7 @@ apiRouter.use('/jobs', jobsRouter)
47apiRouter.use('/search', searchRouter) 48apiRouter.use('/search', searchRouter)
48apiRouter.use('/overviews', overviewsRouter) 49apiRouter.use('/overviews', overviewsRouter)
49apiRouter.use('/plugins', pluginRouter) 50apiRouter.use('/plugins', pluginRouter)
51apiRouter.use('/custom-pages', customPageRouter)
50apiRouter.use('/ping', pong) 52apiRouter.use('/ping', pong)
51apiRouter.use('/*', badRequest) 53apiRouter.use('/*', badRequest)
52 54
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index ee63c7b77..0d5d7a962 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -3,7 +3,7 @@ import { move, readFile } from 'fs-extra'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent' 4import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 5import { join } from 'path'
6import { getEnabledResolutions } from '@server/lib/config' 6import { ServerConfigManager } from '@server/lib/server-config-manager'
7import { setVideoTags } from '@server/lib/video' 7import { setVideoTags } from '@server/lib/video'
8import { FilteredModelAttributes } from '@server/types' 8import { FilteredModelAttributes } from '@server/types'
9import { 9import {
@@ -134,7 +134,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
134 const targetUrl = body.targetUrl 134 const targetUrl = body.targetUrl
135 const user = res.locals.oauth.token.User 135 const user = res.locals.oauth.token.User
136 136
137 const youtubeDL = new YoutubeDL(targetUrl, getEnabledResolutions('vod')) 137 const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
138 138
139 // Get video infos 139 // Get video infos
140 let youtubeDLInfo: YoutubeDLInfo 140 let youtubeDLInfo: YoutubeDLInfo
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 8a747ec52..3870ebfe9 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -2,7 +2,7 @@ import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { join } from 'path' 3import { join } from 'path'
4import { serveIndexHTML } from '@server/lib/client-html' 4import { serveIndexHTML } from '@server/lib/client-html'
5import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config' 5import { ServerConfigManager } from '@server/lib/server-config-manager'
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/nodeinfo.model' 7import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model'
8import { root } from '../helpers/core-utils' 8import { root } from '../helpers/core-utils'
@@ -203,10 +203,10 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
203 } 203 }
204 }, 204 },
205 plugin: { 205 plugin: {
206 registered: getRegisteredPlugins() 206 registered: ServerConfigManager.Instance.getRegisteredPlugins()
207 }, 207 },
208 theme: { 208 theme: {
209 registered: getRegisteredThemes(), 209 registered: ServerConfigManager.Instance.getRegisteredThemes(),
210 default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) 210 default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
211 }, 211 },
212 email: { 212 email: {
@@ -222,13 +222,13 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
222 webtorrent: { 222 webtorrent: {
223 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED 223 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
224 }, 224 },
225 enabledResolutions: getEnabledResolutions('vod') 225 enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod')
226 }, 226 },
227 live: { 227 live: {
228 enabled: CONFIG.LIVE.ENABLED, 228 enabled: CONFIG.LIVE.ENABLED,
229 transcoding: { 229 transcoding: {
230 enabled: CONFIG.LIVE.TRANSCODING.ENABLED, 230 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
231 enabledResolutions: getEnabledResolutions('live') 231 enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('live')
232 } 232 }
233 }, 233 },
234 import: { 234 import: {
diff --git a/server/helpers/markdown.ts b/server/helpers/markdown.ts
index 2126bb752..41e57d857 100644
--- a/server/helpers/markdown.ts
+++ b/server/helpers/markdown.ts
@@ -1,4 +1,6 @@
1import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' 1import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
2
3const sanitizeOptions = getSanitizeOptions()
2 4
3const sanitizeHtml = require('sanitize-html') 5const sanitizeHtml = require('sanitize-html')
4const markdownItEmoji = require('markdown-it-emoji/light') 6const markdownItEmoji = require('markdown-it-emoji/light')
@@ -18,7 +20,7 @@ const toSafeHtml = text => {
18 const html = markdownIt.render(textWithLineFeed) 20 const html = markdownIt.render(textWithLineFeed)
19 21
20 // Convert to safe Html 22 // Convert to safe Html
21 return sanitizeHtml(html, SANITIZE_OPTIONS) 23 return sanitizeHtml(html, sanitizeOptions)
22} 24}
23 25
24const mdToPlainText = text => { 26const mdToPlainText = text => {
@@ -28,7 +30,7 @@ const mdToPlainText = text => {
28 const html = markdownIt.render(text) 30 const html = markdownIt.render(text)
29 31
30 // Convert to safe Html 32 // Convert to safe Html
31 const safeHtml = sanitizeHtml(html, SANITIZE_OPTIONS) 33 const safeHtml = sanitizeHtml(html, sanitizeOptions)
32 34
33 return safeHtml.replace(/<[^>]+>/g, '') 35 return safeHtml.replace(/<[^>]+>/g, '')
34 .replace(/\n$/, '') 36 .replace(/\n$/, '')
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 4cf7dcf0a..919f9ea6e 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 645 27const LAST_MIGRATION_VERSION = 650
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 75a13ec8b..38e7a76d0 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -44,6 +44,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla
44import { VideoTagModel } from '../models/video/video-tag' 44import { VideoTagModel } from '../models/video/video-tag'
45import { VideoViewModel } from '../models/video/video-view' 45import { VideoViewModel } from '../models/video/video-view'
46import { CONFIG } from './config' 46import { CONFIG } from './config'
47import { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
47 48
48require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 49require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
49 50
@@ -141,7 +142,8 @@ async function initDatabaseModels (silent: boolean) {
141 ThumbnailModel, 142 ThumbnailModel,
142 TrackerModel, 143 TrackerModel,
143 VideoTrackerModel, 144 VideoTrackerModel,
144 PluginModel 145 PluginModel,
146 ActorCustomPageModel
145 ]) 147 ])
146 148
147 // Check extensions exist in the database 149 // Check extensions exist in the database
diff --git a/server/initializers/migrations/0650-actor-custom-pages.ts b/server/initializers/migrations/0650-actor-custom-pages.ts
new file mode 100644
index 000000000..1338327e8
--- /dev/null
+++ b/server/initializers/migrations/0650-actor-custom-pages.ts
@@ -0,0 +1,33 @@
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 query = `
11 CREATE TABLE IF NOT EXISTS "actorCustomPage" (
12 "id" serial,
13 "content" TEXT,
14 "type" varchar(255) NOT NULL,
15 "actorId" integer NOT NULL REFERENCES "actor" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
16 "createdAt" timestamp WITH time zone NOT NULL,
17 "updatedAt" timestamp WITH time zone NOT NULL,
18 PRIMARY KEY ("id")
19 );
20 `
21
22 await utils.sequelize.query(query)
23 }
24}
25
26function down (options) {
27 throw new Error('Not implemented.')
28}
29
30export {
31 up,
32 down
33}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 85fdc8754..4b2968e8b 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -26,7 +26,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
26import { getActivityStreamDuration } from '../models/video/video-format-utils' 26import { getActivityStreamDuration } from '../models/video/video-format-utils'
27import { VideoPlaylistModel } from '../models/video/video-playlist' 27import { VideoPlaylistModel } from '../models/video/video-playlist'
28import { MAccountActor, MChannelActor } from '../types/models' 28import { MAccountActor, MChannelActor } from '../types/models'
29import { getHTMLServerConfig } from './config' 29import { ServerConfigManager } from './server-config-manager'
30 30
31type Tags = { 31type Tags = {
32 ogType: string 32 ogType: string
@@ -211,7 +211,7 @@ class ClientHtml {
211 if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] 211 if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
212 212
213 const buffer = await readFile(path) 213 const buffer = await readFile(path)
214 const serverConfig = await getHTMLServerConfig() 214 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
215 215
216 let html = buffer.toString() 216 let html = buffer.toString()
217 html = await ClientHtml.addAsyncPluginCSS(html) 217 html = await ClientHtml.addAsyncPluginCSS(html)
@@ -280,7 +280,7 @@ class ClientHtml {
280 if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] 280 if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
281 281
282 const buffer = await readFile(path) 282 const buffer = await readFile(path)
283 const serverConfig = await getHTMLServerConfig() 283 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
284 284
285 let html = buffer.toString() 285 let html = buffer.toString()
286 286
diff --git a/server/lib/config.ts b/server/lib/config.ts
deleted file mode 100644
index 18d49f05a..000000000
--- a/server/lib/config.ts
+++ /dev/null
@@ -1,274 +0,0 @@
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 { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
6import { Hooks } from './plugins/hooks'
7import { PluginManager } from './plugins/plugin-manager'
8import { getThemeOrDefault } from './plugins/theme-utils'
9import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
10
11async function getServerConfig (ip?: string): Promise<ServerConfig> {
12 const { allowed } = await Hooks.wrapPromiseFun(
13 isSignupAllowed,
14 {
15 ip
16 },
17 'filter:api.user.signup.allowed.result'
18 )
19
20 const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
21
22 const signup = {
23 allowed,
24 allowedForCurrentIP,
25 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
26 }
27
28 const htmlConfig = await getHTMLServerConfig()
29
30 return { ...htmlConfig, signup }
31}
32
33// Config injected in HTML
34let serverCommit: string
35async function getHTMLServerConfig (): Promise<HTMLServerConfig> {
36 if (serverCommit === undefined) serverCommit = await getServerCommit()
37
38 const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
39
40 return {
41 instance: {
42 name: CONFIG.INSTANCE.NAME,
43 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
44 isNSFW: CONFIG.INSTANCE.IS_NSFW,
45 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
46 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
47 customizations: {
48 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
49 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
50 }
51 },
52 search: {
53 remoteUri: {
54 users: CONFIG.SEARCH.REMOTE_URI.USERS,
55 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
56 },
57 searchIndex: {
58 enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
59 url: CONFIG.SEARCH.SEARCH_INDEX.URL,
60 disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
61 isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
62 }
63 },
64 plugin: {
65 registered: getRegisteredPlugins(),
66 registeredExternalAuths: getExternalAuthsPlugins(),
67 registeredIdAndPassAuths: getIdAndPassAuthPlugins()
68 },
69 theme: {
70 registered: getRegisteredThemes(),
71 default: defaultTheme
72 },
73 email: {
74 enabled: isEmailEnabled()
75 },
76 contactForm: {
77 enabled: CONFIG.CONTACT_FORM.ENABLED
78 },
79 serverVersion: PEERTUBE_VERSION,
80 serverCommit,
81 transcoding: {
82 hls: {
83 enabled: CONFIG.TRANSCODING.HLS.ENABLED
84 },
85 webtorrent: {
86 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
87 },
88 enabledResolutions: getEnabledResolutions('vod'),
89 profile: CONFIG.TRANSCODING.PROFILE,
90 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
91 },
92 live: {
93 enabled: CONFIG.LIVE.ENABLED,
94
95 allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
96 maxDuration: CONFIG.LIVE.MAX_DURATION,
97 maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
98 maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
99
100 transcoding: {
101 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
102 enabledResolutions: getEnabledResolutions('live'),
103 profile: CONFIG.LIVE.TRANSCODING.PROFILE,
104 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
105 },
106
107 rtmp: {
108 port: CONFIG.LIVE.RTMP.PORT
109 }
110 },
111 import: {
112 videos: {
113 http: {
114 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
115 },
116 torrent: {
117 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
118 }
119 }
120 },
121 autoBlacklist: {
122 videos: {
123 ofUsers: {
124 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
125 }
126 }
127 },
128 avatar: {
129 file: {
130 size: {
131 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
132 },
133 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
134 }
135 },
136 banner: {
137 file: {
138 size: {
139 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
140 },
141 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
142 }
143 },
144 video: {
145 image: {
146 extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
147 size: {
148 max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
149 }
150 },
151 file: {
152 extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
153 }
154 },
155 videoCaption: {
156 file: {
157 size: {
158 max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
159 },
160 extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
161 }
162 },
163 user: {
164 videoQuota: CONFIG.USER.VIDEO_QUOTA,
165 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
166 },
167 trending: {
168 videos: {
169 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS,
170 algorithms: {
171 enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
172 default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT
173 }
174 }
175 },
176 tracker: {
177 enabled: CONFIG.TRACKER.ENABLED
178 },
179
180 followings: {
181 instance: {
182 autoFollowIndex: {
183 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
184 }
185 }
186 },
187
188 broadcastMessage: {
189 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
190 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
191 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
192 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
193 }
194 }
195}
196
197function getRegisteredThemes () {
198 return PluginManager.Instance.getRegisteredThemes()
199 .map(t => ({
200 name: t.name,
201 version: t.version,
202 description: t.description,
203 css: t.css,
204 clientScripts: t.clientScripts
205 }))
206}
207
208function getRegisteredPlugins () {
209 return PluginManager.Instance.getRegisteredPlugins()
210 .map(p => ({
211 name: p.name,
212 version: p.version,
213 description: p.description,
214 clientScripts: p.clientScripts
215 }))
216}
217
218function getEnabledResolutions (type: 'vod' | 'live') {
219 const transcoding = type === 'vod'
220 ? CONFIG.TRANSCODING
221 : CONFIG.LIVE.TRANSCODING
222
223 return Object.keys(transcoding.RESOLUTIONS)
224 .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
225 .map(r => parseInt(r, 10))
226}
227
228// ---------------------------------------------------------------------------
229
230export {
231 getServerConfig,
232 getRegisteredThemes,
233 getEnabledResolutions,
234 getRegisteredPlugins,
235 getHTMLServerConfig
236}
237
238// ---------------------------------------------------------------------------
239
240function getIdAndPassAuthPlugins () {
241 const result: RegisteredIdAndPassAuthConfig[] = []
242
243 for (const p of PluginManager.Instance.getIdAndPassAuths()) {
244 for (const auth of p.idAndPassAuths) {
245 result.push({
246 npmName: p.npmName,
247 name: p.name,
248 version: p.version,
249 authName: auth.authName,
250 weight: auth.getWeight()
251 })
252 }
253 }
254
255 return result
256}
257
258function getExternalAuthsPlugins () {
259 const result: RegisteredExternalAuthConfig[] = []
260
261 for (const p of PluginManager.Instance.getExternalAuths()) {
262 for (const auth of p.externalAuths) {
263 result.push({
264 npmName: p.npmName,
265 name: p.name,
266 version: p.version,
267 authName: auth.authName,
268 authDisplayName: auth.authDisplayName()
269 })
270 }
271 }
272
273 return result
274}
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 3067ce214..d71053e87 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -2,8 +2,10 @@ import * as Bull from 'bull'
2import { move, remove, stat } from 'fs-extra' 2import { move, remove, stat } from 'fs-extra'
3import { extname } from 'path' 3import { extname } from 'path'
4import { retryTransactionWrapper } from '@server/helpers/database-utils' 4import { retryTransactionWrapper } from '@server/helpers/database-utils'
5import { YoutubeDL } from '@server/helpers/youtube-dl'
5import { isPostImportVideoAccepted } from '@server/lib/moderation' 6import { isPostImportVideoAccepted } from '@server/lib/moderation'
6import { Hooks } from '@server/lib/plugins/hooks' 7import { Hooks } from '@server/lib/plugins/hooks'
8import { ServerConfigManager } from '@server/lib/server-config-manager'
7import { isAbleToUploadVideo } from '@server/lib/user' 9import { isAbleToUploadVideo } from '@server/lib/user'
8import { addOptimizeOrMergeAudioJob } from '@server/lib/video' 10import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
9import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 11import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
@@ -33,8 +35,6 @@ import { MThumbnail } from '../../../types/models/video/thumbnail'
33import { federateVideoIfNeeded } from '../../activitypub/videos' 35import { federateVideoIfNeeded } from '../../activitypub/videos'
34import { Notifier } from '../../notifier' 36import { Notifier } from '../../notifier'
35import { generateVideoMiniature } from '../../thumbnail' 37import { generateVideoMiniature } from '../../thumbnail'
36import { YoutubeDL } from '@server/helpers/youtube-dl'
37import { getEnabledResolutions } from '@server/lib/config'
38 38
39async function processVideoImport (job: Bull.Job) { 39async function processVideoImport (job: Bull.Job) {
40 const payload = job.data as VideoImportPayload 40 const payload = job.data as VideoImportPayload
@@ -76,7 +76,7 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
76 videoImportId: videoImport.id 76 videoImportId: videoImport.id
77 } 77 }
78 78
79 const youtubeDL = new YoutubeDL(videoImport.targetUrl, getEnabledResolutions('vod')) 79 const youtubeDL = new YoutubeDL(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
80 80
81 return processFile( 81 return processFile(
82 () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), 82 () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT),
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts
index cb1cd4d9a..8487672ba 100644
--- a/server/lib/plugins/plugin-helpers-builder.ts
+++ b/server/lib/plugins/plugin-helpers-builder.ts
@@ -15,7 +15,7 @@ import { MPlugin } from '@server/types/models'
15import { PeerTubeHelpers } from '@server/types/plugins' 15import { PeerTubeHelpers } from '@server/types/plugins'
16import { VideoBlacklistCreate } from '@shared/models' 16import { VideoBlacklistCreate } from '@shared/models'
17import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' 17import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
18import { getServerConfig } from '../config' 18import { ServerConfigManager } from '../server-config-manager'
19import { blacklistVideo, unblacklistVideo } from '../video-blacklist' 19import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
20import { UserModel } from '@server/models/user/user' 20import { UserModel } from '@server/models/user/user'
21 21
@@ -147,7 +147,7 @@ function buildConfigHelpers () {
147 }, 147 },
148 148
149 getServerConfig () { 149 getServerConfig () {
150 return getServerConfig() 150 return ServerConfigManager.Instance.getServerConfig()
151 } 151 }
152 } 152 }
153} 153}
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
new file mode 100644
index 000000000..1aff6f446
--- /dev/null
+++ b/server/lib/server-config-manager.ts
@@ -0,0 +1,303 @@
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 { ActorCustomPageModel } from '@server/models/account/actor-custom-page'
6import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
7import { Hooks } from './plugins/hooks'
8import { PluginManager } from './plugins/plugin-manager'
9import { getThemeOrDefault } from './plugins/theme-utils'
10import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
11
12/**
13 *
14 * Used to send the server config to clients (using REST/API or plugins API)
15 * We need a singleton class to manage config state depending on external events (to build menu entries etc)
16 *
17 */
18
19class ServerConfigManager {
20
21 private static instance: ServerConfigManager
22
23 private serverCommit: string
24
25 private homepageEnabled = false
26
27 private constructor () {}
28
29 async init () {
30 const instanceHomepage = await ActorCustomPageModel.loadInstanceHomepage()
31
32 this.updateHomepageState(instanceHomepage?.content)
33 }
34
35 updateHomepageState (content: string) {
36 this.homepageEnabled = !!content
37 }
38
39 async getHTMLServerConfig (): Promise<HTMLServerConfig> {
40 if (this.serverCommit === undefined) this.serverCommit = await getServerCommit()
41
42 const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
43
44 return {
45 instance: {
46 name: CONFIG.INSTANCE.NAME,
47 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
48 isNSFW: CONFIG.INSTANCE.IS_NSFW,
49 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
50 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
51 customizations: {
52 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
53 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
54 }
55 },
56 search: {
57 remoteUri: {
58 users: CONFIG.SEARCH.REMOTE_URI.USERS,
59 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
60 },
61 searchIndex: {
62 enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
63 url: CONFIG.SEARCH.SEARCH_INDEX.URL,
64 disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
65 isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
66 }
67 },
68 plugin: {
69 registered: this.getRegisteredPlugins(),
70 registeredExternalAuths: this.getExternalAuthsPlugins(),
71 registeredIdAndPassAuths: this.getIdAndPassAuthPlugins()
72 },
73 theme: {
74 registered: this.getRegisteredThemes(),
75 default: defaultTheme
76 },
77 email: {
78 enabled: isEmailEnabled()
79 },
80 contactForm: {
81 enabled: CONFIG.CONTACT_FORM.ENABLED
82 },
83 serverVersion: PEERTUBE_VERSION,
84 serverCommit: this.serverCommit,
85 transcoding: {
86 hls: {
87 enabled: CONFIG.TRANSCODING.HLS.ENABLED
88 },
89 webtorrent: {
90 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
91 },
92 enabledResolutions: this.getEnabledResolutions('vod'),
93 profile: CONFIG.TRANSCODING.PROFILE,
94 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod')
95 },
96 live: {
97 enabled: CONFIG.LIVE.ENABLED,
98
99 allowReplay: CONFIG.LIVE.ALLOW_REPLAY,
100 maxDuration: CONFIG.LIVE.MAX_DURATION,
101 maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES,
102 maxUserLives: CONFIG.LIVE.MAX_USER_LIVES,
103
104 transcoding: {
105 enabled: CONFIG.LIVE.TRANSCODING.ENABLED,
106 enabledResolutions: this.getEnabledResolutions('live'),
107 profile: CONFIG.LIVE.TRANSCODING.PROFILE,
108 availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live')
109 },
110
111 rtmp: {
112 port: CONFIG.LIVE.RTMP.PORT
113 }
114 },
115 import: {
116 videos: {
117 http: {
118 enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
119 },
120 torrent: {
121 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
122 }
123 }
124 },
125 autoBlacklist: {
126 videos: {
127 ofUsers: {
128 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
129 }
130 }
131 },
132 avatar: {
133 file: {
134 size: {
135 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
136 },
137 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
138 }
139 },
140 banner: {
141 file: {
142 size: {
143 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
144 },
145 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
146 }
147 },
148 video: {
149 image: {
150 extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
151 size: {
152 max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
153 }
154 },
155 file: {
156 extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
157 }
158 },
159 videoCaption: {
160 file: {
161 size: {
162 max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
163 },
164 extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
165 }
166 },
167 user: {
168 videoQuota: CONFIG.USER.VIDEO_QUOTA,
169 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
170 },
171 trending: {
172 videos: {
173 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS,
174 algorithms: {
175 enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
176 default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT
177 }
178 }
179 },
180 tracker: {
181 enabled: CONFIG.TRACKER.ENABLED
182 },
183
184 followings: {
185 instance: {
186 autoFollowIndex: {
187 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
188 }
189 }
190 },
191
192 broadcastMessage: {
193 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
194 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
195 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
196 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
197 },
198
199 homepage: {
200 enabled: this.homepageEnabled
201 }
202 }
203 }
204
205 async getServerConfig (ip?: string): Promise<ServerConfig> {
206 const { allowed } = await Hooks.wrapPromiseFun(
207 isSignupAllowed,
208 {
209 ip
210 },
211 'filter:api.user.signup.allowed.result'
212 )
213
214 const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
215
216 const signup = {
217 allowed,
218 allowedForCurrentIP,
219 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
220 }
221
222 const htmlConfig = await this.getHTMLServerConfig()
223
224 return { ...htmlConfig, signup }
225 }
226
227 getRegisteredThemes () {
228 return PluginManager.Instance.getRegisteredThemes()
229 .map(t => ({
230 name: t.name,
231 version: t.version,
232 description: t.description,
233 css: t.css,
234 clientScripts: t.clientScripts
235 }))
236 }
237
238 getRegisteredPlugins () {
239 return PluginManager.Instance.getRegisteredPlugins()
240 .map(p => ({
241 name: p.name,
242 version: p.version,
243 description: p.description,
244 clientScripts: p.clientScripts
245 }))
246 }
247
248 getEnabledResolutions (type: 'vod' | 'live') {
249 const transcoding = type === 'vod'
250 ? CONFIG.TRANSCODING
251 : CONFIG.LIVE.TRANSCODING
252
253 return Object.keys(transcoding.RESOLUTIONS)
254 .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
255 .map(r => parseInt(r, 10))
256 }
257
258 private getIdAndPassAuthPlugins () {
259 const result: RegisteredIdAndPassAuthConfig[] = []
260
261 for (const p of PluginManager.Instance.getIdAndPassAuths()) {
262 for (const auth of p.idAndPassAuths) {
263 result.push({
264 npmName: p.npmName,
265 name: p.name,
266 version: p.version,
267 authName: auth.authName,
268 weight: auth.getWeight()
269 })
270 }
271 }
272
273 return result
274 }
275
276 private getExternalAuthsPlugins () {
277 const result: RegisteredExternalAuthConfig[] = []
278
279 for (const p of PluginManager.Instance.getExternalAuths()) {
280 for (const auth of p.externalAuths) {
281 result.push({
282 npmName: p.npmName,
283 name: p.name,
284 version: p.version,
285 authName: auth.authName,
286 authDisplayName: auth.authDisplayName()
287 })
288 }
289 }
290
291 return result
292 }
293
294 static get Instance () {
295 return this.instance || (this.instance = new this())
296 }
297}
298
299// ---------------------------------------------------------------------------
300
301export {
302 ServerConfigManager
303}
diff --git a/server/models/account/actor-custom-page.ts b/server/models/account/actor-custom-page.ts
new file mode 100644
index 000000000..893023181
--- /dev/null
+++ b/server/models/account/actor-custom-page.ts
@@ -0,0 +1,69 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { CustomPage } from '@shared/models'
3import { ActorModel } from '../actor/actor'
4import { getServerActor } from '../application/application'
5
6@Table({
7 tableName: 'actorCustomPage',
8 indexes: [
9 {
10 fields: [ 'actorId', 'type' ],
11 unique: true
12 }
13 ]
14})
15export class ActorCustomPageModel extends Model {
16
17 @AllowNull(true)
18 @Column(DataType.TEXT)
19 content: string
20
21 @AllowNull(false)
22 @Column
23 type: 'homepage'
24
25 @CreatedAt
26 createdAt: Date
27
28 @UpdatedAt
29 updatedAt: Date
30
31 @ForeignKey(() => ActorModel)
32 @Column
33 actorId: number
34
35 @BelongsTo(() => ActorModel, {
36 foreignKey: {
37 name: 'actorId',
38 allowNull: false
39 },
40 onDelete: 'cascade'
41 })
42 Actor: ActorModel
43
44 static async updateInstanceHomepage (content: string) {
45 const serverActor = await getServerActor()
46
47 return ActorCustomPageModel.upsert({
48 content,
49 actorId: serverActor.id,
50 type: 'homepage'
51 })
52 }
53
54 static async loadInstanceHomepage () {
55 const serverActor = await getServerActor()
56
57 return ActorCustomPageModel.findOne({
58 where: {
59 actorId: serverActor.id
60 }
61 })
62 }
63
64 toFormattedJSON (): CustomPage {
65 return {
66 content: this.content
67 }
68 }
69}
diff --git a/server/tests/api/check-params/custom-pages.ts b/server/tests/api/check-params/custom-pages.ts
new file mode 100644
index 000000000..74ca3384c
--- /dev/null
+++ b/server/tests/api/check-params/custom-pages.ts
@@ -0,0 +1,81 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
5import {
6 cleanupTests,
7 createUser,
8 flushAndRunServer,
9 ServerInfo,
10 setAccessTokensToServers,
11 userLogin
12} from '../../../../shared/extra-utils'
13import { makeGetRequest, makePutBodyRequest } from '../../../../shared/extra-utils/requests/requests'
14
15describe('Test custom pages validators', function () {
16 const path = '/api/v1/custom-pages/homepage/instance'
17
18 let server: ServerInfo
19 let userAccessToken: string
20
21 // ---------------------------------------------------------------
22
23 before(async function () {
24 this.timeout(120000)
25
26 server = await flushAndRunServer(1)
27 await setAccessTokensToServers([ server ])
28
29 const user = { username: 'user1', password: 'password' }
30 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
31
32 userAccessToken = await userLogin(server, user)
33 })
34
35 describe('When updating instance homepage', function () {
36
37 it('Should fail with an unauthenticated user', async function () {
38 await makePutBodyRequest({
39 url: server.url,
40 path,
41 fields: { content: 'super content' },
42 statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
43 })
44 })
45
46 it('Should fail with a non admin user', async function () {
47 await makePutBodyRequest({
48 url: server.url,
49 path,
50 token: userAccessToken,
51 fields: { content: 'super content' },
52 statusCodeExpected: HttpStatusCode.FORBIDDEN_403
53 })
54 })
55
56 it('Should succeed with the correct params', async function () {
57 await makePutBodyRequest({
58 url: server.url,
59 path,
60 token: server.accessToken,
61 fields: { content: 'super content' },
62 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
63 })
64 })
65 })
66
67 describe('When getting instance homapage', function () {
68
69 it('Should succeed with the correct params', async function () {
70 await makeGetRequest({
71 url: server.url,
72 path,
73 statusCodeExpected: HttpStatusCode.OK_200
74 })
75 })
76 })
77
78 after(async function () {
79 await cleanupTests([ server ])
80 })
81})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 143515838..ce2335e42 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -3,6 +3,7 @@ import './accounts'
3import './blocklist' 3import './blocklist'
4import './bulk' 4import './bulk'
5import './config' 5import './config'
6import './custom-pages'
6import './contact-form' 7import './contact-form'
7import './debug' 8import './debug'
8import './follows' 9import './follows'
diff --git a/server/tests/api/server/homepage.ts b/server/tests/api/server/homepage.ts
new file mode 100644
index 000000000..e8ba89ca6
--- /dev/null
+++ b/server/tests/api/server/homepage.ts
@@ -0,0 +1,85 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { HttpStatusCode } from '@shared/core-utils'
6import { CustomPage, ServerConfig } from '@shared/models'
7import {
8 cleanupTests,
9 flushAndRunServer,
10 getConfig,
11 getInstanceHomepage,
12 killallServers,
13 reRunServer,
14 ServerInfo,
15 setAccessTokensToServers,
16 updateInstanceHomepage
17} from '../../../../shared/extra-utils/index'
18
19const expect = chai.expect
20
21async function getHomepageState (server: ServerInfo) {
22 const res = await getConfig(server.url)
23
24 const config = res.body as ServerConfig
25 return config.homepage.enabled
26}
27
28describe('Test instance homepage actions', function () {
29 let server: ServerInfo
30
31 before(async function () {
32 this.timeout(30000)
33
34 server = await flushAndRunServer(1)
35 await setAccessTokensToServers([ server ])
36 })
37
38 it('Should not have a homepage', async function () {
39 const state = await getHomepageState(server)
40 expect(state).to.be.false
41
42 await getInstanceHomepage(server.url, HttpStatusCode.NOT_FOUND_404)
43 })
44
45 it('Should set a homepage', async function () {
46 await updateInstanceHomepage(server.url, server.accessToken, '<picsou-magazine></picsou-magazine>')
47
48 const res = await getInstanceHomepage(server.url)
49 const page: CustomPage = res.body
50 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
51
52 const state = await getHomepageState(server)
53 expect(state).to.be.true
54 })
55
56 it('Should have the same homepage after a restart', async function () {
57 this.timeout(30000)
58
59 killallServers([ server ])
60
61 await reRunServer(server)
62
63 const res = await getInstanceHomepage(server.url)
64 const page: CustomPage = res.body
65 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
66
67 const state = await getHomepageState(server)
68 expect(state).to.be.true
69 })
70
71 it('Should empty the homepage', async function () {
72 await updateInstanceHomepage(server.url, server.accessToken, '')
73
74 const res = await getInstanceHomepage(server.url)
75 const page: CustomPage = res.body
76 expect(page.content).to.be.empty
77
78 const state = await getHomepageState(server)
79 expect(state).to.be.false
80 })
81
82 after(async function () {
83 await cleanupTests([ server ])
84 })
85})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index be743973a..56e6eb5da 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -5,6 +5,7 @@ import './email'
5import './follow-constraints' 5import './follow-constraints'
6import './follows' 6import './follows'
7import './follows-moderation' 7import './follows-moderation'
8import './homepage'
8import './handle-down' 9import './handle-down'
9import './jobs' 10import './jobs'
10import './logs' 11import './logs'
diff --git a/server/types/models/account/actor-custom-page.ts b/server/types/models/account/actor-custom-page.ts
new file mode 100644
index 000000000..2cb8aa7e4
--- /dev/null
+++ b/server/types/models/account/actor-custom-page.ts
@@ -0,0 +1,4 @@
1
2import { ActorCustomPageModel } from '../../../models/account/actor-custom-page'
3
4export type MActorCustomPage = Omit<ActorCustomPageModel, 'Actor'>
diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts
index dab2eea7e..9679c01e4 100644
--- a/server/types/models/account/index.ts
+++ b/server/types/models/account/index.ts
@@ -1,2 +1,3 @@
1export * from './account' 1export * from './account'
2export * from './actor-custom-page'
2export * from './account-blocklist' 3export * from './account-blocklist'