├── requirements.txt ├── web ├── assets │ └── img │ │ └── logo.gif └── util.js ├── nodejs └── project │ ├── website │ ├── linked.mp4 │ ├── index.html │ ├── style.css │ └── script.js │ ├── package.json │ └── app.js ├── pyproject.toml ├── .github └── workflows │ └── publish.yml ├── LICENSE ├── classes ├── WA_ImageSaver.py ├── NodeScriptRunner.py └── NodeInstaller.py ├── config.py ├── .gitignore ├── __init__.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/assets/img/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxcay/ComfyUI-WA/HEAD/web/assets/img/logo.gif -------------------------------------------------------------------------------- /nodejs/project/website/linked.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxcay/ComfyUI-WA/HEAD/nodejs/project/website/linked.mp4 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-wa" 3 | description = "Comfy UI in WhatsApp" 4 | version = "1.0.0" 5 | license = { file = "LICENSE" } 6 | dependencies = [] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/daxcay/ComfyUI-WA" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "daxcay" 14 | DisplayName = "ComfyUI-WA" 15 | Icon = "https://raw.githubusercontent.com/daxcay/ComfyUI-WA/main/web/assets/img/logo.gif" 16 | -------------------------------------------------------------------------------- /nodejs/project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comfy_ui_whatsapp", 3 | "version": "1.0.0", 4 | "description": "This web app lets you connect comfyui with whatsapp", 5 | "main": "app.js", 6 | "scripts": { 7 | "production": "node app.js --pd=D:\\1_StabilityMatrix\\Data\\Packages\\ComfyUI\\WhatsApp" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.19.2", 13 | "qrcode": "^1.5.4", 14 | "whatsapp-web.js": "^1.25.0" 15 | } 16 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v4 17 | - name: Publish Custom Node 18 | uses: Comfy-Org/publish-node-action@main 19 | with: 20 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 daxcay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nodejs/project/website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Comfy-WA 8 | 9 | 10 | 11 | 12 |
13 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
Drop WorkFlow files here (API Version)
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
NameDate CreatedActions
36 |
37 |
38 |
39 |

To log out, first stop ComfyUI and delete the user folder from the ComfyUI/WhatsApp directory. Then, restart ComfyUI, log out from the linked device in WhatsApp, and re-link the device.

40 |

Repository: GitHub

41 |

Discord Server: Discord

