Merge branch 'staging' into support-multiple-expressions
| @@ -85,6 +85,11 @@ cookieSecret: '' | |||
| 85 | disableCsrfProtection: false | 85 | disableCsrfProtection: false |
| 86 | # Disable startup security checks - NOT RECOMMENDED | 86 | # Disable startup security checks - NOT RECOMMENDED |
| 87 | securityOverride: false | 87 | securityOverride: false |
| 88 | # -- RATE LIMITING CONFIGURATION -- | ||
| 89 | rateLimiting: | ||
| 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 automatically | 94 | # Open the browser automatically |
| 90 | autorun: true | 95 | autorun: true |
| @@ -1,4 +1,4 @@ | |||
| 1 | import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js'; | 1 | import getWebpackServeMiddleware from '../src/middleware/webpack-serve.js'; |
| 2 | 2 | ||
| 3 | const middleware = getWebpackServeMiddleware(); | 3 | const middleware = getWebpackServeMiddleware(); |
| 4 | await middleware.runWebpackCompiler(); | 4 | await middleware.runWebpackCompiler({ forceDist: true }); |
| @@ -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", |
| @@ -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", |
| @@ -8,7 +8,7 @@ import path from 'node:path'; | |||
| 8 | import process from 'node:process'; | 8 | import process from 'node:process'; |
| 9 | import { fileURLToPath } from 'node:url'; | 9 | import { fileURLToPath } from 'node:url'; |
| 10 | 10 | ||
| 11 | import { default as git } from 'simple-git'; | 11 | import { default as git, CheckRepoActions } from 'simple-git'; |
| 12 | import { color } from './src/util.js'; | 12 | import { color } from './src/util.js'; |
| 13 | 13 | ||
| 14 | const __dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); | 14 | const __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); |
| 51 | 51 | ||
| 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; |
| @@ -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> |
| @@ -24,10 +24,22 @@ if (typeof Array.prototype.indexOf === 'function') { | |||
| 24 | 24 | ||
| 25 | 25 | ||
| 26 | /* Polyfill EventEmitter. */ | 26 | /* Polyfill EventEmitter. */ |
| 27 | var EventEmitter = function () { | 27 | /** |
| 28 | * Creates an event emitter. | ||
| 29 | * @param {string[]} autoFireAfterEmit Auto-fire event names | ||
| 30 | */ | ||
| 31 | var EventEmitter = function (autoFireAfterEmit = []) { | ||
| 28 | this.events = {}; | 32 | this.events = {}; |
| 33 | this.autoFireLastArgs = new Map(); | ||
| 34 | this.autoFireAfterEmit = new Set(autoFireAfterEmit); | ||
| 29 | }; | 35 | }; |
| 30 | 36 | ||
| 37 | /** | ||
| 38 | * Adds a listener to an event. | ||
| 39 | * @param {string} event Event name | ||
| 40 | * @param {function} listener Event listener | ||
| 41 | * @returns | ||
| 42 | */ | ||
| 31 | EventEmitter.prototype.on = function (event, listener) { | 43 | EventEmitter.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 | } |
| 41 | 53 | ||
| 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 | }; |
| 44 | 60 | ||
| 45 | /** | 61 | /** |
| @@ -60,6 +76,10 @@ EventEmitter.prototype.makeLast = function (event, listener) { | |||
| 60 | } | 76 | } |
| 61 | 77 | ||
| 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 | } |
| 64 | 84 | ||
| 65 | /** | 85 | /** |
| @@ -80,8 +100,17 @@ EventEmitter.prototype.makeFirst = function (event, listener) { | |||
| 80 | } | 100 | } |
| 81 | 101 | ||
| 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 | } |
| 84 | 108 | ||
| 109 | /** | ||
| 110 | * Removes a listener from an event. | ||
| 111 | * @param {string} event Event name | ||
| 112 | * @param {function} listener Event listener | ||
| 113 | */ | ||
| 85 | EventEmitter.prototype.removeListener = function (event, listener) { | 114 | EventEmitter.prototype.removeListener = function (event, listener) { |
| 86 | var idx; | 115 | var idx; |
| 87 | 116 | ||
| @@ -94,6 +123,10 @@ EventEmitter.prototype.removeListener = function (event, listener) { | |||
| 94 | } | 123 | } |
| 95 | }; | 124 | }; |
| 96 | 125 | ||
| 126 | /** | ||
| 127 | * Emits an event with optional arguments. | ||
| 128 | * @param {string} event Event name | ||
| 129 | */ | ||
| 97 | EventEmitter.prototype.emit = async function (event) { | 130 | EventEmitter.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 | }; |
| 122 | 159 | ||
| 123 | EventEmitter.prototype.emitAndWait = function (event) { | 160 | EventEmitter.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 | }; |
| 148 | 189 | ||
| 149 | EventEmitter.prototype.once = function (event, listener) { | 190 | EventEmitter.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 | }); |
| @@ -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": "عدد الكلمات الممنوعة للسحب", |
| @@ -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", |
| @@ -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", |
| @@ -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", |
| @@ -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", |
| @@ -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", |
| @@ -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": "スワイプするブラックリストされた単語の数", |
| @@ -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": "스와이프할 금지어 개수", |
| @@ -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", |
| @@ -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", |
| @@ -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 | } | ||
| @@ -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": "Кількість заборонених слів для змаху", |
| @@ -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", |
| @@ -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": "触发滑动的黑名单词语数量", |
| @@ -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": "滑動的黑名單詞語數量", |
| @@ -512,7 +512,7 @@ export const event_types = { | |||
| 512 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', | 512 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', |
| 513 | }; | 513 | }; |
| 514 | 514 | ||
| 515 | export const eventSource = new EventEmitter(); | 515 | export const eventSource = new EventEmitter([event_types.APP_READY]); |
| 516 | 516 | ||
| 517 | eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); | 517 | eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); |
| 518 | 518 | ||
| @@ -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 | ]; |
| 1489 | 1489 | ||
| 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 | }); |
| 1492 | 1492 | ||
| 1493 | // Do not change. #attachFile is added by extension. | 1493 | // Do not change. #attachFile is added by extension. |
| @@ -338,14 +338,15 @@ export class ReasoningHandler { | |||
| 338 | return mesChanged; | 338 | return mesChanged; |
| 339 | } | 339 | } |
| 340 | 340 | ||
| 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 reasoning | 342 | // 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; |
| 345 | 345 | ||
| 346 | // Manually set starting state here, as we might already have received the ending suffix | 346 | // 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 | } |
| 351 | 352 | ||
| @@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti | |||
| 9 | export let currentUser = null; | 9 | export let currentUser = null; |
| 10 | export let accountsEnabled = false; | 10 | export let accountsEnabled = false; |
| 11 | 11 | ||
| 12 | // Extend the session every 30 minutes | ||
| 13 | const 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 enabled | 17 | * @param {boolean} isEnabled User account controls enabled |
| @@ -894,6 +897,24 @@ async function slugify(text) { | |||
| 894 | } | 897 | } |
| 895 | } | 898 | } |
| 896 | 899 | ||
| 900 | /** | ||
| 901 | * Pings the server to extend the user session. | ||
| 902 | */ | ||
| 903 | async 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 | |||
| 897 | jQuery(() => { | 918 | jQuery(() => { |
| 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 | }); |
| @@ -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); |
| 57 | 57 | ||
| 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 | |||
| 58 | 62 | ||
| 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 { | |||
| 348 | 352 | ||
| 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 | } |
| 359 | 363 | ||
| 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 | } |
| 376 | 380 | ||
| 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 | } |
| 462 | 454 | ||
| 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 | } |
| 469 | 463 | ||
| 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 | } | ||
| 474 | 472 | ||
| 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 | } | ||
| 479 | 479 | ||
| 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 | } | ||
| 484 | 486 | ||
| 485 | .mes_text font[color] em, | 487 | .mes_text font[color] em, |
| 486 | .mes_text font[color] i, | 488 | .mes_text font[color] i, |
| @@ -556,7 +556,13 @@ app.use('/api/users', usersPublicRouter); | |||
| 556 | 556 | ||
| 557 | // Everything below this line requires authentication | 557 | // Everything below this line requires authentication |
| 558 | app.use(requireLoginMiddleware); | 558 | app.use(requireLoginMiddleware); |
| 559 | app.get('/api/ping', (_, response) => response.sendStatus(204)); | 559 | app.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 | }); | ||
| 560 | 566 | ||
| 561 | // File uploads | 567 | // File uploads |
| 562 | app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); | 568 | app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); |
| @@ -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 repo | 231 | // 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 either | 232 | // 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 | } |
| 235 | 235 | ||
| 236 | const currentBranch = await git.cwd(extensionPath).branch(); | 236 | const currentBranch = await git.cwd(extensionPath).branch(); |
| @@ -3,13 +3,16 @@ import crypto from 'node:crypto'; | |||
| 3 | import storage from 'node-persist'; | 3 | import storage from 'node-persist'; |
| 4 | import express from 'express'; | 4 | import express from 'express'; |
| 5 | import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; | 5 | import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; |
| 6 | import { jsonParser, getIpFromRequest } from '../express-common.js'; | 6 | import { jsonParser, getIpFromRequest, getRealIpFromHeader } from '../express-common.js'; |
| 7 | import { color, Cache, getConfigValue } from '../util.js'; | 7 | import { color, Cache, getConfigValue } from '../util.js'; |
| 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; | 8 | import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js'; |
| 9 | 9 | ||
| 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false); | 10 | const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false); |
| 11 | const PREFER_REAL_IP_HEADER = getConfigValue('rateLimiting.preferRealIpHeader', false); | ||
| 11 | const MFA_CACHE = new Cache(5 * 60 * 1000); | 12 | const MFA_CACHE = new Cache(5 * 60 * 1000); |
| 12 | 13 | ||
| 14 | const getIpAddress = (request) => PREFER_REAL_IP_HEADER ? getRealIpFromHeader(request) : getIpFromRequest(request); | ||
| 15 | |||
| 13 | export const router = express.Router(); | 16 | export const router = express.Router(); |
| 14 | const loginLimiter = new RateLimiterMemory({ | 17 | const 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 | } |
| 62 | 65 | ||
| 63 | const ip = getIpFromRequest(request); | 66 | const ip = getIpAddress(request); |
| 64 | await loginLimiter.consume(ip); | 67 | await loginLimiter.consume(ip); |
| 65 | 68 | ||
| 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 | } |
| 98 | 101 | ||
| @@ -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 | } |
| 110 | 113 | ||
| 111 | const ip = getIpFromRequest(request); | 114 | const ip = getIpAddress(request); |
| 112 | await recoverLimiter.consume(ip); | 115 | await recoverLimiter.consume(ip); |
| 113 | 116 | ||
| 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 | } |
| 138 | 141 | ||
| @@ -150,7 +153,7 @@ router.post('/recover-step2', jsonParser, async (request, response) => { | |||
| 150 | 153 | ||
| 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); |
| 154 | 157 | ||
| 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 | } |
| 192 | 195 | ||
| @@ -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 | */ | ||
| 35 | export 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 | } | ||
| @@ -1,11 +1,8 @@ | |||
| 1 | import path from 'node:path'; | 1 | import path from 'node:path'; |
| 2 | import webpack from 'webpack'; | 2 | import webpack from 'webpack'; |
| 3 | import { publicLibConfig } from '../../webpack.config.js'; | 3 | import getPublicLibConfig from '../../webpack.config.js'; |
| 4 | 4 | ||
| 5 | export default function getWebpackServeMiddleware() { | 5 | export 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() { | |||
| 23 | 24 | ||
| 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); |
| 30 | 34 | ||
| 31 | return new Promise((resolve) => { | 35 | return new Promise((resolve) => { |
| @@ -3,7 +3,7 @@ import path from 'node:path'; | |||
| 3 | import url from 'node:url'; | 3 | import url from 'node:url'; |
| 4 | 4 | ||
| 5 | import express from 'express'; | 5 | import express from 'express'; |
| 6 | import { default as git } from 'simple-git'; | 6 | import { default as git, CheckRepoActions } from 'simple-git'; |
| 7 | import { sync as commandExistsSync } from 'command-exists'; | 7 | import { sync as commandExistsSync } from 'command-exists'; |
| 8 | import { getConfigValue, color } from './util.js'; | 8 | import { getConfigValue, color } from './util.js'; |
| 9 | 9 | ||
| @@ -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); |
| 258 | 258 | ||
| 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 | } |
| @@ -1,35 +1,72 @@ | |||
| 1 | import process from 'node:process'; | 1 | import process from 'node:process'; |
| 2 | import path from 'node:path'; | 2 | import path from 'node:path'; |
| 3 | import isDocker from 'is-docker'; | ||
| 3 | 4 | ||
| 4 | /** @type {import('webpack').Configuration} */ | 5 | /** |
| 5 | export 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', | 13 | export 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 | } | ||