Merge branch 'staging' into support-multiple-expressions

1adde74f38ff51083a652edd87cdb39891cb6ed8

Cohee <18619528+Cohee1207@users.noreply.github.com>

34 files changed, +269 -98Ignore whitespace
default/config.yaml+5 -0
@@ -85,6 +85,11 @@ cookieSecret: ''
85disableCsrfProtection: false85disableCsrfProtection: false
86# Disable startup security checks - NOT RECOMMENDED86# Disable startup security checks - NOT RECOMMENDED
87securityOverride: false87securityOverride: false
88# -- RATE LIMITING CONFIGURATION --
89rateLimiting:
90 # Use X-Real-IP header instead of socket IP for rate limiting
91 # Only enable this if you are using a properly configured reverse proxy (like Nginx/traefik/Caddy)
92 preferRealIpHeader: false
88# -- ADVANCED CONFIGURATION --93# -- ADVANCED CONFIGURATION --
89# Open the browser automatically94# Open the browser automatically
90autorun: true95autorun: true
docker/build-lib.js+1 -1
@@ -1,4 +1,4 @@
1import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';1import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js';
22
3const middleware = getWebpackServeMiddleware();3const middleware = getWebpackServeMiddleware();
4await middleware.runWebpackCompiler();4await middleware.runWebpackCompiler({ forceDist: true });
package-lock.json+35 -4
@@ -43,6 +43,7 @@
43 "ip-matching": "^2.1.2",43 "ip-matching": "^2.1.2",
44 "ip-regex": "^5.0.0",44 "ip-regex": "^5.0.0",
45 "ipaddr.js": "^2.0.1",45 "ipaddr.js": "^2.0.1",
46 "is-docker": "^3.0.0",
46 "jimp": "^0.22.10",47 "jimp": "^0.22.10",
47 "localforage": "^1.10.0",48 "localforage": "^1.10.0",
48 "lodash": "^4.17.21",49 "lodash": "^4.17.21",
@@ -4649,15 +4650,15 @@
4649 "license": "MIT"4650 "license": "MIT"
4650 },4651 },
4651 "node_modules/is-docker": {4652 "node_modules/is-docker": {
4652 "version": "2.2.1",4653 "version": "3.0.0",
4653 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",4654 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
4654 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",4655 "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
4655 "license": "MIT",4656 "license": "MIT",
4656 "bin": {4657 "bin": {
4657 "is-docker": "cli.js"4658 "is-docker": "cli.js"
4658 },4659 },
4659 "engines": {4660 "engines": {
4660 "node": ">=8"4661 "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
4661 },4662 },
4662 "funding": {4663 "funding": {
4663 "url": "https://github.com/sponsors/sindresorhus"4664 "url": "https://github.com/sponsors/sindresorhus"
@@ -4734,6 +4735,21 @@
4734 "node": ">=8"4735 "node": ">=8"
4735 }4736 }
4736 },4737 },
4738 "node_modules/is-wsl/node_modules/is-docker": {
4739 "version": "2.2.1",
4740 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
4741 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
4742 "license": "MIT",
4743 "bin": {
4744 "is-docker": "cli.js"
4745 },
4746 "engines": {
4747 "node": ">=8"
4748 },
4749 "funding": {
4750 "url": "https://github.com/sponsors/sindresorhus"
4751 }
4752 },
4737 "node_modules/isarray": {4753 "node_modules/isarray": {
4738 "version": "1.0.0",4754 "version": "1.0.0",
4739 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",4755 "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -5518,6 +5534,21 @@
5518 "url": "https://github.com/sponsors/sindresorhus"5534 "url": "https://github.com/sponsors/sindresorhus"
5519 }5535 }
5520 },5536 },
5537 "node_modules/open/node_modules/is-docker": {
5538 "version": "2.2.1",
5539 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
5540 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
5541 "license": "MIT",
5542 "bin": {
5543 "is-docker": "cli.js"
5544 },
5545 "engines": {
5546 "node": ">=8"
5547 },
5548 "funding": {
5549 "url": "https://github.com/sponsors/sindresorhus"
5550 }
5551 },
5521 "node_modules/openai": {5552 "node_modules/openai": {
5522 "version": "4.17.4",5553 "version": "4.17.4",
5523 "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",5554 "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz",
package.json+1 -0
@@ -33,6 +33,7 @@
33 "ip-matching": "^2.1.2",33 "ip-matching": "^2.1.2",
34 "ip-regex": "^5.0.0",34 "ip-regex": "^5.0.0",
35 "ipaddr.js": "^2.0.1",35 "ipaddr.js": "^2.0.1",
36 "is-docker": "^3.0.0",
36 "jimp": "^0.22.10",37 "jimp": "^0.22.10",
37 "localforage": "^1.10.0",38 "localforage": "^1.10.0",
38 "lodash": "^4.17.21",39 "lodash": "^4.17.21",
plugins.js+2 -2
@@ -8,7 +8,7 @@ import path from 'node:path';
8import process from 'node:process';8import process from 'node:process';
9import { fileURLToPath } from 'node:url';9import { fileURLToPath } from 'node:url';
1010
11import { default as git } from 'simple-git';11import { default as git, CheckRepoActions } from 'simple-git';
12import { color } from './src/util.js';12import { color } from './src/util.js';
1313
14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));14const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
@@ -49,7 +49,7 @@ async function updatePlugins() {
49 const pluginPath = path.join(pluginsPath, directory);49 const pluginPath = path.join(pluginsPath, directory);
50 const pluginRepo = git(pluginPath);50 const pluginRepo = git(pluginPath);
5151
52 const isRepo = await pluginRepo.checkIsRepo();52 const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
53 if (!isRepo) {53 if (!isRepo) {
54 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);54 console.log(`Directory ${color.yellow(directory)} is not a Git repository`);
55 continue;55 continue;
public/index.html+1 -1
@@ -4666,7 +4666,7 @@
4666 <small data-i18n="Enabled">Enabled</small>4666 <small data-i18n="Enabled">Enabled</small>
4667 </label>4667 </label>
4668 <small data-i18n="Minimum generated message length">Minimum generated message length</small>4668 <small data-i18n="Minimum generated message length">Minimum generated message length</small>
4669 <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than this, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than this, trigger an auto-swipe">4669 <input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole" title="If the generated message is shorter than these many characters, trigger an auto-swipe." data-i18n="[title]If the generated message is shorter than these many characters, trigger an auto-swipe">
4670 <small data-i18n="Blacklisted words">Blacklisted words</small>4670 <small data-i18n="Blacklisted words">Blacklisted words</small>
4671 <div class="auto_swipe">4671 <div class="auto_swipe">
4672 <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>4672 <textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" data-i18n="[placeholder]words you dont want generated separated by comma ','" placeholder="words you don't want generated separated by comma ','" class="text_pole textarea_compact" value="" autocomplete="off" rows="3"></textarea>
public/lib/eventemitter.js+43 -2
@@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') {
2424
2525
26/* Polyfill EventEmitter. */26/* Polyfill EventEmitter. */
27var EventEmitter = function () {27/**
28 * Creates an event emitter.
29 * @param {string[]} autoFireAfterEmit Auto-fire event names
30 */
31var EventEmitter = function (autoFireAfterEmit = []) {
28 this.events = {};32 this.events = {};
33 this.autoFireLastArgs = new Map();
34 this.autoFireAfterEmit = new Set(autoFireAfterEmit);
29};35};
3036
37/**
38 * Adds a listener to an event.
39 * @param {string} event Event name
40 * @param {function} listener Event listener
41 * @returns
42 */
31EventEmitter.prototype.on = function (event, listener) {43EventEmitter.prototype.on = function (event, listener) {
32 // Unknown event used by external libraries?44 // Unknown event used by external libraries?
33 if (event === undefined) {45 if (event === undefined) {
@@ -40,6 +52,10 @@ EventEmitter.prototype.on = function (event, listener) {
40 }52 }
4153
42 this.events[event].push(listener);54 this.events[event].push(listener);
55
56 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
57 listener.apply(this, this.autoFireLastArgs.get(event));
58 }
43};59};
4460
45/**61/**
@@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) {
60 }76 }
6177
62 events.push(listener);78 events.push(listener);
79
80 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
81 listener.apply(this, this.autoFireLastArgs.get(event));
82 }
63}83}
6484
65/**85/**
@@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) {
80 }100 }
81101
82 events.unshift(listener);102 events.unshift(listener);
103
104 if (this.autoFireAfterEmit.has(event) && this.autoFireLastArgs.has(event)) {
105 listener.apply(this, this.autoFireLastArgs.get(event));
106 }
83}107}
84108
109/**
110 * Removes a listener from an event.
111 * @param {string} event Event name
112 * @param {function} listener Event listener
113 */
85EventEmitter.prototype.removeListener = function (event, listener) {114EventEmitter.prototype.removeListener = function (event, listener) {
86 var idx;115 var idx;
87116
@@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) {
94 }123 }
95};124};
96125
126/**
127 * Emits an event with optional arguments.
128 * @param {string} event Event name
129 */
97EventEmitter.prototype.emit = async function (event) {130EventEmitter.prototype.emit = async function (event) {
98 let args = [].slice.call(arguments, 1);131 let args = [].slice.call(arguments, 1);
99 if (localStorage.getItem('eventTracing') === 'true') {132 if (localStorage.getItem('eventTracing') === 'true') {
@@ -118,6 +151,10 @@ EventEmitter.prototype.emit = async function (event) {
118 }151 }
119 }152 }
120 }153 }
154
155 if (this.autoFireAfterEmit.has(event)) {
156 this.autoFireLastArgs.set(event, args);
157 }
121};158};
122159
123EventEmitter.prototype.emitAndWait = function (event) {160EventEmitter.prototype.emitAndWait = function (event) {
@@ -144,10 +181,14 @@ EventEmitter.prototype.emitAndWait = function (event) {
144 }181 }
145 }182 }
146 }183 }
184
185 if (this.autoFireAfterEmit.has(event)) {
186 this.autoFireLastArgs.set(event, args);
187 }
147};188};
148189
149EventEmitter.prototype.once = function (event, listener) {190EventEmitter.prototype.once = function (event, listener) {
150 this.on(event, function g () {191 this.on(event, function g() {
151 this.removeListener(event, g);192 this.removeListener(event, g);
152 listener.apply(this, arguments);193 listener.apply(this, arguments);
153 });194 });
public/locales/ar-sa.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "السحب التلقائي",709 "Auto-swipe": "السحب التلقائي",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "تمكين وظيفة السحب التلقائي. الإعدادات في هذا القسم تؤثر فقط عند تمكين السحب التلقائي",
711 "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",711 "Minimum generated message length": "الحد الأدنى لطول الرسالة المولدة",
712 "If the generated message is shorter than this, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "إذا كانت الرسالة المولدة أقصر من هذا، فتحريض السحب التلقائي",
713 "Blacklisted words": "الكلمات الممنوعة",713 "Blacklisted words": "الكلمات الممنوعة",
714 "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",714 "words you dont want generated separated by comma ','": "الكلمات التي لا تريد توليدها مفصولة بفاصلة ','",
715 "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",715 "Blacklisted word count to swipe": "عدد الكلمات الممنوعة للسحب",
public/locales/de-de.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Automatisches Wischen",709 "Auto-swipe": "Automatisches Wischen",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Aktiviere die Auto-Wisch-Funktion. Einstellungen in diesem Abschnitt haben nur dann Auswirkungen, wenn das automatische Wischen aktiviert ist",
711 "Minimum generated message length": "Minimale generierte Nachrichtenlänge",711 "Minimum generated message length": "Minimale generierte Nachrichtenlänge",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Wenn die generierte Nachricht kürzer ist als diese, löse automatisches Wischen aus",
713 "Blacklisted words": "Verbotene Wörter",713 "Blacklisted words": "Verbotene Wörter",
714 "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",714 "words you dont want generated separated by comma ','": "Wörter, die du nicht generiert haben möchtest, durch Komma ',' getrennt",
715 "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",715 "Blacklisted word count to swipe": "Anzahl der verbotenen Wörter, um zu wischen",
public/locales/es-es.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Deslizamiento automático",709 "Auto-swipe": "Deslizamiento automático",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Habilitar la función de deslizamiento automático. La configuración en esta sección solo tiene efecto cuando el deslizamiento automático está habilitado",
711 "Minimum generated message length": "Longitud mínima del mensaje generado",711 "Minimum generated message length": "Longitud mínima del mensaje generado",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Si el mensaje generado es más corto que esto, activar un deslizamiento automático",
713 "Blacklisted words": "Palabras prohibidas",713 "Blacklisted words": "Palabras prohibidas",
714 "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",714 "words you dont want generated separated by comma ','": "palabras que no desea generar separadas por coma ','",
715 "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",715 "Blacklisted word count to swipe": "Número de palabras prohibidas para deslizar",
public/locales/fr-fr.json+1 -1
@@ -656,7 +656,7 @@
656 "Auto-swipe": "Balayage automatique",656 "Auto-swipe": "Balayage automatique",
657 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",657 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Activer la fonction de balayage automatique. Les paramètres de cette section n'ont d'effet que lorsque le balayage automatique est activé",
658 "Minimum generated message length": "Longueur minimale du message généré",658 "Minimum generated message length": "Longueur minimale du message généré",
659 "If the generated message is shorter than this, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",659 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Si le message généré est plus court que cela, déclenchez un balayage automatique",
660 "Blacklisted words": "Mots en liste noire",660 "Blacklisted words": "Mots en liste noire",
661 "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",661 "words you dont want generated separated by comma ','": "mots que vous ne voulez pas générer séparés par des virgules ','",
662 "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",662 "Blacklisted word count to swipe": "Nombre de mots en liste noire pour balayer",
public/locales/is-is.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Sjálfvirkur sveip",709 "Auto-swipe": "Sjálfvirkur sveip",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Virkjaðu sjálfvirka sveiflugerð. Stillingar í þessum hluta hafa aðeins áhrif þegar sjálfvirkur sveiflugerð er virk",
711 "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",711 "Minimum generated message length": "Lágmarks lengd á mynduðum skilaboðum",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Ef mynduðu skilaboðin eru styttri en þessi, kallaðu fram sjálfvirkar sveiflugerðar",
713 "Blacklisted words": "Svört orð",713 "Blacklisted words": "Svört orð",
714 "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",714 "words you dont want generated separated by comma ','": "orð sem þú vilt ekki að framleiða aðskilin með kommu ','",
715 "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",715 "Blacklisted word count to swipe": "Fjöldi svörtra orða til að sveipa",
public/locales/it-it.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Auto-swipe",709 "Auto-swipe": "Auto-swipe",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Abilita la funzione di auto-swipe. Le impostazioni in questa sezione hanno effetto solo quando l'auto-swipe è abilitato",
711 "Minimum generated message length": "Lunghezza minima del messaggio generato",711 "Minimum generated message length": "Lunghezza minima del messaggio generato",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Se il messaggio generato è più breve di questo, attiva un'automatica rimozione",
713 "Blacklisted words": "Parole in blacklist",713 "Blacklisted words": "Parole in blacklist",
714 "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",714 "words you dont want generated separated by comma ','": "parole che non vuoi generate separate da virgola ','",
715 "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",715 "Blacklisted word count to swipe": "Numero di parole in blacklist per attivare un'automatica rimozione",
public/locales/ja-jp.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "オートスワイプ",709 "Auto-swipe": "オートスワイプ",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "自動スワイプ機能を有効にします。このセクションの設定は、自動スワイプが有効になっている場合にのみ効果があります",
711 "Minimum generated message length": "生成されたメッセージの最小長",711 "Minimum generated message length": "生成されたメッセージの最小長",
712 "If the generated message is shorter than this, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "生成されたメッセージがこれよりも短い場合、自動スワイプをトリガーします",
713 "Blacklisted words": "ブラックリストされた単語",713 "Blacklisted words": "ブラックリストされた単語",
714 "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",714 "words you dont want generated separated by comma ','": "コンマ ',' で区切られた生成したくない単語",
715 "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",715 "Blacklisted word count to swipe": "スワイプするブラックリストされた単語の数",
public/locales/ko-kr.json+1 -1
@@ -724,7 +724,7 @@
724 "Auto-swipe": "자동 스와이프",724 "Auto-swipe": "자동 스와이프",
725 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",725 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "자동 스와이프 기능을 활성화합니다. 이 섹션의 설정은 자동 스와이프가 활성화되었을 때만 영향을 미칩니다",
726 "Minimum generated message length": "생성된 메시지 최소 길이",726 "Minimum generated message length": "생성된 메시지 최소 길이",
727 "If the generated message is shorter than this, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",727 "If the generated message is shorter than these many characters, trigger an auto-swipe": "생성된 메시지가이보다 짧으면 자동 스와이프를 트리거합니다",
728 "Blacklisted words": "금지어",728 "Blacklisted words": "금지어",
729 "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",729 "words you dont want generated separated by comma ','": "쉼표로 구분된 생성하지 않으려는 단어",
730 "Blacklisted word count to swipe": "스와이프할 금지어 개수",730 "Blacklisted word count to swipe": "스와이프할 금지어 개수",
public/locales/nl-nl.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Automatisch vegen",709 "Auto-swipe": "Automatisch vegen",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Schakel de automatische-vegen functie in. Instellingen in dit gedeelte hebben alleen effect wanneer automatisch vegen is ingeschakeld",
711 "Minimum generated message length": "Minimale gegenereerde berichtlengte",711 "Minimum generated message length": "Minimale gegenereerde berichtlengte",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Als het gegenereerde bericht korter is dan dit, activeer dan een automatische veeg",
713 "Blacklisted words": "Verboden woorden",713 "Blacklisted words": "Verboden woorden",
714 "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",714 "words you dont want generated separated by comma ','": "woorden die je niet gegenereerd wilt hebben gescheiden door komma ','",
715 "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",715 "Blacklisted word count to swipe": "Aantal verboden woorden om te vegen",
public/locales/pt-pt.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Auto-swipe",709 "Auto-swipe": "Auto-swipe",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Ativar a função de auto-swipe. As configurações nesta seção só têm efeito quando o auto-swipe está ativado",
711 "Minimum generated message length": "Comprimento mínimo da mensagem gerada",711 "Minimum generated message length": "Comprimento mínimo da mensagem gerada",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Se a mensagem gerada for mais curta que isso, acione um auto-swipe",
713 "Blacklisted words": "Palavras proibidas",713 "Blacklisted words": "Palavras proibidas",
714 "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",714 "words you dont want generated separated by comma ','": "palavras que você não quer geradas separadas por vírgula ','",
715 "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",715 "Blacklisted word count to swipe": "Contagem de palavras proibidas para swipe",
public/locales/ru-ru.json+2 -2
@@ -426,7 +426,7 @@
426 "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",426 "Requests logprobs from the API for the Token Probabilities feature": "Запросить логпробы из API для функции Token Probabilities.",
427 "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",427 "Automatically reject and re-generate AI message based on configurable criteria": "Автоматическое отклонение и повторная генерация сообщений AI на основе настраиваемых критериев.",
428 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",428 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Включить авто-свайп. Настройки в этом разделе действуют только при включенном авто-свайпе.",
429 "If the generated message is shorter than this, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",429 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Если сгенерированное сообщение короче этого значения, срабатывает авто-свайп.",
430 "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",430 "Reload and redraw the currently open chat": "Перезагрузить и перерисовать открытый в данный момент чат.",
431 "Auto-Expand Message Actions": "Развернуть действия",431 "Auto-Expand Message Actions": "Развернуть действия",
432 "Persona Management": "Управление персоной",432 "Persona Management": "Управление персоной",
@@ -2205,4 +2205,4 @@
2205 "Tokenized text:": "Токенизированный текст:",2205 "Tokenized text:": "Токенизированный текст:",
2206 "Token IDs:": "Идентификаторы токенов:",2206 "Token IDs:": "Идентификаторы токенов:",
2207 "Tokens:": "Токенов:"2207 "Tokens:": "Токенов:"
2208}
2208 \ No newline at end of file \ No newline at end of file
2208}
public/locales/uk-ua.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Автоматичний змах",709 "Auto-swipe": "Автоматичний змах",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Вмикає функцію автоматичного змаху. Налаштування в цьому розділі діють лише тоді, коли увімкнено автоматичний змах",
711 "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",711 "Minimum generated message length": "Мінімальна довжина згенерованого повідомлення",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Якщо згенероване повідомлення коротше за це, викликайте автоматичний змаху",
713 "Blacklisted words": "Список заборонених слів",713 "Blacklisted words": "Список заборонених слів",
714 "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",714 "words you dont want generated separated by comma ','": "слова, які ви не хочете генерувати, розділені комою ','",
715 "Blacklisted word count to swipe": "Кількість заборонених слів для змаху",715 "Blacklisted word count to swipe": "Кількість заборонених слів для змаху",
public/locales/vi-vn.json+1 -1
@@ -709,7 +709,7 @@
709 "Auto-swipe": "Tự động vuốt",709 "Auto-swipe": "Tự động vuốt",
710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",710 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "Bật chức năng tự động vuốt. Các cài đặt trong phần này chỉ có tác dụng khi tự động vuốt được bật",
711 "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",711 "Minimum generated message length": "Độ dài tối thiểu của tin nhắn được tạo",
712 "If the generated message is shorter than this, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",712 "If the generated message is shorter than these many characters, trigger an auto-swipe": "Nếu tin nhắn được tạo ra ngắn hơn điều này, kích hoạt tự động vuốt",
713 "Blacklisted words": "Từ trong danh sách đen",713 "Blacklisted words": "Từ trong danh sách đen",
714 "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",714 "words you dont want generated separated by comma ','": "các từ bạn không muốn được tạo ra được phân tách bằng dấu phẩy ','",
715 "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",715 "Blacklisted word count to swipe": "Số từ trong danh sách đen để vuốt",
public/locales/zh-cn.json+1 -1
@@ -804,7 +804,7 @@
804 "Auto-swipe": "自动滑动",804 "Auto-swipe": "自动滑动",
805 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",805 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "启用自动滑动功能。仅当启用自动滑动时,本节中的设置才会生效",
806 "Minimum generated message length": "生成的消息的最小长度",806 "Minimum generated message length": "生成的消息的最小长度",
807 "If the generated message is shorter than this, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",807 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的消息短于此长度,则触发自动滑动",
808 "Blacklisted words": "屏蔽词",808 "Blacklisted words": "屏蔽词",
809 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",809 "words you dont want generated separated by comma ','": "不想生成的词语,用半角逗号“,”分隔",
810 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",810 "Blacklisted word count to swipe": "触发滑动的黑名单词语数量",
public/locales/zh-tw.json+1 -1
@@ -710,7 +710,7 @@
710 "Auto-swipe": "自動滑動",710 "Auto-swipe": "自動滑動",
711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",711 "Enable the auto-swipe function. Settings in this section only have an effect when auto-swipe is enabled": "啟用自動滑動功能。此部分的設定僅在啟用自動滑動時有效。",
712 "Minimum generated message length": "生成訊息的最小長度",712 "Minimum generated message length": "生成訊息的最小長度",
713 "If the generated message is shorter than this, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",713 "If the generated message is shorter than these many characters, trigger an auto-swipe": "如果生成的訊息比這個短,將觸發自動滑動。",
714 "Blacklisted words": "黑名單詞語",714 "Blacklisted words": "黑名單詞語",
715 "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",715 "words you dont want generated separated by comma ','": "您不想生成的文字,使用逗號分隔",
716 "Blacklisted word count to swipe": "滑動的黑名單詞語數量",716 "Blacklisted word count to swipe": "滑動的黑名單詞語數量",
public/script.js+1 -1
@@ -512,7 +512,7 @@ export const event_types = {
512 TOOL_CALLS_RENDERED: 'tool_calls_rendered',512 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
513};513};
514514
515export const eventSource = new EventEmitter();515export const eventSource = new EventEmitter([event_types.APP_READY]);
516516
517eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);517eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
518518
public/scripts/chats.js+1 -1
@@ -1487,7 +1487,7 @@ jQuery(function () {
1487 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),1487 ...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
1488 ];1488 ];
14891489
1490 download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');1490 download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
1491 });1491 });
14921492
1493 // Do not change. #attachFile is added by extension.1493 // Do not change. #attachFile is added by extension.
public/scripts/reasoning.js+3 -2
@@ -338,14 +338,15 @@ export class ReasoningHandler {
338 return mesChanged;338 return mesChanged;
339 }339 }
340340
341 if (this.state === ReasoningState.None) {341 if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
342 // If streamed message starts with the opening, cut it out and put all inside reasoning342 // If streamed message starts with the opening, cut it out and put all inside reasoning
343 if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {343 if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
344 this.#isParsingReasoning = true;344 this.#isParsingReasoning = true;
345345
346 // Manually set starting state here, as we might already have received the ending suffix346 // Manually set starting state here, as we might already have received the ending suffix
347 this.state = ReasoningState.Thinking;347 this.state = ReasoningState.Thinking;
348 this.startTime = this.initialTime;348 this.startTime = this.startTime ?? this.initialTime;
349 this.endTime = null;
349 }350 }
350 }351 }
351352
public/scripts/user.js+26 -0
@@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti
9export let currentUser = null;9export let currentUser = null;
10export let accountsEnabled = false;10export let accountsEnabled = false;
1111
12// Extend the session every 30 minutes
13const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
14
12/**15/**
13 * Enable or disable user account controls in the UI.16 * Enable or disable user account controls in the UI.
14 * @param {boolean} isEnabled User account controls enabled17 * @param {boolean} isEnabled User account controls enabled
@@ -894,6 +897,24 @@ async function slugify(text) {
894 }897 }
895}898}
896899
900/**
901 * Pings the server to extend the user session.
902 */
903async function extendUserSession() {
904 try {
905 const response = await fetch('/api/ping?extend=1', {
906 method: 'GET',
907 headers: getRequestHeaders(),
908 });
909
910 if (!response.ok) {
911 throw new Error('Ping did not succeed', { cause: response.status });
912 }
913 } catch (error) {
914 console.error('Failed to extend user session', error);
915 }
916}
917
897jQuery(() => {918jQuery(() => {
898 $('#logout_button').on('click', () => {919 $('#logout_button').on('click', () => {
899 logout();920 logout();
@@ -904,4 +925,9 @@ jQuery(() => {
904 $('#account_button').on('click', () => {925 $('#account_button').on('click', () => {
905 openUserProfile();926 openUserProfile();
906 });927 });
928 setInterval(async () => {
929 if (currentUser) {
930 await extendUserSession();
931 }
932 }, SESSION_EXTEND_INTERVAL);
907});933});
public/style.css+22 -20
@@ -55,6 +55,10 @@
55 --interactable-outline-color: var(--white100);55 --interactable-outline-color: var(--white100);
56 --interactable-outline-color-faint: var(--white20a);56 --interactable-outline-color-faint: var(--white20a);
5757
58 --reasoning-body-color: var(--SmartThemeEmColor);
59 --reasoning-em-color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
60 --reasoning-saturation: 0.5;
61
5862
59 /*Default Theme, will be changed by ToolCool Color Picker*/63 /*Default Theme, will be changed by ToolCool Color Picker*/
60 --SmartThemeBodyColor: rgb(220, 220, 210);64 --SmartThemeBodyColor: rgb(220, 220, 210);
@@ -348,13 +352,13 @@ input[type='checkbox']:focus-visible {
348352
349.mes_reasoning {353.mes_reasoning {
350 display: block;354 display: block;
351 border-left: 2px solid var(--SmartThemeEmColor);355 border-left: 2px solid var(--reasoning-body-color);
352 border-radius: 2px;356 border-radius: 2px;
353 padding: 5px;357 padding: 5px;
354 padding-left: 14px;358 padding-left: 14px;
355 margin-bottom: 0.5em;359 margin-bottom: 0.5em;
356 overflow-y: auto;360 overflow-y: auto;
357 color: var(--SmartThemeEmColor);361 color: hsl(from var(--reasoning-body-color) h calc(s * var(--reasoning-saturation)) l);
358}362}
359363
360.mes_reasoning_details {364.mes_reasoning_details {
@@ -374,18 +378,6 @@ input[type='checkbox']:focus-visible {
374 margin-bottom: 0;378 margin-bottom: 0;
375}379}
376380
377.mes_reasoning em,
378.mes_reasoning i,
379.mes_reasoning u,
380.mes_reasoning q,
381.mes_reasoning blockquote {
382 filter: saturate(0.5);
383}
384
385.mes_reasoning_details .mes_reasoning em {
386 color: color-mix(in srgb, var(--SmartThemeEmColor) 67%, var(--SmartThemeBlurTintColor) 33%);
387}
388
389.mes_reasoning_header_block {381.mes_reasoning_header_block {
390 flex-grow: 1;382 flex-grow: 1;
391}383}
@@ -461,26 +453,36 @@ input[type='checkbox']:focus-visible {
461}453}
462454
463.mes_text i,455.mes_text i,
464.mes_text em,456.mes_text em {
457 color: var(--SmartThemeEmColor);
458}
465.mes_reasoning i,459.mes_reasoning i,
466.mes_reasoning em {460.mes_reasoning em {
467 color: var(--SmartThemeEmColor);461 color: hsl(from var(--reasoning-em-color) h calc(s * var(--reasoning-saturation)) l);
468}462}
469463
470.mes_text q i,464.mes_text q i,
471.mes_text q em {465.mes_text q em {
472 color: inherit;466 color: inherit;
473}467}
468.mes_reasoning q i,
469.mes_reasoning q em {
470 color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
471}
474472
475.mes_text u,473.mes_text u {
476.mes_reasoning u {
477 color: var(--SmartThemeUnderlineColor);474 color: var(--SmartThemeUnderlineColor);
478}475}
476.mes_reasoning u {
477 color: hsl(from var(--SmartThemeUnderlineColor) h calc(s * var(--reasoning-saturation)) l);
478}
479479
480.mes_text q,480.mes_text q {
481.mes_reasoning q {
482 color: var(--SmartThemeQuoteColor);481 color: var(--SmartThemeQuoteColor);
483}482}
483.mes_reasoning q {
484 color: hsl(from var(--SmartThemeQuoteColor) h calc(s * var(--reasoning-saturation)) l);
485}
484486
485.mes_text font[color] em,487.mes_text font[color] em,
486.mes_text font[color] i,488.mes_text font[color] i,
server.js+7 -1
@@ -556,7 +556,13 @@ app.use('/api/users', usersPublicRouter);
556556
557// Everything below this line requires authentication557// Everything below this line requires authentication
558app.use(requireLoginMiddleware);558app.use(requireLoginMiddleware);
559app.get('/api/ping', (_, response) => response.sendStatus(204));559app.get('/api/ping', (request, response) => {
560 if (request.query.extend && request.session) {
561 request.session.touch = Date.now();
562 }
563
564 response.sendStatus(204);
565});
560566
561// File uploads567// File uploads
562app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));568app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar'));
src/endpoints/extensions.js+1 -1
@@ -230,7 +230,7 @@ router.post('/version', jsonParser, async (request, response) => {
230 } catch (error) {230 } catch (error) {
231 // it is not a git repo, or has no commits yet, or is a bare repo231 // it is not a git repo, or has no commits yet, or is a bare repo
232 // not possible to update it, most likely can't get the branch name either232 // not possible to update it, most likely can't get the branch name either
233 return response.send({ currentBranchName: null, currentCommitHash, isUpToDate: true, remoteUrl: null });233 return response.send({ currentBranchName: '', currentCommitHash: '', isUpToDate: true, remoteUrl: '' });
234 }234 }
235235
236 const currentBranch = await git.cwd(extensionPath).branch();236 const currentBranch = await git.cwd(extensionPath).branch();
src/endpoints/users-public.js+10 -7
@@ -3,13 +3,16 @@ import crypto from 'node:crypto';
3import storage from 'node-persist';3import storage from 'node-persist';
4import express from 'express';4import express from 'express';
5import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';5import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
6import { jsonParser, getIpFromRequest } from '../express-common.js';6import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js';
7import { color, Cache, getConfigValue } from '../util.js';7import { color, Cache, getConfigValue } from '../util.js';
8import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';8import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
99
10const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);10const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false);
11const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false);
11const MFA_CACHE = new Cache(5 * 60 * 1000);12const MFA_CACHE = new Cache(5 * 60 * 1000);
1213
14const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request);
15
13export const router = express.Router();16export const router = express.Router();
14const loginLimiter = new RateLimiterMemory({17const loginLimiter = new RateLimiterMemory({
15 points: 5,18 points: 5,
@@ -60,7 +63,7 @@ router.post('/login', jsonParser, async (request, response) => {
60 return response.status(400).json({ error: 'Missing required fields' });63 return response.status(400).json({ error: 'Missing required fields' });
61 }64 }
6265
63 const ip = getIpFromRequest(request);66 const ip = getIpAddress(request);
64 await loginLimiter.consume(ip);67 await loginLimiter.consume(ip);
6568
66 /** @type {import('../users.js').User} */69 /** @type {import('../users.js').User} */
@@ -92,7 +95,7 @@ router.post('/login', jsonParser, async (request, response) => {
92 return response.json({ handle: user.handle });95 return response.json({ handle: user.handle });
93 } catch (error) {96 } catch (error) {
94 if (error instanceof RateLimiterRes) {97 if (error instanceof RateLimiterRes) {
95 console.error('Login failed: Rate limited from', getIpFromRequest(request));98 console.error('Login failed: Rate limited from', getIpAddress(request));
96 return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });99 return response.status(429).send({ error: 'Too many attempts. Try again later or recover your password.' });
97 }100 }
98101
@@ -108,7 +111,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
108 return response.status(400).json({ error: 'Missing required fields' });111 return response.status(400).json({ error: 'Missing required fields' });
109 }112 }
110113
111 const ip = getIpFromRequest(request);114 const ip = getIpAddress(request);
112 await recoverLimiter.consume(ip);115 await recoverLimiter.consume(ip);
113116
114 /** @type {import('../users.js').User} */117 /** @type {import('../users.js').User} */
@@ -132,7 +135,7 @@ router.post('/recover-step1', jsonParser, async (request, response) => {
132 return response.sendStatus(204);135 return response.sendStatus(204);
133 } catch (error) {136 } catch (error) {
134 if (error instanceof RateLimiterRes) {137 if (error instanceof RateLimiterRes) {
135 console.error('Recover step 1 failed: Rate limited from', getIpFromRequest(request));138 console.error('Recover step 1 failed: Rate limited from', getIpAddress(request));
136 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });139 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
137 }140 }
138141
@@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
150153
151 /** @type {import('../users.js').User} */154 /** @type {import('../users.js').User} */
152 const user = await storage.getItem(toKey(request.body.handle));155 const user = await storage.getItem(toKey(request.body.handle));
153 const ip = getIpFromRequest(request);156 const ip = getIpAddress(request);
154157
155 if (!user) {158 if (!user) {
156 console.error('Recover step 2 failed: User', request.body.handle, 'not found');159 console.error('Recover step 2 failed: User', request.body.handle, 'not found');
@@ -186,7 +189,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => {
186 return response.sendStatus(204);189 return response.sendStatus(204);
187 } catch (error) {190 } catch (error) {
188 if (error instanceof RateLimiterRes) {191 if (error instanceof RateLimiterRes) {
189 console.error('Recover step 2 failed: Rate limited from', getIpFromRequest(request));192 console.error('Recover step 2 failed: Rate limited from', getIpAddress(request));
190 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });193 return response.status(429).send({ error: 'Too many attempts. Try again later or contact your admin.' });
191 }194 }
192195
src/express-common.js+14 -0
@@ -25,3 +25,17 @@ export function getIpFromRequest(req) {
25 }25 }
26 return clientIp;26 return clientIp;
27}27}
28
29/**
30 * Gets the IP address of the client when behind reverse proxy using x-real-ip header, falls back to socket remote address.
31 * This function should be used when the application is running behind a reverse proxy (e.g., Nginx, traefik, Caddy...).
32 * @param {import('express').Request} req Request object
33 * @returns {string} IP address of the client
34 */
35export function getRealIpFromHeader(req) {
36 if (req.headers['x-real-ip']) {
37 return req.headers['x-real-ip'].toString();
38 }
39
40 return getIpFromRequest(req);
41}
src/middleware/webpack-serve.js+9 -5
@@ -1,11 +1,8 @@
1import path from 'node:path';1import path from 'node:path';
2import webpack from 'webpack';2import webpack from 'webpack';
3import { publicLibConfig } from '../../webpack.config.js';3import getPublicLibConfig from '../../webpack.config.js';
44
5export default function getWebpackServeMiddleware() {5export default function getWebpackServeMiddleware() {
6 const outputPath = publicLibConfig.output?.path;
7 const outputFile = publicLibConfig.output?.filename;
8
9 /**6 /**
10 * A very spartan recreation of webpack-dev-middleware.7 * A very spartan recreation of webpack-dev-middleware.
11 * @param {import('express').Request} req Request object.8 * @param {import('express').Request} req Request object.
@@ -14,6 +11,10 @@ export default function getWebpackServeMiddleware() {
14 * @type {import('express').RequestHandler}11 * @type {import('express').RequestHandler}
15 */12 */
16 function devMiddleware(req, res, next) {13 function devMiddleware(req, res, next) {
14 const publicLibConfig = getPublicLibConfig();
15 const outputPath = publicLibConfig.output?.path;
16 const outputFile = publicLibConfig.output?.filename;
17
17 if (req.method === 'GET' && path.parse(req.path).base === outputFile) {18 if (req.method === 'GET' && path.parse(req.path).base === outputFile) {
18 return res.sendFile(outputFile, { root: outputPath });19 return res.sendFile(outputFile, { root: outputPath });
19 }20 }
@@ -23,9 +24,12 @@ export default function getWebpackServeMiddleware() {
2324
24 /**25 /**
25 * Wait until Webpack is done compiling.26 * Wait until Webpack is done compiling.
27 * @param {object} param Parameters.
28 * @param {boolean} [param.forceDist] Whether to force the use the /dist folder.
26 * @returns {Promise<void>}29 * @returns {Promise<void>}
27 */30 */
28 devMiddleware.runWebpackCompiler = () => {31 devMiddleware.runWebpackCompiler = ({ forceDist = false } = {}) => {
32 const publicLibConfig = getPublicLibConfig(forceDist);
29 const compiler = webpack(publicLibConfig);33 const compiler = webpack(publicLibConfig);
3034
31 return new Promise((resolve) => {35 return new Promise((resolve) => {
src/plugin-loader.js+2 -2
@@ -3,7 +3,7 @@ import path from 'node:path';
3import url from 'node:url';3import url from 'node:url';
44
5import express from 'express';5import express from 'express';
6import { default as git } from 'simple-git';6import { default as git, CheckRepoActions } from 'simple-git';
7import { sync as commandExistsSync } from 'command-exists';7import { sync as commandExistsSync } from 'command-exists';
8import { getConfigValue, color } from './util.js';8import { getConfigValue, color } from './util.js';
99
@@ -256,7 +256,7 @@ async function updatePlugins(pluginsPath) {
256 const pluginPath = path.join(pluginsPath, directory);256 const pluginPath = path.join(pluginsPath, directory);
257 const pluginRepo = git(pluginPath);257 const pluginRepo = git(pluginPath);
258258
259 const isRepo = await pluginRepo.checkIsRepo();259 const isRepo = await pluginRepo.checkIsRepo(CheckRepoActions.IS_REPO_ROOT);
260 if (!isRepo) {260 if (!isRepo) {
261 continue;261 continue;
262 }262 }
webpack.config.js+69 -32
@@ -1,35 +1,72 @@
1import process from 'node:process';1import process from 'node:process';
2import path from 'node:path';2import path from 'node:path';
3import isDocker from 'is-docker';
34
4/** @type {import('webpack').Configuration} */5/**
5export const publicLibConfig = {6 * Get the Webpack configuration for the public/lib.js file.
6 mode: 'production',7 * 1. Docker has got cache and the output file pre-baked.
7 entry: './public/lib.js',8 * 2. Non-Docker environments use the global DATA_ROOT variable to determine the cache and output directories.
8 cache: {9 * @param {boolean} forceDist Whether to force the use the /dist folder.
9 type: 'filesystem',10 * @returns {import('webpack').Configuration}
10 cacheDirectory: path.resolve(process.cwd(), 'dist/webpack'),11 * @throws {Error} If the DATA_ROOT variable is not set.
11 store: 'pack',12 * */
12 compression: 'gzip',13export default function getPublicLibConfig(forceDist = false) {
13 },14 function getCacheDirectory() {
14 devtool: false,15 if (forceDist || isDocker()) {
15 watch: false,16 return path.resolve(process.cwd(), 'dist/webpack');
16 module: {},17 }
17 stats: {18
18 preset: 'minimal',19 if (typeof globalThis.DATA_ROOT === 'string') {
19 assets: false,20 return path.resolve(globalThis.DATA_ROOT, '_webpack', 'cache');
20 modules: false,21 }
21 colors: true,22
22 timings: true,23 throw new Error('DATA_ROOT variable is not set.');
23 },24 }
24 experiments: {25
25 outputModule: true,26 function getOutputDirectory() {
26 },27 if (forceDist || isDocker()) {
27 performance: {28 return path.resolve(process.cwd(), 'dist');
28 hints: false,29 }
29 },30
30 output: {31 if (typeof globalThis.DATA_ROOT === 'string') {
31 path: path.resolve(process.cwd(), 'dist'),32 return path.resolve(globalThis.DATA_ROOT, '_webpack', 'output');
32 filename: 'lib.js',33 }
33 libraryTarget: 'module',34
34 },35 throw new Error('DATA_ROOT variable is not set.');
35};36 }
37
38 const cacheDirectory = getCacheDirectory();
39 const outputDirectory = getOutputDirectory();
40
41 return {
42 mode: 'production',
43 entry: './public/lib.js',
44 cache: {
45 type: 'filesystem',
46 cacheDirectory: cacheDirectory,
47 store: 'pack',
48 compression: 'gzip',
49 },
50 devtool: false,
51 watch: false,
52 module: {},
53 stats: {
54 preset: 'minimal',
55 assets: false,
56 modules: false,
57 colors: true,
58 timings: true,
59 },
60 experiments: {
61 outputModule: true,
62 },
63 performance: {
64 hints: false,
65 },
66 output: {
67 path: outputDirectory,
68 filename: 'lib.js',
69 libraryTarget: 'module',
70 },
71 };
72}