Files
ERitors-Scribe-Desktop/electron/database/models/Character.ts
natreex e45a15225b Add enable/disable management for book tools (characters, worlds, and locations)
- Introduced toggling functionality for managing `characters`, `worlds`, and `locations` tool availability per book.
- Updated `CharacterComponent`, `WorldSetting`, and `LocationComponent` with toggle switches for tool enablement.
- Added `book_tools` database table and related schema migration for storing tool settings.
- Extended API calls, models, and IPC handlers to support tool enablement states.
- Localized new strings for English with supporting descriptions and messages.
- Adjusted conditional rendering logic across components to respect tool enablement.
2026-01-14 17:42:59 -05:00

373 lines
19 KiB
TypeScript

import CharacterRepo, {
AttributeResult,
CharacterResult,
CompleteCharacterResult
} from "../repositories/character.repository.js";
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
import System from "../System.js";
import {getUserEncryptionKey} from "../keyManager.js";
export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring';
export interface CharacterPropsPost {
id: string | null;
name: string;
lastName: string;
category: CharacterCategory;
title: string;
image: string;
physical: { name: string }[];
psychological: { name: string }[];
relations: { name: string }[];
skills: { name: string }[];
weaknesses: { name: string }[];
strengths: { name: string }[];
goals: { name: string }[];
motivations: { name: string }[];
role: string;
biography?: string;
history?: string;
}
export interface CharacterProps {
id: string;
name: string;
lastName: string;
title: string;
category: string;
image: string;
role: string;
biography: string;
history: string;
}
export interface CharacterListResponse {
characters: CharacterProps[];
enabled: boolean;
}
export interface CompleteCharacterProps {
id?: string;
name: string;
lastName: string;
title: string;
category: string;
image?: string;
role: string;
biography: string;
history: string;
[key: string]: Attribute[] | string | undefined;
}
export interface Attribute {
id: string;
name: string;
}
export interface CharacterAttribute {
type: string;
values: Attribute[];
}
export interface SyncedCharacter {
id: string;
name: string;
lastUpdate: number;
attributes: SyncedCharacterAttribute[];
}
export interface SyncedCharacterAttribute {
id: string;
name: string;
lastUpdate: number;
}
export default class Character {
/**
* Retrieves a list of all characters for a specific book.
* Decrypts character data using the user's encryption key.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language code for localization (defaults to 'fr')
* @returns An array of decrypted character properties
*/
public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterListResponse {
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
const enabled: boolean = bookTools ? bookTools.characters_enabled === 1 : false;
const userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang);
if (!encryptedCharacters || encryptedCharacters.length === 0) {
return { characters: [], enabled };
}
const decryptedCharacterList: CharacterProps[] = [];
for (const encryptedCharacter of encryptedCharacters) {
decryptedCharacterList.push({
id: encryptedCharacter.character_id,
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '',
role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
})
}
return { characters: decryptedCharacterList, enabled };
}
/**
* Creates a new character with all its attributes for a specific book.
* Encrypts all character data before storing in the database.
* @param userId - The unique identifier of the user
* @param character - The character data to be created
* @param bookId - The unique identifier of the book
* @param lang - The language code for localization (defaults to 'fr')
* @param existingCharacterId - Optional existing character ID for updates or imports
* @returns The unique identifier of the newly created character
*/
public static addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string, lang: 'fr' | 'en' = 'fr', existingCharacterId?: string): string {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const characterId: string = existingCharacterId || System.createUniqueId();
const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey);
CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, lang);
const characterPropertyKeys: string[] = Object.keys(character);
for (const propertyKey of characterPropertyKeys) {
if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) {
const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[];
if (attributeArray.length > 0) {
for (const attributeItem of attributeArray) {
const attributeType: string = propertyKey;
const attributeName: string = attributeItem.name;
this.addNewAttribute(characterId, userId, attributeType, attributeName, lang);
}
}
}
}
return characterId;
}
/**
* Updates an existing character's core properties.
* Encrypts all updated data before storing in the database.
* @param userId - The unique identifier of the user
* @param character - The character data with updated values
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the update was successful, false otherwise
*/
static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean {
const userEncryptionKey: string = getUserEncryptionKey(userId);
if (!character.id) {
return false;
}
const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey);
return CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), lang);
}
/**
* Adds a new attribute to a character.
* Attributes are categorized properties like physical traits, skills, or goals.
* @param characterId - The unique identifier of the character
* @param userId - The unique identifier of the user
* @param type - The type/category of the attribute (e.g., 'physical', 'skills')
* @param name - The value/name of the attribute
* @param lang - The language code for localization (defaults to 'fr')
* @param existingAttributeId - Optional existing attribute ID for updates or imports
* @returns The unique identifier of the newly created attribute
*/
static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr', existingAttributeId?: string): string {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const attributeId: string = existingAttributeId || System.createUniqueId();
const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey);
const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang);
}
/**
* Deletes an attribute from a character.
* @param userId - The unique identifier of the user
* @param attributeId - The unique identifier of the attribute to delete
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the deletion was successful, false otherwise
*/
static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return CharacterRepo.deleteAttribute(userId, attributeId, lang);
}
/**
* Retrieves all attributes for a specific character, grouped by type.
* Decrypts attribute data using the user's encryption key.
* @param characterId - The unique identifier of the character
* @param userId - The unique identifier of the user
* @param lang - The language code for localization (defaults to 'fr')
* @returns An array of character attributes grouped by type
*/
static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang);
if (!encryptedAttributes?.length) return [];
const attributesByType: Map<string, Attribute[]> = new Map<string, Attribute[]>();
for (const encryptedAttribute of encryptedAttributes) {
const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey);
const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : '';
if (!attributesByType.has(decryptedType)) {
attributesByType.set(decryptedType, []);
}
attributesByType.get(decryptedType)!.push({
id: encryptedAttribute.attr_id,
name: decryptedValue
});
}
return Array.from<[string, Attribute[]], CharacterAttribute>(
attributesByType,
([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values})
);
}
/**
* Retrieves complete character data including all attributes for multiple characters.
* Used for exporting or displaying full character profiles.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param characters - An array of character IDs to retrieve
* @param lang - The language code for localization (defaults to 'fr')
* @returns An array of complete character objects with all their attributes
*/
static getCompleteCharacterList(userId: string, bookId: string, characters: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterProps[] {
const encryptedCharacterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang);
if (!encryptedCharacterList || encryptedCharacterList.length === 0) {
return [];
}
const userEncryptionKey: string = getUserEncryptionKey(userId);
const completeCharactersMap = new Map<string, CompleteCharacterProps>();
for (const encryptedCharacter of encryptedCharacterList) {
if (!encryptedCharacter.character_id) {
continue;
}
if (!completeCharactersMap.has(encryptedCharacter.character_id)) {
const decryptedCharacter: CompleteCharacterProps = {
id: '',
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
physical: [],
psychological: [],
relations: [],
skills: [],
weaknesses: [],
strengths: [],
goals: [],
motivations: []
};
completeCharactersMap.set(encryptedCharacter.character_id, decryptedCharacter);
}
const characterEntry: CompleteCharacterProps | undefined = completeCharactersMap.get(encryptedCharacter.character_id);
if (!encryptedCharacter.attribute_name || !characterEntry) {
continue;
}
const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey);
const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : '';
if (Array.isArray(characterEntry[decryptedAttributeName])) {
characterEntry[decryptedAttributeName].push({
id: '',
name: decryptedAttributeValue
});
}
}
return Array.from(completeCharactersMap.values());
}
/**
* Generates a formatted vCard-style string representation of characters.
* Useful for AI context or text-based exports.
* @param characters - An array of complete character objects to format
* @returns A formatted string containing all character information
*/
static characterVCard(characters: CompleteCharacterProps[]): string {
const uniqueCharactersMap = new Map<string, CompleteCharacterProps>();
let formattedCharactersDescription: string = '';
characters.forEach((character: CompleteCharacterProps): void => {
const characterIdentifier: string = character.name || character.id || 'unknown';
if (!uniqueCharactersMap.has(characterIdentifier)) {
uniqueCharactersMap.set(characterIdentifier, {
name: character.name,
lastName: character.lastName,
category: character.category,
title: character.title,
role: character.role,
biography: character.biography,
history: character.history
});
}
const aggregatedCharacterData: CompleteCharacterProps = uniqueCharactersMap.get(characterIdentifier)!;
Object.keys(character).forEach((propertyName: string): void => {
if (Array.isArray(character[propertyName])) {
if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = [];
(aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[]));
}
});
});
formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => {
const characterDescriptionLines: string[] = [];
const fullName: string = [character.name, character.lastName].filter(Boolean).join(' ');
if (fullName) characterDescriptionLines.push(`Nom : ${fullName}`);
(['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => {
if (character[propertyKey]) {
characterDescriptionLines.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`);
}
});
Object.keys(character).forEach((propertyKey: string): void => {
const propertyValue: string | Attribute[] | undefined = character[propertyKey];
if (Array.isArray(propertyValue) && propertyValue.length > 0) {
const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', ');
characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`);
}
});
return characterDescriptionLines.join('\n');
}).join('\n\n');
return formattedCharactersDescription;
}
}