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>

0e5b509dc321661489907e2c188230b9e67cbe8b

buzz <buzz@users.noreply.github.com>

Signed
17 files changed, +3 -207Showing whitespace changes
public/locales/ar-sa.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "رابط PNG المباشر (راجع",
13771377 "char_import_7": "للمضيفين المسموح بهم)",
13781378 "char_import_8": "شخصية RisuRealm (رابط مباشر)",
1379- "char_import_9": "شخصية Soulkyn (رابط مباشر)",
13801379 "char_import_10": "شخصية Perchance (رابط مباشر أو UUID + .gz)",
13811380 "Supports importing multiple characters.": "يدعم استيراد أحرف متعددة.",
13821381 "Write each URL or ID into a new line.": "اكتب كل عنوان URL أو معرف في سطر جديد.",
public/locales/de-de.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Direkter PNG-Link (siehe",
13771377 "char_import_7": "für erlaubte Hosts)",
13781378 "char_import_8": "RisuRealm-Charakter (Direktlink)",
1379- "char_import_9": "Soulkyn-Charakter (Direktlink)",
13801379 "char_import_10": "Perchance-Charakter (Direktlink oder UUID + .gz)",
13811380 "Supports importing multiple characters.": "Unterstützt den Import mehrerer Zeichen.",
13821381 "Write each URL or ID into a new line.": "Schreiben Sie jede URL oder ID in eine neue Zeile.",
public/locales/es-es.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Enlace PNG directo (consulte",
13771377 "char_import_7": "para hosts permitidos)",
13781378 "char_import_8": "Personaje RisuRealm (Enlace directo)",
1379- "char_import_9": "Personaje Soulkyn (Enlace directo)",
13801379 "char_import_10": "Personaje Perchance (enlace directo o UUID + .gz)",
13811380 "Supports importing multiple characters.": "Admite la importación de múltiples caracteres.",
13821381 "Write each URL or ID into a new line.": "Escriba cada URL o ID en una nueva línea.",
public/locales/fr-fr.json+0 -1
@@ -1298,7 +1298,6 @@
12981298 "char_import_6": "Lien PNG direct (voir",
12991299 "char_import_7": "pour les hôtes autorisés)",
13001300 "char_import_8": "Personnage de RisuRealm (lien direct)",
1301- "char_import_9": "Personnage de Soulkyn (lien direct)",
13021301 "char_import_10": "Personnage de Perchance (lien direct ou UUID + .gz)",
13031302 "Supports importing multiple characters.": "Prend en charge l'importation de plusieurs caractères.",
13041303 "Write each URL or ID into a new line.": "Écrivez chaque URL ou identifiant dans une nouvelle ligne.",
public/locales/is-is.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Beinn PNG hlekkur (sjá",
13771377 "char_import_7": "fyrir leyfilega gestgjafa)",
13781378 "char_import_8": "RisuRealm karakter (beinn hlekkur)",
1379- "char_import_9": "Soulkyn karakter (beinn hlekkur)",
13801379 "char_import_10": "Perchance karakter (beinn hlekkur eða UUID + .gz)",
13811380 "Supports importing multiple characters.": "Styður innflutning á mörgum stöfum.",
13821381 "Write each URL or ID into a new line.": "Skrifaðu hverja vefslóð eða auðkenni í nýja línu.",
public/locales/it-it.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Collegamento PNG diretto (fare riferimento a",
13771377 "char_import_7": "per gli host consentiti)",
13781378 "char_import_8": "Personaggio RisuRealm (collegamento diretto)",
1379- "char_import_9": "Personaggio Soulkyn (collegamento diretto)",
13801379 "char_import_10": "Perchance Personaggio (collegamento diretto o UUID + .gz)",
13811380 "Supports importing multiple characters.": "Supporta l'importazione di più caratteri.",
13821381 "Write each URL or ID into a new line.": "Scrivi ogni URL o ID in una nuova riga.",
public/locales/ja-jp.json+0 -1
@@ -1378,7 +1378,6 @@
13781378 "char_import_6": "直接PNGリンク(参照",
13791379 "char_import_7": "許可されたホストの場合)",
13801380 "char_import_8": "RisuRealm キャラクター (直接リンク)",
1381- "char_import_9": "Soulkyn キャラクター (直接リンク)",
13821381 "char_import_10": "Perchance キャラクター (直接リンクまたは UUID + .gz)",
13831382 "Supports importing multiple characters.": "複数のキャラクターのインポートをサポートします。",
13841383 "Write each URL or ID into a new line.": "各 URL または ID を新しい行に入力します。",
public/locales/ko-kr.json+0 -1
@@ -1395,7 +1395,6 @@
13951395 "char_import_6": "직접 PNG 링크(참조",
13961396 "char_import_7": "허용된 호스트의 경우)",
13971397 "char_import_8": "RisuRealm 캐릭터 (직접링크)",
1398- "char_import_9": "Soulkyn 캐릭터 (직접링크)",
13991398 "char_import_10": "Perchance 캐릭터 (직접 링크 또는 UUID + .gz)",
14001399 "Supports importing multiple characters.": "여러 문자 가져오기를 지원합니다.",
14011400 "Write each URL or ID into a new line.": "각 URL 또는 ID를 새 줄에 작성합니다.",
public/locales/nl-nl.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Directe PNG-link (zie",
13771377 "char_import_7": "voor toegestane hosts)",
13781378 "char_import_8": "RisuRealm-personage (directe link)",
1379- "char_import_9": "Soulkyn-personage (directe link)",
13801379 "char_import_10": "Perchance-personage (directe link of UUID + .gz)",
13811380 "Supports importing multiple characters.": "Ondersteunt het importeren van meerdere tekens.",
13821381 "Write each URL or ID into a new line.": "Schrijf elke URL of ID op een nieuwe regel.",
public/locales/pt-pt.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Link PNG direto (consulte",
13771377 "char_import_7": "para hosts permitidos)",
13781378 "char_import_8": "Personagem RisuRealm (link direto)",
1379- "char_import_9": "Personagem Soulkyn (link direto)",
13801379 "char_import_10": "Personagem Perchance (Link Direto ou UUID + .gz)",
13811380 "Supports importing multiple characters.": "Suporta importação de vários caracteres.",
13821381 "Write each URL or ID into a new line.": "Escreva cada URL ou ID em uma nova linha.",
public/locales/ru-ru.json+0 -1
@@ -963,7 +963,6 @@
963963 "char_import_5": "Персонаж с AICharacterCards.com (прямая ссылка или ID)",
964964 "char_import_6": "Прямая ссылка на PNG-файл (список разрешённых хостов находится в",
965965 "char_import_7": ")",
966- "char_import_9": "Персонаж с Soulkyn (прямая ссылка)",
967966 "char_import_10": "Персонаж с Perchance (прямая ссылка или UUID + .gz)",
968967 "Grammar String": "Грамматика",
969968 "GBNF or EBNF, depends on the backend in use. If you're using this you should know which.": "GBNF или EBNF, зависит от бэкенда. Если вы это используете, то, скорее всего, сами знаете, какой именно.",
public/locales/uk-ua.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Пряме посилання на PNG (див",
13771377 "char_import_7": "для дозволених хостів)",
13781378 "char_import_8": "Персонаж RisuRealm (пряме посилання)",
1379- "char_import_9": "Персонаж Soulkyn (пряме посилання)",
13801379 "char_import_10": "Персонаж Perchance (пряме посилання або UUID + .gz)",
13811380 "Supports importing multiple characters.": "Підтримується імпорт кількох символів.",
13821381 "Write each URL or ID into a new line.": "Напишіть кожну URL-адресу або ідентифікатор у новому рядку.",
public/locales/vi-vn.json+0 -1
@@ -1376,7 +1376,6 @@
13761376 "char_import_6": "Nhập PNG trực tiếp (tham khảo",
13771377 "char_import_7": "đối với các máy chủ được phép)",
13781378 "char_import_8": "RisuRealm (URL trực tiếp)",
1379- "char_import_9": "Soulkyn (URL trực tiếp)",
13801379 "char_import_10": "Perchance (Nhập URL trực tiếp hoặc UUID + .gz)",
13811380 "Supports importing multiple characters.": "Hỗ trợ nhập nhiều ký tự.",
13821381 "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.",
public/locales/zh-cn.json+0 -1
@@ -1888,7 +1888,6 @@
18881888 "char_import_6": "被允许的PNG直链(请参阅",
18891889 "char_import_7": ")",
18901890 "char_import_8": "RisuRealm 角色(直链)",
1891- "char_import_9": "Soulkyn 角色(直链)",
18921891 "char_import_10": "Perchance 角色(直链或UUID + .gz)",
18931892 "Supports importing multiple characters.": "支持导入多个角色。",
18941893 "Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
public/locales/zh-tw.json+0 -1
@@ -1381,7 +1381,6 @@
13811381 "char_import_6": "直接 PNG 連結(請參閱",
13821382 "char_import_7": "對於允許的主機)",
13831383 "char_import_8": "RisuRealm 角色(直接連結)",
1384- "char_import_9": "Soulkyn 角色(直接連結)",
13851384 "char_import_10": "Perchance 角色(直接連結或 ID + .gz)",
13861385 "Supports importing multiple characters.": "支援匯入多個字元。",
13871386 "Write each URL or ID into a new line.": "將每個 URL 或 ID 寫入新行。",
public/scripts/templates/importCharacters.html+0 -1
@@ -10,7 +10,6 @@
1010 <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>
1111 <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>
1212 <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>
1413 <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>
1514 </ul>
1615</div>
src/endpoints/content-manager.js+3 -191
@@ -639,187 +639,6 @@ async function downloadRisuCharacter(uuid) {
639639 return { buffer, fileName, fileType };
640640}
641641
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-
823642/** * Check if the given string is a valid Perchance UUID.
824643 * @param {string} uuid UUID string to check
825644 * @returns {boolean} True if the UUID is valid, false otherwise
@@ -921,13 +740,14 @@ async function downloadPerchanceCharacter(slug) {
921740 return null;
922741}
923742
924-/** * Extracts Perchance character data from a gzipped response.
743+/**
744+ * Extracts Perchance character data from a gzipped response.
925745 * @param {import('node-fetch').Response} result Fetch response containing gzipped character data
926746 * @returns {Promise<Object>} Parsed Perchance character data
927747 * @throws {Error} If the character data is invalid or missing required fields
928748 */
929749async function extractPerchanceCharacterFromGz(result) {
930750 const compressedBuffer = Buffer.from(await result.arrayBuffer());
931751 const decompressedBuffer = zlib.gunzipSync(compressedBuffer);
932752
933753 // 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) => {
1062882 const isPygmalionContent = host.includes('pygmalion.chat');
1063883 const isAICharacterCardsContent = host.includes('aicharactercards.com');
1064884 const isRisu = host.includes('realm.risuai.net');
1065- const isSoulkyn = host.includes('soulkyn.com');
1066885 const isPerchance = host.includes('perchance.org');
1067886 const isGeneric = isHostWhitelisted(host);
1068887
@@ -1112,13 +931,6 @@ router.post('/importURL', async (request, response) => {
1112931
1113932 type = 'character';
1114933 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);
1122934 } else if (isPerchance) {
1123935 const perchanceSlug = parsePerchanceSlug(url);
1124936 if (!perchanceSlug) {