Files
risuai/src/ts/storage/globalApi.ts
2024-05-24 23:28:19 +09:00

1557 lines
49 KiB
TypeScript

import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs"
import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "../util"
import { convertFileSrc, invoke } from "@tauri-apps/api/tauri"
import { v4 as uuidv4, v4 } from 'uuid';
import { appDataDir, join } from "@tauri-apps/api/path";
import { get } from "svelte/store";
import {open} from '@tauri-apps/api/shell'
import { DataBase, loadedStore, setDatabase, type Database, defaultSdDataFunc } from "./database";
import { appWindow } from "@tauri-apps/api/window";
import { checkUpdate } from "../update";
import { botMakerMode, selectedCharID } from "../stores";
import { Body, ResponseType, fetch as TauriFetch } from "@tauri-apps/api/http";
import { loadPlugins } from "../plugins/plugins";
import { alertConfirm, alertError, alertNormal, alertNormalWait } from "../alert";
import { checkDriverInit, syncDrive } from "../drive/drive";
import { hasher } from "../parser";
import { characterURLImport, hubURL } from "../characterCards";
import { defaultJailbreak, defaultMainPrompt, oldJailbreak, oldMainPrompt } from "./defaultPrompts";
import { loadRisuAccountData } from "../drive/accounter";
import { decodeRisuSave, encodeRisuSave } from "./risuSave";
import { AutoStorage } from "./autoStorage";
import { updateAnimationSpeed } from "../gui/animation";
import { updateColorScheme, updateTextTheme } from "../gui/colorscheme";
import { saveDbKei } from "../kei/backup";
import { Capacitor, CapacitorHttp } from '@capacitor/core';
import * as CapFS from '@capacitor/filesystem'
import { save } from "@tauri-apps/api/dialog";
import type { RisuModule } from "../process/modules";
import { listen } from '@tauri-apps/api/event'
import { registerPlugin } from '@capacitor/core';
import { language } from "src/lang";
import { startObserveDom } from "../observer";
import { removeDefaultHandler } from "src/main";
import { updateGuisize } from "../gui/guisize";
import { encodeCapKeySafe } from "./mobileStorage";
//@ts-ignore
export const isTauri = !!window.__TAURI__
//@ts-ignore
export const isNodeServer = !!globalThis.__NODE__
export const forageStorage = new AutoStorage()
interface fetchLog{
body:string
header:string
response:string
success:boolean,
date:string
url:string
responseType?:string
chatId?:string
}
let fetchLog:fetchLog[] = []
export async function downloadFile(name:string, dat:Uint8Array|ArrayBuffer|string) {
if(typeof(dat) === 'string'){
dat = Buffer.from(dat, 'utf-8')
}
const data = new Uint8Array(dat)
const downloadURL = (data:string, fileName:string) => {
const a = document.createElement('a')
a.href = data
a.download = fileName
document.body.appendChild(a)
a.style.display = 'none'
a.click()
a.remove()
}
if(isTauri){
await writeBinaryFile(name, data, {dir: BaseDirectory.Download})
}
else{
downloadURL(`data:png/image;base64,${Buffer.from(data).toString('base64')}`, name)
}
}
let fileCache:{
origin: string[], res:(Uint8Array|'loading'|'done')[]
} = {
origin: [],
res: []
}
let pathCache:{[key:string]:string} = {}
let checkedPaths:string[] = []
async function checkCapFileExists(getUriOptions: CapFS.GetUriOptions): Promise<boolean> {
try {
await CapFS.Filesystem.stat(getUriOptions);
return true;
} catch (checkDirException) {
if (checkDirException.message === 'File does not exist') {
return false;
} else {
throw checkDirException;
}
}
}
export async function getFileSrc(loc:string) {
if(isTauri){
if(loc.startsWith('assets')){
if(appDataDirPath === ''){
appDataDirPath = await appDataDir();
}
const cached = pathCache[loc]
if(cached){
return convertFileSrc(cached)
}
else{
const joined = await join(appDataDirPath,loc)
pathCache[loc] = joined
return convertFileSrc(joined)
}
}
return convertFileSrc(loc)
}
if(forageStorage.isAccount && loc.startsWith('assets')){
return hubURL + `/rs/` + loc
}
if(Capacitor.isNativePlatform()){
if(!await checkCapFileExists({
path: encodeCapKeySafe(loc),
directory: CapFS.Directory.External
})){
return ''
}
const uri = await CapFS.Filesystem.getUri({
path: encodeCapKeySafe(loc),
directory: CapFS.Directory.External
})
return Capacitor.convertFileSrc(uri.uri)
}
try {
if(usingSw){
const encoded = Buffer.from(loc,'utf-8').toString('hex')
let ind = fileCache.origin.indexOf(loc)
if(ind === -1){
ind = fileCache.origin.length
fileCache.origin.push(loc)
fileCache.res.push('loading')
try {
const hasCache:boolean = (await (await fetch("/sw/check/" + encoded)).json()).able
if(hasCache){
fileCache.res[ind] = 'done'
return "/sw/img/" + encoded
}
else{
const f:Uint8Array = await forageStorage.getItem(loc)
await fetch("/sw/register/" + encoded, {
method: "POST",
body: f
})
fileCache.res[ind] = 'done'
await sleep(10)
}
return "/sw/img/" + encoded
} catch (error) {
}
}
else{
const f = fileCache.res[ind]
if(f === 'loading'){
while(fileCache.res[ind] === 'loading'){
await sleep(10)
}
}
return "/sw/img/" + encoded
}
}
else{
let ind = fileCache.origin.indexOf(loc)
if(ind === -1){
ind = fileCache.origin.length
fileCache.origin.push(loc)
fileCache.res.push('loading')
const f:Uint8Array = await forageStorage.getItem(loc)
fileCache.res[ind] = f
return `data:image/png;base64,${Buffer.from(f).toString('base64')}`
}
else{
const f = fileCache.res[ind]
if(f === 'loading'){
while(fileCache.res[ind] === 'loading'){
await sleep(10)
}
return `data:image/png;base64,${Buffer.from(fileCache.res[ind]).toString('base64')}`
}
return `data:image/png;base64,${Buffer.from(f).toString('base64')}`
}
}
} catch (error) {
console.error(error)
return ''
}
}
let appDataDirPath = ''
export async function readImage(data:string) {
if(isTauri){
if(data.startsWith('assets')){
if(appDataDirPath === ''){
appDataDirPath = await appDataDir();
}
return await readBinaryFile(await join(appDataDirPath,data))
}
return await readBinaryFile(data)
}
else{
return (await forageStorage.getItem(data) as Uint8Array)
}
}
export async function saveAsset(data:Uint8Array, customId:string = '', fileName:string = ''){
let id = ''
if(customId !== ''){
id = customId
}
else{
try {
id = await hasher(data)
} catch (error) {
id = uuidv4()
}
}
let fileExtension:string = 'png'
if(fileName && fileName.split('.').length > 0){
fileExtension = fileName.split('.').pop()
}
if(isTauri){
await writeBinaryFile(`assets/${id}.${fileExtension}`, data ,{dir: BaseDirectory.AppData})
return `assets/${id}.${fileExtension}`
}
else{
let form = `assets/${id}.${fileExtension}`
const replacer = await forageStorage.setItem(form, data)
if(replacer){
return replacer
}
return form
}
}
export async function loadAsset(id:string){
if(isTauri){
return await readBinaryFile(id,{dir: BaseDirectory.AppData})
}
else{
return await forageStorage.getItem(id) as Uint8Array
}
}
let lastSave = ''
export async function saveDb(){
lastSave =JSON.stringify(get(DataBase))
let changed = false
syncDrive()
DataBase.subscribe(() => {
changed = true
})
let gotChannel = false
const sessionID = v4()
let channel:BroadcastChannel
if(window.BroadcastChannel){
channel = new BroadcastChannel('risu-db')
}
if(channel){
channel.onmessage = async (ev) => {
if(ev.data === sessionID){
return
}
if(!gotChannel){
gotChannel = true
await alertNormalWait(language.activeTabChange)
gotChannel = false
}
}
}
let savetrys = 0
while(true){
try {
if(changed){
if(gotChannel){
//Data is saved in other tab
await sleep(1000)
continue
}
if(channel){
channel.postMessage(sessionID)
}
changed = false
let db = get(DataBase)
db.saveTime = Math.floor(Date.now() / 1000)
const dbData = encodeRisuSave(db)
if(isTauri){
await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData})
await writeBinaryFile(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData, {dir: BaseDirectory.AppData})
}
else{
if(!forageStorage.isAccount){
await forageStorage.setItem('database/database.bin', dbData)
await forageStorage.setItem(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData)
}
if(forageStorage.isAccount){
const dbData = encodeRisuSave(db, 'compression')
const z:Database = decodeRisuSave(dbData)
if(z.formatversion){
await forageStorage.setItem('database/database.bin', dbData)
}
await sleep(5000);
}
}
if(!forageStorage.isAccount){
await getDbBackups()
}
savetrys = 0
}
await saveDbKei()
await sleep(500)
} catch (error) {
if(savetrys > 4){
await alertConfirm(`DBSaveError: ${error.message ?? error}. report to the developer.`)
}
else{
}
}
}
}
async function getDbBackups() {
let db = get(DataBase)
if(db?.account?.useSync){
return
}
if(isTauri){
const keys = await readDir('database', {dir: BaseDirectory.AppData})
let backups:number[] = []
for(const key of keys){
if(key.name.startsWith("dbbackup-")){
let da = key.name.substring(9)
da = da.substring(0,da.length-4)
backups.push(parseInt(da))
}
}
backups.sort((a, b) => b - a)
while(backups.length > 20){
const last = backups.pop()
await removeFile(`database/dbbackup-${last}.bin`,{dir: BaseDirectory.AppData})
}
return backups
}
else{
const keys = await forageStorage.keys()
let backups:number[] = []
for(const key of keys){
if(key.startsWith("database/dbbackup-")){
let da = key.substring(18)
da = da.substring(0,da.length-4)
backups.push(parseInt(da))
}
}
while(backups.length > 20){
const last = backups.pop()
await forageStorage.removeItem(`database/dbbackup-${last}.bin`)
}
return backups
}
}
let usingSw = false
export async function loadData() {
const loaded = get(loadedStore)
if(!loaded){
try {
if(isTauri){
appWindow.maximize()
if(!await exists('', {dir: BaseDirectory.AppData})){
await createDir('', {dir: BaseDirectory.AppData})
}
if(!await exists('database', {dir: BaseDirectory.AppData})){
await createDir('database', {dir: BaseDirectory.AppData})
}
if(!await exists('assets', {dir: BaseDirectory.AppData})){
await createDir('assets', {dir: BaseDirectory.AppData})
}
if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){
await writeBinaryFile('database/database.bin',
encodeRisuSave({})
,{dir: BaseDirectory.AppData})
}
try {
setDatabase(
decodeRisuSave(await readBinaryFile('database/database.bin',{dir: BaseDirectory.AppData}))
)
} catch (error) {
const backups = await getDbBackups()
let backupLoaded = false
for(const backup of backups){
try {
const backupData = await readBinaryFile(`database/dbbackup-${backup}.bin`,{dir: BaseDirectory.AppData})
setDatabase(
decodeRisuSave(backupData)
)
backupLoaded = true
} catch (error) {
console.error(error)
}
}
if(!backupLoaded){
throw "Your save file is corrupted"
}
}
await checkUpdate()
await changeFullscreen()
}
else{
let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin')
if(checkNullish(gotStorage)){
gotStorage = encodeRisuSave({})
await forageStorage.setItem('database/database.bin', gotStorage)
}
try {
setDatabase(
decodeRisuSave(gotStorage)
)
} catch (error) {
const backups = await getDbBackups()
let backupLoaded = false
for(const backup of backups){
try {
const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`)
setDatabase(
decodeRisuSave(backupData)
)
backupLoaded = true
} catch (error) {}
}
if(!backupLoaded){
throw "Your save file is corrupted"
}
}
if(await forageStorage.checkAccountSync()){
let gotStorage:Uint8Array = await forageStorage.getItem('database/database.bin')
if(checkNullish(gotStorage)){
gotStorage = encodeRisuSave({})
await forageStorage.setItem('database/database.bin', gotStorage)
}
try {
setDatabase(
decodeRisuSave(gotStorage)
)
} catch (error) {
const backups = await getDbBackups()
let backupLoaded = false
for(const backup of backups){
try {
const backupData:Uint8Array = await forageStorage.getItem(`database/dbbackup-${backup}.bin`)
setDatabase(
decodeRisuSave(backupData)
)
backupLoaded = true
} catch (error) {}
}
if(!backupLoaded){
throw "Your save file is corrupted"
}
}
}
const isDriverMode = await checkDriverInit()
if(isDriverMode){
return
}
if(navigator.serviceWorker && (!Capacitor.isNativePlatform())){
usingSw = true
await registerSw()
}
else{
usingSw = false
}
if(get(DataBase).didFirstSetup){
characterURLImport()
}
}
try {
await pargeChunks()
} catch (error) {}
try {
await loadPlugins()
} catch (error) {}
if(get(DataBase).account){
try {
await loadRisuAccountData()
} catch (error) {}
}
await checkNewFormat()
const db = get(DataBase);
updateColorScheme()
updateTextTheme()
updateAnimationSpeed()
updateHeightMode()
updateErrorHandling()
updateGuisize()
if(db.botSettingAtStart){
botMakerMode.set(true)
}
loadedStore.set(true)
selectedCharID.set(-1)
startObserveDom()
saveDb()
} catch (error) {
alertError(`${error}`)
}
}
}
export async function getFetchData(id:string) {
for(const log of fetchLog){
if(log.chatId === id){
return log
}
}
return null
}
function updateErrorHandling(){
removeDefaultHandler()
const errorHandler = (event: ErrorEvent) => {
console.error(event.error)
alertError(event.error)
}
const rejectHandler = (event: PromiseRejectionEvent) => {
console.error(event.reason)
alertError(event.reason)
}
window.addEventListener('error', errorHandler)
window.addEventListener('unhandledrejection', rejectHandler)
}
const knownHostes = ["localhost","127.0.0.1","0.0.0.0"]
interface GlobalFetchArgs {
plainFetchForce?: boolean;
body?: any;
headers?: { [key: string]: string };
rawResponse?: boolean;
method?: 'POST' | 'GET';
abortSignal?: AbortSignal;
useRisuToken?: boolean;
chatId?: string;
}
interface GlobalFetchResult {
ok: boolean;
data: any;
headers: { [key: string]: string };
}
export function addFetchLog(arg:{
body:any,
headers?:{[key:string]:string},
response:any,
success:boolean,
url:string,
resType?:string,
chatId?:string
}){
fetchLog.unshift({
body: typeof(arg.body) === 'string' ? arg.body : JSON.stringify(arg.body, null, 2),
header: JSON.stringify(arg.headers ?? {}, null, 2),
response: typeof(arg.response) === 'string' ? arg.response : JSON.stringify(arg.response, null, 2),
responseType: arg.resType ?? 'json',
success: arg.success,
date: (new Date()).toLocaleTimeString(),
url: arg.url,
chatId: arg.chatId
})
return fetchLog.length - 1
}
export async function globalFetch(url: string, arg: GlobalFetchArgs = {}): Promise<GlobalFetchResult> {
try {
const db = get(DataBase)
const method = arg.method ?? "POST"
db.requestmet = "normal"
if (arg.abortSignal?.aborted) { return { ok: false, data: 'aborted', headers: {} }}
const urlHost = new URL(url).hostname
const forcePlainFetch = (knownHostes.includes(urlHost) && !isTauri) || db.usePlainFetch || arg.plainFetchForce
if (knownHostes.includes(urlHost) && !isTauri && !isNodeServer){
return { ok: false, headers: {}, data: 'You are trying local request on web version. This is not allowed due to browser security policy. Use the desktop version instead, or use a tunneling service like ngrok and set the CORS to allow all.' }
}
// Simplify the globalFetch function: Detach built-in functions
if (forcePlainFetch) {
return await fetchWithPlainFetch(url, arg);
}
if (isTauri) {
return await fetchWithTauri(url, arg);
}
if (Capacitor.isNativePlatform()) {
return await fetchWithCapacitor(url, arg);
}
return await fetchWithProxy(url, arg);
} catch (error) {
console.error(error);
return { ok: false, data: `${error}`, headers: {} };
}
}
// Decoupled globalFetch built-in function
function addFetchLogInGlobalFetch(response:any, success:boolean, url:string, arg:GlobalFetchArgs){
try{
fetchLog.unshift({
body: JSON.stringify(arg.body, null, 2),
header: JSON.stringify(arg.headers ?? {}, null, 2),
response: JSON.stringify(response, null, 2),
success: success,
date: (new Date()).toLocaleTimeString(),
url: url,
chatId: arg.chatId
})
}
catch{
fetchLog.unshift({
body: JSON.stringify(arg.body, null, 2),
header: JSON.stringify(arg.headers ?? {}, null, 2),
response: `${response}`,
success: success,
date: (new Date()).toLocaleTimeString(),
url: url,
chatId: arg.chatId
})
}
}
// Decoupled globalFetch built-in function
async function fetchWithPlainFetch(url: string, arg: GlobalFetchArgs): Promise<GlobalFetchResult> {
try {
const headers = { 'Content-Type': 'application/json', ...arg.headers };
const response = await fetch(new URL(url), { body: JSON.stringify(arg.body), headers, method: arg.method ?? "POST", signal: arg.abortSignal });
const data = arg.rawResponse ? new Uint8Array(await response.arrayBuffer()) : await response.json();
const ok = response.ok && response.status >= 200 && response.status < 300;
addFetchLogInGlobalFetch(data, ok, url, arg);
return { ok, data, headers: Object.fromEntries(response.headers) };
} catch (error) {
return { ok: false, data: `${error}`, headers: {} };
}
}
// Decoupled globalFetch built-in function
async function fetchWithTauri(url: string, arg: GlobalFetchArgs): Promise<GlobalFetchResult> {
const body = !arg.body ? null : arg.body instanceof URLSearchParams ? Body.text(arg.body.toString()) : Body.json(arg.body);
const headers = arg.headers ?? {};
const fetchPromise = TauriFetch(url, {
body,
method: arg.method ?? 'POST',
headers,
timeout: { secs: get(DataBase).timeOut, nanos: 0 },
responseType: arg.rawResponse ? ResponseType.Binary : ResponseType.JSON,
});
let abortFn = () => {};
const abortPromise = new Promise<"aborted">((res, rej) => {
abortFn = () => res("aborted");
arg.abortSignal?.addEventListener('abort', abortFn);
});
const result = await Promise.any([fetchPromise, abortPromise]);
arg.abortSignal?.removeEventListener('abort', abortFn);
if (result === 'aborted') {
return { ok: false, data: 'aborted', headers: {} };
}
const data = arg.rawResponse ? new Uint8Array(result.data as number[]) : result.data;
addFetchLogInGlobalFetch(data, result.ok, url, arg);
return { ok: result.ok, data, headers: result.headers };
}
// Decoupled globalFetch built-in function
async function fetchWithCapacitor(url: string, arg: GlobalFetchArgs): Promise<GlobalFetchResult> {
const { body, headers = {}, rawResponse } = arg;
headers["Content-Type"] = body instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json";
const res = await CapacitorHttp.request({ url, method: arg.method ?? "POST", headers, data: body, responseType: rawResponse ? "arraybuffer" : "json" });
addFetchLogInGlobalFetch(rawResponse ? "Uint8Array Response" : res.data, true, url, arg);
return {
ok: true,
data: rawResponse ? new Uint8Array(res.data as ArrayBuffer) : res.data,
headers: res.headers,
};
}
// Decoupled globalFetch built-in function
async function fetchWithProxy(url: string, arg: GlobalFetchArgs): Promise<GlobalFetchResult> {
try {
const furl = !isTauri && !isNodeServer ? `${hubURL}/proxy2` : `/proxy2`;
arg.headers["Content-Type"] ??= arg.body instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json";
const headers = {
"risu-header": encodeURIComponent(JSON.stringify(arg.headers)),
"risu-url": encodeURIComponent(url),
"Content-Type": arg.body instanceof URLSearchParams ? "application/x-www-form-urlencoded" : "application/json",
...(arg.useRisuToken && { "x-risu-tk": "use" }),
};
const body = arg.body instanceof URLSearchParams ? arg.body.toString() : JSON.stringify(arg.body);
const response = await fetch(furl, { body, headers, method: arg.method ?? "POST", signal: arg.abortSignal });
const isSuccess = response.ok && response.status >= 200 && response.status < 300;
if (arg.rawResponse) {
const data = new Uint8Array(await response.arrayBuffer());
addFetchLogInGlobalFetch("Uint8Array Response", isSuccess, url, arg);
return { ok: isSuccess, data, headers: Object.fromEntries(response.headers) };
}
const text = await response.text();
try {
const data = JSON.parse(text);
addFetchLogInGlobalFetch(data, isSuccess, url, arg);
return { ok: isSuccess, data, headers: Object.fromEntries(response.headers) };
} catch (error) {
const errorMsg = text.startsWith('<!DOCTYPE') ? "Responded HTML. Is your URL, API key, and password correct?" : text;
addFetchLogInGlobalFetch(text, false, url, arg);
return { ok: false, data: errorMsg, headers: Object.fromEntries(response.headers) };
}
} catch (error) {
return { ok: false, data: `${error}`, headers: {} };
}
}
async function registerSw() {
await navigator.serviceWorker.register("/sw.js", {
scope: "/"
});
await sleep(100)
const da = await fetch('/sw/init')
if(!(da.status >= 200 && da.status < 300)){
location.reload()
}
else{
}
}
const re = /\\/g
function getBasename(data:string){
const splited = data.replace(re, '/').split('/')
const lasts = splited[splited.length-1]
return lasts
}
export function getUnpargeables(db:Database, uptype:'basename'|'pure' = 'basename') {
let unpargeable:string[] = []
function addUnparge(data:string){
if(!data){
return
}
if(data === ''){
return
}
const bn = uptype === 'basename' ? getBasename(data) : data
if(!unpargeable.includes(bn)){
unpargeable.push(bn)
}
}
addUnparge(db.customBackground)
addUnparge(db.userIcon)
for(const cha of db.characters){
if(cha.image){
addUnparge(cha.image)
}
if(cha.emotionImages){
for(const em of cha.emotionImages){
addUnparge(em[1])
}
}
if(cha.type !== 'group'){
if(cha.additionalAssets){
for(const em of cha.additionalAssets){
addUnparge(em[1])
}
}
if(cha.vits){
const keys = Object.keys(cha.vits.files)
for(const key of keys){
const vit = cha.vits.files[key]
addUnparge(vit)
}
}
}
}
if(db.personas){
db.personas.map((v) => {
addUnparge(v.icon)
})
}
return unpargeable
}
export function replaceDbResources(db:Database,replacer:{[key:string]:string}) {
let unpargeable:string[] = []
function replaceData(data:string){
if(!data){
return data
}
return replacer[data] ?? data
}
db.customBackground = replaceData(db.customBackground)
db.userIcon = replaceData(db.userIcon)
for(const cha of db.characters){
if(cha.image){
cha.image = replaceData(cha.image)
}
if(cha.emotionImages){
for(let i=0;i<cha.emotionImages.length;i++){
cha.emotionImages[i][1] = replaceData(cha.emotionImages[i][1])
}
}
if(cha.type !== 'group'){
if(cha.additionalAssets){
for(let i=0;i<cha.additionalAssets.length;i++){
cha.additionalAssets[i][1] = replaceData(cha.additionalAssets[i][1])
}
}
}
}
return db
}
async function checkNewFormat() {
let db = get(DataBase)
//check data integrity
db.characters = db.characters.map((v) => {
if(!v){
return null
}
v.chaId ??= uuidv4()
v.type ??= 'character'
v.chatPage ??= 0
v.chats ??= []
v.customscript ??= []
v.firstMessage ??= ''
v.globalLore ??= []
v.name ??= ''
v.viewScreen ??= 'none'
v.emotionImages = v.emotionImages ?? []
if(v.type === 'character'){
v.bias ??= []
v.characterVersion ??= ''
v.creator ??= ''
v.desc ??= ''
v.utilityBot ??= false
v.tags ??= []
v.systemPrompt ??= ''
v.scenario ??= ''
}
return v
}).filter((v) => {
return v !== null
})
if(!db.formatversion){
function checkParge(data:string){
if(data.startsWith('assets') || (data.length < 3)){
return data
}
else{
const d = 'assets/' + (data.replace(/\\/g, '/').split('assets/')[1])
if(!d){
return data
}
return d
}
}
db.customBackground = checkParge(db.customBackground)
db.userIcon = checkParge(db.userIcon)
for(let i=0;i<db.characters.length;i++){
if(db.characters[i].image){
db.characters[i].image = checkParge(db.characters[i].image)
}
if(db.characters[i].emotionImages){
for(let i2=0;i2<db.characters[i].emotionImages.length;i2++){
if(db.characters[i].emotionImages[i2] && db.characters[i].emotionImages[i2].length >= 2){
db.characters[i].emotionImages[i2][1] = checkParge(db.characters[i].emotionImages[i2][1])
}
}
}
}
db.formatversion = 2
}
if(db.formatversion < 3){
for(let i=0;i<db.characters.length;i++){
let cha = db.characters[i]
if(cha.type === 'character'){
if(checkNullish(cha.sdData)){
cha.sdData = defaultSdDataFunc()
}
}
}
db.formatversion = 3
}
if(db.formatversion < 4){
db.modules ??= []
db.enabledModules ??=[]
//convert globallore and global regex to modules
if(db.globalscript && db.globalscript.length > 0){
const id = v4()
let regexModule:RisuModule = {
name: "Global Regex",
description: "Converted from legacy global regex",
id: id,
regex: structuredClone(db.globalscript)
}
db.modules.push(regexModule)
db.enabledModules.push(id)
db.globalscript = []
}
if(db.loreBook && db.loreBook.length > 0){
const selIndex = db.loreBookPage
for(let i=0;i<db.loreBook.length;i++){
const id = v4()
let lbModule:RisuModule = {
name: db.loreBook[i].name || "Unnamed Global Lorebook",
description: "Converted from legacy global lorebook",
id: id,
lorebook: structuredClone(db.loreBook[i].data)
}
db.modules.push(lbModule)
if(i === selIndex){
db.enabledModules.push(id)
}
db.globalscript = []
}
db.loreBook = []
}
db.formatversion = 4
}
if(!db.characterOrder){
db.characterOrder = []
}
if(db.mainPrompt === oldMainPrompt){
db.mainPrompt = defaultMainPrompt
}
if(db.mainPrompt === oldJailbreak){
db.mainPrompt = defaultJailbreak
}
for(let i=0;i<db.characters.length;i++){
const trashTime = db.characters[i].trashTime
const targetTrashTime = trashTime ? trashTime + 1000 * 60 * 60 * 24 * 3 : 0
if(trashTime && targetTrashTime < Date.now()){
db.characters.splice(i,1)
i--
}
}
setDatabase(db)
checkCharOrder()
}
export function checkCharOrder() {
let db = get(DataBase)
db.characterOrder = db.characterOrder ?? []
let ordered = structuredClone(db.characterOrder ?? [])
for(let i=0;i<db.characterOrder.length;i++){
const folder =db.characterOrder[i]
if(typeof(folder) !== 'string' && folder){
for(const f of folder.data){
ordered.push(f)
}
}
}
let charIdList:string[] = []
for(let i=0;i<db.characters.length;i++){
const char = db.characters[i]
const charId = char.chaId
if(!char.trashTime){
charIdList.push(charId)
}
if(!ordered.includes(charId)){
if(charId !== '§temp' && charId !== '§playground' && !char.trashTime){
db.characterOrder.push(charId)
}
}
}
for(let i=0;i<db.characterOrder.length;i++){
const data =db.characterOrder[i]
if(typeof(data) !== 'string'){
if(!data){
db.characterOrder.splice(i,1)
i--;
continue
}
if(data.data.length === 0){
db.characterOrder.splice(i,1)
i--;
continue
}
for(let i2=0;i2<data.data.length;i2++){
const data2 = data.data[i2]
if(!charIdList.includes(data2)){
data.data.splice(i2,1)
i2--;
}
}
db.characterOrder[i] = data
}
else{
if(!charIdList.includes(data)){
db.characterOrder.splice(i,1)
i--;
}
}
}
setDatabase(db)
}
async function pargeChunks(){
const db = get(DataBase)
if(db.account?.useSync){
return
}
const unpargeable = getUnpargeables(db)
if(isTauri){
const assets = await readDir('assets', {dir: BaseDirectory.AppData})
for(const asset of assets){
const n = getBasename(asset.name)
if(unpargeable.includes(n)){
}
else{
await removeFile(asset.path)
}
}
}
else{
const indexes = await forageStorage.keys()
for(const asset of indexes){
if(!asset.startsWith('assets/')){
continue
}
const n = getBasename(asset)
if(unpargeable.includes(n)){
}
else{
await forageStorage.removeItem(asset)
}
}
}
}
export function getRequestLog(){
let logString = ''
const b = '\n\`\`\`json\n'
const bend = '\n\`\`\`\n'
for(const log of fetchLog){
logString += `## ${log.date}\n\n* Request URL\n\n${b}${log.url}${bend}\n\n* Request Body\n\n${b}${log.body}${bend}\n\n* Request Header\n\n${b}${log.header}${bend}\n\n`
+ `* Response Body\n\n${b}${log.response}${bend}\n\n* Response Success\n\n${b}${log.success}${bend}\n\n`
}
return logString
}
export function openURL(url:string){
if(isTauri){
open(url)
}
else{
window.open(url, "_blank")
}
}
function formDataToString(formData: FormData): string {
const params: string[] = [];
for (const [name, value] of formData.entries()) {
params.push(`${encodeURIComponent(name)}=${encodeURIComponent(value.toString())}`);
}
return params.join('&');
}
export function getModelMaxContext(model:string):number|undefined{
if(model.startsWith('gpt35')){
if(model.includes('16k')){
return 16000
}
return 4000
}
if(model.startsWith('gpt4')){
if(model.includes('turbo')){
return 128000
}
if(model.includes('32k')){
return 32000
}
return 8000
}
return undefined
}
class TauriWriter{
path: string
firstWrite: boolean = true
constructor(path: string){
this.path = path
}
async write(data:Uint8Array) {
await writeBinaryFile(this.path, data, {
append: !this.firstWrite
})
this.firstWrite = false
}
async close(){
// do nothing
}
}
class MobileWriter{
path: string
firstWrite: boolean = true
constructor(path: string){
this.path = path
}
async write(data:Uint8Array) {
if(this.firstWrite){
if(!await CapFS.Filesystem.checkPermissions()){
await CapFS.Filesystem.requestPermissions()
}
await CapFS.Filesystem.writeFile({
path: this.path,
data: Buffer.from(data).toString('base64'),
recursive: true,
directory: CapFS.Directory.Documents
})
}
else{
await CapFS.Filesystem.appendFile({
path: this.path,
data: Buffer.from(data).toString('base64'),
directory: CapFS.Directory.Documents
})
}
this.firstWrite = false
}
async close(){
// do nothing
}
}
export class LocalWriter{
writer: WritableStreamDefaultWriter|TauriWriter|MobileWriter
async init(name = 'Binary', ext = ['bin']) {
if(isTauri){
const filePath = await save({
filters: [{
name: name,
extensions: ext
}]
});
if(!filePath){
return false
}
this.writer = new TauriWriter(filePath)
return true
}
if(Capacitor.isNativePlatform()){
this.writer = new MobileWriter(name + '.' + ext[0])
return true
}
const streamSaver = await import('streamsaver')
const writableStream = streamSaver.createWriteStream(name + '.' + ext[0])
this.writer = writableStream.getWriter()
return true
}
async writeBackup(name:string,data: Uint8Array){
const encodedName = new TextEncoder().encode(getBasename(name))
const nameLength = new Uint32Array([encodedName.byteLength])
await this.writer.write(new Uint8Array(nameLength.buffer))
await this.writer.write(encodedName)
const dataLength = new Uint32Array([data.byteLength])
await this.writer.write(new Uint8Array(dataLength.buffer))
await this.writer.write(data)
}
async write(data:Uint8Array) {
await this.writer.write(data)
}
async close(){
await this.writer.close()
}
}
export class VirtualWriter{
buf = new AppendableBuffer()
async write(data:Uint8Array) {
this.buf.append(data)
}
async close(){
// do nothing
}
}
let fetchIndex = 0
let nativeFetchData:{[key:string]:StreamedFetchChunk[]} = {}
interface StreamedFetchChunkData{
type:'chunk',
body:string,
id:string
}
interface StreamedFetchHeaderData{
type:'headers',
body:{[key:string]:string},
id:string,
status:number
}
interface StreamedFetchEndData{
type:'end',
id:string
}
type StreamedFetchChunk = StreamedFetchChunkData|StreamedFetchHeaderData|StreamedFetchEndData
interface StreamedFetchPlugin {
streamedFetch(options: { id: string, url:string, body:string, headers:{[key:string]:string} }): Promise<{"error":string,"success":boolean}>;
addListener(eventName: 'streamed_fetch', listenerFunc: (data:StreamedFetchChunk) => void): void;
}
let streamedFetchListening = false
let capStreamedFetch:StreamedFetchPlugin|undefined
if(isTauri){
listen('streamed_fetch', (event) => {
try {
const parsed = JSON.parse(event.payload as string)
const id = parsed.id
nativeFetchData[id]?.push(parsed)
} catch (error) {
console.error(error)
}
}).then((v) => {
streamedFetchListening = true
})
}
if(Capacitor.isNativePlatform()){
capStreamedFetch = registerPlugin<StreamedFetchPlugin>('CapacitorHttp', CapacitorHttp)
capStreamedFetch.addListener('streamed_fetch', (data) => {
try {
nativeFetchData[data.id]?.push(data)
} catch (error) {
console.error(error)
}
})
streamedFetchListening = true
}
export class AppendableBuffer{
buffer:Uint8Array
deapended:number = 0
constructor(){
this.buffer = new Uint8Array(0)
}
append(data:Uint8Array){
const newBuffer = new Uint8Array(this.buffer.length + data.length)
newBuffer.set(this.buffer, 0)
newBuffer.set(data, this.buffer.length)
this.buffer = newBuffer
}
deappend(length:number){
this.buffer = this.buffer.slice(length)
this.deapended += length
}
slice(start:number, end:number){
return this.buffer.slice(start - this.deapended, end - this.deapended)
}
length(){
return this.buffer.length + this.deapended
}
}
const pipeFetchLog = (fetchLogIndex:number, readableStream:ReadableStream<Uint8Array>) => {
let textDecoderBuffer = new AppendableBuffer()
let textDecoderPointer = 0
const textDecoder = TextDecoderStream ? (new TextDecoderStream()) : new TransformStream<Uint8Array, string>({
transform(chunk, controller) {
try{
textDecoderBuffer.append(chunk)
const decoded = new TextDecoder('utf-8', {
fatal: true
}).decode(textDecoderBuffer.buffer)
let newString = decoded.slice(textDecoderPointer)
textDecoderPointer = decoded.length
controller.enqueue(newString)
}
catch{}
}
})
textDecoder.readable.pipeTo(new WritableStream({
write(chunk) {
fetchLog[fetchLogIndex].response += chunk
}
}))
const writer = textDecoder.writable.getWriter()
return new ReadableStream<Uint8Array>({
start(controller) {
readableStream.pipeTo(new WritableStream({
write(chunk) {
controller.enqueue(chunk)
writer.write(chunk)
},
close() {
controller.close()
writer.close()
}
}))
}
})
}
export async function fetchNative(url:string, arg:{
body:string,
headers?:{[key:string]:string},
method?:"POST",
signal?:AbortSignal,
useRisuTk?:boolean,
chatId?:string
}):Promise<{ body: ReadableStream<Uint8Array>; headers: Headers; status: number }> {
let headers = arg.headers ?? {}
const db = get(DataBase)
let throughProxy = (!isTauri) && (!isNodeServer) && (!db.usePlainFetch)
let fetchLogIndex = addFetchLog({
body: arg.body,
headers: arg.headers,
response: 'Streamed Fetch',
success: true,
url: url,
resType: 'stream',
chatId: arg.chatId
})
if(isTauri){
fetchIndex++
if(arg.signal && arg.signal.aborted){
throw new Error('aborted')
}
if(fetchIndex >= 100000){
fetchIndex = 0
}
let fetchId = fetchIndex.toString().padStart(5,'0')
nativeFetchData[fetchId] = []
let resolved = false
let error = ''
while(!streamedFetchListening){
await sleep(100)
}
if(isTauri){
invoke('streamed_fetch', {
id: fetchId,
url: url,
headers: JSON.stringify(headers),
body: arg.body,
}).then((res) => {
try {
const parsedRes = JSON.parse(res as string)
if(!parsedRes.success){
error = parsedRes.body
resolved = true
}
} catch (error) {
error = JSON.stringify(error)
resolved = true
}
})
}
else if(capStreamedFetch){
capStreamedFetch.streamedFetch({
id: fetchId,
url: url,
headers: headers,
body: Buffer.from(arg.body).toString('base64'),
}).then((res) => {
if(!res.success){
error = res.error
resolved = true
}
})
}
let resHeaders:{[key:string]:string} = null
let status = 400
let readableStream = pipeFetchLog(fetchLogIndex,new ReadableStream<Uint8Array>({
async start(controller) {
while(!resolved || nativeFetchData[fetchId].length > 0){
if(nativeFetchData[fetchId].length > 0){
const data = nativeFetchData[fetchId].shift()
if(data.type === 'chunk'){
const chunk = Buffer.from(data.body, 'base64')
controller.enqueue(chunk)
}
if(data.type === 'headers'){
resHeaders = data.body
status = data.status
}
if(data.type === 'end'){
resolved = true
}
}
await sleep(10)
}
controller.close()
}
}))
while(resHeaders === null && !resolved){
await sleep(10)
}
if(resHeaders === null){
resHeaders = {}
}
if(error !== ''){
throw new Error(error)
}
return {
body: readableStream,
headers: new Headers(resHeaders),
status: status
}
}
else if(throughProxy){
const r = await fetch(hubURL + `/proxy2`, {
body: arg.body,
headers: arg.useRisuTk ? {
"risu-header": encodeURIComponent(JSON.stringify(headers)),
"risu-url": encodeURIComponent(url),
"Content-Type": "application/json",
"x-risu-tk": "use"
}: {
"risu-header": encodeURIComponent(JSON.stringify(headers)),
"risu-url": encodeURIComponent(url),
"Content-Type": "application/json"
},
method: "POST",
signal: arg.signal
})
return {
body: pipeFetchLog(fetchLogIndex, r.body),
headers: r.headers,
status: r.status
}
}
else{
return await fetch(url, {
body: arg.body,
headers: headers,
method: arg.method,
signal: arg.signal
})
}
}
export function textifyReadableStream(stream:ReadableStream<Uint8Array>){
return new Response(stream).text()
}
export function toggleFullscreen(){
const fullscreenElement = document.fullscreenElement
fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen({
navigationUI: "hide"
})
}
export function trimNonLatin(data:string){
return data .replace(/[^\x00-\x7F]/g, "")
.replace(/ +/g, ' ')
.trim()
}
export function updateHeightMode(){
const db = get(DataBase)
const root = document.querySelector(':root') as HTMLElement;
switch(db.heightMode){
case 'auto':
root.style.setProperty('--risu-height-size', '100%');
break
case 'vh':
root.style.setProperty('--risu-height-size', '100vh');
break
case 'dvh':
root.style.setProperty('--risu-height-size', '100dvh');
break
case 'lvh':
root.style.setProperty('--risu-height-size', '100lvh');
break
case 'svh':
root.style.setProperty('--risu-height-size', '100svh');
break
case 'percent':
root.style.setProperty('--risu-height-size', '100%');
break
}
}