mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-06 12:47:57 +01:00
Merge pull request #2171 from 24adamcho/generic-card-download
Character card import from generic sources (specifically Discord, Catbox.moe)
This commit is contained in:
commit
2f85e50c6f
@ -48,6 +48,12 @@ allowKeysExposure: false
|
|||||||
skipContentCheck: false
|
skipContentCheck: false
|
||||||
# Disable automatic chats backup
|
# Disable automatic chats backup
|
||||||
disableChatBackup: false
|
disableChatBackup: false
|
||||||
|
# Allowed hosts for card downloads
|
||||||
|
whitelistImportDomains:
|
||||||
|
- localhost
|
||||||
|
- cdn.discordapp.com
|
||||||
|
- files.catbox.moe
|
||||||
|
- raw.githubusercontent.com
|
||||||
# API request overrides (for KoboldAI and Text Completion APIs)
|
# API request overrides (for KoboldAI and Text Completion APIs)
|
||||||
## Note: host includes the port number if it's not the default (80 or 443)
|
## Note: host includes the port number if it's not the default (80 or 443)
|
||||||
## Format is an array of objects:
|
## Format is an array of objects:
|
||||||
|
@ -10165,7 +10165,7 @@ jQuery(async function () {
|
|||||||
<li>JanitorAI Character (Direct Link or UUID)<br>Example: <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
|
<li>JanitorAI Character (Direct Link or UUID)<br>Example: <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
|
||||||
<li>Pygmalion.chat Character (Direct Link or UUID)<br>Example: <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
|
<li>Pygmalion.chat Character (Direct Link or UUID)<br>Example: <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
|
||||||
<li>AICharacterCard.com Character (Direct Link or ID)<br>Example: <tt>AICC/aicharcards/the-game-master</tt></li>
|
<li>AICharacterCard.com Character (Direct Link or ID)<br>Example: <tt>AICC/aicharcards/the-game-master</tt></li>
|
||||||
<li>More coming soon...</li>
|
<li>Direct PNG Link (refer to <code>config.yaml</code> for allowed hosts)<br>Example: <tt>https://files.catbox.moe/notarealfile.png</tt></li>
|
||||||
<ul>`;
|
<ul>`;
|
||||||
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
|
const input = await callPopup(html, 'input', '', { okButton: 'Import', rows: 4 });
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ const contentDirectory = path.join(process.cwd(), 'default/content');
|
|||||||
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
const contentIndexPath = path.join(contentDirectory, 'index.json');
|
||||||
const characterCardParser = require('../character-card-parser.js');
|
const characterCardParser = require('../character-card-parser.js');
|
||||||
|
|
||||||
|
const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ContentItem
|
* @typedef {Object} ContentItem
|
||||||
* @property {string} filename
|
* @property {string} filename
|
||||||
@ -426,6 +428,32 @@ function parseAICC(url) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download character card from generic url.
|
||||||
|
* @param {String} url
|
||||||
|
*/
|
||||||
|
async function downloadGenericPng(url) {
|
||||||
|
try {
|
||||||
|
const result = await fetch(url);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const buffer = await result.buffer();
|
||||||
|
const fileName = sanitize(result.url.split('?')[0].split('/').reverse()[0]);
|
||||||
|
const contentType = result.headers.get('content-type') || 'image/png'; //yoink it from AICC function lol
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: buffer,
|
||||||
|
fileName: fileName,
|
||||||
|
fileType: contentType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading file: ', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} url
|
* @param {String} url
|
||||||
* @returns {String | null } UUID of the character
|
* @returns {String | null } UUID of the character
|
||||||
@ -440,6 +468,29 @@ function getUuidFromUrl(url) {
|
|||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to get the domain host of a url instead of a blanket string search.
|
||||||
|
* @param {String} url URL to strip
|
||||||
|
* @returns {String} Domain name
|
||||||
|
*/
|
||||||
|
function getHostFromUrl(url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return urlObj.hostname;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if host is part of generic download source whitelist.
|
||||||
|
* @param {String} host Host to check
|
||||||
|
* @returns {boolean} If the host is on the whitelist.
|
||||||
|
*/
|
||||||
|
function isHostWhitelisted(host) {
|
||||||
|
return WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES.includes(host);
|
||||||
|
}
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/importURL', jsonParser, async (request, response) => {
|
router.post('/importURL', jsonParser, async (request, response) => {
|
||||||
@ -449,12 +500,15 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = request.body.url;
|
const url = request.body.url;
|
||||||
|
const host = getHostFromUrl(url);
|
||||||
let result;
|
let result;
|
||||||
let type;
|
let type;
|
||||||
|
|
||||||
const isJannnyContent = url.includes('janitorai');
|
const isChub = host.includes('chub.ai');
|
||||||
const isPygmalionContent = url.includes('pygmalion.chat');
|
const isJannnyContent = host.includes('janitorai');
|
||||||
const isAICharacterCardsContent = url.includes('aicharactercards.com');
|
const isPygmalionContent = host.includes('pygmalion.chat');
|
||||||
|
const isAICharacterCardsContent = host.includes('aicharactercards.com');
|
||||||
|
const isGeneric = isHostWhitelisted(host);
|
||||||
|
|
||||||
if (isPygmalionContent) {
|
if (isPygmalionContent) {
|
||||||
const uuid = getUuidFromUrl(url);
|
const uuid = getUuidFromUrl(url);
|
||||||
@ -479,7 +533,7 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
|||||||
}
|
}
|
||||||
type = 'character';
|
type = 'character';
|
||||||
result = await downloadAICCCharacter(AICCParsed);
|
result = await downloadAICCCharacter(AICCParsed);
|
||||||
} else {
|
} else if (isChub) {
|
||||||
const chubParsed = parseChubUrl(url);
|
const chubParsed = parseChubUrl(url);
|
||||||
type = chubParsed?.type;
|
type = chubParsed?.type;
|
||||||
|
|
||||||
@ -494,10 +548,20 @@ router.post('/importURL', jsonParser, async (request, response) => {
|
|||||||
else {
|
else {
|
||||||
return response.sendStatus(404);
|
return response.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
} else if (isGeneric) {
|
||||||
|
console.log('Downloading from generic url.');
|
||||||
|
type = 'character';
|
||||||
|
result = await downloadGenericPng(url);
|
||||||
|
} else {
|
||||||
|
return response.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return response.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.fileType) response.set('Content-Type', result.fileType);
|
if (result.fileType) response.set('Content-Type', result.fileType);
|
||||||
response.set('Content-Disposition', `attachment; filename="${result.fileName}"`);
|
response.set('Content-Disposition', `attachment; filename="${encodeURI(result.fileName)}"`);
|
||||||
response.set('X-Custom-Content-Type', type);
|
response.set('X-Custom-Content-Type', type);
|
||||||
return response.send(result.buffer);
|
return response.send(result.buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user