<div class="row">
<div class="col-md-12 col-xl-6">
- <div i18n class="about-instance-title">
- About {{ instanceName }} instance
+ <div class="about-instance-title">
+ <div i18n>About {{ instanceName }} instance</div>
+
+ <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
</div>
<div class="short-description">
<my-instance-features-table></my-instance-features-table>
</div>
</div>
+
+<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>
@import '_mixins';
.about-instance-title {
- font-size: 20px;
- font-weight: bold;
- margin-bottom: 15px;
+ display: flex;
+ justify-content: space-between;
+
+ & > div {
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ }
+
+ & > .contact-admin {
+ @include peertube-button;
+ @include orange-button;
+ }
}
.section-title {
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { MarkdownService } from '@app/videos/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
+import { InstanceService } from '@app/shared/instance/instance.service'
@Component({
selector: 'my-about-instance',
styleUrls: [ './about-instance.component.scss' ]
})
export class AboutInstanceComponent implements OnInit {
+ @ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
+
shortDescription = ''
descriptionHTML = ''
termsHTML = ''
constructor (
private notifier: Notifier,
private serverService: ServerService,
+ private instanceService: InstanceService,
private markdownService: MarkdownService,
private i18n: I18n
) {}
return this.serverService.getConfig().signup.allowed
}
+ get isContactFormEnabled () {
+ return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
+ }
+
ngOnInit () {
- this.serverService.getAbout()
+ this.instanceService.getAbout()
.subscribe(
res => {
this.shortDescription = res.instance.shortDescription
)
}
+ openContactModal () {
+ return this.contactAdminModal.show()
+ }
+
}
--- /dev/null
+<ng-template #modal>
+ <div class="modal-header">
+ <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
+ <span class="close" aria-label="Close" role="button" (click)="hide()"></span>
+ </div>
+
+ <div class="modal-body">
+
+ <form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
+ <div class="form-group">
+ <label i18n for="fromName">Your name</label>
+ <input
+ type="text" id="fromName"
+ formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
+ >
+ <div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="fromEmail">Your email</label>
+ <input
+ type="text" id="fromEmail"
+ formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
+ >
+ <div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
+ </div>
+
+ <div class="form-group">
+ <label i18n for="body">Your message</label>
+ <textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
+ </textarea>
+ <div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
+ </div>
+
+ <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+ <div class="form-group inputs">
+ <span i18n class="action-button action-button-cancel" (click)="hide()">
+ Cancel
+ </span>
+
+ <input
+ type="submit" i18n-value value="Submit" class="action-button-submit"
+ [disabled]="!form.valid"
+ >
+ </div>
+ </form>
+
+ </div>
+</ng-template>
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+input[type=text] {
+ @include peertube-input-text(340px);
+ display: block;
+}
+
+textarea {
+ @include peertube-textarea(100%, 200px);
+}
--- /dev/null
+import { Component, OnInit, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { FormReactive, InstanceValidatorsService } from '@app/shared'
+import { InstanceService } from '@app/shared/instance/instance.service'
+
+@Component({
+ selector: 'my-contact-admin-modal',
+ templateUrl: './contact-admin-modal.component.html',
+ styleUrls: [ './contact-admin-modal.component.scss' ]
+})
+export class ContactAdminModalComponent extends FormReactive implements OnInit {
+ @ViewChild('modal') modal: NgbModal
+
+ error: string
+
+ private openedModal: NgbModalRef
+
+ constructor (
+ protected formValidatorService: FormValidatorService,
+ private modalService: NgbModal,
+ private instanceValidatorsService: InstanceValidatorsService,
+ private instanceService: InstanceService,
+ private notifier: Notifier,
+ private i18n: I18n
+ ) {
+ super()
+ }
+
+ ngOnInit () {
+ this.buildForm({
+ fromName: this.instanceValidatorsService.FROM_NAME,
+ fromEmail: this.instanceValidatorsService.FROM_EMAIL,
+ body: this.instanceValidatorsService.BODY
+ })
+ }
+
+ show () {
+ this.openedModal = this.modalService.open(this.modal, { keyboard: false })
+ }
+
+ hide () {
+ this.form.reset()
+ this.error = undefined
+
+ this.openedModal.close()
+ this.openedModal = null
+ }
+
+ sendForm () {
+ const fromName = this.form.value['fromName']
+ const fromEmail = this.form.value[ 'fromEmail' ]
+ const body = this.form.value[ 'body' ]
+
+ this.instanceService.contactAdministrator(fromEmail, fromName, body)
+ .subscribe(
+ () => {
+ this.notifier.success(this.i18n('Your message has been sent.'))
+ this.hide()
+ },
+
+ err => {
+ this.error = err.status === 403
+ ? this.i18n('You already sent this form recently')
+ : err.message
+ }
+ )
+ }
+}
import { SharedModule } from '../shared'
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
+import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
@NgModule({
imports: [
declarations: [
AboutComponent,
AboutInstanceComponent,
- AboutPeertubeComponent
+ AboutPeertubeComponent,
+ ContactAdminModalComponent
],
exports: [
@Injectable()
export class ServerService {
+ private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
return this.videoPrivacies
}
- getAbout () {
- return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
- }
-
private loadVideoAttributeEnum (
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: VideoConstant<string | number>[],
export * from './custom-config-validators.service'
export * from './form-validator.service'
export * from './host'
+export * from './instance-validators.service'
export * from './login-validators.service'
export * from './reset-password-validators.service'
export * from './user-validators.service'
--- /dev/null
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Validators } from '@angular/forms'
+import { BuildFormValidator } from '@app/shared'
+import { Injectable } from '@angular/core'
+
+@Injectable()
+export class InstanceValidatorsService {
+ readonly FROM_EMAIL: BuildFormValidator
+ readonly FROM_NAME: BuildFormValidator
+ readonly BODY: BuildFormValidator
+
+ constructor (private i18n: I18n) {
+
+ this.FROM_EMAIL = {
+ VALIDATORS: [ Validators.required, Validators.email ],
+ MESSAGES: {
+ 'required': this.i18n('Email is required.'),
+ 'email': this.i18n('Email must be valid.')
+ }
+ }
+
+ this.FROM_NAME = {
+ VALIDATORS: [
+ Validators.required,
+ Validators.minLength(1),
+ Validators.maxLength(120)
+ ],
+ MESSAGES: {
+ 'required': this.i18n('Your name is required.'),
+ 'minlength': this.i18n('Your name must be at least 1 character long.'),
+ 'maxlength': this.i18n('Your name cannot be more than 120 characters long.')
+ }
+ }
+
+ this.BODY = {
+ VALIDATORS: [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.maxLength(5000)
+ ],
+ MESSAGES: {
+ 'required': this.i18n('A message is required.'),
+ 'minlength': this.i18n('The message must be at least 3 characters long.'),
+ 'maxlength': this.i18n('The message cannot be more than 5000 characters long.')
+ }
+ }
+ }
+}
--- /dev/null
+import { catchError } from 'rxjs/operators'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { environment } from '../../../environments/environment'
+import { RestExtractor, RestService } from '../rest'
+import { About } from '../../../../../shared/models/server'
+
+@Injectable()
+export class InstanceService {
+ private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
+ private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
+
+ constructor (
+ private authHttp: HttpClient,
+ private restService: RestService,
+ private restExtractor: RestExtractor
+ ) {
+ }
+
+ getAbout () {
+ return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
+ .pipe(catchError(res => this.restExtractor.handleError(res)))
+ }
+
+ contactAdministrator (fromEmail: string, fromName: string, message: string) {
+ const body = {
+ fromEmail,
+ fromName,
+ body: message
+ }
+
+ return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body)
+ .pipe(catchError(res => this.restExtractor.handleError(res)))
+
+ }
+}
LoginValidatorsService,
ReactiveFileComponent,
ResetPasswordValidatorsService,
+ InstanceValidatorsService,
TextareaAutoResizeDirective,
UserValidatorsService,
VideoAbuseValidatorsService,
import { UserHistoryService } from '@app/shared/users/user-history.service'
import { UserNotificationService } from '@app/shared/users/user-notification.service'
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
+import { InstanceService } from '@app/shared/instance/instance.service'
@NgModule({
imports: [
OverviewService,
VideoChangeOwnershipValidatorsService,
VideoAcceptOwnershipValidatorsService,
+ InstanceValidatorsService,
BlocklistService,
UserHistoryService,
+ InstanceService,
I18nPrimengCalendarService,
ScreenService,
}
removeFiles () {
- rm -rf "./test$1" "./config/local-test-$1.json"
+ rm -rf "./test$1" "./config/local-test.json" "./config/local-test-$1.json"
}
dropRedis () {
}
},
email: {
- enabled: Emailer.Instance.isEnabled()
+ enabled: Emailer.isEnabled()
},
contactForm: {
enabled: CONFIG.CONTACT_FORM.ENABLED
import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
import { isArray } from '../helpers/custom-validators/misc'
import { uniq } from 'lodash'
+import { Emailer } from '../lib/emailer'
async function checkActivityPubUrls () {
const actor = await getServerActor()
// Some checks on configuration files
// Return an error message, or null if everything is okay
function checkConfig () {
- const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
+
+ if (!Emailer.isEnabled()) {
+ if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
+ return 'Emailer is disabled but you require signup email verification.'
+ }
+
+ if (CONFIG.CONTACT_FORM.ENABLED) {
+ logger.warn('Emailer is disabled so the contact form will not work.')
+ }
+ }
// NSFW policy
+ const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
{
const available = [ 'do_not_list', 'blur', 'display' ]
if (available.indexOf(defaultNSFWPolicy) === -1) {
}
}
+ // Check storage directory locations
if (isProdInstance()) {
const configStorage = config.get('storage')
for (const key of Object.keys(configStorage)) {
'storage.redundancy', 'storage.tmp',
'log.level',
'user.video_quota', 'user.video_quota_daily',
- 'cache.previews.size', 'admin.email',
+ 'cache.previews.size', 'admin.email', 'contact_form.enabled',
'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval',
private static instance: Emailer
private initialized = false
private transporter: Transporter
- private enabled = false
private constructor () {}
if (this.initialized === true) return
this.initialized = true
- if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) {
+ if (Emailer.isEnabled()) {
logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
let tls
tls,
auth
})
-
- this.enabled = true
} else {
if (!isTestInstance()) {
logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
}
}
- isEnabled () {
- return this.enabled
+ static isEnabled () {
+ return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
}
async checkConnectionOrDie () {
}
sendMail (to: string[], subject: string, text: string, from?: string) {
- if (!this.enabled) {
+ if (!Emailer.isEnabled()) {
throw new Error('Cannot send mail because SMTP is not configured.')
}
.end()
}
- if (Emailer.Instance.isEnabled() === false) {
+ if (Emailer.isEnabled() === false) {
return res
.status(409)
.send({ error: 'Emailer is not enabled on this instance.' })