aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html55
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts6
-rw-r--r--client/src/app/app.component.html10
-rw-r--r--client/src/app/app.component.scss38
-rw-r--r--client/src/app/app.component.ts71
-rw-r--r--client/src/app/core/server/server.service.ts17
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rw-r--r--server/controllers/api/config.ts13
-rw-r--r--server/initializers/checker-after-init.ts14
-rw-r--r--server/initializers/config.ts7
-rw-r--r--server/middlewares/validators/config.ts5
-rw-r--r--server/tests/api/check-params/config.ts6
-rw-r--r--server/tests/api/server/config.ts16
-rw-r--r--shared/extra-utils/server/config.ts6
-rw-r--r--shared/models/server/broadcast-message-level.type.ts1
-rw-r--r--shared/models/server/custom-config.model.ts8
-rw-r--r--shared/models/server/index.ts1
-rw-r--r--shared/models/server/server-config.model.ts10
20 files changed, 282 insertions, 20 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 5703d5a2e..4ee573696 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -276,6 +276,58 @@
276 </div> 276 </div>
277 </div> 277 </div>
278 278
279 <div class="form-row mt-4"> <!-- broadcast grid -->
280 <div class="form-group col-12 col-lg-4 col-xl-3">
281 <div i18n class="inner-form-title">BROADCAST MESSAGE</div>
282 <div i18n class="inner-for-description">
283 Display a message on your instance
284 </div>
285 </div>
286
287 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
288
289 <ng-container formGroupName="broadcastMessage">
290
291 <div class="form-group">
292 <my-peertube-checkbox
293 inputName="broadcastMessageEnabled" formControlName="enabled"
294 i18n-labelText labelText="Enable broadcast message"
295 ></my-peertube-checkbox>
296 </div>
297
298 <div class="form-group">
299 <my-peertube-checkbox
300 inputName="broadcastMessageDismissable" formControlName="dismissable"
301 i18n-labelText labelText="Allow users to dismiss the broadcast message "
302 ></my-peertube-checkbox>
303 </div>
304
305 <div class="form-group">
306 <label i18n for="broadcastMessageLevel">Broadcast message level</label>
307 <div class="peertube-select-container">
308 <select id="broadcastMessageLevel" formControlName="level" class="form-control">
309 <option value="info">info</option>
310 <option value="warning">warning</option>
311 <option value="error">error</option>
312 </select>
313 </div>
314 <div *ngIf="formErrors.broadcastMessage.level" class="form-error">{{ formErrors.broadcastMessage.level }}</div>
315 </div>
316
317 <div class="form-group">
318 <label i18n for="broadcastMessageMessage">Message</label><my-help helpType="markdownText"></my-help>
319 <my-markdown-textarea
320 name="broadcastMessageMessage" formControlName="message" textareaMaxWidth="500px"
321 [classes]="{ 'input-error': formErrors['broadcastMessage.message'] }"
322 ></my-markdown-textarea>
323 <div *ngIf="formErrors.broadcastMessage.message" class="form-error">{{ formErrors.broadcastMessage.message }}</div>
324 </div>
325
326 </ng-container>
327
328 </div>
329 </div>
330
279 <div class="form-row mt-4"> <!-- new users grid --> 331 <div class="form-row mt-4"> <!-- new users grid -->
280 <div class="form-group col-12 col-lg-4 col-xl-3"> 332 <div class="form-group col-12 col-lg-4 col-xl-3">
281 <div i18n class="inner-form-title">NEW USERS</div> 333 <div i18n class="inner-form-title">NEW USERS</div>
@@ -801,8 +853,9 @@
801 <div class="form-row mt-4"> <!-- submit placement block --> 853 <div class="form-row mt-4"> <!-- submit placement block -->
802 <div class="col-md-7 col-xl-5"></div> 854 <div class="col-md-7 col-xl-5"></div>
803 <div class="col-md-5 col-xl-5"> 855 <div class="col-md-5 col-xl-5">
856 <span class="form-error submit-error" i18n *ngIf="!form.valid">It seems like the configuration is invalid. Please search for potential errors in the different tabs.</span>
857
804 <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid"> 858 <input (click)="formValidated()" type="submit" i18n-value value="Update configuration" [disabled]="!form.valid">
805 <span class="form-error" i18n *ngIf="!form.valid">It seems like the configuration is invalid. Please search for potential errors in the different tabs.</span>
806 </div> 859 </div>
807 </div> 860 </div>
808</form> 861</form>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 9ee960ad6..2bfa92da4 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -76,4 +76,8 @@ ngb-tabset:not(.previews) ::ng-deep {
76 .nav-link { 76 .nav-link {
77 font-size: 105%; 77 font-size: 105%;
78 } 78 }
79} \ No newline at end of file 79}
80
81.submit-error {
82 margin-bottom: 20px;
83}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index cea314cea..6d59494c8 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -215,6 +215,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
215 indexUrl: this.customConfigValidatorsService.INDEX_URL 215 indexUrl: this.customConfigValidatorsService.INDEX_URL
216 } 216 }
217 } 217 }
218 },
219 broadcastMessage: {
220 enabled: null,
221 level: null,
222 dismissable: null,
223 message: null
218 } 224 }
219 } 225 }
220 226
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index b0d2e5050..b243c129b 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -25,6 +25,16 @@
25 <div id="content" tabindex="-1" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }"> 25 <div id="content" tabindex="-1" class="main-col" [ngClass]="{ expanded: menu.isMenuDisplayed === false }">
26 26
27 <div class="main-row"> 27 <div class="main-row">
28
29 <div *ngIf="broadcastMessage" class="broadcast-message alert" [ngClass]="broadcastMessage.class">
30 <div [innerHTML]="broadcastMessage.message"></div>
31
32 <my-global-icon
33 *ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()"
34 iconName="cross" role="button" title="Close this message" i18n-title
35 ></my-global-icon>
36 </div>
37
28 <router-outlet></router-outlet> 38 <router-outlet></router-outlet>
29 </div> 39 </div>
30 </div> 40 </div>
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index 0c33dc4a1..27fd69c8d 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -1,5 +1,7 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import '~bootstrap/scss/functions';
4@import '~bootstrap/scss/variables';
3 5
4.peertube-container { 6.peertube-container {
5 padding-bottom: 20px; 7 padding-bottom: 20px;
@@ -88,3 +90,39 @@
88 flex: 1; 90 flex: 1;
89 } 91 }
90} 92}
93
94.broadcast-message {
95 min-height: 50px;
96 text-align: center;
97 margin-bottom: 0;
98 border-radius: 0;
99 display: grid;
100 grid-template-columns: 1fr 30px;
101 column-gap: 10px;
102
103 my-global-icon {
104 justify-self: center;
105 align-self: center;
106 cursor: pointer;
107
108 width: 20px;
109 }
110
111 @each $color, $value in $theme-colors {
112 &.alert-#{$color} {
113 my-global-icon {
114 @include apply-svg-color(theme-color-level($color, $alert-color-level));
115 }
116 }
117 }
118
119 ::ng-deep {
120 p {
121 font-size: 16px;
122 }
123
124 p:last-child {
125 margin-bottom: 0;
126 }
127 }
128}
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 12c0efd8a..a464e90fa 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -4,7 +4,7 @@ import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { filter, map, pairwise } from 'rxjs/operators' 7import { filter, map, pairwise, first } from 'rxjs/operators'
8import { Hotkey, HotkeysService } from 'angular2-hotkeys' 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { PlatformLocation, ViewportScroller } from '@angular/common' 10import { PlatformLocation, ViewportScroller } from '@angular/common'
@@ -19,6 +19,10 @@ import { ServerConfig, UserRole } from '@shared/models'
19import { User } from '@app/shared' 19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service' 20import { InstanceService } from '@app/shared/instance/instance.service'
21import { MenuService } from './core/menu/menu.service' 21import { MenuService } from './core/menu/menu.service'
22import { BroadcastMessageLevel } from '@shared/models/server'
23import { MarkdownService } from './shared/renderer'
24import { concat } from 'rxjs'
25import { peertubeLocalStorage } from './shared/misc/peertube-web-storage'
22 26
23@Component({ 27@Component({
24 selector: 'my-app', 28 selector: 'my-app',
@@ -26,11 +30,14 @@ import { MenuService } from './core/menu/menu.service'
26 styleUrls: [ './app.component.scss' ] 30 styleUrls: [ './app.component.scss' ]
27}) 31})
28export class AppComponent implements OnInit, AfterViewInit { 32export class AppComponent implements OnInit, AfterViewInit {
33 private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
34
29 @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent 35 @ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent
30 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent 36 @ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
31 @ViewChild('customModal') customModal: CustomModalComponent 37 @ViewChild('customModal') customModal: CustomModalComponent
32 38
33 customCSS: SafeHtml 39 customCSS: SafeHtml
40 broadcastMessage: { message: string, dismissable: boolean, class: string } | null = null
34 41
35 private serverConfig: ServerConfig 42 private serverConfig: ServerConfig
36 43
@@ -50,6 +57,7 @@ export class AppComponent implements OnInit, AfterViewInit {
50 private hooks: HooksService, 57 private hooks: HooksService,
51 private location: PlatformLocation, 58 private location: PlatformLocation,
52 private modalService: NgbModal, 59 private modalService: NgbModal,
60 private markdownService: MarkdownService,
53 public menu: MenuService 61 public menu: MenuService
54 ) { } 62 ) { }
55 63
@@ -81,6 +89,7 @@ export class AppComponent implements OnInit, AfterViewInit {
81 this.initRouteEvents() 89 this.initRouteEvents()
82 this.injectJS() 90 this.injectJS()
83 this.injectCSS() 91 this.injectCSS()
92 this.injectBroadcastMessage()
84 93
85 this.initHotkeys() 94 this.initHotkeys()
86 95
@@ -97,6 +106,12 @@ export class AppComponent implements OnInit, AfterViewInit {
97 return this.authService.isLoggedIn() 106 return this.authService.isLoggedIn()
98 } 107 }
99 108
109 hideBroadcastMessage () {
110 peertubeLocalStorage.setItem(AppComponent.BROADCAST_MESSAGE_KEY, this.serverConfig.broadcastMessage.message)
111
112 this.broadcastMessage = null
113 }
114
100 private initRouteEvents () { 115 private initRouteEvents () {
101 let resetScroll = true 116 let resetScroll = true
102 const eventsObs = this.router.events 117 const eventsObs = this.router.events
@@ -165,6 +180,36 @@ export class AppComponent implements OnInit, AfterViewInit {
165 ).subscribe(() => this.menu.isMenuDisplayed = false) // User clicked on a link in the menu, change the page 180 ).subscribe(() => this.menu.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
166 } 181 }
167 182
183 private injectBroadcastMessage () {
184 concat(
185 this.serverService.getConfig().pipe(first()),
186 this.serverService.configReloaded
187 ).subscribe(async config => {
188 this.broadcastMessage = null
189
190 const messageConfig = config.broadcastMessage
191
192 if (messageConfig.enabled) {
193 // Already dismissed this message?
194 if (messageConfig.dismissable && localStorage.getItem(AppComponent.BROADCAST_MESSAGE_KEY) === messageConfig.message) {
195 return
196 }
197
198 const classes: { [id in BroadcastMessageLevel]: string } = {
199 info: 'alert-info',
200 warning: 'alert-warning',
201 error: 'alert-danger'
202 }
203
204 this.broadcastMessage = {
205 message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
206 dismissable: messageConfig.dismissable,
207 class: classes[messageConfig.level]
208 }
209 }
210 })
211 }
212
168 private injectJS () { 213 private injectJS () {
169 // Inject JS 214 // Inject JS
170 this.serverService.getConfig() 215 this.serverService.getConfig()
@@ -182,17 +227,19 @@ export class AppComponent implements OnInit, AfterViewInit {
182 227
183 private injectCSS () { 228 private injectCSS () {
184 // Inject CSS if modified (admin config settings) 229 // Inject CSS if modified (admin config settings)
185 this.serverService.configReloaded 230 concat(
186 .subscribe(() => { 231 this.serverService.getConfig().pipe(first()),
187 const headStyle = document.querySelector('style.custom-css-style') 232 this.serverService.configReloaded
188 if (headStyle) headStyle.parentNode.removeChild(headStyle) 233 ).subscribe(config => {
189 234 const headStyle = document.querySelector('style.custom-css-style')
190 // We test customCSS if the admin removed the css 235 if (headStyle) headStyle.parentNode.removeChild(headStyle)
191 if (this.customCSS || this.serverConfig.instance.customizations.css) { 236
192 const styleTag = '<style>' + this.serverConfig.instance.customizations.css + '</style>' 237 // We test customCSS if the admin removed the css
193 this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag) 238 if (this.customCSS || config.instance.customizations.css) {
194 } 239 const styleTag = '<style>' + config.instance.customizations.css + '</style>'
195 }) 240 this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
241 }
242 })
196 } 243 }
197 244
198 private async loadPlugins () { 245 private async loadPlugins () {
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index eac8f85e4..fdfbe4c02 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -21,7 +21,7 @@ export class ServerService {
21 21
22 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' 22 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
23 23
24 configReloaded = new Subject<void>() 24 configReloaded = new Subject<ServerConfig>()
25 25
26 private localeObservable: Observable<any> 26 private localeObservable: Observable<any>
27 private videoLicensesObservable: Observable<VideoConstant<number>[]> 27 private videoLicensesObservable: Observable<VideoConstant<number>[]>
@@ -139,6 +139,12 @@ export class ServerService {
139 indexUrl: 'https://instances.joinpeertube.org' 139 indexUrl: 'https://instances.joinpeertube.org'
140 } 140 }
141 } 141 }
142 },
143 broadcastMessage: {
144 enabled: false,
145 message: '',
146 level: 'info',
147 dismissable: false
142 } 148 }
143 } 149 }
144 150
@@ -162,6 +168,11 @@ export class ServerService {
162 resetConfig () { 168 resetConfig () {
163 this.configLoaded = false 169 this.configLoaded = false
164 this.configReset = true 170 this.configReset = true
171
172 // Notify config update
173 this.getConfig().subscribe(() => {
174 // empty, to fire a reset config event
175 })
165 } 176 }
166 177
167 getConfig () { 178 getConfig () {
@@ -175,9 +186,9 @@ export class ServerService {
175 this.config = config 186 this.config = config
176 this.configLoaded = true 187 this.configLoaded = true
177 }), 188 }),
178 tap(() => { 189 tap(config => {
179 if (this.configReset) { 190 if (this.configReset) {
180 this.configReloaded.next() 191 this.configReloaded.next(config)
181 this.configReset = false 192 this.configReset = false
182 } 193 }
183 }), 194 }),
diff --git a/config/default.yaml b/config/default.yaml
index a0f2eb3a1..34a0a146f 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -372,3 +372,9 @@ followings:
372 372
373theme: 373theme:
374 default: 'default' 374 default: 'default'
375
376broadcast_message:
377 enabled: false
378 message: '' # Support markdown
379 level: 'info' # 'info' | 'warning' | 'error'
380 dismissable: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 8b8c98f8c..0ac05c515 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -386,3 +386,9 @@ followings:
386 386
387theme: 387theme:
388 default: 'default' 388 default: 'default'
389
390broadcast_message:
391 enabled: false
392 message: '' # Support markdown
393 level: 'info' # 'info' | 'warning' | 'error'
394 dismissable: false
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index edcb0b99e..41e5027b9 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -172,6 +172,13 @@ async function getConfig (req: express.Request, res: express.Response) {
172 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL 172 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
173 } 173 }
174 } 174 }
175 },
176
177 broadcastMessage: {
178 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
179 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
180 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
181 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
175 } 182 }
176 } 183 }
177 184
@@ -432,6 +439,12 @@ function customConfig (): CustomConfig {
432 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL 439 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
433 } 440 }
434 } 441 }
442 },
443 broadcastMessage: {
444 enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
445 message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
446 level: CONFIG.BROADCAST_MESSAGE.LEVEL,
447 dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
435 } 448 }
436 } 449 }
437} 450}
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index f111be2ae..b5b854137 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -107,6 +107,10 @@ function checkConfig () {
107 } 107 }
108 } 108 }
109 109
110 if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) {
111 logger.warn('Redundancy directory should be different than the videos folder.')
112 }
113
110 // Transcoding 114 // Transcoding
111 if (CONFIG.TRANSCODING.ENABLED) { 115 if (CONFIG.TRANSCODING.ENABLED) {
112 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) { 116 if (CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false && CONFIG.TRANSCODING.HLS.ENABLED === false) {
@@ -114,8 +118,14 @@ function checkConfig () {
114 } 118 }
115 } 119 }
116 120
117 if (CONFIG.STORAGE.VIDEOS_DIR === CONFIG.STORAGE.REDUNDANCY_DIR) { 121 // Broadcast message
118 logger.warn('Redundancy directory should be different than the videos folder.') 122 if (CONFIG.BROADCAST_MESSAGE.ENABLED) {
123 const currentLevel = CONFIG.BROADCAST_MESSAGE.LEVEL
124 const available = [ 'info', 'warning', 'error' ]
125
126 if (available.includes(currentLevel) === false) {
127 return 'Broadcast message level should be ' + available.join(' or ') + ' instead of ' + currentLevel
128 }
119 } 129 }
120 130
121 return null 131 return null
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 6932b41e1..e2920ce9e 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -6,6 +6,7 @@ import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-
6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
7import * as bytes from 'bytes' 7import * as bytes from 'bytes'
8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { BroadcastMessageLevel } from '@shared/models/server'
9 10
10// Use a variable to reload the configuration if we need 11// Use a variable to reload the configuration if we need
11let config: IConfig = require('config') 12let config: IConfig = require('config')
@@ -285,6 +286,12 @@ const CONFIG = {
285 }, 286 },
286 THEME: { 287 THEME: {
287 get DEFAULT () { return config.get<string>('theme.default') } 288 get DEFAULT () { return config.get<string>('theme.default') }
289 },
290 BROADCAST_MESSAGE: {
291 get ENABLED () { return config.get<boolean>('broadcast_message.enabled') },
292 get MESSAGE () { return config.get<string>('broadcast_message.message') },
293 get LEVEL () { return config.get<BroadcastMessageLevel>('broadcast_message.level') },
294 get DISMISSABLE () { return config.get<boolean>('broadcast_message.dismissable') }
288 } 295 }
289} 296}
290 297
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index dfa549e76..6905ac762 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -55,6 +55,11 @@ const customConfigUpdateValidator = [
55 55
56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'), 56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
57 57
58 body('broadcastMessage.enabled').isBoolean().withMessage('Should have a valid broadcast message enabled boolean'),
59 body('broadcastMessage.message').exists().withMessage('Should have a valid broadcast message'),
60 body('broadcastMessage.level').exists().withMessage('Should have a valid broadcast level'),
61 body('broadcastMessage.dismissable').exists().withMessage('Should have a valid broadcast dismissable boolean'),
62
58 (req: express.Request, res: express.Response, next: express.NextFunction) => { 63 (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) 64 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
60 65
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index f1a79806b..7c96fa762 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -133,6 +133,12 @@ describe('Test config API validators', function () {
133 indexUrl: 'https://index.example.com' 133 indexUrl: 'https://index.example.com'
134 } 134 }
135 } 135 }
136 },
137 broadcastMessage: {
138 enabled: true,
139 dismissable: true,
140 message: 'super message',
141 level: 'warning'
136 } 142 }
137 } 143 }
138 144
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 8580835d6..d18a93082 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -87,6 +87,11 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
87 expect(data.followings.instance.autoFollowBack.enabled).to.be.false 87 expect(data.followings.instance.autoFollowBack.enabled).to.be.false
88 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false 88 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
89 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('') 89 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('')
90
91 expect(data.broadcastMessage.enabled).to.be.false
92 expect(data.broadcastMessage.level).to.equal('info')
93 expect(data.broadcastMessage.message).to.equal('')
94 expect(data.broadcastMessage.dismissable).to.be.false
90} 95}
91 96
92function checkUpdatedConfig (data: CustomConfig) { 97function checkUpdatedConfig (data: CustomConfig) {
@@ -155,6 +160,11 @@ function checkUpdatedConfig (data: CustomConfig) {
155 expect(data.followings.instance.autoFollowBack.enabled).to.be.true 160 expect(data.followings.instance.autoFollowBack.enabled).to.be.true
156 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true 161 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
157 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com') 162 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
163
164 expect(data.broadcastMessage.enabled).to.be.true
165 expect(data.broadcastMessage.level).to.equal('error')
166 expect(data.broadcastMessage.message).to.equal('super bad message')
167 expect(data.broadcastMessage.dismissable).to.be.true
158} 168}
159 169
160describe('Test config', function () { 170describe('Test config', function () {
@@ -324,6 +334,12 @@ describe('Test config', function () {
324 indexUrl: 'https://updated.example.com' 334 indexUrl: 'https://updated.example.com'
325 } 335 }
326 } 336 }
337 },
338 broadcastMessage: {
339 enabled: true,
340 level: 'error',
341 message: 'super bad message',
342 dismissable: true
327 } 343 }
328 } 344 }
329 await updateCustomConfig(server.url, server.accessToken, newCustomConfig) 345 await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index 743d10316..98cd435f6 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -159,6 +159,12 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
159 enabled: false 159 enabled: false
160 } 160 }
161 } 161 }
162 },
163 broadcastMessage: {
164 enabled: true,
165 level: 'warning',
166 message: 'hello',
167 dismissable: true
162 } 168 }
163 } 169 }
164 170
diff --git a/shared/models/server/broadcast-message-level.type.ts b/shared/models/server/broadcast-message-level.type.ts
new file mode 100644
index 000000000..bf43e18b5
--- /dev/null
+++ b/shared/models/server/broadcast-message-level.type.ts
@@ -0,0 +1 @@
export type BroadcastMessageLevel = 'info' | 'warning' | 'error'
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 07e17bda2..851bf1854 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -1,4 +1,5 @@
1import { NSFWPolicyType } from '../videos/nsfw-policy.type' 1import { NSFWPolicyType } from '../videos/nsfw-policy.type'
2import { BroadcastMessageLevel } from './broadcast-message-level.type'
2 3
3export interface CustomConfig { 4export interface CustomConfig {
4 instance: { 5 instance: {
@@ -131,4 +132,11 @@ export interface CustomConfig {
131 } 132 }
132 } 133 }
133 } 134 }
135
136 broadcastMessage: {
137 enabled: boolean
138 message: string
139 level: BroadcastMessageLevel
140 dismissable: boolean
141 }
134} 142}
diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts
index b0afb2c66..2bb443d46 100644
--- a/shared/models/server/index.ts
+++ b/shared/models/server/index.ts
@@ -1,4 +1,5 @@
1export * from './about.model' 1export * from './about.model'
2export * from './broadcast-message-level.type'
2export * from './contact-form.model' 3export * from './contact-form.model'
3export * from './custom-config.model' 4export * from './custom-config.model'
4export * from './debug.model' 5export * from './debug.model'
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index a1f9b3b5d..9c903b7ee 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -1,5 +1,6 @@
1import { NSFWPolicyType } from '../videos/nsfw-policy.type'
2import { ClientScript } from '../plugins/plugin-package-json.model' 1import { ClientScript } from '../plugins/plugin-package-json.model'
2import { NSFWPolicyType } from '../videos/nsfw-policy.type'
3import { BroadcastMessageLevel } from './broadcast-message-level.type'
3 4
4export interface ServerConfigPlugin { 5export interface ServerConfigPlugin {
5 name: string 6 name: string
@@ -161,4 +162,11 @@ export interface ServerConfig {
161 } 162 }
162 } 163 }
163 } 164 }
165
166 broadcastMessage: {
167 enabled: boolean
168 message: string
169 level: BroadcastMessageLevel
170 dismissable: boolean
171 }
164} 172}