diff --git a/src/lang/en.ts b/src/lang/en.ts
index bf005d77..afe1b768 100644
--- a/src/lang/en.ts
+++ b/src/lang/en.ts
@@ -23,6 +23,7 @@ export const languageEnglish = {
alreadyCharInGroup: "There is already a character with the same name in the group.",
noUserIcon: "You must set your icon first.",
emptyText: "Text is empty.",
+ wrongPassword: "Wrong Password",
},
showHelp: "Show Help",
help:{
@@ -506,4 +507,11 @@ export const languageEnglish = {
openrouterMiddleOut: "Openrouter Middle Out",
geminiApiKey: "Gemini API Key",
removePunctuationHypa: "Memory Punctuation Removal",
+ inputCardPassword: "Input Card Password",
+ ccv2Desc: 'Character Card V2 is is a format widely used in chatbot programs.',
+ rccDesc: 'Risu Refined Character Card is a format with additional features like password, integrity check and etc.',
+ password: "Password",
+ license: "License",
+ licenseDesc: "You can choose license for the downloaders to limit the usages of your card's prompt.",
+ passwordDesc: "You can set a password to protect your card from unauthorized access.",
}
\ No newline at end of file
diff --git a/src/lib/Others/AlertComp.svelte b/src/lib/Others/AlertComp.svelte
index 863a8099..09fb3eff 100644
--- a/src/lib/Others/AlertComp.svelte
+++ b/src/lib/Others/AlertComp.svelte
@@ -7,12 +7,19 @@
import BarIcon from '../SideBars/BarIcon.svelte';
import { User } from 'lucide-svelte';
import { hubURL } from 'src/ts/characterCards';
- import TextInput from '../UI/GUI/TextInput.svelte';
- import { openURL } from 'src/ts/storage/globalApi';
- import Button from '../UI/GUI/Button.svelte';
+ import TextInput from '../UI/GUI/TextInput.svelte';
+ import { openURL } from 'src/ts/storage/globalApi';
+ import Button from '../UI/GUI/Button.svelte';
+ import { XIcon } from "lucide-svelte";
+ import SelectInput from "../UI/GUI/SelectInput.svelte";
+ import { CCLicenseData } from "src/ts/creation/license";
+ import OptionInput from "../UI/GUI/OptionInput.svelte";
+ import { language } from 'src/lang';
let btn
let input = ''
-
+ let cardExportType = ''
+ let cardExportPassword = ''
+ let cardLicense = ''
$: (() => {
if(btn){
btn.focus()
@@ -40,7 +47,7 @@
}
}}>
-{#if $alertStore.type !== 'none' && $alertStore.type !== 'toast'}
+{#if $alertStore.type !== 'none' && $alertStore.type !== 'toast' && $alertStore.type !== 'cardexport'}
{#if $alertStore.type === 'error'}
@@ -160,6 +167,63 @@
{/if}
+
+{:else if $alertStore.type === 'cardexport'}
+
+
+
+
+ Export Character
+
+ {
+ alertStore.set({
+ type: 'none',
+ msg: JSON.stringify({
+ type: 'cancel',
+ password: cardExportPassword,
+ license: cardLicense
+ })
+ })
+ }}>
+
+
+
+
Type
+ {#if cardExportType === ''}
+
{language.ccv2Desc}
+ {:else}
+
{language.rccDesc}
+ {/if}
+
+ {cardExportType = ''}}>Character Card V2
+ {cardExportType = 'rcc'}}>Risu RCC
+
+ {#if cardExportType === 'rcc'}
+
{language.password}
+
{language.passwordDesc}
+
+
{language.license}
+
{language.licenseDesc}
+
+ None
+ {#each Object.keys(CCLicenseData) as ccl}
+ {CCLicenseData[ccl][2]} ({CCLicenseData[ccl][1]})
+ {/each}
+
+ {/if}
+
{
+ alertStore.set({
+ type: 'none',
+ msg: JSON.stringify({
+ type: cardExportType,
+ password: cardExportPassword,
+ license: cardLicense
+ })
+ })
+ }}>{language.export}
+
+
+
{:else if $alertStore.type === 'toast'}
{
diff --git a/src/ts/alert.ts b/src/ts/alert.ts
index 2d88a090..061348cd 100644
--- a/src/ts/alert.ts
+++ b/src/ts/alert.ts
@@ -3,7 +3,7 @@ import { sleep } from "./util"
import { language } from "../lang"
interface alertData{
- type: 'error'| 'normal'|'none'|'ask'|'wait'|'selectChar'|'input'|'toast'|'wait2'|'markdown'|'select'|'login'|'tos'
+ type: 'error'| 'normal'|'none'|'ask'|'wait'|'selectChar'|'input'|'toast'|'wait2'|'markdown'|'select'|'login'|'tos'|'cardexport'
msg: string
}
@@ -100,6 +100,7 @@ export function alertWait(msg:string){
}
+
export function alertClear(){
alertStore.set({
'type': 'none',
@@ -140,6 +141,27 @@ export async function alertConfirm(msg:string){
return get(alertStore).msg === 'yes'
}
+export async function alertCardExport(){
+
+ alertStore.set({
+ 'type': 'cardexport',
+ 'msg': ''
+ })
+
+ while(true){
+ if (get(alertStore).type === 'none'){
+ break
+ }
+ await sleep(10)
+ }
+
+ return JSON.parse(get(alertStore).msg) as {
+ type: string,
+ password: string,
+ license: string
+ }
+}
+
export async function alertTOS(){
// if(localStorage.getItem('tos') === 'true'){
diff --git a/src/ts/characterCards.ts b/src/ts/characterCards.ts
index c7d450f4..f2443a9a 100644
--- a/src/ts/characterCards.ts
+++ b/src/ts/characterCards.ts
@@ -1,14 +1,14 @@
import { get, writable, type Writable } from "svelte/store"
-import { alertConfirm, alertError, alertMd, alertNormal, alertSelect, alertStore, alertTOS, alertWait } from "./alert"
+import { alertCardExport, alertConfirm, alertError, alertInput, alertMd, alertNormal, alertSelect, alertStore, alertTOS, alertWait } from "./alert"
import { DataBase, defaultSdDataFunc, type character, setDatabase, type customscript, type loreSettings, type loreBook, type triggerscript } from "./storage/database"
-import { checkNullish, selectMultipleFile, sleep } from "./util"
+import { checkNullish, decryptBuffer, encryptBuffer, selectMultipleFile, sleep } from "./util"
import { language } from "src/lang"
import { v4 as uuidv4 } from 'uuid';
import { characterFormatUpdate } from "./characters"
import { checkCharOrder, downloadFile, loadAsset, LocalWriter, readImage, saveAsset } from "./storage/globalApi"
import { cloneDeep } from "lodash"
import { selectedCharID } from "./stores"
-import { convertImage } from "./parser"
+import { convertImage, hasher } from "./parser"
import { reencodeImage } from "./image"
import { PngChunk } from "./pngChunk"
@@ -71,7 +71,10 @@ async function importCharacterProcess(f:{
break
}
if(chunk.key === 'chara'){
- readedChara = chunk.value
+ //For memory reason, limit to 2MB
+ if(readedChara.length < 2 * 1024 * 1024){
+ readedChara = chunk.value.replaceAll('\0', '')
+ }
break
}
if(chunk.key.startsWith('chara-ext-asset_')){
@@ -86,7 +89,63 @@ async function importCharacterProcess(f:{
alertError(language.errors.noData)
return
}
- {
+
+ if(readedChara.startsWith('rcc||')){
+ const parts = readedChara.split('||')
+ const type = parts[1]
+ if(type === 'rccv1'){
+ if(parts.length !== 5){
+ alertError(language.errors.noData)
+ return
+ }
+ const encrypted = Buffer.from(parts[2], 'base64')
+ const hashed = await hasher(encrypted)
+ if(hashed !== parts[3]){
+ alertError(language.errors.noData)
+ return
+ }
+ console.log(parts[4])
+ const metaData:RccCardMetaData = JSON.parse(Buffer.from(parts[4], 'base64').toString('utf-8'))
+ console.log(metaData)
+ if(metaData.usePassword){
+ const password = await alertInput(language.inputCardPassword)
+ if(!password){
+ return
+ }
+ else{
+ try {
+ const decrypted = await decryptBuffer(encrypted, password)
+ const charaData:CharacterCardV2 = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
+ if(await importSpecv2(charaData, img, "normal", assets)){
+ let db = get(DataBase)
+ return db.characters.length - 1
+ }
+ else{
+ throw new Error('Error while importing')
+ }
+ } catch (error) {
+ alertError(language.errors.wrongPassword)
+ return
+ }
+ }
+ }
+ else{
+ const decrypted = await decryptBuffer(encrypted, 'RISU_NONE')
+ try {
+ const charaData:CharacterCardV2 = JSON.parse(Buffer.from(decrypted).toString('utf-8'))
+ if(await importSpecv2(charaData, img, "normal", assets)){
+ let db = get(DataBase)
+ return db.characters.length - 1
+ }
+ } catch (error) {
+ alertError(language.errors.noData)
+ return
+ }
+ }
+
+ }
+ }
+ else {
const charaData:CharacterCardV2 = JSON.parse(Buffer.from(readedChara, 'base64').toString('utf-8'))
if(await importSpecv2(charaData, img, "normal", assets)){
let db = get(DataBase)
@@ -217,8 +276,17 @@ export async function exportChar(charaID:number) {
return
}
- const sel = await alertSelect(['Export as PNG', 'Export as JSON'])
- exportSpecV2(char, sel === '1' ? 'json' : 'png')
+ const option = await alertCardExport()
+ if(option.type === 'cancel'){
+ return
+ }
+ else if(option.type === 'rcc'){
+ char.license = option.license
+ exportSpecV2(char, 'rcc', {password:option.password})
+ }
+ else{
+ exportSpecV2(char,'png')
+ }
return
}
@@ -535,7 +603,7 @@ async function createBaseV2(char:character) {
}
-export async function exportSpecV2(char:character, type:'png'|'json' = 'png') {
+export async function exportSpecV2(char:character, type:'png'|'json'|'rcc' = 'png', rcc:{password?:string} = {}) {
let img = await readImage(char.image)
try{
@@ -605,7 +673,22 @@ export async function exportSpecV2(char:character, type:'png'|'json' = 'png') {
type: 'wait',
msg: 'Loading... (Writing)'
})
- await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64'))
+
+ if(type === 'rcc'){
+ const password = rcc.password || 'RISU_NONE'
+ const json = JSON.stringify(card)
+ const encrypted = Buffer.from(await encryptBuffer(Buffer.from(json, 'utf-8'), password))
+ const hashed = await hasher(encrypted)
+ const metaData:RccCardMetaData = {}
+ if(password !== 'RISU_NONE'){
+ metaData.usePassword = true
+ }
+ const rccString = 'rcc||rccv1||' + encrypted.toString('base64') + '||' + hashed + '||' + Buffer.from(JSON.stringify(metaData)).toString('base64')
+ await writer.write("chara", rccString)
+ }
+ else{
+ await writer.write("chara", Buffer.from(JSON.stringify(card)).toString('base64'))
+ }
await writer.end()
@@ -879,4 +962,8 @@ interface charBookEntry{
constant?: boolean // if true, always inserted in the prompt (within budget limit)
position?: 'before_char' | 'after_char' // whether the entry is placed before or after the character defs
case_sensitive?:boolean
+}
+
+interface RccCardMetaData{
+ usePassword?: boolean
}
\ No newline at end of file