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 = new Map(); 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(); 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(); 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; } }