Typescript v5 + eslint 8.57 WIP

This commit also improves some type handling in the project.
This commit is contained in:
John Livingston 2024-09-07 14:49:27 +02:00
parent 64a9c7be21
commit 7b3d93b290
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
41 changed files with 2652 additions and 3054 deletions

View File

@ -1,14 +0,0 @@
{
"root": true,
"env": {},
"extends": [],
"globals": {},
"plugins": [],
"ignorePatterns": [
"node_modules/", "dist/", "webpack.config.js",
"build/",
"vendor/",
"support/documentation",
"build-*js"],
"rules": {}
}

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
'use strict';
'use strict'
module.exports = {
extends: [

View File

@ -10,6 +10,9 @@
### Minor changes and fixes
* Various translation updates.
* Using Typescript 5.5.4, and Eslint 8.57.0 (with new ruleset).
* Fix race condition in bot/ctl.
* Various type improvements.
## 11.0.1

View File

@ -5,7 +5,7 @@
"es6": true
},
"extends": [
"standard-with-typescript",
"eslint-config-love",
"plugin:lit/recommended"
],
"globals": {},

View File

@ -5,7 +5,7 @@
"es6": true
},
"extends": [
"standard-with-typescript"
"eslint-config-love"
],
"globals": {},
"parser": "@typescript-eslint/parser",

94
eslint.config.mjs Normal file
View File

@ -0,0 +1,94 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import love from 'eslint-config-love'
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import typescriptParser from '@typescript-eslint/parser'
import stylistic from '@stylistic/eslint-plugin'
import globals from 'globals'
export default tseslint.config(
{
ignores: [
"node_modules/", "dist/", "webpack.config.js",
"build/",
"vendor/",
"support/documentation", "support",
"build-*js",
"shared", "client", "conversejs"
]
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
...love,
files: ['**/*.ts']
},
{
plugins: {
'@stylistic': stylistic
},
rules: {
"@stylistic/semi": ["error", "never"]
}
},
{
files: ['**/*.ts'],
rules: {
"@typescript-eslint/no-empty-function": ["error", {"allow": ["arrowFunctions"]}],
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_"}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/triple-slash-reference": "off",
"@typescript-eslint/no-explicit-any": "off", // FIXME: should be "error", and we should use 'unknown' in the code.
"init-declarations": "off",
"@typescript-eslint/init-declarations": "off",
"@typescript-eslint/consistent-type-imports": "off",
"@typescript-eslint/consistent-type-exports": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/prefer-promise-reject-errors": "off",
"max-params": "off",
"@typescript-eslint/max-params": ["error", { "max": 8 }], // FIXME: this rules should use the default max value.
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-confusing-void-expression": "off",
"@typescript-eslint/class-methods-use-this": "off",
"@typescript-eslint/non-nullable-type-assertion-style": "off",
"max-len": [
"error",
{
"code": 120,
"comments": 120
}
],
"no-unused-vars": "off"
}
},
{
files: ['.stylelintrc.js'],
languageOptions: {
globals: globals.node
}
},
{
files: ['server/**/*.js', 'server/**/*.ts'], // only ts?
languageOptions: {
ecmaVersion: 6,
globals: {
...globals.node
},
parser: typescriptParser,
parserOptions: {
ecmaVersion: 2018,
project: './server/tsconfig.json'
}
}
}
)

5207
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,32 +38,29 @@
"xmppjs-chat-bot": "^0.4.0"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
"@lit-labs/motion": "^1.0.7",
"@lit/context": "^1.1.1",
"@lit/task": "^1.0.0",
"@peertube/feed": "^5.1.0",
"@peertube/peertube-types": "^5.2.0",
"@stylistic/eslint-plugin": "^2.7.2",
"@tsconfig/node12": "^1.0.9",
"@types/async": "^3.2.9",
"@types/escape-html": "^1.0.4",
"@types/eslint__js": "^8.42.3",
"@types/express": "^4.17.13",
"@types/got": "^9.6.12",
"@types/http-proxy": "^1.17.9",
"@types/node": "^16.11.6",
"@types/winston": "^2.4.4",
"@types/xmpp__jid": "^1.3.5",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"@typescript-eslint/parser": "^8.4.0",
"commander": "^11.0.0",
"esbuild": "^0.16.1",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-lit": "^1.13.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-standard": "^5.0.0",
"eslint": "^8.57.0",
"eslint-config-love": "^64.0.0",
"globals": "^15.9.0",
"lit": "^2.4.0",
"lit-analyzer": "^1.2.1",
"npm-run-all": "^4.1.5",
@ -73,7 +70,8 @@
"stylelint-config-recommended-scss": "^5.0.1",
"stylelint-config-standard-scss": "^2.0.1",
"svgo": "^2.8.0",
"typescript": "^4.3.5",
"typescript": "^5.5.4",
"typescript-eslint": "^8.4.0",
"yaml": "^2.2.1"
},
"engine": {

View File

@ -1,40 +0,0 @@
{
"root": true,
"env": {
"browser": false,
"es6": true
},
"extends": [
"standard-with-typescript"
],
"globals": {},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"project": [
"./server/tsconfig.json"
]
},
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": [],
"rules": {
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
"@typescript-eslint/no-invalid-void-type": "off",
"@typescript-eslint/triple-slash-reference": "off",
"max-len": [
"error",
{
"code": 120,
"comments": 120
}
],
"no-unused-vars": "off"
}
}

