aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/core-utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core-utils')
-rw-r--r--packages/core-utils/package.json19
-rw-r--r--packages/core-utils/src/abuse/abuse-predefined-reasons.ts14
-rw-r--r--packages/core-utils/src/abuse/index.ts1
-rw-r--r--packages/core-utils/src/common/array.ts41
-rw-r--r--packages/core-utils/src/common/date.ts114
-rw-r--r--packages/core-utils/src/common/index.ts10
-rw-r--r--packages/core-utils/src/common/number.ts13
-rw-r--r--packages/core-utils/src/common/object.ts86
-rw-r--r--packages/core-utils/src/common/promises.ts58
-rw-r--r--packages/core-utils/src/common/random.ts8
-rw-r--r--packages/core-utils/src/common/regexp.ts5
-rw-r--r--packages/core-utils/src/common/time.ts7
-rw-r--r--packages/core-utils/src/common/url.ts150
-rw-r--r--packages/core-utils/src/common/version.ts11
-rw-r--r--packages/core-utils/src/i18n/i18n.ts119
-rw-r--r--packages/core-utils/src/i18n/index.ts1
-rw-r--r--packages/core-utils/src/index.ts7
-rw-r--r--packages/core-utils/src/plugins/hooks.ts60
-rw-r--r--packages/core-utils/src/plugins/index.ts1
-rw-r--r--packages/core-utils/src/renderer/html.ts71
-rw-r--r--packages/core-utils/src/renderer/index.ts2
-rw-r--r--packages/core-utils/src/renderer/markdown.ts24
-rw-r--r--packages/core-utils/src/users/index.ts1
-rw-r--r--packages/core-utils/src/users/user-role.ts37
-rw-r--r--packages/core-utils/src/videos/bitrate.ts113
-rw-r--r--packages/core-utils/src/videos/common.ts24
-rw-r--r--packages/core-utils/src/videos/index.ts2
-rw-r--r--packages/core-utils/tsconfig.json11
28 files changed, 1010 insertions, 0 deletions
diff --git a/packages/core-utils/package.json b/packages/core-utils/package.json
new file mode 100644
index 000000000..d3bf18335
--- /dev/null
+++ b/packages/core-utils/package.json
@@ -0,0 +1,19 @@
1{
2 "name": "@peertube/peertube-core-utils",
3 "private": true,
4 "version": "0.0.0",
5 "main": "dist/index.js",
6 "files": [ "dist" ],
7 "exports": {
8 "types": "./dist/index.d.ts",
9 "peertube:tsx": "./src/index.ts",
10 "default": "./dist/index.js"
11 },
12 "type": "module",
13 "devDependencies": {},
14 "scripts": {
15 "build": "tsc",
16 "watch": "tsc -w"
17 },
18 "dependencies": {}
19}
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 @@
1import { AbusePredefinedReasons, AbusePredefinedReasonsString, AbusePredefinedReasonsType } from '@peertube/peertube-models'
2
3export const abusePredefinedReasonsMap: {
4 [key in AbusePredefinedReasonsString]: AbusePredefinedReasonsType
5} = {
6 violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE,
7 hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE,
8 spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING,
9 privacy: AbusePredefinedReasons.PRIVACY,
10 rights: AbusePredefinedReasons.RIGHTS,
11 serverRules: AbusePredefinedReasons.SERVER_RULES,
12 thumbnails: AbusePredefinedReasons.THUMBNAILS,
13 captions: AbusePredefinedReasons.CAPTIONS
14} 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 @@
1function findCommonElement <T> (array1: T[], array2: T[]) {
2 for (const a of array1) {
3 for (const b of array2) {
4 if (a === b) return a
5 }
6 }
7
8 return null
9}
10
11// Avoid conflict with other toArray() functions
12function arrayify <T> (element: T | T[]) {
13 if (Array.isArray(element)) return element
14
15 return [ element ]
16}
17
18// Avoid conflict with other uniq() functions
19function uniqify <T> (elements: T[]) {
20 return Array.from(new Set(elements))
21}
22
23// Thanks: https://stackoverflow.com/a/12646864
24function shuffle <T> (elements: T[]) {
25 const shuffled = [ ...elements ]
26
27 for (let i = shuffled.length - 1; i > 0; i--) {
28 const j = Math.floor(Math.random() * (i + 1));
29
30 [ shuffled[i], shuffled[j] ] = [ shuffled[j], shuffled[i] ]
31 }
32
33 return shuffled
34}
35
36export {
37 uniqify,
38 findCommonElement,
39 shuffle,
40 arrayify
41}
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 @@
1function isToday (d: Date) {
2 const today = new Date()
3
4 return areDatesEqual(d, today)
5}
6
7function isYesterday (d: Date) {
8 const yesterday = new Date()
9 yesterday.setDate(yesterday.getDate() - 1)
10
11 return areDatesEqual(d, yesterday)
12}
13
14function isThisWeek (d: Date) {
15 const minDateOfThisWeek = new Date()
16 minDateOfThisWeek.setHours(0, 0, 0)
17
18 // getDay() -> Sunday - Saturday : 0 - 6
19 // We want to start our week on Monday
20 let dayOfWeek = minDateOfThisWeek.getDay() - 1
21 if (dayOfWeek < 0) dayOfWeek = 6 // Sunday
22
23 minDateOfThisWeek.setDate(minDateOfThisWeek.getDate() - dayOfWeek)
24
25 return d >= minDateOfThisWeek
26}
27
28function isThisMonth (d: Date) {
29 const thisMonth = new Date().getMonth()
30
31 return d.getMonth() === thisMonth
32}
33
34function isLastMonth (d: Date) {
35 const now = new Date()
36
37 return getDaysDifferences(now, d) <= 30
38}
39
40function isLastWeek (d: Date) {
41 const now = new Date()
42
43 return getDaysDifferences(now, d) <= 7
44}
45
46// ---------------------------------------------------------------------------
47
48function timeToInt (time: number | string) {
49 if (!time) return 0
50 if (typeof time === 'number') return time
51
52 const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
53 const matches = time.match(reg)
54
55 if (!matches) return 0
56
57 const hours = parseInt(matches[2] || '0', 10)
58 const minutes = parseInt(matches[4] || '0', 10)
59 const seconds = parseInt(matches[6] || '0', 10)
60
61 return hours * 3600 + minutes * 60 + seconds
62}
63
64function secondsToTime (seconds: number, full = false, symbol?: string) {
65 let time = ''
66
67 if (seconds === 0 && !full) return '0s'
68
69 const hourSymbol = (symbol || 'h')
70 const minuteSymbol = (symbol || 'm')
71 const secondsSymbol = full ? '' : 's'
72
73 const hours = Math.floor(seconds / 3600)
74 if (hours >= 1) time = hours + hourSymbol
75 else if (full) time = '0' + hourSymbol
76
77 seconds %= 3600
78 const minutes = Math.floor(seconds / 60)
79 if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
80 else if (minutes >= 1) time += minutes + minuteSymbol
81 else if (full) time += '00' + minuteSymbol
82
83 seconds %= 60
84 if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
85 else if (seconds >= 1) time += seconds + secondsSymbol
86 else if (full) time += '00'
87
88 return time
89}
90
91// ---------------------------------------------------------------------------
92
93export {
94 isYesterday,
95 isThisWeek,
96 isThisMonth,
97 isToday,
98 isLastMonth,
99 isLastWeek,
100 timeToInt,
101 secondsToTime
102}
103
104// ---------------------------------------------------------------------------
105
106function areDatesEqual (d1: Date, d2: Date) {
107 return d1.getFullYear() === d2.getFullYear() &&
108 d1.getMonth() === d2.getMonth() &&
109 d1.getDate() === d2.getDate()
110}
111
112function getDaysDifferences (d1: Date, d2: Date) {
113 return (d1.getTime() - d2.getTime()) / (86400000)
114}
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 @@
1export * from './array.js'
2export * from './random.js'
3export * from './date.js'
4export * from './number.js'
5export * from './object.js'
6export * from './regexp.js'
7export * from './time.js'
8export * from './promises.js'
9export * from './url.js'
10export * 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 @@
1export function forceNumber (value: any) {
2 return parseInt(value + '')
3}
4
5export function isOdd (num: number) {
6 return (num % 2) !== 0
7}
8
9export function toEven (num: number) {
10 if (isOdd(num)) return num + 1
11
12 return num
13}
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 @@
1function pick <O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
2 const result: any = {}
3
4 for (const key of keys) {
5 if (Object.prototype.hasOwnProperty.call(object, key)) {
6 result[key] = object[key]
7 }
8 }
9
10 return result
11}
12
13function omit <O extends object, K extends keyof O> (object: O, keys: K[]): Exclude<O, K> {
14 const result: any = {}
15 const keysSet = new Set(keys) as Set<string>
16
17 for (const [ key, value ] of Object.entries(object)) {
18 if (keysSet.has(key)) continue
19
20 result[key] = value
21 }
22
23 return result
24}
25
26function objectKeysTyped <O extends object, K extends keyof O> (object: O): K[] {
27 return (Object.keys(object) as K[])
28}
29
30function getKeys <O extends object, K extends keyof O> (object: O, keys: K[]): K[] {
31 return (Object.keys(object) as K[]).filter(k => keys.includes(k))
32}
33
34function hasKey <T extends object> (obj: T, k: keyof any): k is keyof T {
35 return k in obj
36}
37
38function sortObjectComparator (key: string, order: 'asc' | 'desc') {
39 return (a: any, b: any) => {
40 if (a[key] < b[key]) {
41 return order === 'asc' ? -1 : 1
42 }
43
44 if (a[key] > b[key]) {
45 return order === 'asc' ? 1 : -1
46 }
47
48 return 0
49 }
50}
51
52function shallowCopy <T> (o: T): T {
53 return Object.assign(Object.create(Object.getPrototypeOf(o)), o)
54}
55
56function simpleObjectsDeepEqual (a: any, b: any) {
57 if (a === b) return true
58
59 if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
60 return false
61 }
62
63 const keysA = Object.keys(a)
64 const keysB = Object.keys(b)
65
66 if (keysA.length !== keysB.length) return false
67
68 for (const key of keysA) {
69 if (!keysB.includes(key)) return false
70
71 if (!simpleObjectsDeepEqual(a[key], b[key])) return false
72 }
73
74 return true
75}
76
77export {
78 pick,
79 omit,
80 objectKeysTyped,
81 getKeys,
82 hasKey,
83 shallowCopy,
84 sortObjectComparator,
85 simpleObjectsDeepEqual
86}
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 @@
1export function isPromise <T = unknown> (value: T | Promise<T>): value is Promise<T> {
2 return value && typeof (value as Promise<T>).then === 'function'
3}
4
5export function isCatchable (value: any) {
6 return value && typeof value.catch === 'function'
7}
8
9export function timeoutPromise <T> (promise: Promise<T>, timeoutMs: number) {
10 let timer: ReturnType<typeof setTimeout>
11
12 return Promise.race([
13 promise,
14
15 new Promise((_res, rej) => {
16 timer = setTimeout(() => rej(new Error('Timeout')), timeoutMs)
17 })
18 ]).finally(() => clearTimeout(timer))
19}
20
21export function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
22 return function promisified (): Promise<A> {
23 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
24 // eslint-disable-next-line no-useless-call
25 func.apply(null, [ (err: any, res: A) => err ? reject(err) : resolve(res) ])
26 })
27 }
28}
29
30// Thanks to https://gist.github.com/kumasento/617daa7e46f13ecdd9b2
31export function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) => void): (arg: T) => Promise<A> {
32 return function promisified (arg: T): Promise<A> {
33 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
34 // eslint-disable-next-line no-useless-call
35 func.apply(null, [ arg, (err: any, res: A) => err ? reject(err) : resolve(res) ])
36 })
37 }
38}
39
40// eslint-disable-next-line max-len
41export function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
42 return function promisified (arg1: T, arg2: U): Promise<A> {
43 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
44 // eslint-disable-next-line no-useless-call
45 func.apply(null, [ arg1, arg2, (err: any, res: A) => err ? reject(err) : resolve(res) ])
46 })
47 }
48}
49
50// eslint-disable-next-line max-len
51export function promisify3<T, U, V, A> (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise<A> {
52 return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> {
53 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
54 // eslint-disable-next-line no-useless-call
55 func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ])
56 })
57 }
58}
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 @@
1// high excluded
2function randomInt (low: number, high: number) {
3 return Math.floor(Math.random() * (high - low) + low)
4}
5
6export {
7 randomInt
8}
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 @@
1export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
2
3export function removeFragmentedMP4Ext (path: string) {
4 return path.replace(/-fragmented.mp4$/i, '')
5}
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 @@
1function wait (milliseconds: number) {
2 return new Promise(resolve => setTimeout(resolve, milliseconds))
3}
4
5export {
6 wait
7}
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 @@
1import { Video, VideoPlaylist } from '@peertube/peertube-models'
2import { secondsToTime } from './date.js'
3
4function addQueryParams (url: string, params: { [ id: string ]: string }) {
5 const objUrl = new URL(url)
6
7 for (const key of Object.keys(params)) {
8 objUrl.searchParams.append(key, params[key])
9 }
10
11 return objUrl.toString()
12}
13
14function removeQueryParams (url: string) {
15 const objUrl = new URL(url)
16
17 objUrl.searchParams.forEach((_v, k) => objUrl.searchParams.delete(k))
18
19 return objUrl.toString()
20}
21
22function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
23 return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
24}
25
26function buildPlaylistWatchPath (playlist: Pick<VideoPlaylist, 'shortUUID'>) {
27 return '/w/p/' + playlist.shortUUID
28}
29
30function buildVideoWatchPath (video: Pick<Video, 'shortUUID'>) {
31 return '/w/' + video.shortUUID
32}
33
34function buildVideoLink (video: Pick<Video, 'shortUUID'>, base?: string) {
35 return (base ?? window.location.origin) + buildVideoWatchPath(video)
36}
37
38function buildPlaylistEmbedPath (playlist: Pick<VideoPlaylist, 'uuid'>) {
39 return '/video-playlists/embed/' + playlist.uuid
40}
41
42function buildPlaylistEmbedLink (playlist: Pick<VideoPlaylist, 'uuid'>, base?: string) {
43 return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist)
44}
45
46function buildVideoEmbedPath (video: Pick<Video, 'uuid'>) {
47 return '/videos/embed/' + video.uuid
48}
49
50function buildVideoEmbedLink (video: Pick<Video, 'uuid'>, base?: string) {
51 return (base ?? window.location.origin) + buildVideoEmbedPath(video)
52}
53
54function decorateVideoLink (options: {
55 url: string
56
57 startTime?: number
58 stopTime?: number
59
60 subtitle?: string
61
62 loop?: boolean
63 autoplay?: boolean
64 muted?: boolean
65
66 // Embed options
67 title?: boolean
68 warningTitle?: boolean
69
70 controls?: boolean
71 controlBar?: boolean
72
73 peertubeLink?: boolean
74 p2p?: boolean
75}) {
76 const { url } = options
77
78 const params = new URLSearchParams()
79
80 if (options.startTime !== undefined && options.startTime !== null) {
81 const startTimeInt = Math.floor(options.startTime)
82 params.set('start', secondsToTime(startTimeInt))
83 }
84
85 if (options.stopTime) {
86 const stopTimeInt = Math.floor(options.stopTime)
87 params.set('stop', secondsToTime(stopTimeInt))
88 }
89
90 if (options.subtitle) params.set('subtitle', options.subtitle)
91
92 if (options.loop === true) params.set('loop', '1')
93 if (options.autoplay === true) params.set('autoplay', '1')
94 if (options.muted === true) params.set('muted', '1')
95 if (options.title === false) params.set('title', '0')
96 if (options.warningTitle === false) params.set('warningTitle', '0')
97
98 if (options.controls === false) params.set('controls', '0')
99 if (options.controlBar === false) params.set('controlBar', '0')
100
101 if (options.peertubeLink === false) params.set('peertubeLink', '0')
102 if (options.p2p !== undefined) params.set('p2p', options.p2p ? '1' : '0')
103
104 return buildUrl(url, params)
105}
106
107function decoratePlaylistLink (options: {
108 url: string
109
110 playlistPosition?: number
111}) {
112 const { url } = options
113
114 const params = new URLSearchParams()
115
116 if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
117
118 return buildUrl(url, params)
119}
120
121// ---------------------------------------------------------------------------
122
123export {
124 addQueryParams,
125 removeQueryParams,
126
127 buildPlaylistLink,
128 buildVideoLink,
129
130 buildVideoWatchPath,
131 buildPlaylistWatchPath,
132
133 buildPlaylistEmbedPath,
134 buildVideoEmbedPath,
135
136 buildPlaylistEmbedLink,
137 buildVideoEmbedLink,
138
139 decorateVideoLink,
140 decoratePlaylistLink
141}
142
143function buildUrl (url: string, params: URLSearchParams) {
144 let hasParams = false
145 params.forEach(() => { hasParams = true })
146
147 if (hasParams) return url + '?' + params.toString()
148
149 return url
150}
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 @@
1// Thanks https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
2function compareSemVer (a: string, b: string) {
3 if (a.startsWith(b + '-')) return -1
4 if (b.startsWith(a + '-')) return 1
5
6 return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' })
7}
8
9export {
10 compareSemVer
11}
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 @@
1export const LOCALE_FILES = [ 'player', 'server' ]
2
3export const I18N_LOCALES = {
4 // Always first to avoid issues when using express acceptLanguages function when no accept language header is set
5 'en-US': 'English',
6
7 // Keep it alphabetically sorted
8 'ar': 'العربية',
9 'ca-ES': 'Català',
10 'cs-CZ': 'Čeština',
11 'de-DE': 'Deutsch',
12 'el-GR': 'ελληνικά',
13 'eo': 'Esperanto',
14 'es-ES': 'Español',
15 'eu-ES': 'Euskara',
16 'fa-IR': 'فارسی',
17 'fi-FI': 'Suomi',
18 'fr-FR': 'Français',
19 'gd': 'Gàidhlig',
20 'gl-ES': 'Galego',
21 'hr': 'Hrvatski',
22 'hu-HU': 'Magyar',
23 'is': 'Íslenska',
24 'it-IT': 'Italiano',
25 'ja-JP': '日本語',
26 'kab': 'Taqbaylit',
27 'nb-NO': 'Norsk bokmål',
28 'nl-NL': 'Nederlands',
29 'nn': 'Norsk nynorsk',
30 'oc': 'Occitan',
31 'pl-PL': 'Polski',
32 'pt-BR': 'Português (Brasil)',
33 'pt-PT': 'Português (Portugal)',
34 'ru-RU': 'Pусский',
35 'sq': 'Shqip',
36 'sv-SE': 'Svenska',
37 'th-TH': 'ไทย',
38 'tok': 'Toki Pona',
39 'uk-UA': 'украї́нська мо́ва',
40 'vi-VN': 'Tiếng Việt',
41 'zh-Hans-CN': '简体中文(中国)',
42 'zh-Hant-TW': '繁體中文(台灣)'
43}
44
45// Keep it alphabetically sorted
46const I18N_LOCALE_ALIAS = {
47 'ar-001': 'ar',
48 'ca': 'ca-ES',
49 'cs': 'cs-CZ',
50 'de': 'de-DE',
51 'el': 'el-GR',
52 'en': 'en-US',
53 'es': 'es-ES',
54 'eu': 'eu-ES',
55 'fa': 'fa-IR',
56 'fi': 'fi-FI',
57 'fr': 'fr-FR',
58 'gl': 'gl-ES',
59 'hu': 'hu-HU',
60 'it': 'it-IT',
61 'ja': 'ja-JP',
62 'nb': 'nb-NO',
63 'nl': 'nl-NL',
64 'pl': 'pl-PL',
65 'pt': 'pt-BR',
66 'ru': 'ru-RU',
67 'sv': 'sv-SE',
68 'th': 'th-TH',
69 'uk': 'uk-UA',
70 'vi': 'vi-VN',
71 'zh-CN': 'zh-Hans-CN',
72 'zh-Hans': 'zh-Hans-CN',
73 'zh-Hant': 'zh-Hant-TW',
74 'zh-TW': 'zh-Hant-TW',
75 'zh': 'zh-Hans-CN'
76}
77
78export const POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).concat(Object.keys(I18N_LOCALE_ALIAS))
79
80export function getDefaultLocale () {
81 return 'en-US'
82}
83
84export function isDefaultLocale (locale: string) {
85 return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale())
86}
87
88export function peertubeTranslate (str: string, translations?: { [ id: string ]: string }) {
89 if (!translations?.[str]) return str
90
91 return translations[str]
92}
93
94const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
95export function is18nPath (path: string) {
96 return possiblePaths.includes(path)
97}
98
99export function is18nLocale (locale: string) {
100 return POSSIBLE_LOCALES.includes(locale)
101}
102
103export function getCompleteLocale (locale: string) {
104 if (!locale) return locale
105
106 const found = (I18N_LOCALE_ALIAS as any)[locale] as string
107
108 return found || locale
109}
110
111export function getShortLocale (locale: string) {
112 if (locale.includes('-') === false) return locale
113
114 return locale.split('-')[0]
115}
116
117export function buildFileLocale (locale: string) {
118 return getCompleteLocale(locale)
119}
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 @@
1export * from './abuse/index.js'
2export * from './common/index.js'
3export * from './i18n/index.js'
4export * from './plugins/index.js'
5export * from './renderer/index.js'
6export * from './users/index.js'
7export * 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 @@
1import { HookType, HookType_Type, RegisteredExternalAuthConfig } from '@peertube/peertube-models'
2import { isCatchable, isPromise } from '../common/promises.js'
3
4function getHookType (hookName: string) {
5 if (hookName.startsWith('filter:')) return HookType.FILTER
6 if (hookName.startsWith('action:')) return HookType.ACTION
7
8 return HookType.STATIC
9}
10
11async function internalRunHook <T> (options: {
12 handler: Function
13 hookType: HookType_Type
14 result: T
15 params: any
16 onError: (err: Error) => void
17}) {
18 const { handler, hookType, result, params, onError } = options
19
20 try {
21 if (hookType === HookType.FILTER) {
22 const p = handler(result, params)
23
24 const newResult = isPromise(p)
25 ? await p
26 : p
27
28 return newResult
29 }
30
31 // Action/static hooks do not have result value
32 const p = handler(params)
33
34 if (hookType === HookType.STATIC) {
35 if (isPromise(p)) await p
36
37 return undefined
38 }
39
40 if (hookType === HookType.ACTION) {
41 if (isCatchable(p)) p.catch((err: any) => onError(err))
42
43 return undefined
44 }
45 } catch (err) {
46 onError(err)
47 }
48
49 return result
50}
51
52function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) {
53 return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
54}
55
56export {
57 getHookType,
58 internalRunHook,
59 getExternalAuthHref
60}
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 @@
1export function getDefaultSanitizeOptions () {
2 return {
3 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
4 allowedSchemes: [ 'http', 'https' ],
5 allowedAttributes: {
6 'a': [ 'href', 'class', 'target', 'rel' ],
7 '*': [ 'data-*' ]
8 },
9 transformTags: {
10 a: (tagName: string, attribs: any) => {
11 let rel = 'noopener noreferrer'
12 if (attribs.rel === 'me') rel += ' me'
13
14 return {
15 tagName,
16 attribs: Object.assign(attribs, {
17 target: '_blank',
18 rel
19 })
20 }
21 }
22 }
23 }
24}
25
26export function getTextOnlySanitizeOptions () {
27 return {
28 allowedTags: [] as string[]
29 }
30}
31
32export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
33 const base = getDefaultSanitizeOptions()
34
35 return {
36 allowedTags: [
37 ...base.allowedTags,
38 ...additionalAllowedTags,
39 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img'
40 ],
41 allowedSchemes: [
42 ...base.allowedSchemes,
43
44 'mailto'
45 ],
46 allowedAttributes: {
47 ...base.allowedAttributes,
48
49 'img': [ 'src', 'alt' ],
50 '*': [ 'data-*', 'style' ]
51 }
52 }
53}
54
55// Thanks: https://stackoverflow.com/a/12034334
56export function escapeHTML (stringParam: string) {
57 if (!stringParam) return ''
58
59 const entityMap: { [id: string ]: string } = {
60 '&': '&amp;',
61 '<': '&lt;',
62 '>': '&gt;',
63 '"': '&quot;',
64 '\'': '&#39;',
65 '/': '&#x2F;',
66 '`': '&#x60;',
67 '=': '&#x3D;'
68 }
69
70 return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
71}
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 @@
1export * from './markdown.js'
2export * 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 @@
1export const TEXT_RULES = [
2 'linkify',
3 'autolink',
4 'emphasis',
5 'link',
6 'newline',
7 'entity',
8 'list'
9]
10
11export const TEXT_WITH_HTML_RULES = TEXT_RULES.concat([
12 'html_inline',
13 'html_block'
14])
15
16export const ENHANCED_RULES = TEXT_RULES.concat([ 'image' ])
17export const ENHANCED_WITH_HTML_RULES = TEXT_WITH_HTML_RULES.concat([ 'image' ])
18
19export const COMPLETE_RULES = ENHANCED_WITH_HTML_RULES.concat([
20 'block',
21 'inline',
22 'heading',
23 'paragraph'
24])
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 @@
1import { UserRight, UserRightType, UserRole, UserRoleType } from '@peertube/peertube-models'
2
3export const USER_ROLE_LABELS: { [ id in UserRoleType ]: string } = {
4 [UserRole.USER]: 'User',
5 [UserRole.MODERATOR]: 'Moderator',
6 [UserRole.ADMINISTRATOR]: 'Administrator'
7}
8
9const userRoleRights: { [ id in UserRoleType ]: UserRightType[] } = {
10 [UserRole.ADMINISTRATOR]: [
11 UserRight.ALL
12 ],
13
14 [UserRole.MODERATOR]: [
15 UserRight.MANAGE_VIDEO_BLACKLIST,
16 UserRight.MANAGE_ABUSES,
17 UserRight.MANAGE_ANY_VIDEO_CHANNEL,
18 UserRight.REMOVE_ANY_VIDEO,
19 UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
20 UserRight.REMOVE_ANY_VIDEO_COMMENT,
21 UserRight.UPDATE_ANY_VIDEO,
22 UserRight.SEE_ALL_VIDEOS,
23 UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
24 UserRight.MANAGE_SERVERS_BLOCKLIST,
25 UserRight.MANAGE_USERS,
26 UserRight.SEE_ALL_COMMENTS,
27 UserRight.MANAGE_REGISTRATIONS
28 ],
29
30 [UserRole.USER]: []
31}
32
33export function hasUserRight (userRole: UserRoleType, userRight: UserRightType) {
34 const userRights = userRoleRights[userRole]
35
36 return userRights.includes(UserRight.ALL) || userRights.includes(userRight)
37}
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 @@
1import { VideoResolution, VideoResolutionType } from '@peertube/peertube-models'
2
3type BitPerPixel = { [ id in VideoResolutionType ]: number }
4
5// https://bitmovin.com/video-bitrate-streaming-hls-dash/
6
7const minLimitBitPerPixel: BitPerPixel = {
8 [VideoResolution.H_NOVIDEO]: 0,
9 [VideoResolution.H_144P]: 0.02,
10 [VideoResolution.H_240P]: 0.02,
11 [VideoResolution.H_360P]: 0.02,
12 [VideoResolution.H_480P]: 0.02,
13 [VideoResolution.H_720P]: 0.02,
14 [VideoResolution.H_1080P]: 0.02,
15 [VideoResolution.H_1440P]: 0.02,
16 [VideoResolution.H_4K]: 0.02
17}
18
19const averageBitPerPixel: BitPerPixel = {
20 [VideoResolution.H_NOVIDEO]: 0,
21 [VideoResolution.H_144P]: 0.19,
22 [VideoResolution.H_240P]: 0.17,
23 [VideoResolution.H_360P]: 0.15,
24 [VideoResolution.H_480P]: 0.12,
25 [VideoResolution.H_720P]: 0.11,
26 [VideoResolution.H_1080P]: 0.10,
27 [VideoResolution.H_1440P]: 0.09,
28 [VideoResolution.H_4K]: 0.08
29}
30
31const maxBitPerPixel: BitPerPixel = {
32 [VideoResolution.H_NOVIDEO]: 0,
33 [VideoResolution.H_144P]: 0.32,
34 [VideoResolution.H_240P]: 0.29,
35 [VideoResolution.H_360P]: 0.26,
36 [VideoResolution.H_480P]: 0.22,
37 [VideoResolution.H_720P]: 0.19,
38 [VideoResolution.H_1080P]: 0.17,
39 [VideoResolution.H_1440P]: 0.16,
40 [VideoResolution.H_4K]: 0.14
41}
42
43function getAverageTheoreticalBitrate (options: {
44 resolution: number
45 ratio: number
46 fps: number
47}) {
48 const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel })
49 if (!targetBitrate) return 192 * 1000
50
51 return targetBitrate
52}
53
54function getMaxTheoreticalBitrate (options: {
55 resolution: number
56 ratio: number
57 fps: number
58}) {
59 const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel })
60 if (!targetBitrate) return 256 * 1000
61
62 return targetBitrate
63}
64
65function getMinTheoreticalBitrate (options: {
66 resolution: number
67 ratio: number
68 fps: number
69}) {
70 const minLimitBitrate = calculateBitrate({ ...options, bitPerPixel: minLimitBitPerPixel })
71 if (!minLimitBitrate) return 10 * 1000
72
73 return minLimitBitrate
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 getAverageTheoreticalBitrate,
80 getMaxTheoreticalBitrate,
81 getMinTheoreticalBitrate
82}
83
84// ---------------------------------------------------------------------------
85
86function calculateBitrate (options: {
87 bitPerPixel: BitPerPixel
88 resolution: number
89 ratio: number
90 fps: number
91}) {
92 const { bitPerPixel, resolution, ratio, fps } = options
93
94 const resolutionsOrder = [
95 VideoResolution.H_4K,
96 VideoResolution.H_1440P,
97 VideoResolution.H_1080P,
98 VideoResolution.H_720P,
99 VideoResolution.H_480P,
100 VideoResolution.H_360P,
101 VideoResolution.H_240P,
102 VideoResolution.H_144P,
103 VideoResolution.H_NOVIDEO
104 ]
105
106 for (const toTestResolution of resolutionsOrder) {
107 if (toTestResolution <= resolution) {
108 return Math.floor(resolution * resolution * ratio * fps * bitPerPixel[toTestResolution])
109 }
110 }
111
112 throw new Error('Unknown resolution ' + resolution)
113}
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 @@
1import { VideoDetails, VideoPrivacy, VideoStreamingPlaylistType } from '@peertube/peertube-models'
2
3function getAllPrivacies () {
4 return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.PASSWORD_PROTECTED ]
5}
6
7function getAllFiles (video: Partial<Pick<VideoDetails, 'files' | 'streamingPlaylists'>>) {
8 const files = video.files
9
10 const hls = getHLS(video)
11 if (hls) return files.concat(hls.files)
12
13 return files
14}
15
16function getHLS (video: Partial<Pick<VideoDetails, 'streamingPlaylists'>>) {
17 return video.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
18}
19
20export {
21 getAllPrivacies,
22 getAllFiles,
23 getHLS
24}
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 @@
1export * from './bitrate.js'
2export * from './common.js'
diff --git a/packages/core-utils/tsconfig.json b/packages/core-utils/tsconfig.json
new file mode 100644
index 000000000..56ebffbb3
--- /dev/null
+++ b/packages/core-utils/tsconfig.json
@@ -0,0 +1,11 @@
1{
2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "outDir": "./dist",
5 "rootDir": "src",
6 "tsBuildInfoFile": "./dist/.tsbuildinfo"
7 },
8 "references": [
9 { "path": "../models" }
10 ]
11}