Drop Soulkyn import (#4372) * Fix Soulkyn import Generate and supply necessary headers to Soulkyn API request. Fixes #4323 * Drop soulkyn import --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "رابط PNG المباشر (راجع", | 1376 | "char_import_6": "رابط PNG المباشر (راجع", |
| 1377 | "char_import_7": "للمضيفين المسموح بهم)", | 1377 | "char_import_7": "للمضيفين المسموح بهم)", |
| 1378 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", | 1378 | "char_import_8": "شخصية RisuRealm (رابط مباشر)", |
| 1379 | "char_import_9": "شخصية Soulkyn (رابط مباشر)", | ||
| 1380 | "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)", | 1379 | "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.", | 1380 | "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.", |
| 1382 | "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.", | 1381 | "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Direkter PNG-Link (siehe", | 1376 | "char_import_6": "Direkter PNG-Link (siehe", |
| 1377 | "char_import_7": "für erlaubte Hosts)", | 1377 | "char_import_7": "für erlaubte Hosts)", |
| 1378 | "char_import_8": "RisuRealm-Charakter (Direktlink)", | 1378 | "char_import_8": "RisuRealm-Charakter (Direktlink)", |
| 1379 | "char_import_9": "Soulkyn-Charakter (Direktlink)", | ||
| 1380 | "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)", | 1379 | "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.", | 1380 | "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.", |
| 1382 | "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.", | 1381 | "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Enlace PNG directo (consulte", | 1376 | "char_import_6": "Enlace PNG directo (consulte", |
| 1377 | "char_import_7": "para hosts permitidos)", | 1377 | "char_import_7": "para hosts permitidos)", |
| 1378 | "char_import_8": "Personaje RisuRealm (Enlace directo)", | 1378 | "char_import_8": "Personaje RisuRealm (Enlace directo)", |
| 1379 | "char_import_9": "Personaje Soulkyn (Enlace directo)", | ||
| 1380 | "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)", | 1379 | "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.", | 1380 | "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.", |
| 1382 | "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.", | 1381 | "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.", |
| @@ -1298,7 +1298,6 @@ | |||
| 1298 | "char_import_6": "Lien PNG direct (voir", | 1298 | "char_import_6": "Lien PNG direct (voir", |
| 1299 | "char_import_7": "pour les hôtes autorisés)", | 1299 | "char_import_7": "pour les hôtes autorisés)", |
| 1300 | "char_import_8": "Personnage de RisuRealm (lien direct)", | 1300 | "char_import_8": "Personnage de RisuRealm (lien direct)", |
| 1301 | "char_import_9": "Personnage de Soulkyn (lien direct)", | ||
| 1302 | "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)", | 1301 | "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)", |
| 1303 | "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.", | 1302 | "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.", |
| 1304 | "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.", | 1303 | "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Beinn PNG hlekkur (sjá", | 1376 | "char_import_6": "Beinn PNG hlekkur (sjá", |
| 1377 | "char_import_7": "fyrir leyfilega gestgjafa)", | 1377 | "char_import_7": "fyrir leyfilega gestgjafa)", |
| 1378 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", | 1378 | "char_import_8": "RisuRealm karakter (beinn hlekkur)", |
| 1379 | "char_import_9": "Soulkyn karakter (beinn hlekkur)", | ||
| 1380 | "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)", | 1379 | "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.", | 1380 | "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.", |
| 1382 | "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.", | 1381 | "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", | 1376 | "char_import_6": "Collegamento PNG diretto (fare riferimento a", |
| 1377 | "char_import_7": "per gli host consentiti)", | 1377 | "char_import_7": "per gli host consentiti)", |
| 1378 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", | 1378 | "char_import_8": "Personaggio RisuRealm (collegamento diretto)", |
| 1379 | "char_import_9": "Personaggio Soulkyn (collegamento diretto)", | ||
| 1380 | "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)", | 1379 | "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.", | 1380 | "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.", |
| 1382 | "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.", | 1381 | "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.", |
| @@ -1378,7 +1378,6 @@ | |||
| 1378 | "char_import_6": "直接PNGリンク(参照", | 1378 | "char_import_6": "直接PNGリンク(参照", |
| 1379 | "char_import_7": "許可されたホストの場合)", | 1379 | "char_import_7": "許可されたホストの場合)", |
| 1380 | "char_import_8": "RisuRealm キャラクター (直接リンク)", | 1380 | "char_import_8": "RisuRealm キャラクター (直接リンク)", |
| 1381 | "char_import_9": "Soulkyn キャラクター (直接リンク)", | ||
| 1382 | "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)", | 1381 | "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)", |
| 1383 | "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。", | 1382 | "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。", |
| 1384 | "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。", | 1383 | "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。", |
| @@ -1395,7 +1395,6 @@ | |||
| 1395 | "char_import_6": "직접 PNG 링크(참조", | 1395 | "char_import_6": "직접 PNG 링크(참조", |
| 1396 | "char_import_7": "허용된 호스트의 경우)", | 1396 | "char_import_7": "허용된 호스트의 경우)", |
| 1397 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", | 1397 | "char_import_8": "RisuRealm 캐릭터 (직접링크)", |
| 1398 | "char_import_9": "Soulkyn 캐릭터 (직접링크)", | ||
| 1399 | "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)", | 1398 | "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)", |
| 1400 | "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.", | 1399 | "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.", |
| 1401 | "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.", | 1400 | "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Directe PNG-link (zie", | 1376 | "char_import_6": "Directe PNG-link (zie", |
| 1377 | "char_import_7": "voor toegestane hosts)", | 1377 | "char_import_7": "voor toegestane hosts)", |
| 1378 | "char_import_8": "RisuRealm-personage (directe link)", | 1378 | "char_import_8": "RisuRealm-personage (directe link)", |
| 1379 | "char_import_9": "Soulkyn-personage (directe link)", | ||
| 1380 | "char_import_10": "Perchance-personage (directe link of UUID + .gz)", | 1379 | "char_import_10": "Perchance-personage (directe link of UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.", | 1380 | "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.", |
| 1382 | "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.", | 1381 | "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Link PNG direto (consulte", | 1376 | "char_import_6": "Link PNG direto (consulte", |
| 1377 | "char_import_7": "para hosts permitidos)", | 1377 | "char_import_7": "para hosts permitidos)", |
| 1378 | "char_import_8": "Personagem RisuRealm (link direto)", | 1378 | "char_import_8": "Personagem RisuRealm (link direto)", |
| 1379 | "char_import_9": "Personagem Soulkyn (link direto)", | ||
| 1380 | "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)", | 1379 | "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Suporta importação de vários caracteres.", | 1380 | "Supports importing multiple characters.": "Suporta importação de vários caracteres.", |
| 1382 | "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.", | 1381 | "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.", |
| @@ -963,7 +963,6 @@ | |||
| 963 | "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", | 963 | "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)", |
| 964 | "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в", | 964 | "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в", |
| 965 | "char_import_7": ")", | 965 | "char_import_7": ")", |
| 966 | "char_import_9": "Персонаж с Soulkyn (прямая ссылка)", | ||
| 967 | "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)", | 966 | "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)", |
| 968 | "Grammar String": "Грамматика", | 967 | "Grammar String": "Грамматика", |
| 969 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", | 968 | "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Пряме посилання на PNG (див", | 1376 | "char_import_6": "Пряме посилання на PNG (див", |
| 1377 | "char_import_7": "для дозволених хостів)", | 1377 | "char_import_7": "для дозволених хостів)", |
| 1378 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", | 1378 | "char_import_8": "Персонаж RisuRealm (пряме посилання)", |
| 1379 | "char_import_9": "Персонаж Soulkyn (пряме посилання)", | ||
| 1380 | "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)", | 1379 | "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Підтримується імпорт кількох символів.", | 1380 | "Supports importing multiple characters.": "Підтримується імпорт кількох символів.", |
| 1382 | "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.", | 1381 | "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.", |
| @@ -1376,7 +1376,6 @@ | |||
| 1376 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", | 1376 | "char_import_6": "Nhập PNG trực tiếp (tham khảo", |
| 1377 | "char_import_7": "đối với các máy chủ được phép)", | 1377 | "char_import_7": "đối với các máy chủ được phép)", |
| 1378 | "char_import_8": "RisuRealm (URL trực tiếp)", | 1378 | "char_import_8": "RisuRealm (URL trực tiếp)", |
| 1379 | "char_import_9": "Soulkyn (URL trực tiếp)", | ||
| 1380 | "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)", | 1379 | "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)", |
| 1381 | "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.", | 1380 | "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.", |
| 1382 | "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.", | 1381 | "Write each URL or ID into a new line.": "Viết mỗi URL hoặc ID vào một dòng mới.", |
| @@ -1888,7 +1888,6 @@ | |||
| 1888 | "char_import_6": "被允许的PNG直链(请参阅", | 1888 | "char_import_6": "被允许的PNG直链(请参阅", |
| 1889 | "char_import_7": ")", | 1889 | "char_import_7": ")", |
| 1890 | "char_import_8": "RisuRealm 角色(直链)", | 1890 | "char_import_8": "RisuRealm 角色(直链)", |
| 1891 | "char_import_9": "Soulkyn 角色(直链)", | ||
| 1892 | "char_import_10": "Perchance 角色(直链或UUID + .gz)", | 1891 | "char_import_10": "Perchance 角色(直链或UUID + .gz)", |
| 1893 | "Supports importing multiple characters.": "支持导入多个角色。", | 1892 | "Supports importing multiple characters.": "支持导入多个角色。", |
| 1894 | "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。", | 1893 | "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。", |
| @@ -1381,7 +1381,6 @@ | |||
| 1381 | "char_import_6": "直接 PNG 連結(請參閱", | 1381 | "char_import_6": "直接 PNG 連結(請參閱", |
| 1382 | "char_import_7": "對於允許的主機)", | 1382 | "char_import_7": "對於允許的主機)", |
| 1383 | "char_import_8": "RisuRealm 角色(直接連結)", | 1383 | "char_import_8": "RisuRealm 角色(直接連結)", |
| 1384 | "char_import_9": "Soulkyn 角色(直接連結)", | ||
| 1385 | "char_import_10": "Perchance 角色(直接連結或 ID + .gz)", | 1384 | "char_import_10": "Perchance 角色(直接連結或 ID + .gz)", |
| 1386 | "Supports importing multiple characters.": "支援匯入多個字元。", | 1385 | "Supports importing multiple characters.": "支援匯入多個字元。", |
| 1387 | "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。", | 1386 | "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。", |
| @@ -10,7 +10,6 @@ | |||
| 10 | <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li> | 10 | <li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li> |
| 11 | <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li> | 11 | <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li> |
| 12 | <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li> | 12 | <li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li> |
| 13 | <li><span data-i18n="char_import_9">Soulkyn Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://soulkyn.com/l/en-US/@haruka-509al</tt></li> | ||
| 14 | <li><span data-i18n="char_import_10">Perchance Character (Direct Link or UUID + .gz)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://perchance.org/ai-character-chat?data=Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt><br><span data-i18n="char_import_example">Example:</span> <tt>Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt></li> | 13 | <li><span data-i18n="char_import_10">Perchance Character (Direct Link or UUID + .gz)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://perchance.org/ai-character-chat?data=Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt><br><span data-i18n="char_import_example">Example:</span> <tt>Loreena~cb3a1b531477378db7cad0148ba62d71.gz</tt></li> |
| 15 | </ul> | 14 | </ul> |
| 16 | </div> | 15 | </div> |
| @@ -639,187 +639,6 @@ async function downloadRisuCharacter(uuid) { | |||
| 639 | return { buffer, fileName, fileType }; | 639 | return { buffer, fileName, fileType }; |
| 640 | } | 640 | } |
| 641 | 641 | ||
| 642 | /** | ||
| 643 | * Parse Soulkyn URL to extract the character slug. | ||
| 644 | * @param {string} url Soulkyn character URL | ||
| 645 | * @returns {string | null} Slug of the character | ||
| 646 | */ | ||
| 647 | function parseSoulkynUrl(url) { | ||
| 648 | // Example: https://soulkyn.com/l/en-US/@kayla-marie | ||
| 649 | const pattern = /^https:\/\/soulkyn\.com\/l\/[a-z]{2}-[A-Z]{2}\/@([\w\d-]+)/i; | ||
| 650 | const match = url.match(pattern); | ||
| 651 | return match ? match[1] : null; | ||
| 652 | } | ||
| 653 | |||
| 654 | /** | ||
| 655 | * Download Soulkyn character card | ||
| 656 | * @param {string} slug Slug of the character | ||
| 657 | * @returns {Promise<{buffer: Buffer, fileName: string, fileType: string} | null>} | ||
| 658 | */ | ||
| 659 | async function downloadSoulkynCharacter(slug) { | ||
| 660 | const soulkynReplacements = [ | ||
| 661 | // https://soulkyn.com/l/en-US/help/character-backgrounds-advanced#variables-you-can-use-in-character-background-text | ||
| 662 | { pattern: /__USER_?NAME__/gi, replacement: '{{user}}' }, | ||
| 663 | { pattern: /__PERSONA_?NAME__/gi, replacement: '{{char}}' }, | ||
| 664 | // ST doesn't support gender-specific pronoun macros | ||
| 665 | { pattern: /__U_PRONOUN_1__/gi, replacement: 'they' }, | ||
| 666 | { pattern: /__U_PRONOUN_2__/gi, replacement: 'them' }, | ||
| 667 | { pattern: /__U_PRONOUN_3__/gi, replacement: 'their' }, | ||
| 668 | { pattern: /__U_PRONOUN_4__/gi, replacement: 'themselves' }, | ||
| 669 | { pattern: /__(USER_)?PRONOUN__/gi, replacement: 'they' }, | ||
| 670 | { pattern: /__(USER_)?CPRONOUN__/gi, replacement: 'them' }, | ||
| 671 | { pattern: /__(USER_)?UPRONOUN__/gi, replacement: 'their' }, | ||
| 672 | // HTML tags -> Markdown syntax | ||
| 673 | { pattern: /<(strong|b)>/gi, replacement: '**' }, | ||
| 674 | { pattern: /<\/(strong|b)>/gi, replacement: '**' }, | ||
| 675 | { pattern: /<(em|i)>/gi, replacement: '*' }, | ||
| 676 | { pattern: /<\/(em|i)>/gi, replacement: '*' }, | ||
| 677 | ]; | ||
| 678 | |||
| 679 | const normalizeContent = (str) => soulkynReplacements.reduce((acc, { pattern, replacement }) => acc.replace(pattern, replacement), str); | ||
| 680 | |||
| 681 | try { | ||
| 682 | const url = `https://soulkyn.com/_special/rest/Sk/public/Persona/${slug}`; | ||
| 683 | const result = await fetch(url, { | ||
| 684 | headers: { 'Content-Type': 'application/json', 'User-Agent': USER_AGENT }, | ||
| 685 | }); | ||
| 686 | if (result.ok) { | ||
| 687 | /** @type {any} */ | ||
| 688 | const soulkynCharData = await result.json(); | ||
| 689 | |||
| 690 | if (soulkynCharData.result !== 'success') { | ||
| 691 | console.error('Soulkyn returned error', soulkynCharData.message); | ||
| 692 | throw new Error(`Failed to download character: ${soulkynCharData.message}`); | ||
| 693 | } | ||
| 694 | |||
| 695 | // Fetch avatar | ||
| 696 | let avatarBuffer = null; | ||
| 697 | if (soulkynCharData.data?.Avatar?.FWSUUID) { | ||
| 698 | const avatarUrl = `https://rub.soulkyn.com/${soulkynCharData.data.Avatar.FWSUUID}/`; | ||
| 699 | const avatarResult = await fetch(avatarUrl, { headers: { 'User-Agent': USER_AGENT } }); | ||
| 700 | |||
| 701 | if (avatarResult.ok) { | ||
| 702 | const avatarContentType = avatarResult.headers.get('content-type'); | ||
| 703 | if (avatarContentType === 'image/png') { | ||
| 704 | avatarBuffer = Buffer.from(await avatarResult.arrayBuffer()); | ||
| 705 | } else { | ||
| 706 | console.warn(`Soulkyn character (${slug}) avatar is not PNG: ${avatarContentType}`); | ||
| 707 | } | ||
| 708 | } else { | ||
| 709 | console.warn(`Soulkyn character (${slug}) avatar download failed: ${avatarResult.status}`); | ||
| 710 | } | ||
| 711 | } else { | ||
| 712 | console.warn(`Soulkyn character (${slug}) does not have an avatar`); | ||
| 713 | } | ||
| 714 | |||
| 715 | // Fallback to default avatar | ||
| 716 | if (!avatarBuffer) { | ||
| 717 | const defaultAvatarPath = path.join(serverDirectory, DEFAULT_AVATAR_PATH); | ||
| 718 | avatarBuffer = fs.readFileSync(defaultAvatarPath); | ||
| 719 | } | ||
| 720 | |||
| 721 | const d = soulkynCharData.data; | ||
| 722 | soulkynReplacements.push({ pattern: d.Username, replacement: '{{char}}' }); | ||
| 723 | |||
| 724 | // Parse Soulkyn data into character chard | ||
| 725 | const charData = { | ||
| 726 | name: d.Username, | ||
| 727 | first_mes: '', | ||
| 728 | tags: [], | ||
| 729 | description: '', | ||
| 730 | creator: d.User.Username, | ||
| 731 | creator_notes: '', | ||
| 732 | alternate_greetings: [], | ||
| 733 | character_version: '', | ||
| 734 | mes_example: '', | ||
| 735 | post_history_instructions: '', | ||
| 736 | system_prompt: '', | ||
| 737 | scenario: '', | ||
| 738 | personality: '', | ||
| 739 | extensions: { | ||
| 740 | soulkyn_slug: slug, | ||
| 741 | soulkyn_id: d.UUID, | ||
| 742 | }, | ||
| 743 | }; | ||
| 744 | |||
| 745 | if (d?.PersonaIntroText) { | ||
| 746 | const match = d.PersonaIntroText.match(/^(?:\[Scenario:\s*([\s\S]*?)\]\s*)?([\s\S]*)$/); | ||
| 747 | if (match) { | ||
| 748 | if (match[1]) { | ||
| 749 | charData.scenario = normalizeContent(match[1].trim()); | ||
| 750 | } | ||
| 751 | charData.first_mes = normalizeContent(match[2].trim()); | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | const descriptionArr = ['Name: {{char}}']; | ||
| 756 | if (d?.Version?.Age) { | ||
| 757 | descriptionArr.push(`Age: ${d.Version.Age}`); | ||
| 758 | } | ||
| 759 | if (d?.Version?.Gender) { | ||
| 760 | descriptionArr.push(`Gender: ${d.Version.Gender}`); | ||
| 761 | } | ||
| 762 | if (d?.Version?.Race?.Name && !d.Version.Race.Name.match(/no preset/i)) { | ||
| 763 | let race = d.Version.Race.Name; | ||
| 764 | if (d.Version.Race?.Description) { | ||
| 765 | race += ` (${d.Version.Race.Description})`; | ||
| 766 | } | ||
| 767 | descriptionArr.push(`Race: ${race}`); | ||
| 768 | } | ||
| 769 | if (d?.PersonalityType) { | ||
| 770 | descriptionArr.push(`Personality type: ${d.PersonalityType}`); | ||
| 771 | } | ||
| 772 | if (Array.isArray(d?.Version?.PropertyPersonality)) { | ||
| 773 | const traits = d.Version.PropertyPersonality.map((t) => t.Value).join(', '); | ||
| 774 | descriptionArr.push(`Personality Traits: ${traits}`); | ||
| 775 | } | ||
| 776 | if (Array.isArray(d?.Version?.PropertyPhysical)) { | ||
| 777 | const traits = d.Version.PropertyPhysical.map((t) => t.Value).join(', '); | ||
| 778 | descriptionArr.push(`Physical Traits: ${traits}`); | ||
| 779 | } | ||
| 780 | if (Array.isArray(d?.Clothes?.Preset)) { | ||
| 781 | descriptionArr.push(`Clothes: ${d.Clothes.Preset.join(', ')}`); | ||
| 782 | } | ||
| 783 | if (d?.Avatar?.Caption) { | ||
| 784 | descriptionArr.push(`Image description featuring {{char}}: ${d.Avatar.Caption.replace(/\n+/g, ' ')}`); | ||
| 785 | } | ||
| 786 | if (d?.Version?.WelcomeMessage) { | ||
| 787 | if (charData.first_mes) { | ||
| 788 | descriptionArr.push(`{{char}}'s self-description: "${d.Version.WelcomeMessage}"`); | ||
| 789 | } else { | ||
| 790 | // Some characters lack `PersonaIntroText`. In that case we use `Version.WelcomeMessage` for `first_mes` | ||
| 791 | charData.first_mes = normalizeContent(d.Version.WelcomeMessage); | ||
| 792 | } | ||
| 793 | } | ||
| 794 | charData.description = normalizeContent(descriptionArr.join('\n')); | ||
| 795 | |||
| 796 | if (Array.isArray(d?.Version?.ChatExamplesValue)) { | ||
| 797 | charData.mes_example = d.Version.ChatExamplesValue.map((example) => `<START>\n${normalizeContent(example)}`).join('\n'); | ||
| 798 | } | ||
| 799 | |||
| 800 | if (Array.isArray(d?.PersonaTags)) { | ||
| 801 | charData.tags = d.PersonaTags.map((t) => t.Slug); | ||
| 802 | } | ||
| 803 | |||
| 804 | // Character card | ||
| 805 | const buffer = write(avatarBuffer, JSON.stringify({ | ||
| 806 | 'spec': 'chara_card_v2', | ||
| 807 | 'spec_version': '2.0', | ||
| 808 | 'data': charData, | ||
| 809 | })); | ||
| 810 | |||
| 811 | const fileName = `${sanitize(d.UUID)}.png`; | ||
| 812 | const fileType = 'image/png'; | ||
| 813 | |||
| 814 | return { buffer, fileName, fileType }; | ||
| 815 | } | ||
| 816 | } catch (error) { | ||
| 817 | console.error('Error downloading character:', error); | ||
| 818 | throw error; | ||
| 819 | } | ||
| 820 | return null; | ||
| 821 | } | ||
| 822 | |||
| 823 | /** * Check if the given string is a valid Perchance UUID. | 642 | /** * Check if the given string is a valid Perchance UUID. |
| 824 | * @param {string} uuid UUID string to check | 643 | * @param {string} uuid UUID string to check |
| 825 | * @returns {boolean} True if the UUID is valid, false otherwise | 644 | * @returns {boolean} True if the UUID is valid, false otherwise |
| @@ -921,13 +740,14 @@ async function downloadPerchanceCharacter(slug) { | |||
| 921 | return null; | 740 | return null; |
| 922 | } | 741 | } |
| 923 | 742 | ||
| 924 | /** * Extracts Perchance character data from a gzipped response. | 743 | /** |
| 744 | * Extracts Perchance character data from a gzipped response. | ||
| 925 | * @param {import('node-fetch').Response} result Fetch response containing gzipped character data | 745 | * @param {import('node-fetch').Response} result Fetch response containing gzipped character data |
| 926 | * @returns {Promise<Object>} Parsed Perchance character data | 746 | * @returns {Promise<Object>} Parsed Perchance character data |
| 927 | * @throws {Error} If the character data is invalid or missing required fields | 747 | * @throws {Error} If the character data is invalid or missing required fields |
| 928 | */ | 748 | */ |
| 929 | async function extractPerchanceCharacterFromGz(result) { | 749 | async function extractPerchanceCharacterFromGz(result) { |
| 930 | const compressedBuffer = Buffer.from(await result.arrayBuffer()); | 750 | const compressedBuffer = await result.arrayBuffer(); |
| 931 | const decompressedBuffer = zlib.gunzipSync(compressedBuffer); | 751 | const decompressedBuffer = zlib.gunzipSync(compressedBuffer); |
| 932 | 752 | ||
| 933 | // inside the gz file, there is a file of the same name without extensions, but it is a json file | 753 | // inside the gz file, there is a file of the same name without extensions, but it is a json file |
| @@ -1062,7 +882,6 @@ router.post('/importURL', async (request, response) => { | |||
| 1062 | const isPygmalionContent = host.includes('pygmalion.chat'); | 882 | const isPygmalionContent = host.includes('pygmalion.chat'); |
| 1063 | const isAICharacterCardsContent = host.includes('aicharactercards.com'); | 883 | const isAICharacterCardsContent = host.includes('aicharactercards.com'); |
| 1064 | const isRisu = host.includes('realm.risuai.net'); | 884 | const isRisu = host.includes('realm.risuai.net'); |
| 1065 | const isSoulkyn = host.includes('soulkyn.com'); | ||
| 1066 | const isPerchance = host.includes('perchance.org'); | 885 | const isPerchance = host.includes('perchance.org'); |
| 1067 | const isGeneric = isHostWhitelisted(host); | 886 | const isGeneric = isHostWhitelisted(host); |
| 1068 | 887 | ||
| @@ -1112,13 +931,6 @@ router.post('/importURL', async (request, response) => { | |||
| 1112 | 931 | ||
| 1113 | type = 'character'; | 932 | type = 'character'; |
| 1114 | result = await downloadRisuCharacter(uuid); | 933 | result = await downloadRisuCharacter(uuid); |
| 1115 | } else if (isSoulkyn) { | ||
| 1116 | const soulkynSlug = parseSoulkynUrl(url); | ||
| 1117 | if (!soulkynSlug) { | ||
| 1118 | return response.sendStatus(404); | ||
| 1119 | } | ||
| 1120 | type = 'character'; | ||
| 1121 | result = await downloadSoulkynCharacter(soulkynSlug); | ||
| 1122 | } else if (isPerchance) { | 934 | } else if (isPerchance) { |
| 1123 | const perchanceSlug = parsePerchanceSlug(url); | 935 | const perchanceSlug = parsePerchanceSlug(url); |
| 1124 | if (!perchanceSlug) { | 936 | if (!perchanceSlug) { |