2024-10-17 08:03:58 +00:00
import { DOMPurify , Popper } from '../lib.js' ;
2024-10-16 23:11:13 +03:00
2024-06-25 11:54:59 +03:00
import { eventSource , event _types , saveSettings , saveSettingsDebounced , getRequestHeaders , animation _duration } from '../script.js' ;
2024-09-25 22:46:19 +02:00
import { showLoader } from './loader.js' ;
2024-06-25 11:54:59 +03:00
import { POPUP _RESULT , POPUP _TYPE , Popup , callGenericPopup } from './popup.js' ;
2024-04-11 22:39:42 +03:00
import { renderTemplate , renderTemplateAsync } from './templates.js' ;
2024-09-25 23:18:37 +02:00
import { isSubsetOf , setValueByPath } from './utils.js' ;
2024-12-06 16:41:26 +02:00
import { getContext } from './st-context.js' ;
2024-12-07 17:10:26 +02:00
import { isAdmin } from './user.js' ;
import { t } from './i18n.js' ;
2024-12-07 20:31:16 +02:00
import { debounce _timeout } from './constants.js' ;
2023-07-20 20:32:15 +03:00
export {
getContext ,
getApiUrl ,
} ;
2024-09-25 22:46:19 +02:00
/** @type {string[]} */
2023-10-08 23:20:01 +03:00
export let extensionNames = [ ] ;
2024-12-07 20:31:16 +02:00
2024-12-07 18:12:27 +02:00
/ * *
* Holds the type of each extension .
* Don ' t use this directly , use getExtensionType instead !
* @ type { Record < string , string > }
* /
2024-12-07 17:10:26 +02:00
export let extensionTypes = { } ;
2024-09-25 22:46:19 +02:00
2024-12-07 20:31:16 +02:00
/ * *
* A list of active modules provided by the Extras API .
* @ type { string [ ] }
* /
export let modules = [ ] ;
/ * *
* A set of active extensions .
* @ type { Set < string > }
* /
let activeExtensions = new Set ( ) ;
const getApiUrl = ( ) => extension _settings . apiUrl ;
let connectedToApi = false ;
/ * *
* Holds manifest data for each extension .
* @ type { Record < string , object > }
* /
2023-08-22 22:45:12 +03:00
let manifests = { } ;
2023-08-23 21:32:38 +03:00
2024-12-07 20:31:16 +02:00
/ * *
* Default URL for the Extras API .
* /
const defaultUrl = 'http://localhost:5100' ;
2023-08-23 21:32:38 +03:00
2023-12-28 10:46:25 +00:00
let requiresReload = false ;
2024-08-19 23:49:15 +03:00
let stateChanged = false ;
2024-12-07 20:31:16 +02:00
let saveMetadataTimeout = null ;
2023-12-28 10:46:25 +00:00
2023-08-23 21:32:38 +03:00
export function saveMetadataDebounced ( ) {
const context = getContext ( ) ;
const groupId = context . groupId ;
const characterId = context . characterId ;
if ( saveMetadataTimeout ) {
clearTimeout ( saveMetadataTimeout ) ;
}
saveMetadataTimeout = setTimeout ( async ( ) => {
const newContext = getContext ( ) ;
if ( groupId !== newContext . groupId ) {
console . warn ( 'Group changed, not saving metadata' ) ;
return ;
}
if ( characterId !== newContext . characterId ) {
console . warn ( 'Character changed, not saving metadata' ) ;
return ;
}
console . debug ( 'Saving metadata...' ) ;
2024-12-07 20:31:16 +02:00
await newContext . saveMetadata ( ) ;
2023-08-23 21:32:38 +03:00
console . debug ( 'Saved metadata...' ) ;
2024-12-07 20:31:16 +02:00
} , debounce _timeout . relaxed ) ;
2023-08-23 21:32:38 +03:00
}
2023-07-20 20:32:15 +03:00
2023-08-25 20:34:26 +03:00
/ * *
2024-04-11 22:36:23 +03:00
* Provides an ability for extensions to render HTML templates synchronously .
2023-08-25 20:34:26 +03:00
* Templates sanitation and localization is forced .
* @ param { string } extensionName Extension name
* @ param { string } templateId Template ID
* @ param { object } templateData Additional data to pass to the template
* @ returns { string } Rendered HTML
2024-04-11 22:36:23 +03:00
*
* @ deprecated Use renderExtensionTemplateAsync instead .
2023-08-25 20:34:26 +03:00
* /
export function renderExtensionTemplate ( extensionName , templateId , templateData = { } , sanitize = true , localize = true ) {
return renderTemplate ( ` scripts/extensions/ ${ extensionName } / ${ templateId } .html ` , templateData , sanitize , localize , true ) ;
}
2024-04-11 22:36:23 +03:00
/ * *
* Provides an ability for extensions to render HTML templates asynchronously .
* Templates sanitation and localization is forced .
* @ param { string } extensionName Extension name
* @ param { string } templateId Template ID
* @ param { object } templateData Additional data to pass to the template
* @ returns { Promise < string > } Rendered HTML
* /
export function renderExtensionTemplateAsync ( extensionName , templateId , templateData = { } , sanitize = true , localize = true ) {
return renderTemplateAsync ( ` scripts/extensions/ ${ extensionName } / ${ templateId } .html ` , templateData , sanitize , localize , true ) ;
}
2023-07-20 20:32:15 +03:00
// Disables parallel updates
2024-12-07 20:31:16 +02:00
export class ModuleWorkerWrapper {
2023-07-20 20:32:15 +03:00
constructor ( callback ) {
this . isBusy = false ;
this . callback = callback ;
}
// Called by the extension
2023-11-17 01:30:32 +02:00
async update ( ... args ) {
2023-07-20 20:32:15 +03:00
// Don't touch me I'm busy...
if ( this . isBusy ) {
return ;
}
// I'm free. Let's update!
try {
this . isBusy = true ;
2023-11-17 01:30:32 +02:00
await this . callback ( ... args ) ;
2023-07-20 20:32:15 +03:00
}
finally {
this . isBusy = false ;
}
}
}
2024-12-07 20:31:16 +02:00
export const extension _settings = {
2023-07-20 20:32:15 +03:00
apiUrl : defaultUrl ,
apiKey : '' ,
autoConnect : false ,
2023-10-23 16:53:31 +03:00
notifyUpdates : false ,
2023-07-20 20:32:15 +03:00
disabledExtensions : [ ] ,
expressionOverrides : [ ] ,
memory : { } ,
note : {
default : '' ,
chara : [ ] ,
wiAddition : [ ] ,
} ,
caption : {
refine _mode : false ,
} ,
2023-09-14 21:30:02 +03:00
expressions : {
/** @type {string[]} */
custom : [ ] ,
} ,
2024-09-07 00:23:42 +03:00
connectionManager : {
selectedProfile : '' ,
/** @type {import('./extensions/connection-manager/index.js').ConnectionProfile[]} */
profiles : [ ] ,
} ,
2023-07-20 20:32:15 +03:00
dice : { } ,
2024-05-26 22:19:00 +08:00
/** @type {import('./char-data.js').RegexScriptData[]} */
2023-07-20 20:32:15 +03:00
regex : [ ] ,
2024-05-26 22:19:00 +08:00
character _allowed _regex : [ ] ,
2023-07-20 20:32:15 +03:00
tts : { } ,
2023-07-22 21:12:23 +03:00
sd : {
prompts : { } ,
2023-07-22 23:57:48 +03:00
character _prompts : { } ,
2023-12-23 16:19:22 +00:00
character _negative _prompts : { } ,
2023-07-22 21:12:23 +03:00
} ,
2023-07-20 20:32:15 +03:00
chromadb : { } ,
translate : { } ,
objective : { } ,
quickReply : { } ,
randomizer : {
controls : [ ] ,
fluctuation : 0.1 ,
enabled : false ,
} ,
2023-07-27 19:29:36 +02:00
speech _recognition : { } ,
2023-08-10 16:46:04 +02:00
rvc : { } ,
2023-09-08 00:28:06 +03:00
hypebot : { } ,
vectors : { } ,
2023-11-06 22:50:32 +02:00
variables : {
global : { } ,
} ,
2024-04-30 00:06:14 +03:00
/ * *
* @ type { import ( './chats.js' ) . FileAttachment [ ] }
* /
2024-04-16 02:14:34 +03:00
attachments : [ ] ,
2024-04-30 00:06:14 +03:00
/ * *
* @ type { Record < string , import ( './chats.js' ) . FileAttachment [ ] > }
* /
2024-04-18 22:16:51 +03:00
character _attachments : { } ,
2024-04-30 00:06:14 +03:00
/ * *
* @ type { string [ ] }
* /
disabled _attachments : [ ] ,
2023-07-20 20:32:15 +03:00
} ;
function showHideExtensionsMenu ( ) {
// Get the number of menu items that are not hidden
const hasMenuItems = $ ( '#extensionsMenu' ) . children ( ) . filter ( ( _ , child ) => $ ( child ) . css ( 'display' ) !== 'none' ) . length > 0 ;
// We have menu items, so we can stop checking
if ( hasMenuItems ) {
clearInterval ( menuInterval ) ;
}
// Show or hide the menu button
$ ( '#extensionsMenuButton' ) . toggle ( hasMenuItems ) ;
}
// Periodically check for new extensions
const menuInterval = setInterval ( showHideExtensionsMenu , 1000 ) ;
2024-12-07 18:12:27 +02:00
/ * *
* Gets the type of an extension based on its external ID .
* @ param { string } externalId External ID of the extension ( excluding or including the leading 'third-party/' )
* @ returns { string } Type of the extension ( global , local , system , or empty string if not found )
* /
function getExtensionType ( externalId ) {
const id = Object . keys ( extensionTypes ) . find ( id => id === externalId || ( id . startsWith ( 'third-party' ) && id . endsWith ( externalId ) ) ) ;
return id ? extensionTypes [ id ] : '' ;
}
2024-12-07 20:31:16 +02:00
/ * *
* Performs a fetch of the Extras API .
* @ param { string | URL } endpoint Extras API endpoint
* @ param { RequestInit } args Request arguments
* @ returns { Promise < Response > } Response from the fetch
* /
export async function doExtrasFetch ( endpoint , args = { } ) {
2023-07-20 20:32:15 +03:00
if ( ! args ) {
2023-12-02 21:11:06 +02:00
args = { } ;
2023-07-20 20:32:15 +03:00
}
if ( ! args . method ) {
Object . assign ( args , { method : 'GET' } ) ;
}
if ( ! args . headers ) {
2023-12-02 21:11:06 +02:00
args . headers = { } ;
2023-07-20 20:32:15 +03:00
}
2024-01-18 16:33:02 +02:00
if ( extension _settings . apiKey ) {
Object . assign ( args . headers , {
'Authorization' : ` Bearer ${ extension _settings . apiKey } ` ,
} ) ;
}
2023-07-20 20:32:15 +03:00
2024-12-07 20:31:16 +02:00
return await fetch ( endpoint , args ) ;
2023-07-20 20:32:15 +03:00
}
2024-12-07 17:10:26 +02:00
/ * *
* Discovers extensions from the API .
* @ returns { Promise < { name : string , type : string } [ ] > }
* /
2023-07-20 20:32:15 +03:00
async function discoverExtensions ( ) {
try {
2023-09-16 16:16:48 +03:00
const response = await fetch ( '/api/extensions/discover' ) ;
2023-07-20 20:32:15 +03:00
if ( response . ok ) {
const extensions = await response . json ( ) ;
return extensions ;
}
else {
return [ ] ;
}
}
catch ( err ) {
console . error ( err ) ;
return [ ] ;
}
}
function onDisableExtensionClick ( ) {
const name = $ ( this ) . data ( 'name' ) ;
2023-12-28 10:46:25 +00:00
disableExtension ( name , false ) ;
2023-07-20 20:32:15 +03:00
}
function onEnableExtensionClick ( ) {
const name = $ ( this ) . data ( 'name' ) ;
2023-12-28 10:46:25 +00:00
enableExtension ( name , false ) ;
2023-07-20 20:32:15 +03:00
}
2024-12-07 20:31:16 +02:00
/ * *
* Enables an extension by name .
* @ param { string } name Extension name
* @ param { boolean } [ reload = true ] If true , reload the page after enabling the extension
* /
2024-09-25 23:18:37 +02:00
export async function enableExtension ( name , reload = true ) {
2023-07-20 20:32:15 +03:00
extension _settings . disabledExtensions = extension _settings . disabledExtensions . filter ( x => x !== name ) ;
2024-08-19 23:49:15 +03:00
stateChanged = true ;
2023-07-20 20:32:15 +03:00
await saveSettings ( ) ;
2023-12-28 10:46:25 +00:00
if ( reload ) {
location . reload ( ) ;
} else {
requiresReload = true ;
}
2023-07-20 20:32:15 +03:00
}
2024-12-07 20:31:16 +02:00
/ * *
* Disables an extension by name .
* @ param { string } name Extension name
* @ param { boolean } [ reload = true ] If true , reload the page after disabling the extension
* /
2024-09-25 23:18:37 +02:00
export async function disableExtension ( name , reload = true ) {
2023-07-20 20:32:15 +03:00
extension _settings . disabledExtensions . push ( name ) ;
2024-08-19 23:49:15 +03:00
stateChanged = true ;
2023-07-20 20:32:15 +03:00
await saveSettings ( ) ;
2023-12-28 10:46:25 +00:00
if ( reload ) {
location . reload ( ) ;
} else {
requiresReload = true ;
}
2023-07-20 20:32:15 +03:00
}
2024-12-07 20:31:16 +02:00
/ * *
* Loads manifest . json files for extensions .
* @ param { string [ ] } names Array of extension names
* @ returns { Promise < Record < string , object >> } Object with extension names as keys and their manifests as values
* /
2023-07-20 20:32:15 +03:00
async function getManifests ( names ) {
const obj = { } ;
const promises = [ ] ;
for ( const name of names ) {
const promise = new Promise ( ( resolve , reject ) => {
fetch ( ` /scripts/extensions/ ${ name } /manifest.json ` ) . then ( async response => {
if ( response . ok ) {
const json = await response . json ( ) ;
obj [ name ] = json ;
resolve ( ) ;
} else {
reject ( ) ;
}
2023-08-22 22:45:12 +03:00
} ) . catch ( err => {
reject ( ) ;
console . log ( 'Could not load manifest.json for ' + name , err ) ;
} ) ;
2023-07-20 20:32:15 +03:00
} ) ;
promises . push ( promise ) ;
}
await Promise . allSettled ( promises ) ;
return obj ;
}
2024-12-07 20:31:16 +02:00
/ * *
* Tries to activate all available extensions that are not already active .
* @ returns { Promise < void > }
* /
2023-07-20 20:32:15 +03:00
async function activateExtensions ( ) {
const extensions = Object . entries ( manifests ) . sort ( ( a , b ) => a [ 1 ] . loading _order - b [ 1 ] . loading _order ) ;
const promises = [ ] ;
for ( let entry of extensions ) {
const name = entry [ 0 ] ;
const manifest = entry [ 1 ] ;
2024-12-07 20:31:16 +02:00
if ( activeExtensions . has ( name ) ) {
2023-07-20 20:32:15 +03:00
continue ;
}
2024-12-07 20:31:16 +02:00
const meetsModuleRequirements = ! Array . isArray ( manifest . requires ) || isSubsetOf ( modules , manifest . requires ) ;
const isDisabled = extension _settings . disabledExtensions . includes ( name ) ;
2023-07-20 20:32:15 +03:00
2024-12-07 20:31:16 +02:00
if ( meetsModuleRequirements && ! isDisabled ) {
try {
console . debug ( 'Activating extension' , name ) ;
const promise = Promise . all ( [ addExtensionScript ( name , manifest ) , addExtensionStyle ( name , manifest ) ] ) ;
await promise
. then ( ( ) => activeExtensions . add ( name ) )
. catch ( err => console . log ( 'Could not activate extension' , name , err ) ) ;
promises . push ( promise ) ;
2023-07-20 20:32:15 +03:00
}
catch ( error ) {
2024-12-07 20:31:16 +02:00
console . error ( 'Could not activate extension' , name ) ;
2023-07-20 20:32:15 +03:00
console . error ( error ) ;
}
}
}
await Promise . allSettled ( promises ) ;
}
async function connectClickHandler ( ) {
2024-12-07 20:31:16 +02:00
const baseUrl = String ( $ ( '#extensions_url' ) . val ( ) ) ;
extension _settings . apiUrl = baseUrl ;
2023-12-02 13:04:51 -05:00
const testApiKey = $ ( '#extensions_api_key' ) . val ( ) ;
2023-08-22 22:45:12 +03:00
extension _settings . apiKey = String ( testApiKey ) ;
2023-07-20 20:32:15 +03:00
saveSettingsDebounced ( ) ;
await connectToApi ( baseUrl ) ;
}
function autoConnectInputHandler ( ) {
const value = $ ( this ) . prop ( 'checked' ) ;
extension _settings . autoConnect = ! ! value ;
if ( value && ! connectedToApi ) {
2023-12-02 13:04:51 -05:00
$ ( '#extensions_connect' ) . trigger ( 'click' ) ;
2023-07-20 20:32:15 +03:00
}
saveSettingsDebounced ( ) ;
}
2024-06-24 23:17:58 +03:00
async function addExtensionsButtonAndMenu ( ) {
const buttonHTML = await renderTemplateAsync ( 'wandButton' ) ;
const extensionsMenuHTML = await renderTemplateAsync ( 'wandMenu' ) ;
2023-07-20 20:32:15 +03:00
$ ( document . body ) . append ( extensionsMenuHTML ) ;
2024-06-06 02:48:06 +02:00
$ ( '#leftSendForm' ) . append ( buttonHTML ) ;
2023-07-20 20:32:15 +03:00
const button = $ ( '#extensionsMenuButton' ) ;
const dropdown = $ ( '#extensionsMenu' ) ;
//dropdown.hide();
let popper = Popper . createPopper ( button . get ( 0 ) , dropdown . get ( 0 ) , {
2023-11-14 15:53:26 +09:00
placement : 'top-start' ,
2023-07-20 20:32:15 +03:00
} ) ;
$ ( button ) . on ( 'click' , function ( ) {
2023-11-28 02:43:24 +02:00
if ( dropdown . is ( ':visible' ) ) {
dropdown . fadeOut ( animation _duration ) ;
} else {
2023-11-18 21:17:53 +02:00
dropdown . fadeIn ( animation _duration ) ;
}
2023-11-28 02:43:24 +02:00
popper . update ( ) ;
2023-07-20 20:32:15 +03:00
} ) ;
2023-12-02 13:04:51 -05:00
$ ( 'html' ) . on ( 'click' , function ( e ) {
2023-11-19 00:40:21 +02:00
const clickTarget = $ ( e . target ) ;
2024-06-30 21:19:07 +03:00
const noCloseTargets = [ '#sd_gen' , '#extensionsMenuButton' , '#roll_dice' ] ;
2023-11-19 00:40:21 +02:00
if ( dropdown . is ( ':visible' ) && ! noCloseTargets . some ( id => clickTarget . closest ( id ) . length > 0 ) ) {
2023-11-18 21:17:53 +02:00
$ ( dropdown ) . fadeOut ( animation _duration ) ;
2023-07-20 20:32:15 +03:00
}
} ) ;
}
2023-10-23 16:53:31 +03:00
function notifyUpdatesInputHandler ( ) {
extension _settings . notifyUpdates = ! ! $ ( '#extensions_notify_updates' ) . prop ( 'checked' ) ;
saveSettingsDebounced ( ) ;
if ( extension _settings . notifyUpdates ) {
checkForExtensionUpdates ( true ) ;
}
}
2024-12-07 20:31:16 +02:00
/ * *
* Connects to the Extras API .
* @ param { string } baseUrl Extras API base URL
* @ returns { Promise < void > }
* /
2023-07-20 20:32:15 +03:00
async function connectToApi ( baseUrl ) {
if ( ! baseUrl ) {
return ;
}
const url = new URL ( baseUrl ) ;
url . pathname = '/api/modules' ;
try {
const getExtensionsResult = await doExtrasFetch ( url ) ;
if ( getExtensionsResult . ok ) {
const data = await getExtensionsResult . json ( ) ;
modules = data . modules ;
await activateExtensions ( ) ;
2024-12-07 20:31:16 +02:00
await eventSource . emit ( event _types . EXTRAS _CONNECTED , modules ) ;
2023-07-20 20:32:15 +03:00
}
updateStatus ( getExtensionsResult . ok ) ;
}
catch {
updateStatus ( false ) ;
}
}
2024-12-07 20:31:16 +02:00
/ * *
* Updates the status of Extras API connection .
* @ param { boolean } success Whether the connection was successful
* /
2023-07-20 20:32:15 +03:00
function updateStatus ( success ) {
connectedToApi = success ;
2024-12-07 18:42:37 +02:00
const _text = success ? t ` Connected to API ` : t ` Could not connect to API ` ;
2023-07-20 20:32:15 +03:00
const _class = success ? 'success' : 'failure' ;
$ ( '#extensions_status' ) . text ( _text ) ;
$ ( '#extensions_status' ) . attr ( 'class' , _class ) ;
}
2024-12-07 18:12:27 +02:00
/ * *
* Adds a CSS file for an extension .
* @ param { string } name Extension name
* @ param { object } manifest Extension manifest
* @ returns { Promise < void > } When the CSS is loaded
* /
2023-07-20 20:32:15 +03:00
function addExtensionStyle ( name , manifest ) {
2024-12-07 18:12:27 +02:00
if ( ! manifest . css ) {
return Promise . resolve ( ) ;
2023-07-20 20:32:15 +03:00
}
2024-12-07 18:12:27 +02:00
return new Promise ( ( resolve , reject ) => {
const url = ` /scripts/extensions/ ${ name } / ${ manifest . css } ` ;
if ( $ ( ` link[id=" ${ name } "] ` ) . length === 0 ) {
const link = document . createElement ( 'link' ) ;
link . id = name ;
link . rel = 'stylesheet' ;
link . type = 'text/css' ;
link . href = url ;
link . onload = function ( ) {
resolve ( ) ;
} ;
link . onerror = function ( e ) {
reject ( e ) ;
} ;
document . head . appendChild ( link ) ;
}
} ) ;
2023-07-20 20:32:15 +03:00
}
2024-12-07 18:12:27 +02:00
/ * *
* Loads a JS file for an extension .
* @ param { string } name Extension name
* @ param { object } manifest Extension manifest
* @ returns { Promise < void > } When the script is loaded
* /
2023-07-20 20:32:15 +03:00
function addExtensionScript ( name , manifest ) {
2024-12-07 18:12:27 +02:00
if ( ! manifest . js ) {
return Promise . resolve ( ) ;
2023-07-20 20:32:15 +03:00
}
2024-12-07 18:12:27 +02:00
return new Promise ( ( resolve , reject ) => {
const url = ` /scripts/extensions/ ${ name } / ${ manifest . js } ` ;
let ready = false ;
if ( $ ( ` script[id=" ${ name } "] ` ) . length === 0 ) {
const script = document . createElement ( 'script' ) ;
script . id = name ;
script . type = 'module' ;
script . src = url ;
script . async = true ;
script . onerror = function ( err ) {
reject ( err ) ;
} ;
script . onload = function ( ) {
if ( ! ready ) {
ready = true ;
resolve ( ) ;
}
} ;
document . body . appendChild ( script ) ;
}
} ) ;
2023-07-20 20:32:15 +03:00
}
/ * *
* Generates HTML string for displaying an extension in the UI .
*
* @ param { string } name - The name of the extension .
* @ param { object } manifest - The manifest of the extension .
* @ param { boolean } isActive - Whether the extension is active or not .
* @ param { boolean } isDisabled - Whether the extension is disabled or not .
* @ param { boolean } isExternal - Whether the extension is external or not .
* @ param { string } checkboxClass - The class for the checkbox HTML element .
2024-12-03 22:48:10 +02:00
* @ return { string } - The HTML string that represents the extension .
2023-07-20 20:32:15 +03:00
* /
2024-12-03 22:48:10 +02:00
function generateExtensionHtml ( name , manifest , isActive , isDisabled , isExternal , checkboxClass ) {
2024-12-07 18:12:27 +02:00
function getExtensionIcon ( ) {
const type = getExtensionType ( name ) ;
switch ( type ) {
case 'global' :
2024-12-09 22:37:43 +02:00
return '<i class="fa-sm fa-fw fa-solid fa-server" data-i18n="[title]ext_type_global" title="This is a global extension, available for all users."></i>' ;
2024-12-07 18:12:27 +02:00
case 'local' :
2024-12-09 22:37:43 +02:00
return '<i class="fa-sm fa-fw fa-solid fa-user" data-i18n="[title]ext_type_local" title="This is a local extension, available only for you."></i>' ;
2024-12-07 18:12:27 +02:00
case 'system' :
2024-12-09 22:37:43 +02:00
return '<i class="fa-sm fa-fw fa-solid fa-cog" data-i18n="[title]ext_type_system" title="This is a built-in extension. It cannot be deleted and updates with the app."></i>' ;
2024-12-07 18:12:27 +02:00
default :
2024-12-09 22:37:43 +02:00
return '<i class="fa-sm fa-fw fa-solid fa-question" title="Unknown extension type."></i>' ;
2024-12-07 18:12:27 +02:00
}
}
const isUserAdmin = isAdmin ( ) ;
const extensionIcon = getExtensionIcon ( ) ;
2023-07-20 20:32:15 +03:00
const displayName = manifest . display _name ;
2024-12-03 22:48:10 +02:00
const externalId = name . replace ( 'third-party' , '' ) ;
2023-07-20 20:32:15 +03:00
let originHtml = '' ;
if ( isExternal ) {
2024-12-03 22:48:10 +02:00
originHtml = '<a>' ;
2023-07-20 20:32:15 +03:00
}
let toggleElement = isActive || isDisabled ?
` <input type="checkbox" title="Click to toggle" data-name=" ${ name } " class=" ${ isActive ? 'toggle_disable' : 'toggle_enable' } ${ checkboxClass } " ${ isActive ? 'checked' : '' } > ` :
` <input type="checkbox" title="Cannot enable extension" data-name=" ${ name } " class="extension_missing ${ checkboxClass } " disabled> ` ;
2024-12-03 22:48:10 +02:00
let deleteButton = isExternal ? ` <button class="btn_delete menu_button" data-name=" ${ externalId } " title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button> ` : '' ;
let updateButton = isExternal ? ` <button class="btn_update menu_button displayNone" data-name=" ${ externalId } " title="Update available"><i class="fa-solid fa-download fa-fw"></i></button> ` : '' ;
2024-12-07 18:12:27 +02:00
let moveButton = isExternal && isUserAdmin ? ` <button class="btn_move menu_button" data-name=" ${ externalId } " title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button> ` : '' ;
2024-12-03 22:48:10 +02:00
let modulesInfo = '' ;
2023-07-20 20:32:15 +03:00
if ( isActive && Array . isArray ( manifest . optional ) ) {
const optional = new Set ( manifest . optional ) ;
modules . forEach ( x => optional . delete ( x ) ) ;
if ( optional . size > 0 ) {
const optionalString = DOMPurify . sanitize ( [ ... optional ] . join ( ', ' ) ) ;
2024-12-03 22:48:10 +02:00
modulesInfo = ` <div class="extension_modules">Optional modules: <span class="optional"> ${ optionalString } </span></div> ` ;
2023-07-20 20:32:15 +03:00
}
} else if ( ! isDisabled ) { // Neither active nor disabled
const requirements = new Set ( manifest . requires ) ;
modules . forEach ( x => requirements . delete ( x ) ) ;
2024-06-26 12:49:23 +00:00
if ( requirements . size > 0 ) {
const requirementsString = DOMPurify . sanitize ( [ ... requirements ] . join ( ', ' ) ) ;
2024-12-03 22:48:10 +02:00
modulesInfo = ` <div class="extension_modules">Missing modules: <span class="failure"> ${ requirementsString } </span></div> ` ;
2024-06-26 12:49:23 +00:00
}
2023-07-20 20:32:15 +03:00
}
2024-12-03 22:48:10 +02:00
// if external, wrap the name in a link to the repo
let extensionHtml = `
< div class = "extension_block" data - name = "${externalId}" >
< div class = "extension_toggle" >
$ { toggleElement }
< / d i v >
2024-12-07 18:12:27 +02:00
< div class = "extension_icon" >
$ { extensionIcon }
< / d i v >
2024-12-09 22:37:43 +02:00
< div class = "flexGrow extension_text_block" >
2024-12-03 22:48:10 +02:00
$ { originHtml }
< span class = "${isActive ? 'extension_enabled' : isDisabled ? 'extension_disabled' : 'extension_missing'}" >
< span class = "extension_name" > $ { DOMPurify . sanitize ( displayName ) } < / s p a n >
2024-12-09 22:24:02 +02:00
< span class = "extension_version" > < / s p a n >
2024-12-03 22:48:10 +02:00
$ { modulesInfo }
< / s p a n >
$ { isExternal ? '</a>' : '' }
< / d i v >
< div class = "extension_actions flex-container alignItemsCenter" >
$ { updateButton }
2024-12-07 18:12:27 +02:00
$ { moveButton }
2024-12-03 22:48:10 +02:00
$ { deleteButton }
< / d i v >
< / d i v > ` ;
2023-07-20 20:32:15 +03:00
return extensionHtml ;
}
/ * *
* Gets extension data and generates the corresponding HTML for displaying the extension .
*
* @ param { Array } extension - An array where the first element is the extension name and the second element is the extension manifest .
2024-12-03 22:48:10 +02:00
* @ return { object } - An object with 'isExternal' indicating whether the extension is external , and 'extensionHtml' for the extension ' s HTML string .
2023-07-20 20:32:15 +03:00
* /
2024-12-03 22:48:10 +02:00
function getExtensionData ( extension ) {
2023-07-20 20:32:15 +03:00
const name = extension [ 0 ] ;
const manifest = extension [ 1 ] ;
const isActive = activeExtensions . has ( name ) ;
const isDisabled = extension _settings . disabledExtensions . includes ( name ) ;
const isExternal = name . startsWith ( 'third-party' ) ;
2023-12-02 13:04:51 -05:00
const checkboxClass = isDisabled ? 'checkbox_disabled' : '' ;
2023-07-20 20:32:15 +03:00
2024-12-03 22:48:10 +02:00
const extensionHtml = generateExtensionHtml ( name , manifest , isActive , isDisabled , isExternal , checkboxClass ) ;
2023-07-20 20:32:15 +03:00
return { isExternal , extensionHtml } ;
}
/ * *
* Gets the module information to be displayed .
*
* @ return { string } - The HTML string for the module information .
* /
function getModuleInformation ( ) {
let moduleInfo = modules . length ? ` <p> ${ DOMPurify . sanitize ( modules . join ( ', ' ) ) } </p> ` : '<p class="failure">Not connected to the API!</p>' ;
return `
2023-10-31 22:16:33 +02:00
< h3 > Modules provided by your Extras API : < / h 3 >
2023-07-20 20:32:15 +03:00
$ { moduleInfo }
` ;
}
/ * *
* Generates the HTML strings for all extensions and displays them in a popup .
* /
async function showExtensionsDetails ( ) {
2024-12-07 18:12:27 +02:00
const abortController = new AbortController ( ) ;
2023-12-28 10:46:25 +00:00
let popupPromise ;
2023-11-14 15:53:26 +09:00
try {
2024-12-03 22:48:10 +02:00
const htmlDefault = $ ( '<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>' ) ;
const htmlExternal = $ ( '<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>' ) ;
const htmlLoading = $ ( ` <div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
2024-07-20 14:08:24 +03:00
< i class = "fa-solid fa-spinner fa-spin" > < / i >
< span > Loading third - party extensions ... Please wait ... < / s p a n >
2024-12-03 22:48:10 +02:00
< / d i v > ` ) ;
2024-07-20 14:08:24 +03:00
2024-12-03 22:48:10 +02:00
htmlExternal . append ( htmlLoading ) ;
2023-07-20 20:32:15 +03:00
2024-12-03 22:48:10 +02:00
const extensions = Object . entries ( manifests ) . sort ( ( a , b ) => a [ 1 ] . loading _order - b [ 1 ] . loading _order ) . map ( getExtensionData ) ;
2023-10-23 16:53:31 +03:00
2024-12-03 22:48:10 +02:00
extensions . forEach ( value => {
const { isExternal , extensionHtml } = value ;
const container = isExternal ? htmlExternal : htmlDefault ;
container . append ( extensionHtml ) ;
2023-11-10 10:25:34 +02:00
} ) ;
2023-10-23 16:53:31 +03:00
2024-07-20 14:08:24 +03:00
const html = $ ( '<div></div>' )
. addClass ( 'extensions_info' )
. append ( htmlDefault )
2024-12-03 22:48:10 +02:00
. append ( htmlExternal )
. append ( getModuleInformation ( ) ) ;
2024-07-20 14:08:24 +03:00
2024-06-25 11:54:59 +03:00
/** @type {import('./popup.js').CustomPopupButton} */
const updateAllButton = {
text : 'Update all' ,
appendAtEnd : true ,
action : async ( ) => {
requiresReload = true ;
await autoUpdateExtensions ( true ) ;
2024-06-30 20:44:29 +02:00
await popup . complete ( POPUP _RESULT . AFFIRMATIVE ) ;
2024-06-25 11:54:59 +03:00
} ,
} ;
2024-06-30 19:42:17 +02:00
// If we are updating an extension, the "old" popup is still active. We should close that.
const oldPopup = Popup . util . popups . find ( popup => popup . content . querySelector ( '.extensions_info' ) ) ;
if ( oldPopup ) {
2024-06-30 20:44:29 +02:00
await oldPopup . complete ( POPUP _RESULT . CANCELLED ) ;
2024-06-30 19:42:17 +02:00
}
2024-08-20 00:08:36 +03:00
let waitingForSave = false ;
2024-08-19 23:49:15 +03:00
const popup = new Popup ( html , POPUP _TYPE . TEXT , '' , {
okButton : 'Close' ,
wide : true ,
large : true ,
customButtons : [ updateAllButton ] ,
allowVerticalScrolling : true ,
onClosing : async ( ) => {
2024-08-20 00:08:36 +03:00
if ( waitingForSave ) {
return false ;
}
2024-08-19 23:49:15 +03:00
if ( stateChanged ) {
2024-08-20 00:08:36 +03:00
waitingForSave = true ;
2024-12-07 18:42:37 +02:00
const toast = toastr . info ( t ` The page will be reloaded shortly... ` , t ` Extensions state changed ` ) ;
2024-08-19 23:49:15 +03:00
await saveSettings ( ) ;
2024-08-20 00:08:36 +03:00
toastr . clear ( toast ) ;
2024-08-20 00:11:22 +03:00
waitingForSave = false ;
requiresReload = true ;
2024-08-19 23:49:15 +03:00
}
return true ;
} ,
} ) ;
2024-06-25 11:54:59 +03:00
popupPromise = popup . show ( ) ;
2024-12-07 18:12:27 +02:00
checkForUpdatesManual ( abortController . signal ) . finally ( ( ) => htmlLoading . remove ( ) ) ;
2023-11-10 10:25:34 +02:00
} catch ( error ) {
2024-12-07 18:42:37 +02:00
toastr . error ( t ` Error loading extensions. See browser console for details. ` ) ;
2023-11-10 10:25:34 +02:00
console . error ( error ) ;
}
2023-12-28 10:46:25 +00:00
if ( popupPromise ) {
await popupPromise ;
2024-12-07 18:12:27 +02:00
abortController . abort ( ) ;
2023-12-28 10:46:25 +00:00
}
if ( requiresReload ) {
showLoader ( ) ;
location . reload ( ) ;
}
2023-07-20 20:32:15 +03:00
}
/ * *
* Handles the click event for the update button of an extension .
* This function makes a POST request to '/update_extension' with the extension ' s name .
* If the extension is already up to date , it displays a success message .
* If the extension is not up to date , it updates the extension and displays a success message with the new commit hash .
* /
async function onUpdateClick ( ) {
2024-12-07 17:10:26 +02:00
const isCurrentUserAdmin = isAdmin ( ) ;
2023-07-20 20:32:15 +03:00
const extensionName = $ ( this ) . data ( 'name' ) ;
2024-12-07 18:12:27 +02:00
const isGlobal = getExtensionType ( extensionName ) === 'global' ;
2024-12-07 17:10:26 +02:00
if ( isGlobal && ! isCurrentUserAdmin ) {
toastr . error ( t ` You don't have permission to update global extensions. ` ) ;
return ;
}
2023-11-19 01:33:54 +02:00
$ ( this ) . find ( 'i' ) . addClass ( 'fa-spin' ) ;
2023-10-09 23:45:09 +03:00
await updateExtension ( extensionName , false ) ;
}
/ * *
* Updates a third - party extension via the API .
* @ param { string } extensionName Extension folder name
* @ param { boolean } quiet If true , don ' t show a success message
* /
async function updateExtension ( extensionName , quiet ) {
2023-07-20 20:32:15 +03:00
try {
2023-09-22 02:42:06 +09:00
const response = await fetch ( '/api/extensions/update' , {
2023-07-20 20:32:15 +03:00
method : 'POST' ,
headers : getRequestHeaders ( ) ,
2024-12-07 17:10:26 +02:00
body : JSON . stringify ( {
extensionName ,
2024-12-07 18:12:27 +02:00
global : getExtensionType ( extensionName ) === 'global' ,
2024-12-07 17:10:26 +02:00
} ) ,
2023-07-20 20:32:15 +03:00
} ) ;
const data = await response . json ( ) ;
2023-11-19 01:33:54 +02:00
if ( ! quiet ) {
showExtensionsDetails ( ) ;
}
2023-07-20 20:32:15 +03:00
if ( data . isUpToDate ) {
2023-10-09 23:45:09 +03:00
if ( ! quiet ) {
toastr . success ( 'Extension is already up to date' ) ;
}
2023-07-20 20:32:15 +03:00
} else {
2024-01-11 15:03:55 +02:00
toastr . success ( ` Extension ${ extensionName } updated to ${ data . shortCommitHash } ` , 'Reload the page to apply updates' ) ;
2023-10-09 23:45:09 +03:00
}
2023-07-20 20:32:15 +03:00
} catch ( error ) {
console . error ( 'Error:' , error ) ;
}
2023-10-09 23:45:09 +03:00
}
2023-07-20 20:32:15 +03:00
/ * *
* Handles the click event for the delete button of an extension .
2023-09-16 16:16:48 +03:00
* This function makes a POST request to '/api/extensions/delete' with the extension ' s name .
2023-07-20 20:32:15 +03:00
* If the extension is deleted , it displays a success message .
* Creates a popup for the user to confirm before delete .
* /
async function onDeleteClick ( ) {
const extensionName = $ ( this ) . data ( 'name' ) ;
2024-12-07 17:10:26 +02:00
const isCurrentUserAdmin = isAdmin ( ) ;
2024-12-07 18:12:27 +02:00
const isGlobal = getExtensionType ( extensionName ) === 'global' ;
2024-12-07 17:10:26 +02:00
if ( isGlobal && ! isCurrentUserAdmin ) {
toastr . error ( t ` You don't have permission to delete global extensions. ` ) ;
return ;
}
2023-07-20 20:32:15 +03:00
// use callPopup to create a popup for the user to confirm before delete
2024-12-07 18:42:37 +02:00
const confirmation = await callGenericPopup ( t ` Are you sure you want to delete ${ extensionName } ? ` , POPUP _TYPE . CONFIRM , '' , { } ) ;
2024-06-25 11:54:59 +03:00
if ( confirmation === POPUP _RESULT . AFFIRMATIVE ) {
2023-10-08 23:20:01 +03:00
await deleteExtension ( extensionName ) ;
2023-07-20 20:32:15 +03:00
}
2023-12-02 10:15:03 -05:00
}
2023-07-20 20:32:15 +03:00
2024-12-07 18:12:27 +02:00
async function onMoveClick ( ) {
const extensionName = $ ( this ) . data ( 'name' ) ;
const isCurrentUserAdmin = isAdmin ( ) ;
const isGlobal = getExtensionType ( extensionName ) === 'global' ;
if ( isGlobal && ! isCurrentUserAdmin ) {
toastr . error ( t ` You don't have permission to move extensions. ` ) ;
return ;
}
2024-12-07 18:42:37 +02:00
const source = getExtensionType ( extensionName ) ;
const destination = source === 'global' ? 'local' : 'global' ;
const confirmationHeader = t ` Move extension ` ;
const confirmationText = source == 'global'
? t ` Are you sure you want to move ${ extensionName } to your local extensions? This will make it available only for you. `
: t ` Are you sure you want to move ${ extensionName } to the global extensions? This will make it available for all users. ` ;
const confirmation = await Popup . show . confirm ( confirmationHeader , confirmationText ) ;
if ( ! confirmation ) {
return ;
}
$ ( this ) . find ( 'i' ) . addClass ( 'fa-spin' ) ;
await moveExtension ( extensionName , source , destination ) ;
}
/ * *
* Moves an extension via the API .
* @ param { string } extensionName Extension name
* @ param { string } source Source type
* @ param { string } destination Destination type
* @ returns { Promise < void > }
* /
async function moveExtension ( extensionName , source , destination ) {
try {
const result = await fetch ( '/api/extensions/move' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( {
extensionName ,
source ,
destination ,
} ) ,
} ) ;
if ( ! result . ok ) {
const text = await result . text ( ) ;
toastr . error ( text || result . statusText , t ` Extension move failed ` , { timeOut : 5000 } ) ;
console . error ( 'Extension move failed' , result . status , result . statusText , text ) ;
return ;
}
toastr . success ( t ` Extension ${ extensionName } moved. ` ) ;
await loadExtensionSettings ( { } , false , false ) ;
await Popup . util . popups . find ( popup => popup . content . querySelector ( '.extensions_info' ) ) ? . completeCancelled ( ) ;
showExtensionsDetails ( ) ;
} catch ( error ) {
console . error ( 'Error:' , error ) ;
}
2024-12-07 18:12:27 +02:00
}
2024-12-07 17:10:26 +02:00
/ * *
* Deletes an extension via the API .
* @ param { string } extensionName Extension name to delete
* /
2023-10-08 23:20:01 +03:00
export async function deleteExtension ( extensionName ) {
try {
2023-12-02 11:25:30 -05:00
await fetch ( '/api/extensions/delete' , {
2023-10-08 23:20:01 +03:00
method : 'POST' ,
headers : getRequestHeaders ( ) ,
2024-12-07 17:10:26 +02:00
body : JSON . stringify ( {
extensionName ,
2024-12-07 18:12:27 +02:00
global : getExtensionType ( extensionName ) === 'global' ,
2024-12-07 17:10:26 +02:00
} ) ,
2023-10-08 23:20:01 +03:00
} ) ;
} catch ( error ) {
console . error ( 'Error:' , error ) ;
}
2023-07-20 20:32:15 +03:00
2024-12-07 18:42:37 +02:00
toastr . success ( t ` Extension ${ extensionName } deleted ` ) ;
2023-10-08 23:20:01 +03:00
showExtensionsDetails ( ) ;
// reload the page to remove the extension from the list
location . reload ( ) ;
}
2023-07-20 20:32:15 +03:00
/ * *
* Fetches the version details of a specific extension .
*
* @ param { string } extensionName - The name of the extension .
2024-12-07 18:12:27 +02:00
* @ param { AbortSignal } [ abortSignal ] - The signal to abort the operation .
2023-08-22 22:45:12 +03:00
* @ return { Promise < object > } - An object containing the extension ' s version details .
2023-07-20 20:32:15 +03:00
* This object includes the currentBranchName , currentCommitHash , isUpToDate , and remoteUrl .
* @ throws { error } - If there is an error during the fetch operation , it logs the error to the console .
* /
2024-12-07 18:12:27 +02:00
async function getExtensionVersion ( extensionName , abortSignal ) {
2023-07-20 20:32:15 +03:00
try {
2023-09-16 16:16:48 +03:00
const response = await fetch ( '/api/extensions/version' , {
2023-07-20 20:32:15 +03:00
method : 'POST' ,
headers : getRequestHeaders ( ) ,
2024-12-07 18:12:27 +02:00
body : JSON . stringify ( {
extensionName ,
global : getExtensionType ( extensionName ) === 'global' ,
} ) ,
signal : abortSignal ,
2023-07-20 20:32:15 +03:00
} ) ;
const data = await response . json ( ) ;
return data ;
} catch ( error ) {
console . error ( 'Error:' , error ) ;
}
}
2023-10-08 23:20:01 +03:00
/ * *
* Installs a third - party extension via the API .
* @ param { string } url Extension repository URL
2024-12-07 17:10:26 +02:00
* @ param { boolean } global Is the extension global ?
2023-10-08 23:20:01 +03:00
* @ returns { Promise < void > }
* /
2024-12-07 17:10:26 +02:00
export async function installExtension ( url , global ) {
2023-10-23 16:53:31 +03:00
console . debug ( 'Extension installation started' , url ) ;
2023-07-20 20:32:15 +03:00
2024-12-07 18:42:37 +02:00
toastr . info ( t ` Please wait... ` , t ` Installing extension ` ) ;
2023-10-23 13:27:04 +03:00
2023-10-08 23:20:01 +03:00
const request = await fetch ( '/api/extensions/install' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
2024-12-07 17:10:26 +02:00
body : JSON . stringify ( {
url ,
global ,
} ) ,
2023-10-08 23:20:01 +03:00
} ) ;
if ( ! request . ok ) {
2023-10-27 13:07:56 +03:00
const text = await request . text ( ) ;
2024-12-07 18:42:37 +02:00
toastr . warning ( text || request . statusText , t ` Extension installation failed ` , { timeOut : 5000 } ) ;
2023-10-27 13:07:56 +03:00
console . error ( 'Extension installation failed' , request . status , request . statusText , text ) ;
2023-10-08 23:20:01 +03:00
return ;
}
const response = await request . json ( ) ;
2023-10-23 16:53:31 +03:00
toastr . success ( ` Extension " ${ response . display _name } " by ${ response . author } (version ${ response . version } ) has been installed successfully! ` , 'Extension installation successful' ) ;
console . debug ( ` Extension " ${ response . display _name } " has been installed successfully at ${ response . extensionPath } ` ) ;
2024-08-25 19:48:01 +03:00
await loadExtensionSettings ( { } , false , false ) ;
await eventSource . emit ( event _types . EXTENSION _SETTINGS _LOADED ) ;
2023-10-08 23:20:01 +03:00
}
2023-07-20 20:32:15 +03:00
2023-10-09 23:45:09 +03:00
/ * *
* Loads extension settings from the app settings .
* @ param { object } settings App Settings
* @ param { boolean } versionChanged Is this a version change ?
2024-08-25 19:48:01 +03:00
* @ param { boolean } enableAutoUpdate Enable auto - update
2023-10-09 23:45:09 +03:00
* /
2024-12-07 20:31:16 +02:00
export async function loadExtensionSettings ( settings , versionChanged , enableAutoUpdate ) {
2023-07-20 20:32:15 +03:00
if ( settings . extension _settings ) {
Object . assign ( extension _settings , settings . extension _settings ) ;
}
2023-12-02 13:04:51 -05:00
$ ( '#extensions_url' ) . val ( extension _settings . apiUrl ) ;
$ ( '#extensions_api_key' ) . val ( extension _settings . apiKey ) ;
$ ( '#extensions_autoconnect' ) . prop ( 'checked' , extension _settings . autoConnect ) ;
$ ( '#extensions_notify_updates' ) . prop ( 'checked' , extension _settings . notifyUpdates ) ;
2023-07-20 20:32:15 +03:00
// Activate offline extensions
2024-08-25 19:48:01 +03:00
await eventSource . emit ( event _types . EXTENSIONS _FIRST _LOAD ) ;
2024-12-07 17:10:26 +02:00
const extensions = await discoverExtensions ( ) ;
extensionNames = extensions . map ( x => x . name ) ;
extensionTypes = Object . fromEntries ( extensions . map ( x => [ x . name , x . type ] ) ) ;
2023-12-02 21:11:06 +02:00
manifests = await getManifests ( extensionNames ) ;
2023-10-09 23:45:09 +03:00
2024-08-25 19:48:01 +03:00
if ( versionChanged && enableAutoUpdate ) {
2024-06-25 11:54:59 +03:00
await autoUpdateExtensions ( false ) ;
2023-10-09 23:45:09 +03:00
}
2023-07-20 20:32:15 +03:00
await activateExtensions ( ) ;
if ( extension _settings . autoConnect && extension _settings . apiUrl ) {
connectToApi ( extension _settings . apiUrl ) ;
}
2023-10-27 21:23:58 +03:00
}
2023-10-09 23:45:09 +03:00
2023-10-27 21:23:58 +03:00
export function doDailyExtensionUpdatesCheck ( ) {
setTimeout ( ( ) => {
if ( extension _settings . notifyUpdates ) {
checkForExtensionUpdates ( false ) ;
}
} , 1 ) ;
2023-10-23 16:53:31 +03:00
}
2024-12-09 22:24:02 +02:00
const concurrencyLimit = 5 ;
let activeRequestsCount = 0 ;
const versionCheckQueue = [ ] ;
function enqueueVersionCheck ( fn ) {
return new Promise ( ( resolve , reject ) => {
versionCheckQueue . push ( ( ) => fn ( ) . then ( resolve ) . catch ( reject ) ) ;
processVersionCheckQueue ( ) ;
} ) ;
}
function processVersionCheckQueue ( ) {
if ( activeRequestsCount >= concurrencyLimit || versionCheckQueue . length === 0 ) {
return ;
}
activeRequestsCount ++ ;
const fn = versionCheckQueue . shift ( ) ;
fn ( ) . finally ( ( ) => {
activeRequestsCount -- ;
processVersionCheckQueue ( ) ;
} ) ;
}
2024-12-07 18:12:27 +02:00
/ * *
* Performs a manual check for updates on all 3 rd - party extensions .
* @ param { AbortSignal } abortSignal Signal to abort the operation
* @ returns { Promise < any [ ] > }
* /
async function checkForUpdatesManual ( abortSignal ) {
2024-12-03 22:48:10 +02:00
const promises = [ ] ;
for ( const id of Object . keys ( manifests ) . filter ( x => x . startsWith ( 'third-party' ) ) ) {
const externalId = id . replace ( 'third-party' , '' ) ;
2024-12-09 22:24:02 +02:00
const promise = enqueueVersionCheck ( async ( ) => {
2024-12-03 22:48:10 +02:00
try {
2024-12-07 18:12:27 +02:00
const data = await getExtensionVersion ( externalId , abortSignal ) ;
2024-12-03 22:48:10 +02:00
const extensionBlock = document . querySelector ( ` .extension_block[data-name=" ${ externalId } "] ` ) ;
if ( extensionBlock ) {
if ( data . isUpToDate === false ) {
const buttonElement = extensionBlock . querySelector ( '.btn_update' ) ;
if ( buttonElement ) {
buttonElement . classList . remove ( 'displayNone' ) ;
}
const nameElement = extensionBlock . querySelector ( '.extension_name' ) ;
if ( nameElement ) {
nameElement . classList . add ( 'update_available' ) ;
}
}
let branch = data . currentBranchName ;
let commitHash = data . currentCommitHash ;
let origin = data . remoteUrl ;
const originLink = extensionBlock . querySelector ( 'a' ) ;
if ( originLink ) {
originLink . href = origin ;
originLink . target = '_blank' ;
originLink . rel = 'noopener noreferrer' ;
}
const versionElement = extensionBlock . querySelector ( '.extension_version' ) ;
if ( versionElement ) {
versionElement . textContent += ` ( ${ branch } - ${ commitHash . substring ( 0 , 7 ) } ) ` ;
}
}
} catch ( error ) {
console . error ( 'Error checking for extension updates' , error ) ;
}
} ) ;
promises . push ( promise ) ;
}
return Promise . allSettled ( promises ) ;
}
2023-10-23 16:53:31 +03:00
/ * *
* Checks if there are updates available for 3 rd - party extensions .
* @ param { boolean } force Skip nag check
* @ returns { Promise < any > }
* /
async function checkForExtensionUpdates ( force ) {
if ( ! force ) {
const STORAGE _NAG _KEY = 'extension_update_nag' ;
const currentDate = new Date ( ) . toDateString ( ) ;
// Don't nag more than once a day
if ( localStorage . getItem ( STORAGE _NAG _KEY ) === currentDate ) {
return ;
}
localStorage . setItem ( STORAGE _NAG _KEY , currentDate ) ;
}
2024-12-07 17:10:26 +02:00
const isCurrentUserAdmin = isAdmin ( ) ;
2023-10-23 16:53:31 +03:00
const updatesAvailable = [ ] ;
const promises = [ ] ;
for ( const [ id , manifest ] of Object . entries ( manifests ) ) {
2024-12-07 18:12:27 +02:00
const isGlobal = getExtensionType ( id ) === 'global' ;
2024-12-07 17:10:26 +02:00
if ( isGlobal && ! isCurrentUserAdmin ) {
console . debug ( ` Skipping global extension: ${ manifest . display _name } ( ${ id } ) for non-admin user ` ) ;
continue ;
}
2024-12-09 22:24:02 +02:00
2023-10-23 16:53:31 +03:00
if ( manifest . auto _update && id . startsWith ( 'third-party' ) ) {
2024-12-09 22:24:02 +02:00
const promise = enqueueVersionCheck ( async ( ) => {
2023-10-23 16:53:31 +03:00
try {
const data = await getExtensionVersion ( id . replace ( 'third-party' , '' ) ) ;
2024-12-09 22:24:02 +02:00
if ( ! data . isUpToDate ) {
2023-10-23 16:53:31 +03:00
updatesAvailable . push ( manifest . display _name ) ;
}
} catch ( error ) {
console . error ( 'Error checking for extension updates' , error ) ;
}
} ) ;
promises . push ( promise ) ;
}
}
await Promise . allSettled ( promises ) ;
if ( updatesAvailable . length > 0 ) {
toastr . info ( ` ${ updatesAvailable . map ( x => ` • ${ x } ` ) . join ( '\n' ) } ` , 'Extension updates available' ) ;
}
2023-10-09 23:45:09 +03:00
}
2024-06-25 11:54:59 +03:00
/ * *
* Updates all 3 rd - party extensions that have auto - update enabled .
* @ param { boolean } forceAll Force update all even if not auto - updating
* @ returns { Promise < void > }
* /
async function autoUpdateExtensions ( forceAll ) {
2023-11-26 00:52:00 +02:00
if ( ! Object . values ( manifests ) . some ( x => x . auto _update ) ) {
return ;
}
2023-11-30 01:41:20 +02:00
const banner = toastr . info ( 'Auto-updating extensions. This may take several minutes.' , 'Please wait...' , { timeOut : 10000 , extendedTimeOut : 10000 } ) ;
2024-12-07 17:10:26 +02:00
const isCurrentUserAdmin = isAdmin ( ) ;
2023-11-22 00:58:06 +02:00
const promises = [ ] ;
2023-10-09 23:45:09 +03:00
for ( const [ id , manifest ] of Object . entries ( manifests ) ) {
2024-12-07 18:12:27 +02:00
const isGlobal = getExtensionType ( id ) === 'global' ;
2024-12-07 17:10:26 +02:00
if ( isGlobal && ! isCurrentUserAdmin ) {
console . debug ( ` Skipping global extension: ${ manifest . display _name } ( ${ id } ) for non-admin user ` ) ;
continue ;
}
2024-06-25 11:54:59 +03:00
if ( ( forceAll || manifest . auto _update ) && id . startsWith ( 'third-party' ) ) {
2023-10-09 23:45:09 +03:00
console . debug ( ` Auto-updating 3rd-party extension: ${ manifest . display _name } ( ${ id } ) ` ) ;
2023-11-22 00:58:06 +02:00
promises . push ( updateExtension ( id . replace ( 'third-party' , '' ) , true ) ) ;
2023-10-09 23:45:09 +03:00
}
}
2023-11-22 00:58:06 +02:00
await Promise . allSettled ( promises ) ;
2023-11-30 01:41:20 +02:00
toastr . clear ( banner ) ;
2023-07-20 20:32:15 +03:00
}
2023-10-29 18:29:10 +02:00
/ * *
* Runs the generate interceptors for all extensions .
* @ param { any [ ] } chat Chat array
* @ param { number } contextSize Context size
* @ returns { Promise < boolean > } True if generation should be aborted
* /
2024-12-07 20:31:16 +02:00
export async function runGenerationInterceptors ( chat , contextSize ) {
2023-10-29 18:29:10 +02:00
let aborted = false ;
let exitImmediately = false ;
const abort = ( /** @type {boolean} */ immediately ) => {
aborted = true ;
exitImmediately = immediately ;
} ;
2023-12-12 01:08:47 +02:00
for ( const manifest of Object . values ( manifests ) . sort ( ( a , b ) => a . loading _order - b . loading _order ) ) {
2023-07-20 20:32:15 +03:00
const interceptorKey = manifest . generate _interceptor ;
2024-12-07 18:12:27 +02:00
if ( typeof globalThis [ interceptorKey ] === 'function' ) {
2023-07-20 20:32:15 +03:00
try {
2024-12-07 18:12:27 +02:00
await globalThis [ interceptorKey ] ( chat , contextSize , abort ) ;
2023-07-20 20:32:15 +03:00
} catch ( e ) {
console . error ( ` Failed running interceptor for ${ manifest . display _name } ` , e ) ;
}
}
2023-10-29 18:29:10 +02:00
if ( exitImmediately ) {
break ;
}
2023-07-20 20:32:15 +03:00
}
2023-10-29 18:29:10 +02:00
return aborted ;
2023-07-20 20:32:15 +03:00
}
2024-03-12 01:49:05 +02:00
/ * *
* Writes a field to the character ' s data extensions object .
* @ param { number } characterId Index in the character array
* @ param { string } key Field name
* @ param { any } value Field value
* @ returns { Promise < void > } When the field is written
* /
export async function writeExtensionField ( characterId , key , value ) {
const context = getContext ( ) ;
const character = context . characters [ characterId ] ;
if ( ! character ) {
console . warn ( 'Character not found' , characterId ) ;
return ;
}
const path = ` data.extensions. ${ key } ` ;
setValueByPath ( character , path , value ) ;
// Process JSON data
if ( character . json _data ) {
const jsonData = JSON . parse ( character . json _data ) ;
setValueByPath ( jsonData , path , value ) ;
character . json _data = JSON . stringify ( jsonData ) ;
// Make sure the data doesn't get lost when saving the current character
if ( Number ( characterId ) === Number ( context . characterId ) ) {
$ ( '#character_json_data' ) . val ( character . json _data ) ;
}
}
// Save data to the server
const saveDataRequest = {
avatar : character . avatar ,
data : {
extensions : {
[ key ] : value ,
} ,
} ,
} ;
const mergeResponse = await fetch ( '/api/characters/merge-attributes' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( saveDataRequest ) ,
} ) ;
if ( ! mergeResponse . ok ) {
console . error ( 'Failed to save extension field' , mergeResponse . statusText ) ;
}
}
2024-08-13 01:09:14 +03:00
/ * *
* Prompts the user to enter the Git URL of the extension to import .
* After obtaining the Git URL , makes a POST request to '/api/extensions/install' to import the extension .
* If the extension is imported successfully , a success message is displayed .
* If the extension import fails , an error message is displayed and the error is logged to the console .
* After successfully importing the extension , the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted .
* @ param { string } [ suggestUrl ] Suggested URL to install
* @ returns { Promise < void > }
* /
export async function openThirdPartyExtensionMenu ( suggestUrl = '' ) {
2024-12-07 17:10:26 +02:00
const isCurrentUserAdmin = isAdmin ( ) ;
const html = await renderTemplateAsync ( 'installExtension' , { isCurrentUserAdmin } ) ;
const okButton = isCurrentUserAdmin ? t ` Install just for me ` : t ` Install ` ;
let global = false ;
const installForAllButton = {
2024-12-07 18:12:27 +02:00
text : t ` Install for all users ` ,
2024-12-07 17:10:26 +02:00
appendAtEnd : false ,
action : async ( ) => {
global = true ;
await popup . complete ( POPUP _RESULT . AFFIRMATIVE ) ;
} ,
} ;
const customButtons = isCurrentUserAdmin ? [ installForAllButton ] : [ ] ;
const popup = new Popup ( html , POPUP _TYPE . INPUT , suggestUrl ? ? '' , { okButton , customButtons } ) ;
const input = await popup . show ( ) ;
2024-08-13 01:09:14 +03:00
if ( ! input ) {
console . debug ( 'Extension install cancelled' ) ;
return ;
}
const url = String ( input ) . trim ( ) ;
2024-12-07 17:10:26 +02:00
await installExtension ( url , global ) ;
2024-08-13 01:09:14 +03:00
}
2024-09-25 21:58:46 +02:00
export async function initExtensions ( ) {
2024-06-24 23:17:58 +03:00
await addExtensionsButtonAndMenu ( ) ;
2023-12-02 13:04:51 -05:00
$ ( '#extensionsMenuButton' ) . css ( 'display' , 'flex' ) ;
2023-07-20 20:32:15 +03:00
2023-12-02 13:04:51 -05:00
$ ( '#extensions_connect' ) . on ( 'click' , connectClickHandler ) ;
$ ( '#extensions_autoconnect' ) . on ( 'input' , autoConnectInputHandler ) ;
$ ( '#extensions_details' ) . on ( 'click' , showExtensionsDetails ) ;
$ ( '#extensions_notify_updates' ) . on ( 'input' , notifyUpdatesInputHandler ) ;
2024-12-07 18:12:27 +02:00
$ ( document ) . on ( 'click' , '.extensions_info .extension_block .toggle_disable' , onDisableExtensionClick ) ;
$ ( document ) . on ( 'click' , '.extensions_info .extension_block .toggle_enable' , onEnableExtensionClick ) ;
$ ( document ) . on ( 'click' , '.extensions_info .extension_block .btn_update' , onUpdateClick ) ;
$ ( document ) . on ( 'click' , '.extensions_info .extension_block .btn_delete' , onDeleteClick ) ;
$ ( document ) . on ( 'click' , '.extensions_info .extension_block .btn_move' , onMoveClick ) ;
2023-10-23 16:53:31 +03:00
/ * *
* Handles the click event for the third - party extension import button .
*
* @ listens # third _party _extension _button # click - The click event of the '#third_party_extension_button' element .
* /
2024-08-13 01:09:14 +03:00
$ ( '#third_party_extension_button' ) . on ( 'click' , ( ) => openThirdPartyExtensionMenu ( ) ) ;
2024-09-25 21:58:46 +02:00
}