├── 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 |
23 |
24 |
Drop WorkFlow files here (API Version)
25 |
26 |
27 |
28 |
29 | Name
30 | Date Created
31 | Actions
32 |
33 |
34 |
35 |
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 |
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 | 
88 |
89 | 
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 | 
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 | 
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 | 
127 |
128 | Now upload it in workflow section
129 |
130 | 
131 |
132 |
133 |
134 | # WhatsApp Commands
135 |
136 | Writing **/c** in whatsapp will also provide the list of all commands:
137 |
138 | 
139 |
140 | - Write **/wfs** to get a numbered list of uploaded workflows.
141 |
142 | 
143 |
144 | - Write **/wf id** to select the workflow.
145 |
146 | 
147 |
148 | - Write **/wns** to get numbered list of selected workflow nodes.
149 |
150 | 
151 |
152 | - Write **/wn id** to get numbered list of inputs available.
153 |
154 | 
155 |
156 | - Write **/s node_id input_id value** to set value for input selected.
157 |
158 | 
159 |
160 | - Write **/sce** enable auto ksampler seed change.
161 |
162 | 
163 |
164 | - Write **/scd** disable auto ksampler seed change.
165 |
166 | 
167 |
168 | - Write **/r** to reset all to default settings.
169 |
170 | 
171 |
172 | - Write **/q** to queue.
173 |
174 | 
175 |
176 | - Write **/i** to interrupt queue.
177 |
178 | 
179 |
180 | - Write **/m number** to set bot usage mode. **1**: Single User, **2**: Multi User.'
181 |
182 | 
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 | Delete
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 |
--------------------------------------------------------------------------------