View File

@ -88,7 +88,7 @@ class BotsCtl {
moderationBotProcess.stderr?.on('data', (data) => {
// change error level for non-relevant errors:
data = data.toString()
if (/Warning.*NODE_TLS_REJECT_UNAUTHORIZED.*'0'.*TLS/.test(data)) {
if (/Warning.*NODE_TLS_REJECT_UNAUTHORIZED.*'0'.*TLS/.test(data as string)) {
this.logger.debug(`ModerationBot stderr: ${data as string}`)
return
}
@ -123,9 +123,11 @@ class BotsCtl {
}
const p = new Promise<void>((resolve, reject) => {
try {
if (!this.moderationBotProcess) { resolve() }
const moderationBotProcess: ReturnType<typeof child_process.spawn> =
this.moderationBotProcess as ReturnType<typeof child_process.spawn>
if (!this.moderationBotProcess) {
resolve()
return
}
const moderationBotProcess: ReturnType<typeof child_process.spawn> = this.moderationBotProcess
let resolved = false
// Trying to kill, and force kill if it takes more than X seconds

View File

@ -189,7 +189,7 @@ class BotConfiguration {
})).toString()
config = JSON.parse(content)
} catch (err) {
} catch (_err) {
this.logger.info('Error reading the moderation bot global configuration file, assuming it does not exists.')
config = undefined
}
@ -275,7 +275,7 @@ class BotConfiguration {
content = (await fs.promises.readFile(filePath, {
encoding: 'utf-8'
})).toString()
} catch (err) {
} catch (_err) {
this.logger.debug('Failed to read room conf file, assuming it does not exists')
this.roomConfCache.set(roomJID, null)
return null
@ -284,7 +284,7 @@ class BotConfiguration {
let json: RoomConf
try {
json = JSON.parse(content) as RoomConf
} catch (err) {
} catch (_err) {
this.logger.error(`Error parsing JSON file ${filePath}, assuming empty`)
this.roomConfCache.set(roomJID, null)
return null

View File

@ -59,7 +59,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
logger.info(`Channel ${channelId} deleted, removing 'custom emojis' related stuff.`)
try {
Emojis.singletonSafe()?.deleteChannelDefinition(channelId)
await Emojis.singletonSafe()?.deleteChannelDefinition(channelId)
} catch (err) {
logger.error(err)
}
@ -87,7 +87,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
])
await fillVideoCustomFields(options, video)
const hasChat = await videoHasWebchat({
const hasChat = videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],

View File

@ -17,31 +17,36 @@ import { channelTermsMaxLength } from '../../../../shared/lib/constants'
async function sanitizeChannelConfigurationOptions (
_options: RegisterServerOptions,
_channelId: number | string,
data: any
data: unknown
): Promise<ChannelConfigurationOptions> {
if (typeof data !== 'object') {
if (!_assertObjectType(data)) {
throw new Error('Invalid data type')
}
const botData = data.bot
if (typeof botData !== 'object') {
const botData = data.bot ?? {}
if (!_assertObjectType(botData)) {
throw new Error('Invalid data.bot data type')
}
// slowMode not present in livechat <= 8.2.0:
const slowModeData = data.slowMode ?? {}
slowModeData.duration ??= slowModeData.defaultDuration ?? 0 // v8.3.0 to 8.3.2: was in defaultDuration
if (typeof slowModeData !== 'object') {
if (!_assertObjectType(slowModeData)) {
throw new Error('Invalid data.slowMode data type')
}
slowModeData.duration ??= slowModeData.defaultDuration ?? 0 // v8.3.0 to 8.3.2: was in defaultDuration
const moderationData = data.moderation ?? {} // comes with livechat 10.3.0
if (!_assertObjectType(moderationData)) {
throw new Error('Invalid data.moderation data type')
}
moderationData.delay ??= 0
moderationData.anonymize ??= false // comes with livechat 11.0.0
// mute not present in livechat <= 10.2.0
const mute = data.mute ?? {}
if (!_assertObjectType(mute)) {
throw new Error('Invalid data.mute data type')
}
mute.anonymous ??= false
// forbidSpecialChars comes with livechat 11.1.0
@ -51,13 +56,12 @@ async function sanitizeChannelConfigurationOptions (
tolerance: 0,
applyToModerators: false
}
if (typeof mute !== 'object') {
throw new Error('Invalid data.mute data type')
if (!_assertObjectType(botData.forbidSpecialChars)) {
throw new Error('Invalid data.forbidSpecialChars data type')
}
// terms not present in livechat <= 10.2.0
let terms = data.terms
let terms = data.terms ?? undefined
if (terms !== undefined && (typeof terms !== 'string')) {
throw new Error('Invalid data.terms data type')
}
@ -94,7 +98,11 @@ async function sanitizeChannelConfigurationOptions (
return result
}
function _readBoolean (data: any, f: string): boolean {
function _assertObjectType (data: unknown): data is Record<string, unknown> {
return !!data && (typeof data === 'object') && Object.keys(data).every(k => typeof k === 'string')
}
function _readBoolean (data: Record<string, unknown>, f: string): boolean {
if (!(f in data)) {
return false
}
@ -104,11 +112,11 @@ function _readBoolean (data: any, f: string): boolean {
return data[f]
}
function _readInteger (data: any, f: string, min: number, max: number): number {
function _readInteger (data: Record<string, unknown>, f: string, min: number, max: number): number {
if (!(f in data)) {
throw new Error('Missing integer value for field ' + f)
}
const v = parseInt(data[f])
const v = typeof data[f] === 'number' ? Math.trunc(data[f]) : parseInt(data[f] as string)
if (isNaN(v)) {
throw new Error('Invalid value type for field ' + f)
}
@ -121,7 +129,7 @@ function _readInteger (data: any, f: string, min: number, max: number): number {
return v
}
function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boolean): string {
function _readSimpleInput (data: Record<string, unknown>, f: string, strict?: boolean, noSpace?: boolean): string {
if (!(f in data)) {
return ''
}
@ -130,7 +138,7 @@ function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boo
}
// Removing control characters.
// eslint-disable-next-line no-control-regex
let s = (data[f] as string).replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
let s = data[f].replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
if (strict) {
// Replacing all invalid characters, no need to throw an error..
s = s.replace(/[^\p{L}\p{N}\p{Z}_-]$/gu, '')
@ -141,7 +149,7 @@ function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boo
return s
}
function _readStringArray (data: any, f: string): string[] {
function _readStringArray (data: Record<string, unknown>, f: string): string[] {
if (!(f in data)) {
return []
}
@ -162,7 +170,7 @@ function _readStringArray (data: any, f: string): string[] {
return result
}
function _readMultiLineString (data: any, f: string): string {
function _readMultiLineString (data: Record<string, unknown>, f: string): string {
if (!(f in data)) {
return ''
}
@ -171,11 +179,11 @@ function _readMultiLineString (data: any, f: string): string {
}
// Removing control characters (must authorize \u001A: line feed)
// eslint-disable-next-line no-control-regex
const s = (data[f] as string).replace(/[\u0000-\u0009\u001B-\u001F\u007F-\u009F]/g, '')
const s = data[f].replace(/[\u0000-\u0009\u001B-\u001F\u007F-\u009F]/g, '')
return s
}
async function _readRegExpArray (data: any, f: string): Promise<string[]> {
async function _readRegExpArray (data: Record<string, unknown>, f: string): Promise<string[]> {
// Note: this function can instanciate a lot of RegExp.
// To avoid freezing the server, we make it async, and will validate each regexp in a separate tick.
if (!(f in data)) {
@ -195,11 +203,11 @@ async function _readRegExpArray (data: any, f: string): Promise<string[]> {
}
// value must be a valid regexp
try {
async function _validate (): Promise<void> {
async function _validate (v: string): Promise<void> {
// eslint-disable-next-line no-new
new RegExp(v)
}
await _validate()
await _validate(v)
} catch (_err) {
throw new Error('Invalid value in field ' + f)
}
@ -208,12 +216,17 @@ async function _readRegExpArray (data: any, f: string): Promise<string[]> {
return result
}
async function _readForbiddenWords (botData: any): Promise<ChannelConfigurationOptions['bot']['forbiddenWords']> {
async function _readForbiddenWords (
botData: Record<string, unknown>
): Promise<ChannelConfigurationOptions['bot']['forbiddenWords']> {
if (!Array.isArray(botData.forbiddenWords)) {
throw new Error('Invalid forbiddenWords data')
}
const result: ChannelConfigurationOptions['bot']['forbiddenWords'] = []
for (const fw of botData.forbiddenWords) {
if (!_assertObjectType(fw)) {
throw new Error('Invalid entry in botData.forbiddenWords')
}
const regexp = !!fw.regexp
let entries
if (regexp) {
@ -239,9 +252,9 @@ async function _readForbiddenWords (botData: any): Promise<ChannelConfigurationO
}
async function _readForbidSpecialChars (
botData: any
botData: Record<string, unknown>
): Promise<ChannelConfigurationOptions['bot']['forbidSpecialChars']> {
if (typeof botData.forbidSpecialChars !== 'object') {
if (!_assertObjectType(botData.forbidSpecialChars)) {
throw new Error('Invalid forbidSpecialChars data')
}
const result: ChannelConfigurationOptions['bot']['forbidSpecialChars'] = {
@ -253,12 +266,15 @@ async function _readForbidSpecialChars (
return result
}
function _readQuotes (botData: any): ChannelConfigurationOptions['bot']['quotes'] {
function _readQuotes (botData: Record<string, unknown>): ChannelConfigurationOptions['bot']['quotes'] {
if (!Array.isArray(botData.quotes)) {
throw new Error('Invalid quotes data')
}
const result: ChannelConfigurationOptions['bot']['quotes'] = []
for (const qs of botData.quotes) {
if (!_assertObjectType(qs)) {
throw new Error('Invalid entry in botData.quotes')
}
const messages = _readStringArray(qs, 'messages')
const delay = _readInteger(qs, 'delay', 1, 6000)
@ -270,12 +286,15 @@ function _readQuotes (botData: any): ChannelConfigurationOptions['bot']['quotes'
return result
}
function _readCommands (botData: any): ChannelConfigurationOptions['bot']['commands'] {
function _readCommands (botData: Record<string, unknown>): ChannelConfigurationOptions['bot']['commands'] {
if (!Array.isArray(botData.commands)) {
throw new Error('Invalid commands data')
}
const result: ChannelConfigurationOptions['bot']['commands'] = []
for (const cs of botData.commands) {
if (!_assertObjectType(cs)) {
throw new Error('Invalid entry in botData.commands')
}
const message = _readSimpleInput(cs, 'message')
const command = _readSimpleInput(cs, 'command', false, true)

View File

@ -140,7 +140,7 @@ function channelConfigurationOptionsToBotRoomConf (
for (const handler of previousRoomConf.handlers) {
if (!handlersIds.has(handler.id)) {
// cloning to avoid issues...
const disabledHandler = JSON.parse(JSON.stringify(handler))
const disabledHandler = JSON.parse(JSON.stringify(handler)) as typeof handler
disabledHandler.enabled = false
handlers.push(disabledHandler)
}

View File

@ -100,7 +100,7 @@ async function getConverseJSParams (
externalAuthOIDC ??= []
externalAuthOIDC.push({
type: oidc.type,
buttonLabel: buttonLabel,
buttonLabel,
url: authUrl
})
}
@ -127,7 +127,7 @@ async function getConverseJSParams (
localWebsocketServiceUrl: localWsUri,
remoteBoshServiceUrl: remoteConnectionInfos?.anonymous?.boshUri ?? null,
remoteWebsocketServiceUrl: remoteConnectionInfos?.anonymous?.wsUri ?? null,
authenticationUrl: authenticationUrl,
authenticationUrl,
autoViewerMode,
theme: converseJSTheme,
forceReadonly,
@ -150,7 +150,7 @@ function _interfaceParams (
transparent: InitConverseJSParams['transparent']
converseJSTheme: InitConverseJSParams['theme']
} {
let autoViewerMode: boolean = false
let autoViewerMode = false
const forceReadonly: boolean | 'noscroll' = params.readonly ?? false
if (!forceReadonly) {
autoViewerMode = true // auto join the chat in viewer mode, if not logged in

View File

@ -45,7 +45,7 @@ async function initCustomFields (options: RegisterServerOptions): Promise<void>
const body: any = params.body
const video: Video | undefined = params.video
if (!video || !video.id) {
if (!video?.id) {
return
}
if (!body.pluginData) return
@ -115,7 +115,7 @@ async function fillVideoRemoteLiveChat (
const infos = await getVideoLiveChatInfos(options, video)
if (!infos) { return }
let ok: boolean = false
let ok = false
// We must check if there is a compatible connection protocol...
if (anonymousConnectionInfos(infos)) {
// Connection ok using a remote anonymous account. That's enought.

View File

@ -61,7 +61,7 @@ interface ChannelInfos {
async function getChannelInfosById (
options: RegisterServerOptions,
channelId: number,
restrictToLocalChannels: boolean = false
restrictToLocalChannels = false
): Promise<ChannelInfos | null> {
if (!channelId) {
throw new Error('Missing channelId')

View File

@ -87,7 +87,7 @@ function _getProsodyDebuggerOptions (options: RegisterServerOptions, json: any):
if (!json.debug_prosody.debugger_path) { return undefined }
if (typeof json.debug_prosody.debugger_path !== 'string') { return undefined }
const mobdebugPath = json.debug_prosody.debugger_path
const mobdebugPath = json.debug_prosody.debugger_path as string
if (!fs.statSync(mobdebugPath).isDirectory()) {
options.peertubeHelpers.logger.error('There should be a debugger, but cant find it. Path should be: ', mobdebugPath)

View File

@ -235,7 +235,7 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
title: 'Prosody error log (last 50 lines)',
message: logLines.join('\n')
})
} catch (error) {
} catch (_err) {
// Error should be because file does not exists. This is not an error case, just ignoring.
}

View File

@ -28,7 +28,7 @@ export interface TestResult {
export function newResult (test: string): TestResult {
return {
test: test,
test,
ok: false,
messages: [],
debug: [],

View File

@ -26,7 +26,7 @@ export async function diagVideo (test: string, { settingsManager }: RegisterServ
result.messages.push('Displaying «open in new window» button')
}
let atLeastOne: boolean = false
let atLeastOne = false
if (videoSettings['chat-per-live-video']) {
result.messages.push('Chat can be enabled on live videos.')
atLeastOne = true

View File

@ -114,7 +114,7 @@ export class Emojis {
// File does not exist, this is normal.
return undefined
}
throw err
throw err as Error
}
return JSON.parse(content.toString())
}
@ -316,7 +316,7 @@ export class Emojis {
}
const result: ChannelEmojis = {
customEmojis: customEmojis
customEmojis
}
return [result, buffersInfos]
}
@ -387,7 +387,7 @@ export class Emojis {
})
} catch (err: any) {
if (!(('code' in err) && err.code === 'ENOENT')) {
this.logger.error(err)
this.logger.error(err as string)
}
} finally {
this.channelCache.delete(channelId)
@ -475,15 +475,17 @@ async function _getConverseEmojiCodes (options: RegisterServerOptions): Promise<
const converseEmojiDefPath = path.join(__dirname, '..', '..', '..', 'converse-emoji.json')
options.peertubeHelpers.logger.debug('Loading Converse Emojis from file ' + converseEmojiDefPath)
const converseEmojis: {[key: string]: any} = JSON.parse(
await (await fs.promises.readFile(converseEmojiDefPath)).toString()
const converseEmojis: Record<string, any> = JSON.parse(
(await fs.promises.readFile(converseEmojiDefPath)).toString()
)
const r = []
for (const [key, block] of Object.entries(converseEmojis)) {
if (key === 'custom') { continue } // These are not used.
r.push(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
...Object.values(block)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.map((d: any) => d.cp ? _emojiCpToRegexp(d.cp) : d.sn)
.filter((sn: string) => sn && sn !== '')
)

View File

@ -238,7 +238,7 @@ class ExternalAuthOIDC {
this.logger.debug('OIDC Discovery url is valid: ' + uri.toString())
this.providerHostName = uri.hostname
} catch (err) {
} catch (_err) {
errors.push('Invalid discovery url')
}
}
@ -349,11 +349,17 @@ class ExternalAuthOIDC {
if (!encryptedCodeVerifier) {
throw new Error('Received callback but code verifier not found in request cookies.')
}
if (typeof encryptedCodeVerifier !== 'string') {
throw new Error('Invalid code-verifier type.')
}
const encryptedState = req.cookies[this.cookieNamePrefix + 'state']
if (!encryptedState) {
throw new Error('Received callback but state not found in request cookies.')
}
if (typeof encryptedState !== 'string') {
throw new Error('Invalid state data type')
}
const codeVerifier = await this.decrypt(encryptedCodeVerifier)
const state = await this.decrypt(encryptedState)
@ -451,7 +457,7 @@ class ExternalAuthOIDC {
const decipher = createDecipheriv(algorithm, this.secretKey, iv)
// FIXME: dismiss the "as any" below (dont understand why Typescript is not happy without)
return decipher.update(encrypted as any, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
return decipher.update(encrypted.toString(), outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}
/**
@ -491,8 +497,11 @@ class ExternalAuthOIDC {
if (typeof o.nickname !== 'string' || o.nickname === '') {
throw new Error('No nickname')
}
if (typeof o.expire !== 'string' || o.expire === '') {
throw new Error('Invalid expire data type')
}
const expire = new Date(Date.parse(o.expire))
const expire = new Date(Date.parse(o.expire as string))
if (!(expire instanceof Date) || isNaN(expire.getTime())) {
throw new Error('Invalid expire date')
}
@ -548,7 +557,7 @@ class ExternalAuthOIDC {
if (!(field in userInfos)) { continue }
if (typeof userInfos[field] !== 'string') { continue }
if (userInfos[field] === '') { continue }
return userInfos[field] as string
return userInfos[field]
}
return undefined
}
@ -571,7 +580,7 @@ class ExternalAuthOIDC {
responseType: 'buffer'
}).buffer()
const mimeType = await getMimeTypeFromArrayBuffer(buf)
const mimeType = getMimeTypeFromArrayBuffer(buf as ArrayBuffer)
if (!mimeType) {
throw new Error('Failed to get the avatar file type')
}
@ -603,7 +612,7 @@ class ExternalAuthOIDC {
const filePath = path.resolve(this.avatarsDir, file)
const buf = await fs.promises.readFile(filePath)
const mimeType = await getMimeTypeFromArrayBuffer(buf)
const mimeType = getMimeTypeFromArrayBuffer(buf)
if (!mimeType) {
throw new Error('Failed to get the default avatar file type')
}
@ -778,7 +787,7 @@ class ExternalAuthOIDC {
const m = token.match(/^(\w+)-/)
if (!m) { return null }
return ExternalAuthOIDC.singleton(m[1])
} catch (err) {
} catch (_err) {
return null
}
}

View File

@ -21,7 +21,7 @@ export async function initFederation (options: RegisterServerOptions): Promise<v
registerHook({
target: 'filter:activity-pub.activity.context.build.result',
handler: async (jsonld: any) => {
handler: async (jsonld: any[]) => {
return videoContextBuildJSONLD(options, jsonld)
}
})

View File

@ -56,7 +56,7 @@ async function videoBuildJSONLD (
}
await fillVideoCustomFields(options, video)
const hasChat = await videoHasWebchat({
const hasChat = videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],

View File

@ -169,7 +169,7 @@ function sanitizePeertubeLiveChatServerInfos (
*/
function sanitizeCustomEmojisUrl (
options: RegisterServerOptions,
customEmojisUrl: any,
customEmojisUrl: unknown,
referenceUrl?: string
): string | undefined {
let checkHost: undefined | string
@ -206,8 +206,8 @@ interface URLConstraints {
domain?: string
}
function _validUrl (s: string, constraints: URLConstraints): boolean {
if ((typeof s) !== 'string') { return false }
function _validUrl (s: unknown, constraints: URLConstraints): s is string {
if (typeof s !== 'string') { return false }
if (s === '') { return false }
let url: URL
try {
@ -265,13 +265,13 @@ function _validateHost (s: any, mustBeSubDomainOf?: string): false | string {
}
}
function _readReferenceUrl (s: any): undefined | string {
function _readReferenceUrl (s: unknown): undefined | string {
try {
if (typeof s !== 'string') { return undefined }
if (!s.startsWith('https://') && !s.startsWith('http://')) {
s = 'http://' + s
}
const url = new URL(s)
const url = new URL(s as string)
const host = url.hostname
// Just to avoid some basic spoofing, we must check that host is not simply something like "com".
// We will check if there is at least one dot. This test is not perfect, but can limit spoofing cases.
@ -283,16 +283,16 @@ function _readReferenceUrl (s: any): undefined | string {
}
function _sanitizePeertubeLiveChatInfosV0 (
options: RegisterServerOptions, chatInfos: any, referenceUrl?: string
options: RegisterServerOptions, chatInfos: unknown, referenceUrl?: string
): LiveChatJSONLDAttributeV1 {
const logger = options.peertubeHelpers.logger
logger.debug('We are have to migrate data from the old JSONLD format')
if (chatInfos === false) { return false }
if (typeof chatInfos !== 'object') { return false }
if (!_assertObjectType(chatInfos)) { return false}
if (chatInfos.type !== 'xmpp') { return false }
if ((typeof chatInfos.jid) !== 'string') { return false }
if (typeof chatInfos.jid !== 'string') { return false }
// no link? invalid! dropping all.
if (!Array.isArray(chatInfos.links)) { return false }
@ -327,10 +327,10 @@ function _sanitizePeertubeLiveChatInfosV0 (
}
for (const link of chatInfos.links) {
if ((typeof link) !== 'object') { continue }
if (!_assertObjectType(link) || (typeof link.type !== 'string')) { continue }
if (['xmpp-bosh-anonymous', 'xmpp-websocket-anonymous'].includes(link.type)) {
if ((typeof link.jid) !== 'string') { continue }
if ((typeof link.url) !== 'string') { continue }
if (typeof link.jid !== 'string') { continue }
if (typeof link.url !== 'string') { continue }
if (
!_validUrl(link.url, {
@ -372,6 +372,10 @@ function sanitizeXMPPHostFromInstanceUrl (_options: RegisterServerOptions, s: an
}
}
function _assertObjectType (data: unknown): data is Record<string, unknown> {
return !!data && (typeof data === 'object') && Object.keys(data).every(k => typeof k === 'string')
}
export {
sanitizePeertubeLiveChatInfos,
sanitizePeertubeLiveChatServerInfos,

View File

@ -75,7 +75,7 @@ export async function listModFirewallFiles (
})
return files.map(f => path.join(dir, f.name)).sort()
} catch (err) {
} catch (_err) {
// should be that the directory does not exists
return []
}
@ -148,7 +148,7 @@ export async function sanitizeModFirewallConfig (
throw new Error('Invalid data in data.files (content)')
}
if (entry.name.length > maxFirewallNameLength || !firewallNameRegexp.test(entry.name)) {
if (entry.name.length > maxFirewallNameLength || !firewallNameRegexp.test(entry.name as string)) {
throw new Error('Invalid name in data.files')
}
if (entry.content.length > maxFirewallFileSize) {

View File

@ -6,22 +6,57 @@ import { eachSeries } from 'async'
import type { NextFunction, Request, RequestHandler, Response } from 'express'
// Syntactic sugar to avoid try/catch in express controllers
// Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
export type RequestPromiseHandler = ((req: Request, res: Response, next: NextFunction) => Promise<any>)
function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]): RequestHandler {
// type asyncMiddleWareFunction = (req: Request, res: Response, next: NextFunction) => Promise<void>
// function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]): asyncMiddleWareFunction {
// return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
// if (!Array.isArray(fun)) {
// // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
// Promise.resolve((fun as RequestHandler)(req, res, next))
// .catch(err => { next(err) })
// return
// }
// try {
// for (const f of fun) {
// await new Promise<void>((resolve, reject) => {
// // eslint-disable-next-line @typescript-eslint/no-floating-promises
// asyncMiddleware(f)(req, res, err => {
// if (err) {
// reject(err)
// return
// }
// resolve()
// })
// })
// }
// next()
// } catch (err) {
// next(err)
// }
// }
// }
function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (Array.isArray(fun)) {
eachSeries(fun as RequestHandler[], (f, cb) => {
Promise.resolve(f(req, res, (err: any) => cb(err)))
.catch(err => next(err))
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
Promise.resolve(f(req, res, (err: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
cb(err)
}))
.catch(err => { next(err) })
}, next)
return
}
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
Promise.resolve((fun as RequestHandler)(req, res, next))
.catch(err => next(err))
.catch(err => { next(err) })
}
}

View File

@ -65,8 +65,8 @@ export class LivechatProsodyAuth {
private readonly _prosodyDomain: string
private _userTokensEnabled: boolean
private readonly _tokensPath: string
private readonly _passwords: Map<string, Password> = new Map()
private readonly _tokensInfoByJID: Map<string, LivechatTokenInfos | undefined> = new Map()
private readonly _passwords = new Map<string, Password>()
private readonly _tokensInfoByJID = new Map<string, LivechatTokenInfos | undefined>()
private readonly _secretKey: string
protected readonly _logger: {
debug: (s: string) => void
@ -122,8 +122,8 @@ export class LivechatProsodyAuth {
const nickname: string | undefined = await getUserNickname(this._options, user)
return {
jid: normalizedUsername + '@' + this._prosodyDomain,
password: password,
nickname: nickname,
password,
nickname,
type: 'peertube'
}
}
@ -136,7 +136,7 @@ export class LivechatProsodyAuth {
if (this._userTokensEnabled) {
try {
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
if (!tokensInfo || !tokensInfo.tokens.length) {
if (!tokensInfo?.tokens.length) {
return false
}
// Checking that the user is valid:
@ -159,7 +159,7 @@ export class LivechatProsodyAuth {
if (this._userTokensEnabled) {
try {
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
if (!tokensInfo || !tokensInfo.tokens.length) {
if (!tokensInfo?.tokens.length) {
return false
}
// Checking that the user is valid:
@ -247,7 +247,7 @@ export class LivechatProsodyAuth {
}
const nickname: string | undefined = await getUserNickname(this._options, user)
const jid = normalizedUsername + '@' + this._prosodyDomain
const token = await this._createToken(user.id, jid, label)
const token = await this._createToken(user.id as number, jid, label)
token.nickname = nickname
return token
@ -279,7 +279,7 @@ export class LivechatProsodyAuth {
return false
}
await this._saveTokens(user.id, jid, tokensInfo.tokens.filter(t => t.id !== id))
await this._saveTokens(user.id as number, jid, tokensInfo.tokens.filter(t => t.id !== id))
return true
}
@ -293,8 +293,8 @@ export class LivechatProsodyAuth {
const password = generatePassword(20)
this._passwords.set(normalizedUsername, {
password: password,
validity: validity
password,
validity
})
return password
}
@ -330,7 +330,7 @@ export class LivechatProsodyAuth {
const user = await this._options.peertubeHelpers.user.loadById(userId)
if (!user || user.blocked) { return false }
return true
} catch (err) {
} catch (_err) {
return false
}
}
@ -381,7 +381,7 @@ export class LivechatProsodyAuth {
this._tokensInfoByJID.set(jid, undefined)
return undefined
}
throw err
throw err as Error
}
}
@ -456,7 +456,7 @@ export class LivechatProsodyAuth {
const decipher = createDecipheriv(algorithm, this._secretKey, iv)
// FIXME: dismiss the "as any" below (dont understand why Typescript is not happy without)
return decipher.update(encrypted as any, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
return decipher.update(encrypted.toString(), outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}
public static singleton (): LivechatProsodyAuth {

View File

@ -39,7 +39,7 @@ function startProsodyCertificatesRenewCheck (options: RegisterServerOptions, con
}, checkInterval)
renew = {
timer: timer
timer
}
}

View File

@ -123,7 +123,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
}
return {
dir: dir,
dir,
pid: path.resolve(dir, 'prosody.pid'),
error: path.resolve(dir, 'prosody.err'),
log: path.resolve(dir, 'prosody.log'),
@ -217,7 +217,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
? settings['chat-terms']
: undefined
let useExternal: boolean = false
let useExternal = false
try {
const oidcs = ExternalAuthOIDC.allSingletons()
for (const oidc of oidcs) {
@ -377,7 +377,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.useBotsVirtualHost(paths.botAvatars, paths.botAvatarsFiles)
bots.moderation = await BotConfiguration.singleton().getModerationBotGlobalConf()
if (bots.moderation?.connection?.password && typeof bots.moderation.connection.password === 'string') {
valuesToHideInDiagnostic.set('BotPassword', bots.moderation.connection.password)
valuesToHideInDiagnostic.set('BotPassword', bots.moderation.connection.password as string)
}
}

View File

@ -7,7 +7,7 @@ import { getProsodyDomain } from './domain'
import { getUserNameByChannelId } from '../../database/channel'
import { BotConfiguration } from '../../configuration/bot'
interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' }
type Affiliations = Record<string, 'outcast' | 'none' | 'member' | 'admin' | 'owner'>
async function _getCommonAffiliations (options: RegisterServerOptions, _prosodyDomain: string): Promise<Affiliations> {
const r: Affiliations = {}

View File

@ -97,7 +97,7 @@ abstract class ProsodyConfigBlock {
if (!this.entries.has(name)) {
this.entries.set(name, [])
}
let entry = this.entries.get(name) as ConfigEntryValue
let entry = this.entries.get(name) ?? []
if (!Array.isArray(entry)) {
entry = [entry]
}
@ -112,7 +112,7 @@ abstract class ProsodyConfigBlock {
if (!this.entries.has(name)) {
return
}
let entry = this.entries.get(name) as ConfigEntryValue
let entry = this.entries.get(name) ?? []
if (!Array.isArray(entry)) {
entry = [entry]
}

View File

@ -139,9 +139,9 @@ async function prosodyCtl (
reject(new Error('Missing prosodyctl command executable'))
return
}
let d: string = ''
let e: string = ''
let m: string = ''
let d = ''
let e = ''
let m = ''
const cmdArgs = [
...filePaths.execCtlArgs,
'--config',
@ -196,7 +196,7 @@ async function prosodyCtl (
// (else it can cause trouble by cleaning AppImage extract too soon)
spawned.on('close', (code) => {
resolve({
code: code,
code,
stdout: d,
sterr: e,
message: m
@ -399,8 +399,8 @@ async function ensureProsodyRunning (
})
}
logger.info('Waiting for the prosody process to launch')
let count: number = 0
let processStarted: boolean = false
let count = 0
let processStarted = false
while (!processStarted && count < 5) {
count++
await sleep(500)
@ -418,8 +418,8 @@ async function ensureProsodyRunning (
return
}
logger.info('Prosody is running')
await startProsodyLogRotate(options, filePaths)
await startProsodyCertificatesRenewCheck(options, config)
startProsodyLogRotate(options, filePaths)
startProsodyCertificatesRenewCheck(options, config)
}
async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise<void> {

View File

@ -10,7 +10,7 @@ import { reloadProsody } from './ctl'
type Rotate = (file: string, options: {
count?: number
compress?: boolean
}, cb: Function) => void
}, cb: (err: any) => void) => void
const rotate: Rotate = require('log-rotate')
interface ProsodyLogRotate {
@ -27,9 +27,10 @@ async function _rotate (options: RegisterServerOptions, path: string): Promise<v
rotate(path, { count: 14, compress: false }, (err: any) => {
if (err) {
options.peertubeHelpers.logger.error('Failed to rotate file ' + path, err)
return resolve()
resolve()
return
}
return resolve()
resolve()
})
})
return p
@ -79,7 +80,7 @@ function startProsodyLogRotate (options: RegisterServerOptions, paths: ProsodyFi
}, checkInterval)
logRotate = {
timer: timer,
timer,
lastRotation: Date.now()
}
}

View File

@ -36,11 +36,11 @@ class RoomChannel {
protected room2Channel: Map<string, number> = new Map<string, number>()
protected channel2Rooms: Map<number, Map<string, true>> = new Map<number, Map<string, true>>()
protected needSync: boolean = false
protected needSync = false
protected roomConfToUpdate: Map<string, true> = new Map<string, true>()
protected syncTimeout: ReturnType<typeof setTimeout> | undefined
protected isWriting: boolean = false
protected isWriting = false
constructor (params: {
options: RegisterServerOptions
@ -113,7 +113,7 @@ class RoomChannel {
let content: string
try {
content = (await fs.promises.readFile(this.dataFilePath)).toString()
} catch (err) {
} catch (_err) {
this.logger.info('Failed reading room-channel data file (' + this.dataFilePath + '), assuming it does not exists')
return false
}
@ -122,7 +122,7 @@ class RoomChannel {
let data: any
try {
data = JSON.parse(content)
} catch (err) {
} catch (_err) {
this.logger.error('Unable to parse the content of the room-channel data file, will start with an empty database.')
return false
}
@ -233,7 +233,7 @@ class RoomChannel {
}
await fillVideoCustomFields(this.options, video)
const hasChat = await videoHasWebchat({
const hasChat = videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],
@ -405,7 +405,7 @@ class RoomChannel {
this.syncTimeout = undefined
this.logger.info('Running scheduled sync')
this.sync().then(() => {}, (err) => {
this.logger.error(err)
this.logger.error(err as string)
// We will not re-schedule the sync, to avoid flooding error log if there is an issue with the server
})
}, 100)

View File

@ -86,7 +86,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
// Now, we have two different room type: per video or per channel.
if (settings['prosody-room-type'] === 'channel') {
const matches = jid.match(/^channel\.(\d+)$/)
if (!matches || !matches[1]) {
if (!matches?.[1]) {
logger.warn(`Invalid channel room jid '${jid}'.`)
res.sendStatus(403)
return
@ -117,7 +117,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
},
await _getChannelSpecificOptions(options, channelId)
),
affiliations: affiliations
affiliations
}
RoomChannel.singleton().link(channelId, jid)
@ -172,7 +172,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
},
await _getChannelSpecificOptions(options, video.channelId)
),
affiliations: affiliations
affiliations
}
RoomChannel.singleton().link(video.channelId, jid)

View File

@ -147,7 +147,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
const additionnalMessage: string = escapeHTML(err.livechatError?.message as string ?? '')
const message: string = escapeHTML(loc('chatroom_not_accessible'))
res.status(code)
res.status(typeof code === 'number' ? code : 500)
res.send(`<!DOCTYPE html PUBLIC "-//IETF//DTD HTML 2.0//EN"><html>
<head><title>${message}</title></head>
<body>
@ -272,7 +272,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
res.status(200)
const r: ProsodyListRoomsResult = {
ok: true,
rooms: rooms
rooms
}
res.json(r)
}

View File

@ -46,7 +46,7 @@ async function initRSS (options: RegisterServerOptions): Promise<void> {
}
await fillVideoCustomFields(options, video)
const hasChat = await videoHasWebchat({
const hasChat = videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],

View File

@ -105,7 +105,7 @@ async function register (options: RegisterServerOptions): Promise<any> {
}
)
}
)
).catch(() => {})
}, () => {})
} catch (error) {
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))

View File

@ -5,7 +5,7 @@
"es6": true
},
"extends": [
"standard-with-typescript"
"eslint-config-love"
],
"globals": {},
"parser": "@typescript-eslint/parser",