From 3a4992633ee62d5edfbb484d9c6bcb3cf158489d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 31 Jul 2023 14:34:36 +0200 Subject: Migrate server to ESM Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports) --- .../src/abuse/abuse-predefined-reasons.ts | 14 ++ packages/core-utils/src/abuse/index.ts | 1 + packages/core-utils/src/common/array.ts | 41 ++++++ packages/core-utils/src/common/date.ts | 114 ++++++++++++++++ packages/core-utils/src/common/index.ts | 10 ++ packages/core-utils/src/common/number.ts | 13 ++ packages/core-utils/src/common/object.ts | 86 ++++++++++++ packages/core-utils/src/common/promises.ts | 58 ++++++++ packages/core-utils/src/common/random.ts | 8 ++ packages/core-utils/src/common/regexp.ts | 5 + packages/core-utils/src/common/time.ts | 7 + packages/core-utils/src/common/url.ts | 150 +++++++++++++++++++++ packages/core-utils/src/common/version.ts | 11 ++ packages/core-utils/src/i18n/i18n.ts | 119 ++++++++++++++++ packages/core-utils/src/i18n/index.ts | 1 + packages/core-utils/src/index.ts | 7 + packages/core-utils/src/plugins/hooks.ts | 60 +++++++++ packages/core-utils/src/plugins/index.ts | 1 + packages/core-utils/src/renderer/html.ts | 71 ++++++++++ packages/core-utils/src/renderer/index.ts | 2 + packages/core-utils/src/renderer/markdown.ts | 24 ++++ packages/core-utils/src/users/index.ts | 1 + packages/core-utils/src/users/user-role.ts | 37 +++++ packages/core-utils/src/videos/bitrate.ts | 113 ++++++++++++++++ packages/core-utils/src/videos/common.ts | 24 ++++ packages/core-utils/src/videos/index.ts | 2 + 26 files changed, 980 insertions(+) create mode 100644 packages/core-utils/src/abuse/abuse-predefined-reasons.ts create mode 100644 packages/core-utils/src/abuse/index.ts create mode 100644 packages/core-utils/src/common/array.ts create mode 100644 packages/core-utils/src/common/date.ts create mode 100644 packages/core-utils/src/common/index.ts create mode 100644 packages/core-utils/src/common/number.ts create mode 100644 packages/core-utils/src/common/object.ts create mode 100644 packages/core-utils/src/common/promises.ts create mode 100644 packages/core-utils/src/common/random.ts create mode 100644 packages/core-utils/src/common/regexp.ts create mode 100644 packages/core-utils/src/common/time.ts create mode 100644 packages/core-utils/src/common/url.ts create mode 100644 packages/core-utils/src/common/version.ts create mode 100644 packages/core-utils/src/i18n/i18n.ts create mode 100644 packages/core-utils/src/i18n/index.ts create mode 100644 packages/core-utils/src/index.ts create mode 100644 packages/core-utils/src/plugins/hooks.ts create mode 100644 packages/core-utils/src/plugins/index.ts create mode 100644 packages/core-utils/src/renderer/html.ts create mode 100644 packages/core-utils/src/renderer/index.ts create mode 100644 packages/core-utils/src/renderer/markdown.ts create mode 100644 packages/core-utils/src/users/index.ts create mode 100644 packages/core-utils/src/users/user-role.ts create mode 100644 packages/core-utils/src/videos/bitrate.ts create mode 100644 packages/core-utils/src/videos/common.ts create mode 100644 packages/core-utils/src/videos/index.ts (limited to 'packages/core-utils/src') diff --git a/packages/core-utils/src/abuse/abuse-predefined-reasons.ts b/packages/core-utils/src/abuse/abuse-predefined-reasons.ts new file mode 100644 index 000000000..68534a1e0 --- /dev/null +++ b/packages/core-utils/src/abuse/abuse-predefined-reasons.ts @@ -0,0 +1,14 @@ +import { AbusePredefinedReasons, AbusePredefinedReasonsString, AbusePredefinedReasonsType } from '@peertube/peertube-models' + +export const abusePredefinedReasonsMap: { + [key in AbusePredefinedReasonsString]: AbusePredefinedReasonsType +} = { + violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE, + hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE, + spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING, + privacy: AbusePredefinedReasons.PRIVACY, + rights: AbusePredefinedReasons.RIGHTS, + serverRules: AbusePredefinedReasons.SERVER_RULES, + thumbnails: AbusePredefinedReasons.THUMBNAILS, + captions: AbusePredefinedReasons.CAPTIONS +} as const diff --git a/packages/core-utils/src/abuse/index.ts b/packages/core-utils/src/abuse/index.ts new file mode 100644 index 000000000..b79b86155 --- /dev/null +++ b/packages/core-utils/src/abuse/index.ts @@ -0,0 +1 @@ +export * from './abuse-predefined-reasons.js' diff --git a/packages/core-utils/src/common/array.ts b/packages/core-utils/src/common/array.ts new file mode 100644 index 000000000..878ed1ffe --- /dev/null +++ b/packages/core-utils/src/common/array.ts @@ -0,0 +1,41 @@ +function findCommonElement (array1: T[], array2: T[]) { + for (const a of array1) { + for (const b of array2) { + if (a === b) return a + } + } + + return null +} + +// Avoid conflict with other toArray() functions +function arrayify (element: T | T[]) { + if (Array.isArray(element)) return element + + return [ element ] +} + +// Avoid conflict with other uniq() functions +function uniqify (elements: T[]) { + return Array.from(new Set(elements)) +} + +// Thanks: https://stackoverflow.com/a/12646864 +function shuffle (elements: T[]) { + const shuffled = [ ...elements ] + + for (let i = shuffled.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + + [ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ] + } + + return shuffled +} + +export { + uniqify, + findCommonElement, + shuffle, + arrayify +} diff --git a/packages/core-utils/src/common/date.ts b/packages/core-utils/src/common/date.ts new file mode 100644 index 000000000..f0684ff86 --- /dev/null +++ b/packages/core-utils/src/common/date.ts @@ -0,0 +1,114 @@ +function isToday (d: Date) { + const today = new Date() + + return areDatesEqual(d, today) +} + +function isYesterday (d: Date) { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + + return areDatesEqual(d, yesterday) +} + +function isThisWeek (d: Date) { + const minDateOfThisWeek = new Date() + minDateOfThisWeek.setHours(0, 0, 0) + + // getDay() -> Sunday - Saturday : 0 - 6 + // We want to start our week on Monday + let dayOfWeek = minDateOfThisWeek.getDay() - 1 + if (dayOfWeek < 0) dayOfWeek = 6 // Sunday + + minDateOfThisWeek.setDate(minDateOfThisWeek.getDate() - dayOfWeek) + + return d >= minDateOfThisWeek +} + +function isThisMonth (d: Date) { + const thisMonth = new Date().getMonth() + + return d.getMonth() === thisMonth +} + +function isLastMonth (d: Date) { + const now = new Date() + + return getDaysDifferences(now, d) <= 30 +} + +function isLastWeek (d: Date) { + const now = new Date() + + return getDaysDifferences(now, d) <= 7 +} + +// --------------------------------------------------------------------------- + +function timeToInt (time: number | string) { + if (!time) return 0 + if (typeof time === 'number') return time + + const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/ + const matches = time.match(reg) + + if (!matches) return 0 + + const hours = parseInt(matches[2] || '0', 10) + const minutes = parseInt(matches[4] || '0', 10) + const seconds = parseInt(matches[6] || '0', 10) + + return hours * 3600 + minutes * 60 + seconds +} + +function secondsToTime (seconds: number, full = false, symbol?: string) { + let time = '' + + if (seconds === 0 && !full) return '0s' + + const hourSymbol = (symbol || 'h') + const minuteSymbol = (symbol || 'm') + const secondsSymbol = full ? '' : 's' + + const hours = Math.floor(seconds / 3600) + if (hours >= 1) time = hours + hourSymbol + else if (full) time = '0' + hourSymbol + + seconds %= 3600 + const minutes = Math.floor(seconds / 60) + if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol + else if (minutes >= 1) time += minutes + minuteSymbol + else if (full) time += '00' + minuteSymbol + + seconds %= 60 + if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol + else if (seconds >= 1) time += seconds + secondsSymbol + else if (full) time += '00' + + return time +} + +// --------------------------------------------------------------------------- + +export { + isYesterday, + isThisWeek, + isThisMonth, + isToday, + isLastMonth, + isLastWeek, + timeToInt, + secondsToTime +} + +// --------------------------------------------------------------------------- + +function areDatesEqual (d1: Date, d2: Date) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() +} + +function getDaysDifferences (d1: Date, d2: Date) { + return (d1.getTime() - d2.getTime()) / (86400000) +} diff --git a/packages/core-utils/src/common/index.ts b/packages/core-utils/src/common/index.ts new file mode 100644 index 000000000..d7d8599aa --- /dev/null +++ b/packages/core-utils/src/common/index.ts @@ -0,0 +1,10 @@ +export * from './array.js' +export * from './random.js' +export * from './date.js' +export * from './number.js' +export * from './object.js' +export * from './regexp.js' +export * from './time.js' +export * from './promises.js' +export * from './url.js' +export * from './version.js' diff --git a/packages/core-utils/src/common/number.ts b/packages/core-utils/src/common/number.ts new file mode 100644 index 000000000..ce5a6041a --- /dev/null +++ b/packages/core-utils/src/common/number.ts @@ -0,0 +1,13 @@ +export function forceNumber (value: any) { + return parseInt(value + '') +} + +export function isOdd (num: number) { + return (num % 2) !== 0 +} + +export function toEven (num: number) { + if (isOdd(num)) return num + 1 + + return num +} diff --git a/packages/core-utils/src/common/object.ts b/packages/core-utils/src/common/object.ts new file mode 100644 index 000000000..1276bfcc7 --- /dev/null +++ b/packages/core-utils/src/common/object.ts @@ -0,0 +1,86 @@ +function pick (object: O, keys: K[]): Pick { + const result: any = {} + + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + result[key] = object[key] + } + } + + return result +} + +function omit (object: O, keys: K[]): Exclude { + const result: any = {} + const keysSet = new Set(keys) as Set + + for (const [ key, value ] of Object.entries(object)) { + if (keysSet.has(key)) continue + + result[key] = value + } + + return result +} + +function objectKeysTyped (object: O): K[] { + return (Object.keys(object) as K[]) +} + +function getKeys (object: O, keys: K[]): K[] { + return (Object.keys(object) as K[]).filter(k => keys.includes(k)) +} + +function hasKey (obj: T, k: keyof any): k is keyof T { + return k in obj +} + +function sortObjectComparator (key: string, order: 'asc' | 'desc') { + return (a: any, b: any) => { + if (a[key] < b[key]) { + return order === 'asc' ? -1 : 1 + } + + if (a[key] > b[key]) { + return order === 'asc' ? 1 : -1 + } + + return 0 + } +} + +function shallowCopy (o: T): T { + return Object.assign(Object.create(Object.getPrototypeOf(o)), o) +} + +function simpleObjectsDeepEqual (a: any, b: any) { + if (a === b) return true + + if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) { + return false + } + + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + if (keysA.length !== keysB.length) return false + + for (const key of keysA) { + if (!keysB.includes(key)) return false + + if (!simpleObjectsDeepEqual(a[key], b[key])) return false + } + + return true +} + +export { + pick, + omit, + objectKeysTyped, + getKeys, + hasKey, + shallowCopy, + sortObjectComparator, + simpleObjectsDeepEqual +} diff --git a/packages/core-utils/src/common/promises.ts b/packages/core-utils/src/common/promises.ts new file mode 100644 index 000000000..e3792d12e --- /dev/null +++ b/packages/core-utils/src/common/promises.ts @@ -0,0 +1,58 @@ +export function isPromise (value: T | Promise): value is Promise { + return value && typeof (value as Promise).then === 'function' +} + +export function isCatchable (value: any) { + return value && typeof value.catch === 'function' +} + +export function timeoutPromise (promise: Promise, timeoutMs: number) { + let timer: ReturnType + + return Promise.race([ + promise, + + new Promise((_res, rej) => { + timer = setTimeout(() => rej(new Error('Timeout')), timeoutMs) + }) + ]).finally(() => clearTimeout(timer)) +} + +export function promisify0 (func: (cb: (err: any, result: A) => void) => void): () => Promise { + return function promisified (): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + // eslint-disable-next-line no-useless-call + func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2 +export function promisify1 (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise { + return function promisified (arg: T): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + // eslint-disable-next-line no-useless-call + func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +// eslint-disable-next-line max-len +export function promisify2 (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise { + return function promisified (arg1: T, arg2: U): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + // eslint-disable-next-line no-useless-call + func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + +// eslint-disable-next-line max-len +export function promisify3 (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise { + return function promisified (arg1: T, arg2: U, arg3: V): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + // eslint-disable-next-line no-useless-call + func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} diff --git a/packages/core-utils/src/common/random.ts b/packages/core-utils/src/common/random.ts new file mode 100644 index 000000000..705735d09 --- /dev/null +++ b/packages/core-utils/src/common/random.ts @@ -0,0 +1,8 @@ +// high excluded +function randomInt (low: number, high: number) { + return Math.floor(Math.random() * (high - low) + low) +} + +export { + randomInt +} diff --git a/packages/core-utils/src/common/regexp.ts b/packages/core-utils/src/common/regexp.ts new file mode 100644 index 000000000..59eb87eb6 --- /dev/null +++ b/packages/core-utils/src/common/regexp.ts @@ -0,0 +1,5 @@ +export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + +export function removeFragmentedMP4Ext (path: string) { + return path.replace(/-fragmented.mp4$/i, '') +} diff --git a/packages/core-utils/src/common/time.ts b/packages/core-utils/src/common/time.ts new file mode 100644 index 000000000..2992609ca --- /dev/null +++ b/packages/core-utils/src/common/time.ts @@ -0,0 +1,7 @@ +function wait (milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)) +} + +export { + wait +} diff --git a/packages/core-utils/src/common/url.ts b/packages/core-utils/src/common/url.ts new file mode 100644 index 000000000..449b6c9dc --- /dev/null +++ b/packages/core-utils/src/common/url.ts @@ -0,0 +1,150 @@ +import { Video, VideoPlaylist } from '@peertube/peertube-models' +import { secondsToTime } from './date.js' + +function addQueryParams (url: string, params: { [ id: string ]: string }) { + const objUrl = new URL(url) + + for (const key of Object.keys(params)) { + objUrl.searchParams.append(key, params[key]) + } + + return objUrl.toString() +} + +function removeQueryParams (url: string) { + const objUrl = new URL(url) + + objUrl.searchParams.forEach((_v, k) => objUrl.searchParams.delete(k)) + + return objUrl.toString() +} + +function buildPlaylistLink (playlist: Pick, base?: string) { + return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist) +} + +function buildPlaylistWatchPath (playlist: Pick) { + return '/w/p/' + playlist.shortUUID +} + +function buildVideoWatchPath (video: Pick) { + return '/w/' + video.shortUUID +} + +function buildVideoLink (video: Pick, base?: string) { + return (base ?? window.location.origin) + buildVideoWatchPath(video) +} + +function buildPlaylistEmbedPath (playlist: Pick) { + return '/video-playlists/embed/' + playlist.uuid +} + +function buildPlaylistEmbedLink (playlist: Pick, base?: string) { + return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist) +} + +function buildVideoEmbedPath (video: Pick) { + return '/videos/embed/' + video.uuid +} + +function buildVideoEmbedLink (video: Pick, base?: string) { + return (base ?? window.location.origin) + buildVideoEmbedPath(video) +} + +function decorateVideoLink (options: { + url: string + + startTime?: number + stopTime?: number + + subtitle?: string + + loop?: boolean + autoplay?: boolean + muted?: boolean + + // Embed options + title?: boolean + warningTitle?: boolean + + controls?: boolean + controlBar?: boolean + + peertubeLink?: boolean + p2p?: boolean +}) { + const { url } = options + + const params = new URLSearchParams() + + if (options.startTime !== undefined && options.startTime !== null) { + const startTimeInt = Math.floor(options.startTime) + params.set('start', secondsToTime(startTimeInt)) + } + + if (options.stopTime) { + const stopTimeInt = Math.floor(options.stopTime) + params.set('stop', secondsToTime(stopTimeInt)) + } + + if (options.subtitle) params.set('subtitle', options.subtitle) + + if (options.loop === true) params.set('loop', '1') + if (options.autoplay === true) params.set('autoplay', '1') + if (options.muted === true) params.set('muted', '1') + if (options.title === false) params.set('title', '0') + if (options.warningTitle === false) params.set('warningTitle', '0') + + if (options.controls === false) params.set('controls', '0') + if (options.controlBar === false) params.set('controlBar', '0') + + if (options.peertubeLink === false) params.set('peertubeLink', '0') + if (options.p2p !== undefined) params.set('p2p', options.p2p ? '1' : '0') + + return buildUrl(url, params) +} + +function decoratePlaylistLink (options: { + url: string + + playlistPosition?: number +}) { + const { url } = options + + const params = new URLSearchParams() + + if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition) + + return buildUrl(url, params) +} + +// --------------------------------------------------------------------------- + +export { + addQueryParams, + removeQueryParams, + + buildPlaylistLink, + buildVideoLink, + + buildVideoWatchPath, + buildPlaylistWatchPath, + + buildPlaylistEmbedPath, + buildVideoEmbedPath, + + buildPlaylistEmbedLink, + buildVideoEmbedLink, + + decorateVideoLink, + decoratePlaylistLink +} + +function buildUrl (url: string, params: URLSearchParams) { + let hasParams = false + params.forEach(() => { hasParams = true }) + + if (hasParams) return url + '?' + params.toString() + + return url +} diff --git a/packages/core-utils/src/common/version.ts b/packages/core-utils/src/common/version.ts new file mode 100644 index 000000000..305287233 --- /dev/null +++ b/packages/core-utils/src/common/version.ts @@ -0,0 +1,11 @@ +// Thanks https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb +function compareSemVer (a: string, b: string) { + if (a.startsWith(b + '-')) return -1 + if (b.startsWith(a + '-')) return 1 + + return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' }) +} + +export { + compareSemVer +} diff --git a/packages/core-utils/src/i18n/i18n.ts b/packages/core-utils/src/i18n/i18n.ts new file mode 100644 index 000000000..54b54077a --- /dev/null +++ b/packages/core-utils/src/i18n/i18n.ts @@ -0,0 +1,119 @@ +export const LOCALE_FILES = [ 'player', 'server' ] + +export const I18N_LOCALES = { + // Always first to avoid issues when using express acceptLanguages function when no accept language header is set + 'en-US': 'English', + + // Keep it alphabetically sorted + 'ar': 'العربية', + 'ca-ES': 'Català', + 'cs-CZ': 'Čeština', + 'de-DE': 'Deutsch', + 'el-GR': 'ελληνικά', + 'eo': 'Esperanto', + 'es-ES': 'Español', + 'eu-ES': 'Euskara', + 'fa-IR': 'فارسی', + 'fi-FI': 'Suomi', + 'fr-FR': 'Français', + 'gd': 'Gàidhlig', + 'gl-ES': 'Galego', + 'hr': 'Hrvatski', + 'hu-HU': 'Magyar', + 'is': 'Íslenska', + 'it-IT': 'Italiano', + 'ja-JP': '日本語', + 'kab': 'Taqbaylit', + 'nb-NO': 'Norsk bokmål', + 'nl-NL': 'Nederlands', + 'nn': 'Norsk nynorsk', + 'oc': 'Occitan', + 'pl-PL': 'Polski', + 'pt-BR': 'Português (Brasil)', + 'pt-PT': 'Português (Portugal)', + 'ru-RU': 'Pусский', + 'sq': 'Shqip', + 'sv-SE': 'Svenska', + 'th-TH': 'ไทย', + 'tok': 'Toki Pona', + 'uk-UA': 'украї́нська мо́ва', + 'vi-VN': 'Tiếng Việt', + 'zh-Hans-CN': '简体中文(中国)', + 'zh-Hant-TW': '繁體中文(台灣)' +} + +// Keep it alphabetically sorted +const I18N_LOCALE_ALIAS = { + 'ar-001': 'ar', + 'ca': 'ca-ES', + 'cs': 'cs-CZ', + 'de': 'de-DE', + 'el': 'el-GR', + 'en': 'en-US', + 'es': 'es-ES', + 'eu': 'eu-ES', + 'fa': 'fa-IR', + 'fi': 'fi-FI', + 'fr': 'fr-FR', + 'gl': 'gl-ES', + 'hu': 'hu-HU', + 'it': 'it-IT', + 'ja': 'ja-JP', + 'nb': 'nb-NO', + 'nl': 'nl-NL', + 'pl': 'pl-PL', + 'pt': 'pt-BR', + 'ru': 'ru-RU', + 'sv': 'sv-SE', + 'th': 'th-TH', + 'uk': 'uk-UA', + 'vi': 'vi-VN', + 'zh-CN': 'zh-Hans-CN', + 'zh-Hans': 'zh-Hans-CN', + 'zh-Hant': 'zh-Hant-TW', + 'zh-TW': 'zh-Hant-TW', + 'zh': 'zh-Hans-CN' +} + +export const POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).concat(Object.keys(I18N_LOCALE_ALIAS)) + +export function getDefaultLocale () { + return 'en-US' +} + +export function isDefaultLocale (locale: string) { + return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale()) +} + +export function peertubeTranslate (str: string, translations?: { [ id: string ]: string }) { + if (!translations?.[str]) return str + + return translations[str] +} + +const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l) +export function is18nPath (path: string) { + return possiblePaths.includes(path) +} + +export function is18nLocale (locale: string) { + return POSSIBLE_LOCALES.includes(locale) +} + +export function getCompleteLocale (locale: string) { + if (!locale) return locale + + const found = (I18N_LOCALE_ALIAS as any)[locale] as string + + return found || locale +} + +export function getShortLocale (locale: string) { + if (locale.includes('-') === false) return locale + + return locale.split('-')[0] +} + +export function buildFileLocale (locale: string) { + return getCompleteLocale(locale) +} diff --git a/packages/core-utils/src/i18n/index.ts b/packages/core-utils/src/i18n/index.ts new file mode 100644 index 000000000..758e54b73 --- /dev/null +++ b/packages/core-utils/src/i18n/index.ts @@ -0,0 +1 @@ +export * from './i18n.js' diff --git a/packages/core-utils/src/index.ts b/packages/core-utils/src/index.ts new file mode 100644 index 000000000..3ca5d9d47 --- /dev/null +++ b/packages/core-utils/src/index.ts @@ -0,0 +1,7 @@ +export * from './abuse/index.js' +export * from './common/index.js' +export * from './i18n/index.js' +export * from './plugins/index.js' +export * from './renderer/index.js' +export * from './users/index.js' +export * from './videos/index.js' diff --git a/packages/core-utils/src/plugins/hooks.ts b/packages/core-utils/src/plugins/hooks.ts new file mode 100644 index 000000000..fe7c4a74f --- /dev/null +++ b/packages/core-utils/src/plugins/hooks.ts @@ -0,0 +1,60 @@ +import { HookType, HookType_Type, RegisteredExternalAuthConfig } from '@peertube/peertube-models' +import { isCatchable, isPromise } from '../common/promises.js' + +function getHookType (hookName: string) { + if (hookName.startsWith('filter:')) return HookType.FILTER + if (hookName.startsWith('action:')) return HookType.ACTION + + return HookType.STATIC +} + +async function internalRunHook (options: { + handler: Function + hookType: HookType_Type + result: T + params: any + onError: (err: Error) => void +}) { + const { handler, hookType, result, params, onError } = options + + try { + if (hookType === HookType.FILTER) { + const p = handler(result, params) + + const newResult = isPromise(p) + ? await p + : p + + return newResult + } + + // Action/static hooks do not have result value + const p = handler(params) + + if (hookType === HookType.STATIC) { + if (isPromise(p)) await p + + return undefined + } + + if (hookType === HookType.ACTION) { + if (isCatchable(p)) p.catch((err: any) => onError(err)) + + return undefined + } + } catch (err) { + onError(err) + } + + return result +} + +function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) { + return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` +} + +export { + getHookType, + internalRunHook, + getExternalAuthHref +} diff --git a/packages/core-utils/src/plugins/index.ts b/packages/core-utils/src/plugins/index.ts new file mode 100644 index 000000000..3462bf41e --- /dev/null +++ b/packages/core-utils/src/plugins/index.ts @@ -0,0 +1 @@ +export * from './hooks.js' diff --git a/packages/core-utils/src/renderer/html.ts b/packages/core-utils/src/renderer/html.ts new file mode 100644 index 000000000..365bf7612 --- /dev/null +++ b/packages/core-utils/src/renderer/html.ts @@ -0,0 +1,71 @@ +export function getDefaultSanitizeOptions () { + return { + allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], + allowedSchemes: [ 'http', 'https' ], + allowedAttributes: { + 'a': [ 'href', 'class', 'target', 'rel' ], + '*': [ 'data-*' ] + }, + transformTags: { + a: (tagName: string, attribs: any) => { + let rel = 'noopener noreferrer' + if (attribs.rel === 'me') rel += ' me' + + return { + tagName, + attribs: Object.assign(attribs, { + target: '_blank', + rel + }) + } + } + } + } +} + +export function getTextOnlySanitizeOptions () { + return { + allowedTags: [] as string[] + } +} + +export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) { + const base = getDefaultSanitizeOptions() + + return { + allowedTags: [ + ...base.allowedTags, + ...additionalAllowedTags, + 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img' + ], + allowedSchemes: [ + ...base.allowedSchemes, + + 'mailto' + ], + allowedAttributes: { + ...base.allowedAttributes, + + 'img': [ 'src', 'alt' ], + '*': [ 'data-*', 'style' ] + } + } +} + +// Thanks: https://stackoverflow.com/a/12034334 +export function escapeHTML (stringParam: string) { + if (!stringParam) return '' + + const entityMap: { [id: string ]: string } = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/', + '`': '`', + '=': '=' + } + + return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s]) +} diff --git a/packages/core-utils/src/renderer/index.ts b/packages/core-utils/src/renderer/index.ts new file mode 100644 index 000000000..0dd0a8808 --- /dev/null +++ b/packages/core-utils/src/renderer/index.ts @@ -0,0 +1,2 @@ +export * from './markdown.js' +export * from './html.js' diff --git a/packages/core-utils/src/renderer/markdown.ts b/packages/core-utils/src/renderer/markdown.ts new file mode 100644 index 000000000..ddf608d7b --- /dev/null +++ b/packages/core-utils/src/renderer/markdown.ts @@ -0,0 +1,24 @@ +export const TEXT_RULES = [ + 'linkify', + 'autolink', + 'emphasis', + 'link', + 'newline', + 'entity', + 'list' +] + +export const TEXT_WITH_HTML_RULES = TEXT_RULES.concat([ + 'html_inline', + 'html_block' +]) + +export const ENHANCED_RULES = TEXT_RULES.concat([ 'image' ]) +export const ENHANCED_WITH_HTML_RULES = TEXT_WITH_HTML_RULES.concat([ 'image' ]) + +export const COMPLETE_RULES = ENHANCED_WITH_HTML_RULES.concat([ + 'block', + 'inline', + 'heading', + 'paragraph' +]) diff --git a/packages/core-utils/src/users/index.ts b/packages/core-utils/src/users/index.ts new file mode 100644 index 000000000..3fd9dc448 --- /dev/null +++ b/packages/core-utils/src/users/index.ts @@ -0,0 +1 @@ +export * from './user-role.js' diff --git a/packages/core-utils/src/users/user-role.ts b/packages/core-utils/src/users/user-role.ts new file mode 100644 index 000000000..0add3a0a8 --- /dev/null +++ b/packages/core-utils/src/users/user-role.ts @@ -0,0 +1,37 @@ +import { UserRight, UserRightType, UserRole, UserRoleType } from '@peertube/peertube-models' + +export const USER_ROLE_LABELS: { [ id in UserRoleType ]: string } = { + [UserRole.USER]: 'User', + [UserRole.MODERATOR]: 'Moderator', + [UserRole.ADMINISTRATOR]: 'Administrator' +} + +const userRoleRights: { [ id in UserRoleType ]: UserRightType[] } = { + [UserRole.ADMINISTRATOR]: [ + UserRight.ALL + ], + + [UserRole.MODERATOR]: [ + UserRight.MANAGE_VIDEO_BLACKLIST, + UserRight.MANAGE_ABUSES, + UserRight.MANAGE_ANY_VIDEO_CHANNEL, + UserRight.REMOVE_ANY_VIDEO, + UserRight.REMOVE_ANY_VIDEO_PLAYLIST, + UserRight.REMOVE_ANY_VIDEO_COMMENT, + UserRight.UPDATE_ANY_VIDEO, + UserRight.SEE_ALL_VIDEOS, + UserRight.MANAGE_ACCOUNTS_BLOCKLIST, + UserRight.MANAGE_SERVERS_BLOCKLIST, + UserRight.MANAGE_USERS, + UserRight.SEE_ALL_COMMENTS, + UserRight.MANAGE_REGISTRATIONS + ], + + [UserRole.USER]: [] +} + +export function hasUserRight (userRole: UserRoleType, userRight: UserRightType) { + const userRights = userRoleRights[userRole] + + return userRights.includes(UserRight.ALL) || userRights.includes(userRight) +} diff --git a/packages/core-utils/src/videos/bitrate.ts b/packages/core-utils/src/videos/bitrate.ts new file mode 100644 index 000000000..b28eaf460 --- /dev/null +++ b/packages/core-utils/src/videos/bitrate.ts @@ -0,0 +1,113 @@ +import { VideoResolution, VideoResolutionType } from '@peertube/peertube-models' + +type BitPerPixel = { [ id in VideoResolutionType ]: number } + +// https://bitmovin.com/video-bitrate-streaming-hls-dash/ + +const minLimitBitPerPixel: BitPerPixel = { + [VideoResolution.H_NOVIDEO]: 0, + [VideoResolution.H_144P]: 0.02, + [VideoResolution.H_240P]: 0.02, + [VideoResolution.H_360P]: 0.02, + [VideoResolution.H_480P]: 0.02, + [VideoResolution.H_720P]: 0.02, + [VideoResolution.H_1080P]: 0.02, + [VideoResolution.H_1440P]: 0.02, + [VideoResolution.H_4K]: 0.02 +} + +const averageBitPerPixel: BitPerPixel = { + [VideoResolution.H_NOVIDEO]: 0, + [VideoResolution.H_144P]: 0.19, + [VideoResolution.H_240P]: 0.17, + [VideoResolution.H_360P]: 0.15, + [VideoResolution.H_480P]: 0.12, + [VideoResolution.H_720P]: 0.11, + [VideoResolution.H_1080P]: 0.10, + [VideoResolution.H_1440P]: 0.09, + [VideoResolution.H_4K]: 0.08 +} + +const maxBitPerPixel: BitPerPixel = { + [VideoResolution.H_NOVIDEO]: 0, + [VideoResolution.H_144P]: 0.32, + [VideoResolution.H_240P]: 0.29, + [VideoResolution.H_360P]: 0.26, + [VideoResolution.H_480P]: 0.22, + [VideoResolution.H_720P]: 0.19, + [VideoResolution.H_1080P]: 0.17, + [VideoResolution.H_1440P]: 0.16, + [VideoResolution.H_4K]: 0.14 +} + +function getAverageTheoreticalBitrate (options: { + resolution: number + ratio: number + fps: number +}) { + const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel }) + if (!targetBitrate) return 192 * 1000 + + return targetBitrate +} + +function getMaxTheoreticalBitrate (options: { + resolution: number + ratio: number + fps: number +}) { + const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel }) + if (!targetBitrate) return 256 * 1000 + + return targetBitrate +} + +function getMinTheoreticalBitrate (options: { + resolution: number + ratio: number + fps: number +}) { + const minLimitBitrate = calculateBitrate({ ...options, bitPerPixel: minLimitBitPerPixel }) + if (!minLimitBitrate) return 10 * 1000 + + return minLimitBitrate +} + +// --------------------------------------------------------------------------- + +export { + getAverageTheoreticalBitrate, + getMaxTheoreticalBitrate, + getMinTheoreticalBitrate +} + +// --------------------------------------------------------------------------- + +function calculateBitrate (options: { + bitPerPixel: BitPerPixel + resolution: number + ratio: number + fps: number +}) { + const { bitPerPixel, resolution, ratio, fps } = options + + const resolutionsOrder = [ + VideoResolution.H_4K, + VideoResolution.H_1440P, + VideoResolution.H_1080P, + VideoResolution.H_720P, + VideoResolution.H_480P, + VideoResolution.H_360P, + VideoResolution.H_240P, + VideoResolution.H_144P, + VideoResolution.H_NOVIDEO + ] + + for (const toTestResolution of resolutionsOrder) { + if (toTestResolution <= resolution) { + return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution]) + } + } + + throw new Error('Unknown resolution ' + resolution) +} diff --git a/packages/core-utils/src/videos/common.ts b/packages/core-utils/src/videos/common.ts new file mode 100644 index 000000000..47564fb2a --- /dev/null +++ b/packages/core-utils/src/videos/common.ts @@ -0,0 +1,24 @@ +import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models' + +function getAllPrivacies () { + return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ] +} + +function getAllFiles (video: Partial>) { + const files = video.files + + const hls = getHLS(video) + if (hls) return files.concat(hls.files) + + return files +} + +function getHLS (video: Partial>) { + return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) +} + +export { + getAllPrivacies, + getAllFiles, + getHLS +} diff --git a/packages/core-utils/src/videos/index.ts b/packages/core-utils/src/videos/index.ts new file mode 100644 index 000000000..7d3dacdd4 --- /dev/null +++ b/packages/core-utils/src/videos/index.ts @@ -0,0 +1,2 @@ +export * from './bitrate.js' +export * from './common.js' -- cgit v1.2.3