Files
daxcay 1b55c4ef7a COMFY URL
COMFY URL INSTANCE SUPPORT
2024-09-16 10:23:19 +05:30

634 lines
21 KiB
JavaScript

const path = require('path');
const express = require('express');
const args = process.argv.slice(2);
const fs = require('fs');
const fsp = require('fs/promises')
const qrcode = require('qrcode');
const { Client, LocalAuth, MessageMedia } = require('whatsapp-web.js');
let CONFIG = {
PORT: 4000,
BROWSER_DIRECTORY: path.join(__dirname, 'user'),
MODELS_DIRECTORY: path.join(__dirname, 'models'),
TEMP_DIRECTORY: path.join(__dirname, 'temp'),
OUTPUT_DIRECTORY: path.join(__dirname, 'output'),
WEB_DIRECTORY: path.join(__dirname, 'website'),
API_URL: 'http://127.0.0.1:8188',
CHROME: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
MODE: null,
ADMIN: null,
EXPRESS: express(),
WA: null,
QRCODE: null,
READY: false,
KSAMPLER_SEED_CHANGE: 1,
MODELS: {},
PROMPT: {
MODEL: {},
NODES: {}
},
WATCHER: {}
}
const deleteContentsInsideDirectory = async (dirPath) => {
try {
const files = await fsp.readdir(dirPath);
await Promise.all(files.map(async (file) => {
const filePath = path.join(dirPath, file);
const stats = await fsp.stat(filePath);
if (stats.isDirectory()) {
await deleteContentsInsideDirectory(filePath);
await fsp.rmdir(filePath);
} else {
await fsp.unlink(filePath);
}
}));
console.log(`Successfully deleted contents inside ${dirPath}`);
} catch (error) {
console.error(`Error deleting contents inside ${dirPath}:`, error);
throw error;
}
};
function readJsonFile(filePath) {
try {
const data = fs.readFileSync(filePath, 'utf8');
const jsonData = JSON.parse(data);
return jsonData;
} catch (err) {
console.error(`Error reading or parsing file: ${err}`);
return null;
}
}
function addDirectories() {
CONFIG.BROWSER_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'user')
CONFIG.MODELS_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'workflows')
CONFIG.TEMP_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'temp')
CONFIG.OUTPUT_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'output')
WA_CONFIG = readJsonFile(path.join(CONFIG.MAIN_DIRECTORY, 'whatsapp.json'))
CONFIG.MODE = parseInt(WA_CONFIG.mode);
CONFIG.ADMIN = `${WA_CONFIG.phone_code}${WA_CONFIG.phone}@c.us`
CONFIG.CHROME = WA_CONFIG.chrome
CONFIG.API_URL = WA_CONFIG.comfy_url;
}
args.forEach(arg => {
const [key, value] = arg.split('=');
if (key === '--mode') {
CONFIG.MODE = parseInt(value);
} else if (key === '--admin') {
CONFIG.ADMIN = `${value}@c.us`;
} else if (key === '--api') {
CONFIG.API_URL = value
} else if (key === '--pd') {
CONFIG.MAIN_DIRECTORY = value
addDirectories()
}
});
function createDirectories(config) {
for (let key in config) {
if (key.endsWith('_DIRECTORY')) {
const dir = config[key];
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`Directory created: ${dir}`);
} else {
console.log(`Directory already exists: ${dir}`);
}
}
}
}
function botMediaMessage(to, message, media) {
CONFIG.WA.sendMessage(to, "🤖\n\n" + message, { media })
}
function botTextMessage(to, message) {
CONFIG.WA.sendMessage(to, "🤖\n\n" + message)
}
function botTextMessagReply(message_object, message) {
message_object.reply("🤖\n\n" + message)
}
function isAdmin(user) {
return CONFIG.ADMIN == user
}
function allCommands() {
let rules = ''
rules += 'Write */wfs* to get a numbered list of uploaded workflows.\n\n'
rules += 'Write */wf id* to select the workflow.\n\n'
rules += 'Write */wns* to get numbered list of selected workflow nodes.\n\n'
rules += 'Write */wn id* to get numbered list of inputs available.\n\n'
rules += 'Write */s node_id input_id value* to set value for input selected.\n\n'
rules += 'Write */sce* enable auto ksampler seed change.\n\n'
rules += 'Write */scd* disable auto ksampler seed change.\n\n'
rules += 'Write */r* to reset all to default settings.\n\n'
rules += 'Write */q* to queue.\n\n'
rules += 'Write */i* to interrupt queue.\n\n'
rules += 'Write */m number* to set bot usage mode. *1*: Single User, *2*: Multi User.\n\n'
return rules
}
function readModels(folderPath) {
return fs.readdirSync(folderPath);
}
function formatModelObject(models) {
return Object.entries(models).map(([key, value]) => `*${key}* | ${value}`).join("\n");
}
function containsIndex(arr, index) {
return index >= 0 && index < arr.length && arr[index] !== undefined;
}
function containsValue(arr, value) {
return arr.includes(value);
}
function containsValueAtIndex(arr, index, value) {
return index >= 0 && index < arr.length && arr[index] === value;
}
function readJSONFile(file) {
try {
let file_path = path.join(CONFIG.MODELS_DIRECTORY, file)
const data = fs.readFileSync(file_path, 'utf8');
const jsonData = JSON.parse(data);
return jsonData;
} catch (err) {
console.error("Error reading file:", err);
return null;
}
}
function extractTitleFromModel(data) {
const titles = {};
if (data) {
for (const key in data) {
if (data.hasOwnProperty(key)) {
titles[key] = data[key]._meta.title;
}
}
}
return titles;
}
function formatOutput(titles) {
let output = '*ID* | Node Name\n';
for (const key in titles) {
if (titles.hasOwnProperty(key)) {
output += `*${key}* | ${titles[key]}\n`;
}
}
return output;
}
function formatInputsIgnoreArray(number, data) {
const inputs = data[number].inputs;
let formattedInputs = `ID | Input Name | Input Value\n`;
let counter = 0
for (const inputName in inputs) {
if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) {
formattedInputs += `*${counter++}* | *${inputName}* | *${inputs[inputName]}*\n`;
}
}
return formattedInputs.trim();
}
function containsSettings(number, setting_number, data) {
if (data[number] && data[number].inputs) {
const inputs = data[number].inputs;
let counter = 0
for (const inputName in inputs) {
if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) {
if (counter == setting_number) {
break;
}
counter++
}
}
return (counter == setting_number)
}
}
function getSettingName(number, setting_number, data) {
if (data[number] && data[number].inputs) {
const inputs = data[number].inputs;
let counter = 0
let name = ""
for (const inputName in inputs) {
if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) {
if (counter == setting_number) {
name = inputName
break;
}
counter++
}
}
return name
}
}
function editInputs(jsonObj, title, inputName, value) {
for (let key in jsonObj) {
if (jsonObj[key]._meta && jsonObj[key]._meta.title === title) {
if (jsonObj[key].inputs) {
jsonObj[key].inputs[inputName] = value;
} else {
jsonObj[key].inputs = { [inputName]: value };
}
return;
}
}
}
function getNodeID(jsonObj, title) {
let id = -1
for (let key in jsonObj) {
if (jsonObj[key]._meta && jsonObj[key]._meta.title === title) {
id = key
break
}
}
return id
}
function editJSON(number, inputName, inputValue, data) {
if (data[number] && data[number].inputs) {
const existingValue = data[number].inputs[inputName];
if (existingValue !== undefined) {
const existingValueType = typeof existingValue;
let convertedValue;
if (existingValueType === 'number' && Number.isInteger(existingValue)) {
convertedValue = parseInt(inputValue);
} else if (existingValueType === 'number' && !Number.isInteger(existingValue)) {
convertedValue = parseFloat(inputValue);
} else if (existingValueType === 'boolean') {
convertedValue = (inputValue.toLowerCase() === 'true');
} else {
convertedValue = inputValue
}
data[number].inputs[inputName] = convertedValue;
return data;
} else {
console.log("Invalid input name.");
return null;
}
} else {
console.log("Invalid number.");
return null;
}
}
function extractPhoneNumber(inputString) {
const parts = inputString.split('@');
const firstPart = parts[0];
const phoneNumber = firstPart.slice(-10);
return phoneNumber;
}
async function sendResultToUser(user, images) {
if (images.length > 0) {
images.forEach(async (image) => {
let file_path = path.join(image.subfolder, image.filename)
const media = await MessageMedia.fromFilePath(file_path);
botMediaMessage(user, "Here is your image", media)
})
}
}
async function watch(user, data) {
try {
if (!CONFIG.WATCHER[user]) {
CONFIG.WATCHER[user] = {
count: 0,
}
}
let id = data.prompt_id
let res = await fetch(`${CONFIG.API_URL}/history/${id}`).then(response => response.json())
if (res && res[id]) {
let status = res[id].status
if (status.completed && status.status_str === "success") {
let nodeid = getNodeID(CONFIG.PROMPT.NODES[user], "WA_ImageSaver")
if (nodeid != -1) {
let images = res[id].outputs[nodeid].images || []
sendResultToUser(user, images)
}
}
else {
botTextMessage(user, "Workflow failed/interrupted to generate result. Please try again!")
}
}
else {
setTimeout(watch, 1000, user, data)
}
} catch (error) {
console.log(error)
botTextMessage(user, "Workflow failed/interrupted to generate result. Please try again!")
}
}
function setCommand(message) {
let sliced = message.body.split(" ")
let command = sliced[0]
switch (command) {
case '/c':
botTextMessagReply(message, allCommands())
break;
case '/wfs':
CONFIG.MODELS[message.from] = readModels(CONFIG.MODELS_DIRECTORY)
let models = formatModelObject(CONFIG.MODELS[message.from])
botTextMessagReply(message, "Here are your workflows:\n\n" + "*ID* | Model Name\n\n" + models + "\n\nTo select workfloe write /wf id")
break;
case '/wf':
if (!CONFIG.MODELS[message.from]) {
CONFIG.MODELS[message.from] = readModels(CONFIG.MODELS_DIRECTORY)
}
let index = parseInt(sliced[1])
if (!containsIndex(CONFIG.MODELS[message.from], index)) {
botTextMessagReply(message, "Workflows does not exists!")
}
else {
CONFIG.PROMPT.MODEL[message.from] = CONFIG.MODELS[message.from][index]
if (!CONFIG.PROMPT.NODES[message.from]) {
CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from])
}
botTextMessagReply(message, `Workflow *${CONFIG.PROMPT.MODEL[message.from]}* selected!`)
}
break;
case '/wns':
if (!CONFIG.MODELS[message.from]) {
botTextMessagReply(message, "Workflow not selected!")
}
else {
CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from])
let nodes = formatOutput(extractTitleFromModel(CONFIG.PROMPT.NODES[message.from]))
botTextMessagReply(message, "Here are your workflow nodes:\n\n" + nodes + "\n\nTo get the datail of a particular node write /wn id")
}
break;
case '/wn':
if (!CONFIG.MODELS[message.from]) {
botTextMessagReply(message, "Workflow not selected!")
}
else {
if (!CONFIG.PROMPT.NODES[message.from]) {
CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from])
}
let index = sliced[1]
if (!containsValue(Object.keys(CONFIG.PROMPT.NODES[message.from]), index)) {
botTextMessagReply(message, "Node does not exists!")
}
else {
let nodes = formatInputsIgnoreArray(index, CONFIG.PROMPT.NODES[message.from])
botTextMessagReply(message, "Here are your node inputs:\n\n" + nodes + "\n\nTo edit a particular input write /s wns_id wn_id value")
}
}
break;
case '/sce':
CONFIG.KSAMPLER_SEED_CHANGE = 1
botTextMessagReply(message, `KSampler Seed Change: *Enabled*`)
break;
case '/scd':
CONFIG.KSAMPLER_SEED_CHANGE = 0
botTextMessagReply(message, `KSampler Seed Change: *Disabled*`)
break;
case '/s':
if (!CONFIG.MODELS[message.from]) {
botTextMessagReply(message, "Workflow not selected!")
}
else {
if (!CONFIG.PROMPT.NODES[message.from]) {
CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from])
}
let node_number = sliced[1]
let setting_number = sliced[2]
let value = sliced[3]
if (sliced.length > 4) {
for (let index = 4; index < sliced.length; index++) {
const element = sliced[index];
value += " " + element
}
}
if (!containsValue(Object.keys(CONFIG.PROMPT.NODES[message.from]), node_number)) {
botTextMessagReply(message, "Node does not exists.")
}
else if (!containsSettings(node_number, setting_number, CONFIG.PROMPT.NODES[message.from])) {
botTextMessagReply(message, "Node setting does not exists.")
}
else {
let setting_name = getSettingName(node_number, setting_number, CONFIG.PROMPT.NODES[message.from])
editJSON(node_number, setting_name, value, CONFIG.PROMPT.NODES[message.from])
let nodes = formatInputsIgnoreArray(node_number, CONFIG.PROMPT.NODES[message.from])
botTextMessagReply(message, "Node setting changed.\n\n" + nodes)
}
}
break;
case '/r':
CONFIG.MODELS[message.from] = null
CONFIG.PROMPT.MODEL[message.from] = null
CONFIG.PROMPT.NODES[message.from] = null
botTextMessagReply(message, "Reset Done.\n")
break;
case '/q':
if(CONFIG.KSAMPLER_SEED_CHANGE) {
editInputs(CONFIG.PROMPT.NODES[message.from], "KSampler", "seed", Date.now())
}
editInputs(CONFIG.PROMPT.NODES[message.from], "WA_ImageSaver", "Path", path.join(CONFIG.OUTPUT_DIRECTORY, extractPhoneNumber(message.from)))
let requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ "prompt": CONFIG.PROMPT.NODES[message.from] })
};
fetch(CONFIG.API_URL + "/prompt", requestOptions)
.then(response => response.json())
.then(data => {
botTextMessagReply(message, "Promt Submitted.\n")
watch(message.from, data)
})
.catch(error => console.error('Error:', error));
break;
case '/m':
if (!isAdmin(message.from)) {
botTextMessagReply(message, `You are not Admin.`)
return
}
let mode = parseInt(sliced[1])
if (mode == 1) {
CONFIG.MODE = 1
botTextMessagReply(message, `Usage Mode changed to: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`)
}
else if (mode == 2) {
CONFIG.MODE = 2
botTextMessagReply(message, `Usage Mode changed to: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`)
}
else {
botTextMessagReply(message, `Invalid Mode.`)
}
break;
case '/i':
fetch(CONFIG.API_URL + "/interrupt", {
method: 'POST',
})
.then(data => { botTextMessagReply(message, "Promt Interrupted.\n\n") })
.catch(error => console.error('Error:', error));
break;
default:
if (mode == 2) {
botTextMessagReply(message, `Invalid command. Message */c*`)
}
break;
}
}
//==================================== WA ======================================================
let options = {
executablePath: CONFIG.CHROME,
headless: true
}
if (!CONFIG.CHROME && CONFIG.CHROME.length == 0) {
delete options.executablePath
}
CONFIG.WA = new Client({ authStrategy: new LocalAuth({ dataPath: CONFIG.BROWSER_DIRECTORY }), puppeteer: options })
CONFIG.WA.on('qr', (qr) => {
CONFIG.QRCODE = qr
qrcode.toFile(path.join(CONFIG.TEMP_DIRECTORY, 'qr.png'), qr, {
color: {
dark: '#000',
light: '#fff'
}
}, (err) => {
if (err) throw err;
console.log('QR code generated successfully!');
});
});
CONFIG.WA.on('ready', () => {
CONFIG.READY = true
if (CONFIG.ADMIN)
botTextMessage(CONFIG.ADMIN, `ComfyUI Bot is *Online*.\n\nMessage */c* to start.\n\nUsage Mode: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`)
});
CONFIG.WA.on('message_create', message => {
if (message.body.includes('🤖')) {
return
}
if (CONFIG.MODE == 1 && message.id.fromMe == true) {
setCommand(message)
} else if (CONFIG.MODE == 1 && message.id.fromMe == false) {
botTextMessagReply("I can't fulfill your request sorry 😅.")
} else if (CONFIG.MODE == 2) {
setCommand(message)
}
});
// =================================== EXPRESS ========================================
CONFIG.EXPRESS.use(express.static(CONFIG.WEB_DIRECTORY))
CONFIG.EXPRESS.use(express.json());
CONFIG.EXPRESS.get('/ping', (req, res) => {
return res.json({ TIME: +new Date() })
});
CONFIG.EXPRESS.get('/qr', (req, res) => {
const qrImagePath = path.join(CONFIG.TEMP_DIRECTORY, 'qr.png');
if (fs.existsSync(qrImagePath)) {
return res.sendFile(path.resolve(qrImagePath));
} else {
return res.status(404).send("QR code image not found");
}
});
CONFIG.EXPRESS.get('/ready', (req, res) => {
return res.json({ ready: CONFIG.READY })
});
CONFIG.EXPRESS.post('/model', async (req, res) => {
try {
const data = req.body;
const filename = data.name
const filePath = path.join(CONFIG.MODELS_DIRECTORY, filename);
await fsp.mkdir(CONFIG.MODELS_DIRECTORY, { recursive: true });
await fsp.writeFile(filePath, JSON.stringify(data.data, null, 2), 'utf8');
res.status(200).json({ success: true, message: 'Model data saved successfully' });
} catch (error) {
console.error('Error saving model data:', error);
res.status(500).json({ success: false, message: 'Failed to save model data' });
}
});
CONFIG.EXPRESS.get('/models', async (req, res) => {
try {
const files = await fsp.readdir(CONFIG.MODELS_DIRECTORY);
const models = await Promise.all(files.map(async (file) => {
const filePath = path.join(CONFIG.MODELS_DIRECTORY, file);
const stats = await fsp.stat(filePath);
return {
name: file.split('.')[0],
dateCreated: stats.mtime.toISOString()
};
}));
res.status(200).json(models);
} catch (error) {
console.error('Error retrieving models:', error);
res.status(500).json({ success: false, message: 'Failed to retrieve models' });
}
});
CONFIG.EXPRESS.delete('/model/:name', async (req, res) => {
try {
const { name } = req.params;
const filePath = path.join(CONFIG.MODELS_DIRECTORY, name + '.json');
await fsp.unlink(filePath);
res.status(200).json({ success: true, message: 'Model deleted successfully' });
} catch (error) {
console.error('Error deleting model:', error);
res.status(500).json({ success: false, message: 'Failed to delete model' });
}
});
CONFIG.EXPRESS.get('/', (req, res) => {
return res.sendFile(path.join(__dirname, 'website', 'index.html'))
})
//==============================================INIT============================
let ON = true
if (ON) {
createDirectories(CONFIG);
CONFIG.WA.initialize()
CONFIG.EXPRESS.listen(CONFIG.PORT, () => {
console.log(`Server running at http://localhost:${CONFIG.PORT}`);
});
}