42 |
43 |
44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /classes/WA_ImageSaver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import json 4 | import numpy as np 5 | from PIL import Image 6 | from PIL.PngImagePlugin import PngInfo 7 | from comfy.cli_args import args 8 | 9 | def count_files_in_folder(folder_path): 10 | file_count = 0 11 | for _, _, files in os.walk(folder_path): 12 | file_count += len(files) 13 | return file_count 14 | 15 | def delete_files_in_folder(folder_path): 16 | for file in os.listdir(folder_path): 17 | file_path = os.path.join(folder_path, file) 18 | try: 19 | if os.path.isfile(file_path): 20 | os.unlink(file_path) 21 | except Exception as e: 22 | print(f"[COMFYUI_WA] --> Failed to delete file '{file_path}': {e}") 23 | 24 | class WA_ImageSaver: 25 | 26 | def __init__(self): 27 | self.compression = 4 28 | 29 | @classmethod 30 | def INPUT_TYPES(s): 31 | return { 32 | "required": { 33 | "Images": ("IMAGE",), 34 | "Path": ("STRING", {}), 35 | }, 36 | "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, 37 | } 38 | 39 | RETURN_TYPES = () 40 | FUNCTION = "Save" 41 | OUTPUT_NODE = True 42 | 43 | CATEGORY = "🟢 COMFYUI-WA 🟢" 44 | 45 | def Save(self, Images, Path, prompt=None, extra_pnginfo=None): 46 | 47 | results = [] 48 | 49 | try: 50 | 51 | if not os.path.exists(Path): 52 | os.makedirs(Path) 53 | 54 | # delete_files_in_folder(Path) 55 | lastIndex = count_files_in_folder(Path) 56 | index = 1 57 | 58 | for image in Images: 59 | image = image.cpu().numpy() 60 | image = (image * 255).astype(np.uint8) 61 | img = Image.fromarray(image) 62 | metadata = None 63 | if not args.disable_metadata: 64 | metadata = PngInfo() 65 | if prompt is not None: 66 | metadata.add_text("prompt", json.dumps(prompt)) 67 | if extra_pnginfo is not None: 68 | for x in extra_pnginfo: 69 | metadata.add_text(x, json.dumps(extra_pnginfo[x])) 70 | 71 | padding = str(lastIndex+index).zfill(4) 72 | file_name = f"{padding}.png" 73 | file_path = os.path.join(Path, file_name) 74 | img.save(file_path, pnginfo=metadata, compress_level=self.compression) 75 | index = index + 1 76 | results.append({ 77 | "filename": file_name, 78 | "subfolder": Path, 79 | "type": "output" 80 | }) 81 | 82 | except Exception as e: 83 | print(f"[COMFYUI_WA] --> Error saving image: {e}") 84 | 85 | return ({ "ui": { "images": results } }) 86 | 87 | 88 | N_CLASS_MAPPINGS = { 89 | "WA_ImageSaver": WA_ImageSaver, 90 | } 91 | 92 | N_DISPLAY_NAME_MAPPINGS = { 93 | "WA_ImageSaver": "WA_ImageSaver", 94 | } 95 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Daxton Caylor 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import os, json 22 | 23 | NODE_JS_ACTIVE = True 24 | NODE_JS_INSTALLER_URL = "https://nodejs.org/dist/v20.11.1/node-v20.11.1-x64.msi" 25 | NODE_JS_FOLDER = "nodejs" 26 | 27 | def edit_package_json(file_path, updates): 28 | """ 29 | Edits the given JSON file dynamically based on the updates provided. 30 | 31 | :param file_path: The path to the JSON file. 32 | :param updates: A dictionary with the updates to apply. 33 | :return: None 34 | """ 35 | if not os.path.exists(file_path): 36 | raise FileNotFoundError(f"[COMFYUI_WA] --> {file_path} does not exist") 37 | 38 | with open(file_path, 'r') as file: 39 | package_data = json.load(file) 40 | 41 | for key, value in updates.items(): 42 | if isinstance(value, dict) and key in package_data: 43 | package_data[key].update(value) 44 | else: 45 | package_data[key] = value 46 | 47 | with open(file_path, 'w') as file: 48 | json.dump(package_data, file, indent=4) 49 | 50 | print(f"[COMFYUI_WA] --> Updated {file_path} successfully.") 51 | 52 | 53 | def update_config(config_file, default_config): 54 | # If the config file doesn't exist, create it with the default values 55 | if not os.path.exists(config_file): 56 | with open(config_file, 'w') as f: 57 | json.dump(default_config, f, indent=4) 58 | print(f"Config file created with default settings.") 59 | else: 60 | # Load the existing config 61 | with open(config_file, 'r') as f: 62 | config_data = json.load(f) 63 | 64 | # Check for missing keys and update them 65 | updated = False 66 | for key, value in default_config.items(): 67 | if key not in config_data: 68 | config_data[key] = value 69 | updated = True 70 | 71 | # If updates are made, rewrite the config file 72 | if updated: 73 | with open(config_file, 'w') as f: 74 | json.dump(config_data, f, indent=4) 75 | print(f"Config file updated with missing keys.") 76 | else: 77 | print(f"No updates needed, config file is up to date.") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.pyc 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | *.wwebjs_cache 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | # Cython debug symbols 147 | cython_debug/ 148 | 149 | # PyCharm 150 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 151 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 152 | # and can be added to the global gitignore or merged into this file. For a more nuclear 153 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 154 | #.idea/ 155 | *.pyc 156 | 157 | .idea 158 | /node_modules 159 | /frontend 160 | -------------------------------------------------------------------------------- /classes/NodeScriptRunner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Daxton Caylor 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import shutil 22 | import subprocess 23 | import time 24 | import threading 25 | 26 | 27 | class NodeScriptRunner: 28 | def __init__(self): 29 | self.scripts = [] 30 | self.processes = [] 31 | self.should_run = True 32 | 33 | def check_for_node_js(self): 34 | return shutil.which('node') is not None 35 | 36 | def add(self, cwd, script): 37 | self.scripts.append((cwd, script)) 38 | 39 | def start_script(self, cwd, script): 40 | process = subprocess.Popen(script, cwd=cwd, stdout=None, stderr=None) 41 | self.processes.append((process, cwd, script)) 42 | print(f"[COMFYUI_WA] --> Project script '{script}' in '{cwd}' started.") 43 | return process 44 | 45 | def monitor_scripts(self): 46 | while self.should_run: 47 | for i, (process, cwd, script) in enumerate(self.processes): 48 | if process.poll() is not None: # Check if the process has terminated 49 | print(f"[COMFYUI_WA] --> Project script '{script}' in '{cwd}' terminated unexpectedly.") 50 | new_process = self.start_script(cwd, script) 51 | self.processes[i] = (new_process, cwd, script) 52 | time.sleep(1) # Check every second 53 | 54 | def run(self): 55 | try: 56 | if not self.check_for_node_js(): 57 | print("[COMFYUI_WA] --> Node.js is not installed or not found in the system path.") 58 | return 59 | 60 | for cwd, script in self.scripts: 61 | self.start_script(cwd, script) 62 | 63 | # monitor_thread = threading.Thread(target=self.monitor_scripts) 64 | # monitor_thread.start() 65 | except FileNotFoundError: 66 | print("[COMFYUI_WA] --> Node.js is not installed or not found in the system path.") 67 | except Exception as e: 68 | print(f'[COMFYUI_WA] --> NodeJS failed to start script. Error: {e}') 69 | 70 | def terminate_background_js(self): 71 | self.should_run = False # Stop monitoring 72 | for process, cwd, script in self.processes: 73 | if process: 74 | process.terminate() 75 | print(f"[COMFYUI_WA] --> Project script '{script}' in '{cwd}' terminated.") 76 | else: 77 | print("[COMFYUI_WA] --> No background JavaScript process is running.") 78 | 79 | def __del__(self): 80 | self.terminate_background_js() 81 | -------------------------------------------------------------------------------- /nodejs/project/website/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | } 7 | 8 | body { 9 | width: 100%; 10 | height: 100vh; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | background-color: #404040; 15 | } 16 | 17 | .container { 18 | width: 100%; 19 | height: 600px; 20 | background: #181818; 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | .menu { 26 | padding: 8px; 27 | display: flex; 28 | width: 100%; 29 | gap: 8px; 30 | border-bottom: 1px solid #404040; 31 | } 32 | 33 | .menu .item { 34 | border: none; 35 | width: 100%; 36 | padding: 12px; 37 | border-radius: 10px; 38 | cursor: pointer; 39 | transition: opacity 0.20s ease-in-out; 40 | opacity: 0.5; 41 | position: relative; 42 | background-color: #727272; 43 | font-weight: 500; 44 | } 45 | 46 | .menu .item.active { 47 | color: #fff; 48 | background-color: #40c351; 49 | opacity: 1; 50 | } 51 | 52 | .screen { 53 | width: 100%; 54 | height: 100%; 55 | color: #fff; 56 | font-weight: 500; 57 | } 58 | 59 | .screen div { 60 | display: none; 61 | transition: opacity 0.20s ease-in-out; 62 | width: 100%; 63 | height: 100%; 64 | display: flex; 65 | align-items: center; 66 | justify-content: center; 67 | flex-direction: column; 68 | gap: 18px; 69 | } 70 | 71 | .screen div[data-id="models"] { 72 | width: 100%; 73 | display: flex; 74 | align-items: center; 75 | justify-content: flex-start; 76 | } 77 | 78 | .screen div[data-id="help"] { 79 | padding: 18px; 80 | text-align: center; 81 | } 82 | 83 | .screen div.active { 84 | opacity: 1; 85 | } 86 | 87 | .screen .qrcode { 88 | width: 100%; 89 | height: 100%; 90 | display: flex; 91 | flex-direction: column; 92 | } 93 | 94 | .qrcode img { 95 | width: 276px; 96 | height: 276px; 97 | } 98 | 99 | .qrcode video { 100 | width: 50%; 101 | height: 50%; 102 | } 103 | 104 | .loader { 105 | width: 48px; 106 | height: 48px; 107 | border-radius: 50%; 108 | display: inline-block; 109 | border-top: 3px solid #FFF; 110 | border-right: 3px solid transparent; 111 | box-sizing: border-box; 112 | animation: rotation 1s linear infinite; 113 | } 114 | 115 | a { 116 | color: #40c351; 117 | } 118 | 119 | #drop-zone { 120 | width: 100%; 121 | height: 200px; 122 | border: 2px dashed #ccc; 123 | border-radius: 8px; 124 | display: flex; 125 | align-items: center; 126 | justify-content: center; 127 | text-align: center; 128 | color: #FFF; 129 | font-size: 16px; 130 | transition: background-color 0.3s; 131 | } 132 | 133 | #drop-zone.hover { 134 | background-color: #f0f0f0; 135 | } 136 | 137 | @keyframes rotation { 138 | 0% { 139 | transform: rotate(0deg); 140 | } 141 | 142 | 100% { 143 | transform: rotate(360deg); 144 | } 145 | } 146 | 147 | table { 148 | width: 100%; 149 | border-collapse: collapse; 150 | } 151 | 152 | th, td { 153 | padding: 10px; 154 | text-align: center; 155 | border-bottom: 1px solid #727272; 156 | } 157 | 158 | th { 159 | background-color: #404040; 160 | } 161 | 162 | button { 163 | margin: 0; 164 | padding: 5px 10px; 165 | border: none; 166 | border-radius: 4px; 167 | color: white; 168 | cursor: pointer; 169 | } 170 | 171 | .delete-btn { 172 | background-color: red; 173 | } 174 | 175 | .table-zone { 176 | max-height: calc(600px - 58px - 18px - 200px); 177 | width: 100%; 178 | overflow-y: auto; 179 | display: block !important; 180 | } -------------------------------------------------------------------------------- /classes/NodeInstaller.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Daxton Caylor 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import os 22 | import json 23 | import requests 24 | import subprocess 25 | import tempfile 26 | from tqdm import tqdm 27 | import shutil 28 | 29 | 30 | class NodeInstaller: 31 | 32 | def __init__(self, installer_url): 33 | self.installer_url = installer_url 34 | self.installer_path = None 35 | 36 | def check_for_node_js(self): 37 | return (shutil.which('node') is not None) 38 | 39 | def download_nodejs(self): 40 | temp_dir = tempfile.gettempdir() 41 | self.installer_path = os.path.join(temp_dir, "nodejs_installer.msi") 42 | response = requests.get(self.installer_url, stream=True) 43 | total_size = int(response.headers.get('content-length', 0)) 44 | block_size = 1024 45 | progress_bar = tqdm(total=total_size, unit='iB', unit_scale=True) 46 | 47 | with open(self.installer_path, 'wb') as file: 48 | for data in response.iter_content(block_size): 49 | progress_bar.update(len(data)) 50 | file.write(data) 51 | 52 | progress_bar.close() 53 | if total_size != 0 and progress_bar.n != total_size: 54 | print("[COMFYUI_WA] --> An error occurred during the download.") 55 | else: 56 | print("[COMFYUI_WA] --> Node.js installer downloaded successfully.") 57 | 58 | def install_nodejs(self): 59 | if self.installer_path is None: 60 | print("[COMFYUI_WA] --> Node.js installer has not been downloaded yet.") 61 | return 62 | process = subprocess.Popen([self.installer_path], shell=True) 63 | process.wait() 64 | 65 | def install_all_packages(self, package_list): 66 | for package_name in package_list: 67 | self.install_npm_package(package_name) 68 | 69 | def install_npm_package(self, package_name): 70 | install_command = f"npm install {package_name}" 71 | 72 | try: 73 | print("") 74 | process = subprocess.Popen( 75 | install_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) 76 | for line in tqdm(iter(process.stdout.readline, b''), desc=f"Installing {package_name}", unit='B', unit_scale=True, leave=False): 77 | pass 78 | process.stdout.close() 79 | process.wait() 80 | print(f"\n[COMFYUI_WA] --> npm package '{package_name}' installed successfully.") 81 | except subprocess.CalledProcessError as e: 82 | print(f"[COMFYUI_WA] --> An error occurred: {e}") 83 | 84 | def get_dependencies_and_production_scripts(self, directory_path): 85 | folders_with_info = {} 86 | try: 87 | entries = os.listdir(directory_path) 88 | folders = [entry for entry in entries if os.path.isdir( 89 | os.path.join(directory_path, entry))] 90 | for folder in folders: 91 | folder_path = os.path.join(directory_path, folder) 92 | package_json_path = os.path.join(folder_path, 'package.json') 93 | if os.path.exists(package_json_path): 94 | with open(package_json_path, 'r') as json_file: 95 | data = json.load(json_file) 96 | dependencies = data.get('dependencies', {}) 97 | production_script = data.get('scripts', {}).get('production', None) 98 | folders_with_info[folder] = {'dependencies': dependencies, 'production': production_script} 99 | else: 100 | folders_with_info[folder] = {'dependencies': None, 'production': None} 101 | return folders_with_info 102 | except FileNotFoundError: 103 | return f"[COMFYUI_WA] --> The directory {directory_path} does not exist." 104 | except PermissionError: 105 | return f"[COMFYUI_WA] --> Permission denied to access the directory {directory_path}." 106 | except Exception as e: 107 | return f"[COMFYUI_WA] --> An error occurred: {e}" 108 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Daxton Caylor 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | """ 22 | @author: Daxton Caylor 23 | @title: ComfyUI-WA 24 | @nickname: ComfyUI-WA 25 | @description: This node enables someone to run comfyui in whatsapp. 26 | """ 27 | 28 | import os, platform, json 29 | from .classes.NodeInstaller import NodeInstaller 30 | from .classes.NodeScriptRunner import NodeScriptRunner 31 | from .classes.WA_ImageSaver import N_CLASS_MAPPINGS as WA_ImageSaverMappins, N_DISPLAY_NAME_MAPPINGS as WA_ImageSaverNameMappings 32 | from .config import * 33 | 34 | if NODE_JS_ACTIVE: 35 | 36 | print("=============================================================================================================") 37 | 38 | canRunScripts = 0 39 | nodeInstaller = NodeInstaller(NODE_JS_INSTALLER_URL) 40 | nodeScriptRunner = NodeScriptRunner() 41 | 42 | DATA_FOLDER = './WhatsApp' 43 | CONFIG_FILE = os.path.join(DATA_FOLDER, 'whatsapp.json') 44 | 45 | NODE_JS_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), NODE_JS_FOLDER) 46 | 47 | PD = os.path.abspath(DATA_FOLDER) 48 | 49 | if not os.path.exists(DATA_FOLDER): 50 | os.makedirs(DATA_FOLDER) 51 | 52 | config_data = { 53 | "phone_code": "xx", 54 | "phone": "xxxxxxxxxx", 55 | "mode": "1", 56 | "chrome": "", 57 | "comfy_url": "http://127.0.0.1:8188" 58 | } 59 | 60 | if not os.path.exists(CONFIG_FILE): 61 | with open(CONFIG_FILE, 'w') as f: 62 | json.dump(config_data, f, indent=4) 63 | else: 64 | update_config(CONFIG_FILE, config_data) 65 | 66 | updates = { 67 | "scripts" : { 68 | "production": f"node app.js --pd={PD}" 69 | } 70 | } 71 | 72 | edit_package_json(os.path.join(NODE_JS_FOLDER,'project','package.json'),updates) 73 | 74 | if platform.system() == "Windows": 75 | if nodeInstaller.check_for_node_js() is not True: 76 | print("[COMFYUI_WA] --> DOWNLOADING NODEJS") 77 | nodeInstaller.download_nodejs() 78 | print("[COMFYUI_WA] --> INSTALLING NODEJS") 79 | nodeInstaller.install_nodejs() 80 | else: 81 | print("[COMFYUI_WA] --> NODEJS WAS FOUND IN YOUR OS") 82 | 83 | canRunScripts = 1 84 | else: 85 | if nodeInstaller.check_for_node_js() is not True: 86 | print("[COMFYUI_WA] --> NODEJS WAS NOT FOUND IN YOUR OS PLEASE INSTALL NODEJS TO RUN THIS NODE CORRECTLY") 87 | else: 88 | canRunScripts = 1 89 | 90 | if canRunScripts: 91 | projects = nodeInstaller.get_dependencies_and_production_scripts(NODE_JS_FOLDER) 92 | 93 | for project_name, project_info in projects.items(): 94 | 95 | dependencies = project_info['dependencies'] 96 | packages = [] 97 | if dependencies: 98 | print(f"[COMFYUI_WA] --> Dependencies for project '{project_name}':") 99 | for dependency, version in dependencies.items(): 100 | version = version.replace('^', '@') 101 | # nodeInstaller.install_npm_package(f"{dependency}@{version}") 102 | packages.append(dependency) 103 | print(f"{dependency}: {version}") 104 | else: 105 | print(f"[COMFYUI_WA] --> No dependencies found for project '{project_name}'") 106 | 107 | nodeInstaller.install_all_packages(packages) 108 | 109 | production = project_info['production'] 110 | if production: 111 | print(f"[COMFYUI_WA] --> Script for project '{project_name}':") 112 | nodeScriptRunner.add(os.path.join(NODE_JS_FOLDER, project_name),production.split()) 113 | else: 114 | print(f"[COMFYUI_WA] --> No scripts found for project '{project_name}'") 115 | 116 | nodeScriptRunner.run() 117 | 118 | else: 119 | print("[COMFYUI_WA] --> CONTACT DEVELOPER FOR ASSISTANCE") 120 | 121 | NODE_CLASS_MAPPINGS = {} 122 | NODE_DISPLAY_NAME_MAPPINGS = {} 123 | 124 | NODE_CLASS_MAPPINGS.update(WA_ImageSaverMappins) 125 | NODE_DISPLAY_NAME_MAPPINGS.update(WA_ImageSaverNameMappings) 126 | 127 | WEB_DIRECTORY = "./web" 128 | -------------------------------------------------------------------------------- /web/util.js: -------------------------------------------------------------------------------- 1 | 2 | let app = window.comfyAPI.app.app; 3 | 4 | class ComfyWA { 5 | 6 | constructor() { 7 | this.icon = null; 8 | this.iframe = null; 9 | this.iframe_visible = false; 10 | this.url = 'http://127.0.0.1:4000'; 11 | } 12 | 13 | getWhatsAppIcon() { 14 | let svg = '' 15 | svg += '' 16 | svg += '' 17 | svg += '' 18 | return svg 19 | } 20 | 21 | addWhatsAppIcon() { 22 | this.icon = document.createElement('div'); 23 | this.icon.innerHTML = this.getWhatsAppIcon(); 24 | this.icon.style.position = 'fixed'; 25 | this.icon.style.bottom = '96px'; 26 | this.icon.style.left = '20px'; 27 | this.icon.style.width = '48px'; 28 | this.icon.style.height = '48px'; 29 | this.icon.style.cursor = 'pointer'; 30 | this.icon.style.zIndex = '1000'; 31 | this.icon.addEventListener('click', () => this.toggleIframe()); 32 | document.body.appendChild(this.icon); 33 | } 34 | 35 | addIframe() { 36 | if (this.url) { 37 | this.iframe = document.createElement('iframe'); 38 | this.iframe.style.width = '480px'; 39 | this.iframe.style.height = '600px'; 40 | this.iframe.style.position = 'fixed'; 41 | this.iframe.style.bottom = '96px'; 42 | this.iframe.style.left = '96px'; 43 | this.iframe.style.border = 'none'; 44 | this.iframe.style.display = 'none'; 45 | this.iframe.style.borderRadius = "18px"; 46 | this.iframe.style.zIndex = 100000; 47 | this.iframe.style.opacity = 0; 48 | this.iframe.style.transition = 'opacity 0.25s ease, transform 0.25s ease'; 49 | document.body.appendChild(this.iframe); 50 | } 51 | } 52 | 53 | showIframe() { 54 | if (this.iframe) { 55 | this.iframe.style.display = 'block'; 56 | this.iframe.src = this.url 57 | setTimeout(() => { 58 | this.iframe.style.opacity = 1; 59 | this.iframe.style.transform = "translateY(-10px)" 60 | }, 10); 61 | this.iframe_visible = true; 62 | } 63 | } 64 | 65 | hideIframe() { 66 | if (this.iframe) { 67 | this.iframe.style.opacity = 0; 68 | this.iframe.style.transform = "translateY(0px)" 69 | setTimeout(() => { 70 | this.iframe.style.display = 'none'; 71 | this.iframe.src = "" 72 | }, 500); 73 | this.iframe_visible = false; 74 | } 75 | } 76 | 77 | toggleIframe() { 78 | if (this.iframe_visible) { 79 | this.hideIframe(); 80 | } else { 81 | this.showIframe(); 82 | } 83 | } 84 | 85 | } 86 | 87 | let ComfyWAExtension = { 88 | 89 | name: "Comfy-WA", 90 | async setup() { 91 | 92 | let comfyWA = new ComfyWA() 93 | comfyWA.addWhatsAppIcon() 94 | comfyWA.addIframe() 95 | 96 | }, 97 | 98 | } 99 | 100 | app.registerExtension(ComfyWAExtension); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 7 | 8 | 9 |

Comfy UI in WhatsApp.

10 | 11 |

12 | Report Bug 13 | · 14 | Request Feature 15 |

16 | 17 |

18 | 19 | 20 |

21 | 22 |
23 | 24 |
25 | 26 | # About The Project 27 | 28 | This project enables the use of ComfyUI Workflows in WhatsApp. 29 | 30 | Video Tutorial: **Coming Soon!** 31 | 32 |
33 | 34 | > [!IMPORTANT] 35 | > To log out, first stop ComfyUI and delete the `user` folder from the ComfyUI/WhatsApp directory. Then, restart ComfyUI, and log out from the linked device in WhatsApp. 36 | 37 | > [!IMPORTANT] 38 | > Currently, only Windows is supported. 39 | 40 |
41 | 42 | # Installation 43 | 44 | - ### Installing Using `comfy-cli` 45 | - `comfy node registry-install ComfyUI-WA` 46 | - https://registry.comfy.org/publishers/daxcay/nodes/comfyui-wa 47 | 48 | - ### Manual Method 49 | - Go to your `ComfyUI\custom_nodes` and Run CMD. 50 | - Copy and paste this command: `git clone https://github.com/daxcay/ComfyUI-WA.git` 51 | 52 | - ### Automatic Method with [Comfy Manager](https://github.com/ltdrdata/ComfyUI-Manager) 53 | - Inside ComfyUI > Click the Manager Button on the side. 54 | - Click `Custom Nodes Manager` and search for `ComfyUI-WA`, then install this node. 55 | 56 |
57 | 58 | > [!CAUTION] 59 | > Update to nodejs version v20.17.0 if you already have it. https://nodejs.org/dist/v20.17.0/node-v20.17.0-x64.msi 60 | 61 | 62 | >[!IMPORTANT] 63 | > #### **Restart ComfyUI and Stop ComfyUI before proceeding to next step** 64 | 65 |
66 | 67 | # NodeJS Installation 68 | - In case you have error in automated nodejs installation. 69 | - https://nodejs.org/en/download/prebuilt-installer 70 | - Use above link to manually install nodejs. 71 | 72 |
73 | 74 | # Setup 75 | 76 | ### Location of WhatsApp folder 77 | 78 | #### ComfyUI Folder 79 | - `Drive:/ComfyUI_windows_portable/WhatsApp` 80 | 81 | #### Stable Matrix 82 | - **Full Version**: `Drive:/StabilityMatrix/Packages/ComfyUI/WhatsApp` 83 | - **Portable Version**: `Drive:/StabilityMatrix/Data/Packages/ComfyUI/WhatsApp` 84 | 85 | From `ComfyUI/WhatsApp` folder open `whatsapp.json` 86 | 87 | ![image](https://github.com/user-attachments/assets/bf244483-690a-4cb3-9e1b-5016cc78c13e) 88 | 89 | ![image](https://github.com/user-attachments/assets/b7648e31-7be8-450c-a93f-f04072c694d2) 90 | 91 | > [!IMPORTANT] 92 | > Fill `phone_code` and `phone` and save it. this will your admin account. 93 | 94 | `phone_code` is tobe entered without `+` and `-` 95 | 96 | >[!IMPORTANT] 97 | > #### **Start ComfyUI before proceeding to next step** 98 | 99 |
100 | 101 | ## Device Link 102 | 103 | >[!IMPORTANT] 104 | > #### **Login from the same number you defined in `whatsapp.json` above** 105 | 106 | ![download](https://github.com/user-attachments/assets/2a7b080c-8e31-4bc8-b571-f8604dcc202b) 107 | 108 |
109 | 110 | ## Enable Dev Mode 111 | 112 | >[!IMPORTANT] 113 | > #### **Enable dev mode and save workflow in `api` format to make it compatible.** 114 | 115 | ![download (1)](https://github.com/user-attachments/assets/15d6fda5-86af-4514-9884-32e7bb4cde84) 116 | 117 |
118 | 119 | ## Uploading WorkFlow 120 | 121 | To upload a workflow to be used in in whatsapp use the `workflow` button in whatsapp dashboard. 122 | 123 | >[!IMPORTANT] 124 | > #### **Attach `WA-ImageSaver` Node before saving the workflow in api format** 125 | 126 | ![image](https://github.com/user-attachments/assets/42a54f56-8dcc-4831-9d20-1c24ede24b46) 127 | 128 | Now upload it in workflow section 129 | 130 | ![image](https://github.com/user-attachments/assets/10d7a0e6-5279-4d4e-a580-2b1235229a78) 131 | 132 |
133 | 134 | # WhatsApp Commands 135 | 136 | Writing **/c** in whatsapp will also provide the list of all commands: 137 | 138 | ![image](https://github.com/user-attachments/assets/d6ffb055-6285-4648-8396-9aa4bd48091d) 139 | 140 | - Write **/wfs** to get a numbered list of uploaded workflows. 141 | 142 | ![image](https://github.com/user-attachments/assets/f4bafaf7-35e9-4a52-a7a0-7f81544870d9) 143 | 144 | - Write **/wf id** to select the workflow. 145 | 146 | ![image](https://github.com/user-attachments/assets/73fdd686-02d0-4eba-a871-0c8dcc6b403c) 147 | 148 | - Write **/wns** to get numbered list of selected workflow nodes. 149 | 150 | ![image](https://github.com/user-attachments/assets/cebc3fc5-16c9-4257-ad05-01689e4a4861) 151 | 152 | - Write **/wn id** to get numbered list of inputs available. 153 | 154 | ![image](https://github.com/user-attachments/assets/37201990-4e30-4485-a176-730f7e400df1) 155 | 156 | - Write **/s node_id input_id value** to set value for input selected. 157 | 158 | ![image](https://github.com/user-attachments/assets/c5efac5f-fbfc-4b7a-aa2f-835d4a207c99) 159 | 160 | - Write **/sce** enable auto ksampler seed change. 161 | 162 | ![image](https://github.com/user-attachments/assets/8a2975e4-9f5a-4e7b-81be-ac5cf90dd07a) 163 | 164 | - Write **/scd** disable auto ksampler seed change. 165 | 166 | ![image](https://github.com/user-attachments/assets/965b293b-217a-4f52-90ee-7dcb4740f48d) 167 | 168 | - Write **/r** to reset all to default settings. 169 | 170 | ![image](https://github.com/user-attachments/assets/0488b0c2-b42c-487c-b5ca-5330fcfed0d0) 171 | 172 | - Write **/q** to queue. 173 | 174 | ![image](https://github.com/user-attachments/assets/d740d8c9-8e8c-4d5b-b8be-5251b6f2d3e7) 175 | 176 | - Write **/i** to interrupt queue. 177 | 178 | ![image](https://github.com/user-attachments/assets/b6f25a49-1066-4c33-955a-90c652ff3aee) 179 | 180 | - Write **/m number** to set bot usage mode. **1**: Single User, **2**: Multi User.' 181 | 182 | ![image](https://github.com/user-attachments/assets/09c8a252-2fc0-41be-84af-1fac38e74b36) 183 | 184 | **Multi User** mode allows some other person to use ComfyUI by directly messaging you!. 185 | 186 |
187 | 188 | # Contact 189 | 190 | ### Daxton Caylor - ComfyUI Node Developer 191 | 192 | - ### Contact 193 | - **Email** - daxtoncaylor+Github@gmail.com 194 | - **Discord Server**: https://discord.gg/UyGkJycvyW 195 | 196 | - ### Support 197 | - **Patreon**: https://patreon.com/daxtoncaylor 198 | - **Buy me a coffee**: https://buymeacoffee.com/daxtoncaylor 199 | 200 |
201 | 202 | # Disclaimer 203 | 204 | This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. The official WhatsApp website can be found at whatsapp.com. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners. Also it is not guaranteed you will not be blocked by using this method. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe. 205 | 206 | I have used `NodeJS` and `Python` combined to make this project the library, I am using the following library in nodejs to enable whatsapp functionality. 207 | 208 | https://github.com/pedroslopez/whatsapp-web.js 209 | -------------------------------------------------------------------------------- /nodejs/project/website/script.js: -------------------------------------------------------------------------------- 1 | class ComfyWA { 2 | constructor() { 3 | this.buttons = document.querySelectorAll(".menu .item"); 4 | this.screens = document.querySelectorAll(".screen div"); 5 | this.qrCodeContainer = document.querySelector('.qrcode'); 6 | this.qrCodeContainerLoader = document.querySelector('.qrloading'); 7 | this.imgElement = this.createElement('img', { display: "none" }); 8 | this.textElement = this.createElement('span', { display: "none" }); 9 | this.videoElement = this.createElement('video', { display: "none" }); 10 | 11 | this.qrCodeContainer.append(this.imgElement, this.textElement, this.videoElement); 12 | this.qrCodeContainer.style.display = "none"; 13 | 14 | this.connection = { watch: 0, loadTime: 0 }; 15 | this.init(); 16 | 17 | this.handleFiles = this.handleFiles.bind(this); 18 | this.deleteModel = this.deleteModel.bind(this); 19 | this.submitJsonToServer = this.submitJsonToServer.bind(this); 20 | 21 | const dropZone = document.getElementById('drop-zone'); 22 | 23 | dropZone.addEventListener('dragover', (event) => { 24 | event.preventDefault(); 25 | event.stopPropagation(); 26 | dropZone.classList.add('hover'); 27 | }); 28 | 29 | dropZone.addEventListener('dragleave', (event) => { 30 | event.preventDefault(); 31 | event.stopPropagation(); 32 | dropZone.classList.remove('hover'); 33 | }); 34 | 35 | dropZone.addEventListener('drop', (event) => { 36 | event.preventDefault(); 37 | event.stopPropagation(); 38 | dropZone.classList.remove('hover'); 39 | 40 | const files = event.dataTransfer.files; 41 | this.handleFiles(files); 42 | }); 43 | } 44 | 45 | createElement(tag, styles = {}) { 46 | const element = document.createElement(tag); 47 | Object.assign(element.style, styles); 48 | return element; 49 | } 50 | 51 | async handleFiles(files) { 52 | files = Array.from(files); 53 | console.log(files) 54 | for (const file of files) { 55 | if (file.type === 'application/json') { 56 | const reader = new FileReader(); 57 | reader.onload = async (e) => { 58 | try { 59 | const json = JSON.parse(e.target.result); 60 | // console.log('JSON file content:', json); 61 | const cleanedName = file.name.split('.')[0].replace(/[^a-zA-Z0-9.]/g, '').replace(/\s+/g, '_').substr(0,16); 62 | await this.submitJsonToServer(json, cleanedName+".json"); 63 | this.fetchModels() 64 | } catch (err) { 65 | console.error('Invalid JSON file:', err); 66 | } 67 | }; 68 | reader.readAsText(file); 69 | } else { 70 | alert('Please drop a JSON file.'); 71 | } 72 | } 73 | } 74 | 75 | formatDateToDDMMYYYY(isoDateString) { 76 | const date = new Date(isoDateString); 77 | 78 | const day = String(date.getDate()).padStart(2, '0'); 79 | const month = String(date.getMonth() + 1).padStart(2, '0'); 80 | const year = date.getFullYear(); 81 | 82 | return `${day}-${month}-${year}`; 83 | } 84 | 85 | async fetchModels() { 86 | try { 87 | const response = await fetch('/models'); 88 | if (!response.ok) throw new Error('Failed to fetch models'); 89 | const models = await response.json(); 90 | const tableBody = document.querySelector('#models-table tbody'); 91 | tableBody.innerHTML = ''; 92 | models.forEach(model => { 93 | const row = document.createElement('tr'); 94 | row.innerHTML = ` 95 | ${model.name} 96 | ${this.formatDateToDDMMYYYY(model.dateCreated)} 97 | 98 | 99 | 100 | `; 101 | tableBody.appendChild(row); 102 | }); 103 | this.addDeleteEventListeners(); 104 | } catch (error) { 105 | console.error('Error fetching models:', error); 106 | } 107 | } 108 | 109 | addDeleteEventListeners() { 110 | const deleteButtons = document.querySelectorAll('.delete-btn'); 111 | deleteButtons.forEach(button => { 112 | button.addEventListener('click', () => { 113 | const name = button.getAttribute('data-name'); 114 | this.deleteModel(name); 115 | }); 116 | }); 117 | } 118 | 119 | async deleteModel(name) { 120 | try { 121 | const response = await fetch(`/model/${name}`, { 122 | method: 'DELETE' 123 | }); 124 | if (!response.ok) throw new Error('Failed to delete model'); 125 | this.fetchModels(); 126 | } catch (error) { 127 | console.error('Error deleting model:', error); 128 | alert('Failed to delete model'); 129 | } 130 | } 131 | 132 | async submitJsonToServer(jsonData, name) { 133 | try { 134 | const response = await fetch('/model', { 135 | method: 'POST', 136 | headers: { 137 | 'Content-Type': 'application/json' 138 | }, 139 | body: JSON.stringify({ data:jsonData, name }) 140 | }); 141 | 142 | if (!response.ok) { 143 | throw new Error('Network response was not ok'); 144 | } 145 | 146 | const result = await response.json(); 147 | console.log('Server response:', result); 148 | } catch (error) { 149 | console.error('Error submitting JSON to server:', error); 150 | } 151 | } 152 | 153 | init() { 154 | this.buttons.forEach(button => { 155 | button.addEventListener("click", () => this.activateScreen(button.id)); 156 | }); 157 | this.activateScreen("connection"); 158 | } 159 | 160 | async updateQrCode() { 161 | try { 162 | const response = await fetch('/qr'); 163 | if (response.ok) { 164 | const blob = await response.blob(); 165 | this.imgElement.src = URL.createObjectURL(blob); 166 | this.imgElement.style.display = "block"; 167 | } else { 168 | throw new Error("QR code image not found"); 169 | } 170 | } catch (error) { 171 | this.imgElement.style.display = "none"; 172 | } 173 | } 174 | 175 | async watchAction(id) { 176 | if (id !== 'connection') return; 177 | 178 | const status = await fetch("ready").then(res => res.json()); 179 | 180 | if (!status.ready) { 181 | this.showQrCode(); 182 | 183 | if (this.connection.watch > 0) { 184 | this.connection.loadTime--; 185 | setTimeout(() => this.watchAction(id), 1000); 186 | } 187 | 188 | } else { 189 | this.showVideo(); 190 | } 191 | } 192 | 193 | async showQrCode() { 194 | if (this.connection.loadTime < 0) { 195 | this.qrCodeContainerLoader.style.display = "none"; 196 | this.qrCodeContainer.style.display = "flex"; 197 | 198 | this.imgElement.style.display = "block"; 199 | this.textElement.textContent = "Scan From WhatsApp"; 200 | this.textElement.style.display = "block"; 201 | 202 | this.videoElement.style.display = "none"; 203 | 204 | this.updateQrCode(); 205 | } 206 | } 207 | 208 | async showVideo() { 209 | this.qrCodeContainerLoader.style.display = "none"; 210 | this.qrCodeContainer.style.display = "flex"; 211 | 212 | this.textElement.style.display = "none"; 213 | this.imgElement.style.display = "none"; 214 | 215 | this.textElement.textContent = "WhatsApp is Connected!"; 216 | this.textElement.style.display = "block"; 217 | 218 | this.videoElement.src = "./linked.mp4"; 219 | this.videoElement.autoplay = true; 220 | this.videoElement.muted = true; 221 | this.videoElement.style.display = "block"; 222 | } 223 | 224 | async screenFunctions(id) { 225 | this.videoElement.src = ""; 226 | this.connection.watch = 0; 227 | this.textElement.style.display = "none" 228 | 229 | if (id === 'connection') { 230 | const ping = await fetch("ping").then(res => res.json()); 231 | 232 | if (ping) { 233 | this.connection.watch = 1; 234 | this.connection.loadTime = 10; 235 | this.watchAction(id); 236 | } 237 | } else if (id == 'models') { 238 | this.fetchModels() 239 | } 240 | } 241 | 242 | activateScreen(id) { 243 | this.buttons.forEach(button => button.classList.remove("active")); 244 | this.screens.forEach(screen => { 245 | if (screen.dataset.screen == 1) { 246 | screen.classList.remove("active"); 247 | setTimeout(() => { 248 | if (screen.dataset.id !== id) { 249 | screen.style.display = "none"; 250 | } else { 251 | const activeButton = document.getElementById(id); 252 | activeButton.classList.add("active"); 253 | const activeScreen = document.querySelector(`.screen div[data-id="${id}"]`); 254 | activeScreen.style.display = "flex"; 255 | setTimeout(() => activeScreen.classList.add("active"), 20); 256 | } 257 | }, 10); 258 | } 259 | }); 260 | this.screenFunctions(id); 261 | } 262 | } 263 | 264 | document.addEventListener("DOMContentLoaded", () => { 265 | new ComfyWA(); 266 | }); 267 | -------------------------------------------------------------------------------- /nodejs/project/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const args = process.argv.slice(2); 4 | const fs = require('fs'); 5 | const fsp = require('fs/promises') 6 | const qrcode = require('qrcode'); 7 | const { Client, LocalAuth, MessageMedia } = require('whatsapp-web.js'); 8 | 9 | let CONFIG = { 10 | PORT: 4000, 11 | BROWSER_DIRECTORY: path.join(__dirname, 'user'), 12 | MODELS_DIRECTORY: path.join(__dirname, 'models'), 13 | TEMP_DIRECTORY: path.join(__dirname, 'temp'), 14 | OUTPUT_DIRECTORY: path.join(__dirname, 'output'), 15 | WEB_DIRECTORY: path.join(__dirname, 'website'), 16 | API_URL: 'http://127.0.0.1:8188', 17 | CHROME: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", 18 | MODE: null, 19 | ADMIN: null, 20 | EXPRESS: express(), 21 | WA: null, 22 | QRCODE: null, 23 | READY: false, 24 | KSAMPLER_SEED_CHANGE: 1, 25 | MODELS: {}, 26 | PROMPT: { 27 | MODEL: {}, 28 | NODES: {} 29 | }, 30 | WATCHER: {} 31 | } 32 | 33 | const deleteContentsInsideDirectory = async (dirPath) => { 34 | try { 35 | const files = await fsp.readdir(dirPath); 36 | await Promise.all(files.map(async (file) => { 37 | const filePath = path.join(dirPath, file); 38 | const stats = await fsp.stat(filePath); 39 | if (stats.isDirectory()) { 40 | await deleteContentsInsideDirectory(filePath); 41 | await fsp.rmdir(filePath); 42 | } else { 43 | await fsp.unlink(filePath); 44 | } 45 | })); 46 | 47 | console.log(`Successfully deleted contents inside ${dirPath}`); 48 | } catch (error) { 49 | console.error(`Error deleting contents inside ${dirPath}:`, error); 50 | throw error; 51 | } 52 | }; 53 | 54 | function readJsonFile(filePath) { 55 | try { 56 | const data = fs.readFileSync(filePath, 'utf8'); 57 | const jsonData = JSON.parse(data); 58 | return jsonData; 59 | } catch (err) { 60 | console.error(`Error reading or parsing file: ${err}`); 61 | return null; 62 | } 63 | } 64 | 65 | function addDirectories() { 66 | 67 | CONFIG.BROWSER_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'user') 68 | CONFIG.MODELS_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'workflows') 69 | CONFIG.TEMP_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'temp') 70 | CONFIG.OUTPUT_DIRECTORY = path.join(CONFIG.MAIN_DIRECTORY, 'output') 71 | 72 | WA_CONFIG = readJsonFile(path.join(CONFIG.MAIN_DIRECTORY, 'whatsapp.json')) 73 | 74 | CONFIG.MODE = parseInt(WA_CONFIG.mode); 75 | CONFIG.ADMIN = `${WA_CONFIG.phone_code}${WA_CONFIG.phone}@c.us` 76 | 77 | CONFIG.CHROME = WA_CONFIG.chrome 78 | CONFIG.API_URL = WA_CONFIG.comfy_url; 79 | 80 | } 81 | 82 | args.forEach(arg => { 83 | const [key, value] = arg.split('='); 84 | if (key === '--mode') { 85 | CONFIG.MODE = parseInt(value); 86 | } else if (key === '--admin') { 87 | CONFIG.ADMIN = `${value}@c.us`; 88 | } else if (key === '--api') { 89 | CONFIG.API_URL = value 90 | } else if (key === '--pd') { 91 | CONFIG.MAIN_DIRECTORY = value 92 | addDirectories() 93 | } 94 | }); 95 | 96 | function createDirectories(config) { 97 | for (let key in config) { 98 | if (key.endsWith('_DIRECTORY')) { 99 | const dir = config[key]; 100 | if (!fs.existsSync(dir)) { 101 | fs.mkdirSync(dir, { recursive: true }); 102 | console.log(`Directory created: ${dir}`); 103 | } else { 104 | console.log(`Directory already exists: ${dir}`); 105 | } 106 | } 107 | } 108 | } 109 | 110 | function botMediaMessage(to, message, media) { 111 | CONFIG.WA.sendMessage(to, "🤖\n\n" + message, { media }) 112 | } 113 | 114 | function botTextMessage(to, message) { 115 | CONFIG.WA.sendMessage(to, "🤖\n\n" + message) 116 | } 117 | 118 | function botTextMessagReply(message_object, message) { 119 | message_object.reply("🤖\n\n" + message) 120 | } 121 | 122 | function isAdmin(user) { 123 | return CONFIG.ADMIN == user 124 | } 125 | 126 | function allCommands() { 127 | let rules = '' 128 | rules += 'Write */wfs* to get a numbered list of uploaded workflows.\n\n' 129 | rules += 'Write */wf id* to select the workflow.\n\n' 130 | rules += 'Write */wns* to get numbered list of selected workflow nodes.\n\n' 131 | rules += 'Write */wn id* to get numbered list of inputs available.\n\n' 132 | rules += 'Write */s node_id input_id value* to set value for input selected.\n\n' 133 | rules += 'Write */sce* enable auto ksampler seed change.\n\n' 134 | rules += 'Write */scd* disable auto ksampler seed change.\n\n' 135 | rules += 'Write */r* to reset all to default settings.\n\n' 136 | rules += 'Write */q* to queue.\n\n' 137 | rules += 'Write */i* to interrupt queue.\n\n' 138 | rules += 'Write */m number* to set bot usage mode. *1*: Single User, *2*: Multi User.\n\n' 139 | return rules 140 | } 141 | 142 | function readModels(folderPath) { 143 | return fs.readdirSync(folderPath); 144 | } 145 | 146 | function formatModelObject(models) { 147 | return Object.entries(models).map(([key, value]) => `*${key}* | ${value}`).join("\n"); 148 | } 149 | 150 | function containsIndex(arr, index) { 151 | return index >= 0 && index < arr.length && arr[index] !== undefined; 152 | } 153 | 154 | function containsValue(arr, value) { 155 | return arr.includes(value); 156 | } 157 | 158 | function containsValueAtIndex(arr, index, value) { 159 | return index >= 0 && index < arr.length && arr[index] === value; 160 | } 161 | 162 | function readJSONFile(file) { 163 | try { 164 | let file_path = path.join(CONFIG.MODELS_DIRECTORY, file) 165 | const data = fs.readFileSync(file_path, 'utf8'); 166 | const jsonData = JSON.parse(data); 167 | return jsonData; 168 | } catch (err) { 169 | console.error("Error reading file:", err); 170 | return null; 171 | } 172 | } 173 | 174 | function extractTitleFromModel(data) { 175 | const titles = {}; 176 | if (data) { 177 | for (const key in data) { 178 | if (data.hasOwnProperty(key)) { 179 | titles[key] = data[key]._meta.title; 180 | } 181 | } 182 | } 183 | return titles; 184 | } 185 | 186 | function formatOutput(titles) { 187 | let output = '*ID* | Node Name\n'; 188 | for (const key in titles) { 189 | if (titles.hasOwnProperty(key)) { 190 | output += `*${key}* | ${titles[key]}\n`; 191 | } 192 | } 193 | return output; 194 | } 195 | 196 | function formatInputsIgnoreArray(number, data) { 197 | const inputs = data[number].inputs; 198 | let formattedInputs = `ID | Input Name | Input Value\n`; 199 | let counter = 0 200 | for (const inputName in inputs) { 201 | if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) { 202 | formattedInputs += `*${counter++}* | *${inputName}* | *${inputs[inputName]}*\n`; 203 | } 204 | } 205 | return formattedInputs.trim(); 206 | } 207 | 208 | function containsSettings(number, setting_number, data) { 209 | if (data[number] && data[number].inputs) { 210 | const inputs = data[number].inputs; 211 | let counter = 0 212 | for (const inputName in inputs) { 213 | if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) { 214 | if (counter == setting_number) { 215 | break; 216 | } 217 | counter++ 218 | } 219 | } 220 | return (counter == setting_number) 221 | } 222 | } 223 | 224 | function getSettingName(number, setting_number, data) { 225 | if (data[number] && data[number].inputs) { 226 | const inputs = data[number].inputs; 227 | let counter = 0 228 | let name = "" 229 | for (const inputName in inputs) { 230 | if (inputs.hasOwnProperty(inputName) && !Array.isArray(inputs[inputName])) { 231 | if (counter == setting_number) { 232 | name = inputName 233 | break; 234 | } 235 | counter++ 236 | } 237 | } 238 | return name 239 | } 240 | } 241 | 242 | function editInputs(jsonObj, title, inputName, value) { 243 | for (let key in jsonObj) { 244 | if (jsonObj[key]._meta && jsonObj[key]._meta.title === title) { 245 | if (jsonObj[key].inputs) { 246 | jsonObj[key].inputs[inputName] = value; 247 | } else { 248 | jsonObj[key].inputs = { [inputName]: value }; 249 | } 250 | return; 251 | } 252 | } 253 | } 254 | 255 | function getNodeID(jsonObj, title) { 256 | let id = -1 257 | for (let key in jsonObj) { 258 | if (jsonObj[key]._meta && jsonObj[key]._meta.title === title) { 259 | id = key 260 | break 261 | } 262 | } 263 | return id 264 | } 265 | 266 | function editJSON(number, inputName, inputValue, data) { 267 | if (data[number] && data[number].inputs) { 268 | const existingValue = data[number].inputs[inputName]; 269 | if (existingValue !== undefined) { 270 | const existingValueType = typeof existingValue; 271 | let convertedValue; 272 | if (existingValueType === 'number' && Number.isInteger(existingValue)) { 273 | convertedValue = parseInt(inputValue); 274 | } else if (existingValueType === 'number' && !Number.isInteger(existingValue)) { 275 | convertedValue = parseFloat(inputValue); 276 | } else if (existingValueType === 'boolean') { 277 | convertedValue = (inputValue.toLowerCase() === 'true'); 278 | } else { 279 | convertedValue = inputValue 280 | } 281 | data[number].inputs[inputName] = convertedValue; 282 | return data; 283 | } else { 284 | console.log("Invalid input name."); 285 | return null; 286 | } 287 | } else { 288 | console.log("Invalid number."); 289 | return null; 290 | } 291 | } 292 | 293 | function extractPhoneNumber(inputString) { 294 | const parts = inputString.split('@'); 295 | const firstPart = parts[0]; 296 | const phoneNumber = firstPart.slice(-10); 297 | return phoneNumber; 298 | } 299 | 300 | async function sendResultToUser(user, images) { 301 | 302 | if (images.length > 0) { 303 | 304 | images.forEach(async (image) => { 305 | let file_path = path.join(image.subfolder, image.filename) 306 | const media = await MessageMedia.fromFilePath(file_path); 307 | botMediaMessage(user, "Here is your image", media) 308 | }) 309 | 310 | } 311 | } 312 | 313 | async function watch(user, data) { 314 | try { 315 | if (!CONFIG.WATCHER[user]) { 316 | CONFIG.WATCHER[user] = { 317 | count: 0, 318 | } 319 | } 320 | let id = data.prompt_id 321 | let res = await fetch(`${CONFIG.API_URL}/history/${id}`).then(response => response.json()) 322 | if (res && res[id]) { 323 | let status = res[id].status 324 | if (status.completed && status.status_str === "success") { 325 | let nodeid = getNodeID(CONFIG.PROMPT.NODES[user], "WA_ImageSaver") 326 | if (nodeid != -1) { 327 | let images = res[id].outputs[nodeid].images || [] 328 | sendResultToUser(user, images) 329 | } 330 | } 331 | else { 332 | botTextMessage(user, "Workflow failed/interrupted to generate result. Please try again!") 333 | } 334 | } 335 | else { 336 | setTimeout(watch, 1000, user, data) 337 | } 338 | } catch (error) { 339 | console.log(error) 340 | botTextMessage(user, "Workflow failed/interrupted to generate result. Please try again!") 341 | } 342 | } 343 | 344 | function setCommand(message) { 345 | 346 | let sliced = message.body.split(" ") 347 | let command = sliced[0] 348 | 349 | switch (command) { 350 | case '/c': 351 | botTextMessagReply(message, allCommands()) 352 | break; 353 | case '/wfs': 354 | CONFIG.MODELS[message.from] = readModels(CONFIG.MODELS_DIRECTORY) 355 | let models = formatModelObject(CONFIG.MODELS[message.from]) 356 | botTextMessagReply(message, "Here are your workflows:\n\n" + "*ID* | Model Name\n\n" + models + "\n\nTo select workfloe write /wf id") 357 | break; 358 | case '/wf': 359 | if (!CONFIG.MODELS[message.from]) { 360 | CONFIG.MODELS[message.from] = readModels(CONFIG.MODELS_DIRECTORY) 361 | } 362 | let index = parseInt(sliced[1]) 363 | if (!containsIndex(CONFIG.MODELS[message.from], index)) { 364 | botTextMessagReply(message, "Workflows does not exists!") 365 | } 366 | else { 367 | CONFIG.PROMPT.MODEL[message.from] = CONFIG.MODELS[message.from][index] 368 | if (!CONFIG.PROMPT.NODES[message.from]) { 369 | CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from]) 370 | } 371 | botTextMessagReply(message, `Workflow *${CONFIG.PROMPT.MODEL[message.from]}* selected!`) 372 | } 373 | break; 374 | case '/wns': 375 | if (!CONFIG.MODELS[message.from]) { 376 | botTextMessagReply(message, "Workflow not selected!") 377 | } 378 | else { 379 | CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from]) 380 | let nodes = formatOutput(extractTitleFromModel(CONFIG.PROMPT.NODES[message.from])) 381 | botTextMessagReply(message, "Here are your workflow nodes:\n\n" + nodes + "\n\nTo get the datail of a particular node write /wn id") 382 | } 383 | break; 384 | case '/wn': 385 | if (!CONFIG.MODELS[message.from]) { 386 | botTextMessagReply(message, "Workflow not selected!") 387 | } 388 | else { 389 | if (!CONFIG.PROMPT.NODES[message.from]) { 390 | CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from]) 391 | } 392 | let index = sliced[1] 393 | 394 | if (!containsValue(Object.keys(CONFIG.PROMPT.NODES[message.from]), index)) { 395 | botTextMessagReply(message, "Node does not exists!") 396 | } 397 | else { 398 | let nodes = formatInputsIgnoreArray(index, CONFIG.PROMPT.NODES[message.from]) 399 | botTextMessagReply(message, "Here are your node inputs:\n\n" + nodes + "\n\nTo edit a particular input write /s wns_id wn_id value") 400 | } 401 | } 402 | break; 403 | case '/sce': 404 | CONFIG.KSAMPLER_SEED_CHANGE = 1 405 | botTextMessagReply(message, `KSampler Seed Change: *Enabled*`) 406 | break; 407 | case '/scd': 408 | CONFIG.KSAMPLER_SEED_CHANGE = 0 409 | botTextMessagReply(message, `KSampler Seed Change: *Disabled*`) 410 | break; 411 | case '/s': 412 | if (!CONFIG.MODELS[message.from]) { 413 | botTextMessagReply(message, "Workflow not selected!") 414 | } 415 | else { 416 | if (!CONFIG.PROMPT.NODES[message.from]) { 417 | CONFIG.PROMPT.NODES[message.from] = readJSONFile(CONFIG.PROMPT.MODEL[message.from]) 418 | } 419 | 420 | let node_number = sliced[1] 421 | let setting_number = sliced[2] 422 | let value = sliced[3] 423 | 424 | if (sliced.length > 4) { 425 | for (let index = 4; index < sliced.length; index++) { 426 | const element = sliced[index]; 427 | value += " " + element 428 | } 429 | } 430 | 431 | if (!containsValue(Object.keys(CONFIG.PROMPT.NODES[message.from]), node_number)) { 432 | botTextMessagReply(message, "Node does not exists.") 433 | } 434 | else if (!containsSettings(node_number, setting_number, CONFIG.PROMPT.NODES[message.from])) { 435 | botTextMessagReply(message, "Node setting does not exists.") 436 | } 437 | else { 438 | let setting_name = getSettingName(node_number, setting_number, CONFIG.PROMPT.NODES[message.from]) 439 | editJSON(node_number, setting_name, value, CONFIG.PROMPT.NODES[message.from]) 440 | let nodes = formatInputsIgnoreArray(node_number, CONFIG.PROMPT.NODES[message.from]) 441 | botTextMessagReply(message, "Node setting changed.\n\n" + nodes) 442 | } 443 | } 444 | break; 445 | case '/r': 446 | CONFIG.MODELS[message.from] = null 447 | CONFIG.PROMPT.MODEL[message.from] = null 448 | CONFIG.PROMPT.NODES[message.from] = null 449 | botTextMessagReply(message, "Reset Done.\n") 450 | break; 451 | case '/q': 452 | if(CONFIG.KSAMPLER_SEED_CHANGE) { 453 | editInputs(CONFIG.PROMPT.NODES[message.from], "KSampler", "seed", Date.now()) 454 | } 455 | editInputs(CONFIG.PROMPT.NODES[message.from], "WA_ImageSaver", "Path", path.join(CONFIG.OUTPUT_DIRECTORY, extractPhoneNumber(message.from))) 456 | let requestOptions = { 457 | method: 'POST', 458 | headers: { 459 | 'Content-Type': 'application/json' 460 | }, 461 | body: JSON.stringify({ "prompt": CONFIG.PROMPT.NODES[message.from] }) 462 | }; 463 | fetch(CONFIG.API_URL + "/prompt", requestOptions) 464 | .then(response => response.json()) 465 | .then(data => { 466 | botTextMessagReply(message, "Promt Submitted.\n") 467 | watch(message.from, data) 468 | }) 469 | .catch(error => console.error('Error:', error)); 470 | break; 471 | case '/m': 472 | if (!isAdmin(message.from)) { 473 | botTextMessagReply(message, `You are not Admin.`) 474 | return 475 | } 476 | let mode = parseInt(sliced[1]) 477 | if (mode == 1) { 478 | CONFIG.MODE = 1 479 | botTextMessagReply(message, `Usage Mode changed to: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`) 480 | } 481 | else if (mode == 2) { 482 | CONFIG.MODE = 2 483 | botTextMessagReply(message, `Usage Mode changed to: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`) 484 | } 485 | else { 486 | botTextMessagReply(message, `Invalid Mode.`) 487 | } 488 | break; 489 | case '/i': 490 | fetch(CONFIG.API_URL + "/interrupt", { 491 | method: 'POST', 492 | }) 493 | .then(data => { botTextMessagReply(message, "Promt Interrupted.\n\n") }) 494 | .catch(error => console.error('Error:', error)); 495 | break; 496 | default: 497 | if (mode == 2) { 498 | botTextMessagReply(message, `Invalid command. Message */c*`) 499 | } 500 | break; 501 | } 502 | } 503 | 504 | //==================================== WA ====================================================== 505 | 506 | let options = { 507 | executablePath: CONFIG.CHROME, 508 | headless: true 509 | } 510 | 511 | if (!CONFIG.CHROME && CONFIG.CHROME.length == 0) { 512 | delete options.executablePath 513 | } 514 | 515 | CONFIG.WA = new Client({ authStrategy: new LocalAuth({ dataPath: CONFIG.BROWSER_DIRECTORY }), puppeteer: options }) 516 | 517 | CONFIG.WA.on('qr', (qr) => { 518 | CONFIG.QRCODE = qr 519 | qrcode.toFile(path.join(CONFIG.TEMP_DIRECTORY, 'qr.png'), qr, { 520 | color: { 521 | dark: '#000', 522 | light: '#fff' 523 | } 524 | }, (err) => { 525 | if (err) throw err; 526 | console.log('QR code generated successfully!'); 527 | }); 528 | }); 529 | 530 | CONFIG.WA.on('ready', () => { 531 | CONFIG.READY = true 532 | if (CONFIG.ADMIN) 533 | botTextMessage(CONFIG.ADMIN, `ComfyUI Bot is *Online*.\n\nMessage */c* to start.\n\nUsage Mode: *${CONFIG.MODE == 1 ? 'Single User' : 'Multi User'}*`) 534 | }); 535 | 536 | CONFIG.WA.on('message_create', message => { 537 | 538 | if (message.body.includes('🤖')) { 539 | return 540 | } 541 | 542 | if (CONFIG.MODE == 1 && message.id.fromMe == true) { 543 | setCommand(message) 544 | } else if (CONFIG.MODE == 1 && message.id.fromMe == false) { 545 | botTextMessagReply("I can't fulfill your request sorry 😅.") 546 | } else if (CONFIG.MODE == 2) { 547 | setCommand(message) 548 | } 549 | 550 | }); 551 | 552 | // =================================== EXPRESS ======================================== 553 | 554 | CONFIG.EXPRESS.use(express.static(CONFIG.WEB_DIRECTORY)) 555 | CONFIG.EXPRESS.use(express.json()); 556 | 557 | CONFIG.EXPRESS.get('/ping', (req, res) => { 558 | return res.json({ TIME: +new Date() }) 559 | }); 560 | 561 | CONFIG.EXPRESS.get('/qr', (req, res) => { 562 | const qrImagePath = path.join(CONFIG.TEMP_DIRECTORY, 'qr.png'); 563 | if (fs.existsSync(qrImagePath)) { 564 | return res.sendFile(path.resolve(qrImagePath)); 565 | } else { 566 | return res.status(404).send("QR code image not found"); 567 | } 568 | }); 569 | 570 | CONFIG.EXPRESS.get('/ready', (req, res) => { 571 | return res.json({ ready: CONFIG.READY }) 572 | }); 573 | 574 | CONFIG.EXPRESS.post('/model', async (req, res) => { 575 | try { 576 | const data = req.body; 577 | const filename = data.name 578 | const filePath = path.join(CONFIG.MODELS_DIRECTORY, filename); 579 | await fsp.mkdir(CONFIG.MODELS_DIRECTORY, { recursive: true }); 580 | await fsp.writeFile(filePath, JSON.stringify(data.data, null, 2), 'utf8'); 581 | res.status(200).json({ success: true, message: 'Model data saved successfully' }); 582 | } catch (error) { 583 | console.error('Error saving model data:', error); 584 | res.status(500).json({ success: false, message: 'Failed to save model data' }); 585 | } 586 | }); 587 | 588 | CONFIG.EXPRESS.get('/models', async (req, res) => { 589 | try { 590 | const files = await fsp.readdir(CONFIG.MODELS_DIRECTORY); 591 | const models = await Promise.all(files.map(async (file) => { 592 | const filePath = path.join(CONFIG.MODELS_DIRECTORY, file); 593 | const stats = await fsp.stat(filePath); 594 | return { 595 | name: file.split('.')[0], 596 | dateCreated: stats.mtime.toISOString() 597 | }; 598 | })); 599 | res.status(200).json(models); 600 | } catch (error) { 601 | console.error('Error retrieving models:', error); 602 | res.status(500).json({ success: false, message: 'Failed to retrieve models' }); 603 | } 604 | }); 605 | 606 | CONFIG.EXPRESS.delete('/model/:name', async (req, res) => { 607 | try { 608 | const { name } = req.params; 609 | const filePath = path.join(CONFIG.MODELS_DIRECTORY, name + '.json'); 610 | await fsp.unlink(filePath); 611 | res.status(200).json({ success: true, message: 'Model deleted successfully' }); 612 | } catch (error) { 613 | console.error('Error deleting model:', error); 614 | res.status(500).json({ success: false, message: 'Failed to delete model' }); 615 | } 616 | }); 617 | 618 | CONFIG.EXPRESS.get('/', (req, res) => { 619 | return res.sendFile(path.join(__dirname, 'website', 'index.html')) 620 | }) 621 | 622 | //==============================================INIT============================ 623 | 624 | let ON = true 625 | 626 | if (ON) { 627 | createDirectories(CONFIG); 628 | CONFIG.WA.initialize() 629 | 630 | CONFIG.EXPRESS.listen(CONFIG.PORT, () => { 631 | console.log(`Server running at http://localhost:${CONFIG.PORT}`); 632 | }); 633 | } 634 | --------------------------------------------------------------------------------