size: 500 # Max number of video captions/subtitles you want to cache
admin:
- email: 'admin@example.com' # Your personal email as administrator
+ # Used to generate the root user at first startup
+ # And to receive emails from the contact form
+ email: 'admin@example.com'
+
+contact_form:
+ enabled: true
signup:
enabled: false
size: 500 # Max number of video captions/subtitles you want to cache
admin:
+ # Used to generate the root user at first startup
+ # And to receive emails from the contact form
email: 'admin@example.com'
+contact_form:
+ enabled: true
+
signup:
enabled: false
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
log:
level: 'debug'
+contact_form:
+ enabled: true
+
redundancy:
videos:
check_interval: '10 minutes'
import * as express from 'express'
-import { omit } from 'lodash'
+import { omit, snakeCase } from 'lodash'
import { ServerConfig, UserRight } from '../../../shared'
import { About } from '../../../shared/models/server/about.model'
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { remove, writeJSON } from 'fs-extra'
import { getServerCommit } from '../../helpers/utils'
import { Emailer } from '../../lib/emailer'
+import { isNumeric } from 'validator'
+import { objectConverter } from '../../helpers/core-utils'
const packageJSON = require('../../../../package.json')
const configRouter = express.Router()
email: {
enabled: Emailer.Instance.isEnabled()
},
+ contactForm: {
+ enabled: CONFIG.CONTACT_FORM.ENABLED
+ },
serverVersion: packageJSON.version,
serverCommit,
signup: {
}
async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
- const toUpdate: CustomConfig = req.body
const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
- // Force number conversion
- toUpdate.cache.previews.size = parseInt('' + toUpdate.cache.previews.size, 10)
- toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10)
- toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10)
- toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10)
- toUpdate.user.videoQuotaDaily = parseInt('' + toUpdate.user.videoQuotaDaily, 10)
- toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
-
- // camelCase to snake_case key
- const toUpdateJSON = omit(
- toUpdate,
- 'user.videoQuota',
- 'instance.defaultClientRoute',
- 'instance.shortDescription',
- 'cache.videoCaptions',
- 'signup.requiresEmailVerification',
- 'transcoding.allowAdditionalExtensions'
- )
- toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
- toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
- toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
- toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
- toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
- toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
- toUpdateJSON.transcoding['allow_additional_extensions'] = toUpdate.transcoding.allowAdditionalExtensions
+ // camelCase to snake_case key + Force number conversion
+ const toUpdateJSON = convertCustomConfigBody(req.body)
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
admin: {
email: CONFIG.ADMIN.EMAIL
},
+ contactForm: {
+ enabled: CONFIG.CONTACT_FORM.ENABLED
+ },
user: {
videoQuota: CONFIG.USER.VIDEO_QUOTA,
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
}
}
}
+
+function convertCustomConfigBody (body: CustomConfig) {
+ function keyConverter (k: string) {
+ // Transcoding resolutions exception
+ if (/^\d{3,4}p$/.exec(k)) return k
+
+ return snakeCase(k)
+ }
+
+ function valueConverter (v: any) {
+ if (isNumeric(v + '')) return parseInt('' + v, 10)
+
+ return v
+ }
+
+ return objectConverter(body, keyConverter, valueConverter)
+}
--- /dev/null
+import * as express from 'express'
+import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares'
+import { Redis } from '../../../lib/redis'
+import { Emailer } from '../../../lib/emailer'
+import { ContactForm } from '../../../../shared/models/server'
+
+const contactRouter = express.Router()
+
+contactRouter.post('/contact',
+ asyncMiddleware(contactAdministratorValidator),
+ asyncMiddleware(contactAdministrator)
+)
+
+async function contactAdministrator (req: express.Request, res: express.Response) {
+ const data = req.body as ContactForm
+
+ await Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.body)
+
+ await Redis.Instance.setContactFormIp(req.ip)
+
+ return res.status(204).end()
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ contactRouter
+}
import { statsRouter } from './stats'
import { serverRedundancyRouter } from './redundancy'
import { serverBlocklistRouter } from './server-blocklist'
+import { contactRouter } from './contact'
const serverRouter = express.Router()
serverRouter.use('/', serverRedundancyRouter)
serverRouter.use('/', statsRouter)
serverRouter.use('/', serverBlocklistRouter)
+serverRouter.use('/', contactRouter)
// ---------------------------------------------------------------------------
import { URL } from 'url'
import { truncate } from 'lodash'
import { exec } from 'child_process'
+import { isArray } from './custom-validators/misc'
+
+const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
+ if (!oldObject || typeof oldObject !== 'object') {
+ return valueConverter(oldObject)
+ }
+
+ if (isArray(oldObject)) {
+ return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
+ }
+
+ const newObject = {}
+ Object.keys(oldObject).forEach(oldKey => {
+ const newKey = keyConverter(oldKey)
+ newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter)
+ })
+
+ return newObject
+}
const timeTable = {
ms: 1,
isTestInstance,
isProdInstance,
+ objectConverter,
root,
escapeHTML,
pageToStartAndCount,
import { isArray, exists } from './misc'
import { isTestInstance } from '../core-utils'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
function isHostValid (host: string) {
const isURLOptions = {
})
}
+function isValidContactBody (value: any) {
+ return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
+}
+
+function isValidContactFromName (value: any) {
+ return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
+}
+
// ---------------------------------------------------------------------------
export {
+ isValidContactBody,
+ isValidContactFromName,
isEachUniqueHostValid,
isHostValid
}
import { Instance as ParseTorrent } from 'parse-torrent'
import { remove } from 'fs-extra'
import * as memoizee from 'memoizee'
+import { isArray } from './custom-validators/misc'
function deleteFileAsync (path: string) {
remove(path)
return raw.toString('hex')
}
-interface FormattableToJSON {
- toFormattedJSON (args?: any)
-}
-
+interface FormattableToJSON { toFormattedJSON (args?: any) }
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
const formattedObjects: U[] = []
ADMIN: {
get EMAIL () { return config.get<string>('admin.email') }
},
+ CONTACT_FORM: {
+ get ENABLED () { return config.get<boolean>('contact_form.enabled') }
+ },
SIGNUP: {
get ENABLED () { return config.get<boolean>('signup.enabled') },
get LIMIT () { return config.get<number>('signup.limit') },
},
VIDEO_SHARE: {
URL: { min: 3, max: 2000 } // Length
+ },
+ CONTACT_FORM: {
+ FROM_NAME: { min: 1, max: 120 }, // Length
+ BODY: { min: 3, max: 5000 } // Length
}
}
}
let VIDEO_VIEW_LIFETIME = 60000 * 60 // 1 hour
+let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
+
const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
MIN: 10,
AVERAGE: 30,
REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
VIDEO_VIEW_LIFETIME = 1000 // 1 second
+ CONTACT_FORM_LIFETIME = 1000 // 1 second
JOB_ATTEMPTS['email'] = 1
HTTP_SIGNATURE,
VIDEO_IMPORT_STATES,
VIDEO_VIEW_LIFETIME,
+ CONTACT_FORM_LIFETIME,
buildLanguages
}
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
- sendMail (to: string[], subject: string, text: string) {
+ addContactFormJob (fromEmail: string, fromName: string, body: string) {
+ const text = 'Hello dear admin,\n\n' +
+ fromName + ' sent you a message' +
+ '\n\n---------------------------------------\n\n' +
+ body +
+ '\n\n---------------------------------------\n\n' +
+ 'Cheers,\n' +
+ 'PeerTube.'
+
+ const emailPayload: EmailPayload = {
+ from: fromEmail,
+ to: [ CONFIG.ADMIN.EMAIL ],
+ subject: '[PeerTube] Contact form submitted',
+ text
+ }
+
+ return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+ }
+
+ sendMail (to: string[], subject: string, text: string, from?: string) {
if (!this.enabled) {
throw new Error('Cannot send mail because SMTP is not configured.')
}
return this.transporter.sendMail({
- from: CONFIG.SMTP.FROM_ADDRESS,
+ from: from || CONFIG.SMTP.FROM_ADDRESS,
to: to.join(','),
subject,
text
to: string[]
subject: string
text: string
+ from?: string
}
async function processEmail (job: Bull.Job) {
const payload = job.data as EmailPayload
logger.info('Processing email in job %d.', job.id)
- return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text)
+ return Emailer.Instance.sendMail(payload.to, payload.subject, payload.text, payload.from)
}
// ---------------------------------------------------------------------------
import { createClient, RedisClient } from 'redis'
import { logger } from '../helpers/logger'
import { generateRandomString } from '../helpers/utils'
-import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
+import {
+ CONFIG,
+ CONTACT_FORM_LIFETIME,
+ USER_EMAIL_VERIFY_LIFETIME,
+ USER_PASSWORD_RESET_LIFETIME,
+ VIDEO_VIEW_LIFETIME
+} from '../initializers'
type CachedRoute = {
body: string,
return this.getValue(this.generateVerifyEmailKey(userId))
}
+ /************* Contact form per IP *************/
+
+ async setContactFormIp (ip: string) {
+ return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
+ }
+
+ async isContactFormIpExists (ip: string) {
+ return this.exists(this.generateContactFormKey(ip))
+ }
+
/************* Views per IP *************/
setIPVideoView (ip: string, videoUUID: string) {
}
private generateViewKey (ip: string, videoUUID: string) {
- return videoUUID + '-' + ip
+ return `views-${videoUUID}-${ip}`
+ }
+
+ private generateContactFormKey (ip: string) {
+ return 'contact-form-' + ip
}
/************* Redis helpers *************/
import * as express from 'express'
import { body } from 'express-validator/check'
-import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
+import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils'
const customConfigUpdateValidator = [
body('instance.name').exists().withMessage('Should have a valid instance name'),
+ body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
body('instance.description').exists().withMessage('Should have a valid instance description'),
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
- body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),
+
+ body('services.twitter.username').exists().withMessage('Should have a valid twitter username'),
+ body('services.twitter.whitelisted').isBoolean().withMessage('Should have a valid twitter whitelisted boolean'),
+
+ body('cache.previews.size').isInt().withMessage('Should have a valid previews cache size'),
+ body('cache.captions.size').isInt().withMessage('Should have a valid captions cache size'),
+
body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
+ body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'),
+
body('admin.email').isEmail().withMessage('Should have a valid administrator email'),
+ body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'),
+
body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'),
+ body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily video quota'),
+
body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'),
+ body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'),
body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'),
body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'),
body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'),
body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
+
body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
import * as express from 'express'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils'
-import { isHostValid } from '../../helpers/custom-validators/servers'
+import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
import { ServerModel } from '../../models/server/server'
import { body } from 'express-validator/check'
+import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
+import { Emailer } from '../../lib/emailer'
+import { Redis } from '../../lib/redis'
+import { CONFIG } from '../../initializers/constants'
const serverGetValidator = [
body('host').custom(isHostValid).withMessage('Should have a valid host'),
}
]
+const contactAdministratorValidator = [
+ body('fromName')
+ .custom(isUserDisplayNameValid).withMessage('Should have a valid name'),
+ body('fromEmail')
+ .isEmail().withMessage('Should have a valid email'),
+ body('body')
+ .custom(isValidContactBody).withMessage('Should have a valid body'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking contactAdministratorValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+
+ if (CONFIG.CONTACT_FORM.ENABLED === false) {
+ return res
+ .status(409)
+ .send({ error: 'Contact form is not enabled on this instance.' })
+ .end()
+ }
+
+ if (Emailer.Instance.isEnabled() === false) {
+ return res
+ .status(409)
+ .send({ error: 'Emailer is not enabled on this instance.' })
+ .end()
+ }
+
+ if (await Redis.Instance.isContactFormIpExists(req.ip)) {
+ logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
+
+ return res
+ .status(403)
+ .send({ error: 'You already sent a contact form recently.' })
+ .end()
+ }
+
+ return next()
+ }
+]
+
// ---------------------------------------------------------------------------
export {
- serverGetValidator
+ serverGetValidator,
+ contactAdministratorValidator
}
admin: {
email: 'superadmin1@example.com'
},
+ contactForm: {
+ enabled: false
+ },
user: {
videoQuota: 5242881,
videoQuotaDaily: 318742
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+
+import {
+ flushTests,
+ immutableAssign,
+ killallServers,
+ reRunServer,
+ runServer,
+ ServerInfo,
+ setAccessTokensToServers
+} from '../../../../shared/utils'
+import {
+ checkBadCountPagination,
+ checkBadSortPagination,
+ checkBadStartPagination
+} from '../../../../shared/utils/requests/check-api-params'
+import { getAccount } from '../../../../shared/utils/users/accounts'
+import { sendContactForm } from '../../../../shared/utils/server/contact-form'
+import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
+
+describe('Test contact form API validators', function () {
+ let server: ServerInfo
+ const emails: object[] = []
+ const defaultBody = {
+ fromName: 'super name',
+ fromEmail: 'toto@example.com',
+ body: 'Hello, how are you?'
+ }
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(60000)
+
+ await flushTests()
+ await MockSmtpServer.Instance.collectEmails(emails)
+
+ // Email is disabled
+ server = await runServer(1)
+ })
+
+ it('Should not accept a contact form if emails are disabled', async function () {
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 }))
+ })
+
+ it('Should not accept a contact form if it is disabled in the configuration', async function () {
+ killallServers([ server ])
+
+ // Contact form is disabled
+ await reRunServer(server, { smtp: { hostname: 'localhost' }, contact_form: { enabled: false } })
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 }))
+ })
+
+ it('Should not accept a contact form if from email is invalid', async function () {
+ killallServers([ server ])
+
+ // Email & contact form enabled
+ await reRunServer(server, { smtp: { hostname: 'localhost' } })
+
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail' }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail@' }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: undefined }))
+ })
+
+ it('Should not accept a contact form if from name is invalid', async function () {
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: 'name'.repeat(100) }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: '' }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromName: undefined }))
+ })
+
+ it('Should not accept a contact form if body is invalid', async function () {
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'body'.repeat(5000) }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: 'a' }))
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, body: undefined }))
+ })
+
+ it('Should accept a contact form with the correct parameters', async function () {
+ await sendContactForm(immutableAssign(defaultBody, { url: server.url }))
+ })
+
+ after(async function () {
+ MockSmtpServer.Instance.kill()
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
-// Order of the tests we want to execute
import './accounts'
import './blocklist'
import './config'
+import './contact-form'
import './follows'
import './jobs'
import './redundancy'
expect(data.instance.defaultNSFWPolicy).to.equal('display')
expect(data.instance.customizations.css).to.be.empty
expect(data.instance.customizations.javascript).to.be.empty
+
expect(data.services.twitter.username).to.equal('@Chocobozzz')
expect(data.services.twitter.whitelisted).to.be.false
+
expect(data.cache.previews.size).to.equal(1)
expect(data.cache.captions.size).to.equal(1)
+
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
expect(data.signup.requiresEmailVerification).to.be.false
+
expect(data.admin.email).to.equal('admin1@example.com')
+ expect(data.contactForm.enabled).to.be.true
+
expect(data.user.videoQuota).to.equal(5242880)
expect(data.user.videoQuotaDaily).to.equal(-1)
expect(data.transcoding.enabled).to.be.false
expect(data.instance.defaultNSFWPolicy).to.equal('blur')
expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
+
expect(data.services.twitter.username).to.equal('@Kuja')
expect(data.services.twitter.whitelisted).to.be.true
+
expect(data.cache.previews.size).to.equal(2)
expect(data.cache.captions.size).to.equal(3)
+
expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5)
expect(data.signup.requiresEmailVerification).to.be.true
+
expect(data.admin.email).to.equal('superadmin1@example.com')
+ expect(data.contactForm.enabled).to.be.false
+
expect(data.user.videoQuota).to.equal(5242881)
expect(data.user.videoQuotaDaily).to.equal(318742)
+
expect(data.transcoding.enabled).to.be.true
expect(data.transcoding.threads).to.equal(1)
expect(data.transcoding.allowAdditionalExtensions).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
expect(data.transcoding.resolutions['720p']).to.be.false
expect(data.transcoding.resolutions['1080p']).to.be.false
+
expect(data.import.videos.http.enabled).to.be.false
expect(data.import.videos.torrent.enabled).to.be.false
}
expect(data.video.file.extensions).to.contain('.mp4')
expect(data.video.file.extensions).to.contain('.webm')
expect(data.video.file.extensions).to.contain('.ogv')
+
+ expect(data.contactForm.enabled).to.be.true
})
it('Should get the customized configuration', async function () {
admin: {
email: 'superadmin1@example.com'
},
+ contactForm: {
+ enabled: false
+ },
user: {
videoQuota: 5242881,
videoQuotaDaily: 318742
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/utils'
+import { MockSmtpServer } from '../../../../shared/utils/miscs/email'
+import { waitJobs } from '../../../../shared/utils/server/jobs'
+import { sendContactForm } from '../../../../shared/utils/server/contact-form'
+
+const expect = chai.expect
+
+describe('Test contact form', function () {
+ let server: ServerInfo
+ const emails: object[] = []
+
+ before(async function () {
+ this.timeout(30000)
+
+ await MockSmtpServer.Instance.collectEmails(emails)
+
+ await flushTests()
+
+ const overrideConfig = {
+ smtp: {
+ hostname: 'localhost'
+ }
+ }
+ server = await runServer(1, overrideConfig)
+ await setAccessTokensToServers([ server ])
+ })
+
+ it('Should send a contact form', async function () {
+ await sendContactForm({
+ url: server.url,
+ fromEmail: 'toto@example.com',
+ body: 'my super message',
+ fromName: 'Super toto'
+ })
+
+ await waitJobs(server)
+
+ expect(emails).to.have.lengthOf(1)
+
+ const email = emails[0]
+
+ expect(email['from'][0]['address']).equal('toto@example.com')
+ expect(email['to'][0]['address']).equal('admin1@example.com')
+ expect(email['subject']).contains('Contact form')
+ expect(email['text']).contains('my super message')
+ })
+
+ it('Should not be able to send another contact form because of the anti spam checker', async function () {
+ await sendContactForm({
+ url: server.url,
+ fromEmail: 'toto@example.com',
+ body: 'my super message',
+ fromName: 'Super toto'
+ })
+
+ await sendContactForm({
+ url: server.url,
+ fromEmail: 'toto@example.com',
+ body: 'my super message',
+ fromName: 'Super toto',
+ expectedStatus: 403
+ })
+ })
+
+ it('Should be able to send another contact form after a while', async function () {
+ await wait(1000)
+
+ await sendContactForm({
+ url: server.url,
+ fromEmail: 'toto@example.com',
+ body: 'my super message',
+ fromName: 'Super toto'
+ })
+ })
+
+ after(async function () {
+ MockSmtpServer.Instance.kill()
+ killallServers([ server ])
+ })
+})
import {
completeVideoCheck,
- getVideo,
- immutableAssign,
- reRunServer,
- unfollow,
- viewVideo,
flushAndRunMultipleServers,
+ getVideo,
getVideosList,
+ immutableAssign,
killallServers,
+ reRunServer,
ServerInfo,
setAccessTokensToServers,
- uploadVideo,
+ unfollow,
updateVideo,
+ uploadVideo,
wait
} from '../../../../shared/utils'
import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows'
import './config'
+import './contact-form'
import './email'
import './follow-constraints'
import './follows'
import * as chai from 'chai'
import 'mocha'
+import { snakeCase, isNumber } from 'lodash'
import {
- parseBytes
+ parseBytes, objectConverter
} from '../../helpers/core-utils'
+import { isNumeric } from 'validator'
const expect = chai.expect
describe('Parse Bytes', function () {
+
it('Should pass when given valid value', async function () {
// just return it
expect(parseBytes(1024)).to.be.eq(1024)
it('Should be invalid when given invalid value', async function () {
expect(parseBytes('6GB 1GB')).to.be.eq(6)
})
+
+ it('Should convert an object', async function () {
+ function keyConverter (k: string) {
+ return snakeCase(k)
+ }
+
+ function valueConverter (v: any) {
+ if (isNumeric(v + '')) return parseInt('' + v, 10)
+
+ return v
+ }
+
+ const obj = {
+ mySuperKey: 'hello',
+ mySuper2Key: '45',
+ mySuper3Key: {
+ mySuperSubKey: '15',
+ mySuperSub2Key: 'hello',
+ mySuperSub3Key: [ '1', 'hello', 2 ],
+ mySuperSub4Key: 4
+ },
+ mySuper4Key: 45,
+ toto: {
+ super_key: '15',
+ superKey2: 'hello'
+ },
+ super_key: {
+ superKey4: 15
+ }
+ }
+
+ const res = objectConverter(obj, keyConverter, valueConverter)
+
+ expect(res.my_super_key).to.equal('hello')
+ expect(res.my_super_2_key).to.equal(45)
+ expect(res.my_super_3_key.my_super_sub_key).to.equal(15)
+ expect(res.my_super_3_key.my_super_sub_2_key).to.equal('hello')
+ expect(res.my_super_3_key.my_super_sub_3_key).to.deep.equal([ 1, 'hello', 2 ])
+ expect(res.my_super_3_key.my_super_sub_4_key).to.equal(4)
+ expect(res.toto.super_key).to.equal(15)
+ expect(res.toto.super_key_2).to.equal('hello')
+ expect(res.super_key.super_key_4).to.equal(15)
+
+ // Immutable
+ expect(res.mySuperKey).to.be.undefined
+ expect(obj['my_super_key']).to.be.undefined
+ })
})
--- /dev/null
+export interface ContactForm {
+ fromEmail: string
+ fromName: string
+ body: string
+}
email: string
}
+ contactForm: {
+ enabled: boolean
+ }
+
user: {
videoQuota: number
videoQuotaDaily: number
--- /dev/null
+export * from './about.model'
+export * from './contact-form.model'
+export * from './custom-config.model'
+export * from './job.model'
+export * from './server-config.model'
+export * from './server-stats.model'
enabled: boolean
}
+ contactForm: {
+ enabled: boolean
+ }
+
signup: {
allowed: boolean,
allowedForCurrentIP: boolean,
return this.emails.push(msg.email)
}
})
+
+ process.on('exit', () => this.kill())
}
collectEmails (emailsCollection: object[]) {
}
kill () {
+ if (!this.emailChildProcess) return
+
process.kill(this.emailChildProcess.pid)
this.emailChildProcess = null
admin: {
email: 'superadmin1@example.com'
},
+ contactForm: {
+ enabled: true
+ },
user: {
videoQuota: 5242881,
videoQuotaDaily: 318742
--- /dev/null
+import * as request from 'supertest'
+import { ContactForm } from '../../models/server'
+
+function sendContactForm (options: {
+ url: string,
+ fromEmail: string,
+ fromName: string,
+ body: string,
+ expectedStatus?: number
+}) {
+ const path = '/api/v1/server/contact'
+
+ const body: ContactForm = {
+ fromEmail: options.fromEmail,
+ fromName: options.fromName,
+ body: options.body
+ }
+ return request(options.url)
+ .post(path)
+ .send(body)
+ .expect(options.expectedStatus || 204)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ sendContactForm
+}
# /!\ Prefer to use the PeerTube admin interface to set the following configurations /!\
#PEERTUBE_SIGNUP_ENABLED=true
#PEERTUBE_TRANSCODING_ENABLED=true
+#PEERTUBE_CONTACT_FORM_ENABLED=true
admin:
email: "PEERTUBE_ADMIN_EMAIL"
+contact_form:
+ enabled:
+ __name: "PEERTUBE_CONTACT_FORM_ENABLED"
+ __format: "json"
+
signup:
enabled:
__name: "PEERTUBE_SIGNUP_ENABLED"