diff --git a/.DS_Store b/.DS_Store
index bc41a65..eb00466 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..556b5dc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/.DS_Store
+assets/.DS_Store
+.DS_Store
diff --git a/README.md b/README.md
index 2df6439..86f40d6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,15 @@
-# WebhookImage
-# WebhookImage
+# ComfyUI WebhookImage
+
+This package provides a custom node to ComfyUI to send a message and image by means of a webhook
+
+## WebhookImage
+
+Send a webhook to the specified URL.
+If you are using this on a local installation, the webhook could be generate by using ngrok https://ngrok.com/
+
+
+
+## Further details
+
+Will be added soon
+
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..ec5e0f0
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,11 @@
+from .nodes.webhook import WebhookImage
+
+NODE_CLASS_MAPPINGS = {
+ "Notif-Webhook": WebhookImage,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "Notif-Webhook": "WebhookImage",
+}
+
+WEB_DIRECTORY = "./web"
diff --git a/assets/webhookimage_node.png b/assets/webhookimage_node.png
new file mode 100644
index 0000000..8529898
Binary files /dev/null and b/assets/webhookimage_node.png differ
diff --git a/nodes/util.py b/nodes/util.py
new file mode 100644
index 0000000..cc986b3
--- /dev/null
+++ b/nodes/util.py
@@ -0,0 +1,5 @@
+# Hack: string type that is always equal in not equal comparisons
+# Thanks to https://github.com/pythongosssss/ComfyUI-Custom-Scripts
+class ComfyAnyType(str):
+ def __ne__(self, __value: object) -> bool:
+ return False
diff --git a/nodes/webhook.py b/nodes/webhook.py
new file mode 100644
index 0000000..4e5931a
--- /dev/null
+++ b/nodes/webhook.py
@@ -0,0 +1,120 @@
+import requests
+import json
+import os
+from PIL import Image
+from PIL.PngImagePlugin import PngInfo
+import numpy as np
+
+from datetime import datetime
+import folder_paths
+
+class WebhookImage:
+ def __init__(self):
+ self.output_dir = folder_paths.get_output_directory()
+ self.type = 'output'
+ self.prefix_append = ''
+
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ """
+ Defines the input types for the node.
+
+ Returns:
+ dict: A dictionary specifying required and hidden inputs.
+ """
+ return {
+ "required": {
+ 'images': ('IMAGE', ),
+ "webhook_url": ("STRING", {'default': ''}),
+ "verify_ssl": ("BOOLEAN", {"default": True}),
+ "safe_prompt": (["enable", "disable"], {"default": "disable"}),
+ "notification_text": ('STRING', {'default': 'Your image is ready.'}),
+ "json_format": ('STRING', {'default': '{"text": ""}'}),
+ "send_notification": (["enable", "disable"], {"default": "disable"}),
+ "timeout": ('FLOAT', {'default': 3, 'min': 0, 'max': 60}),
+ 'image_preview': (['disabled', 'enabled'], {'default': 'enabled'}),
+ },
+ "optional": {
+ "positive_text_opt": ("STRING", {"forceInput": True}),
+ "negative_text_opt": ("STRING", {"forceInput": True}),
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
+ }
+
+ FUNCTION = 'hookImage'
+ OUTPUT_NODE = True
+ RETURN_TYPES = tuple()
+ CATEGORY = "notifications"
+
+ @staticmethod
+ def sendTxtMessage( webhook_url, notification_text, json_format, timeout, verify_ssl ):
+ """
+ handles sending text message to the webhook
+
+ Args:
+ notification_text (str):
+ """
+ payload = json_format.replace("", notification_text)
+ payload = json.loads(payload)
+ res = requests.post( webhook_url, json=payload, timeout=timeout, verify=verify_ssl)
+ res.raise_for_status()
+
+ def hookImage(self, images, webhook_url, notification_text, json_format, timeout, verify_ssl, negative_text_opt=None, positive_text_opt=None, extra_pnginfo=None, prompt=None, safe_prompt="disable", image_preview="disable", send_notification="disable" ):
+ counter = 1
+
+
+ results = list()
+
+ for image in images:
+ i = 255. * image.cpu().numpy()
+ img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
+
+ metadata = PngInfo()
+ metadata.add_text("webhook_url", webhook_url )
+ metadata.add_text("notification_text", notification_text)
+
+ if prompt is not None:
+ if safe_prompt == 'enable':
+ metadata.add_text("prompt", json.dumps(prompt))
+
+ if positive_text_opt is not None:
+ metadata.add_text("positive_text_opt", json.dumps(positive_text_opt))
+ if negative_text_opt is not None:
+ metadata.add_text("negative_text_opt", json.dumps(negative_text_opt))
+ if extra_pnginfo is not None:
+ for x in extra_pnginfo:
+ metadata.add_text(x, json.dumps(extra_pnginfo[x]))
+
+ timestamp = datetime.now().strftime('%c')
+ file = f"telegram_notification_{counter:05}_{timestamp}.png"
+
+ image_path = os.path.join(self.output_dir, file)
+ img.save(image_path, format="PNG", pnginfo=metadata)
+
+ results.append({ 'filename': file, 'subfolder': '', 'type': self.type})
+ counter += 1
+
+ try:
+ with open(image_path, 'rb') as image_file:
+ files = {
+ 'file': ('photo', image_file, 'image/png')
+ }
+
+ res = requests.post( webhook_url, files=files, timeout=timeout, verify=verify_ssl)
+ res.raise_for_status()
+
+ if res.status_code == 200:
+ print("Image posted successfully!")
+ else:
+ print(f"Failed to post image. Status code: {res.status_code}")
+
+ except OSError as e:
+ print(f'An error occurred while sending: {e}')
+ else:
+ if send_notification == 'enable':
+ WebhookImage.sendTxtMessage( webhook_url, notification_text, json_format, timeout, verify_ssl)
+
+ if image_preview == 'disabled':
+ results = list()
+ return { 'ui': { 'images': results } }
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..bffa19b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+numpy
diff --git a/util.js b/util.js
new file mode 100644
index 0000000..ff223ae
--- /dev/null
+++ b/util.js
@@ -0,0 +1,52 @@
+export const notificationSetup = () => {
+ if (!('Notification' in window)) {
+ console.log('This browser does not support notifications.')
+ alert('This browser does not support notifications.')
+ return
+ }
+ if (Notification.permission === 'denied') {
+ console.log('Notifications are blocked. Please enable them in your browser settings.')
+ alert('Notifications are blocked. Please enable them in your browser settings.')
+ return
+ }
+ if (Notification.permission !== 'granted') {
+ Notification.requestPermission()
+ }
+ return true
+}
+
+export const sendNotification = (title, body) => {
+ if (!notificationSetup()) return
+ const notification = new Notification(title, { body })
+ return notification
+}
+
+export const getNotificationTextFromArguments = (args) => {
+ return args[0]?.notification_text[0] ?? 'Your notification has triggered.'
+}
+
+export const playSound = (file, volume) => {
+ if (!file) {
+ file = 'notify.mp3'
+ }
+ if (!file.startsWith('http')) {
+ if (!file.includes('/')) {
+ file = 'assets/' + file
+ }
+ file = new URL(file, import.meta.url)
+ }
+ const url = new URL(file)
+ const audio = new Audio(url)
+ audio.volume = volume
+ audio.play()
+}
+
+export const appQueueIsEmpty = async (app) => {
+ if (app.ui.lastQueueSize !== 0) {
+ await new Promise((r) => setTimeout(r, 500))
+ }
+ if (app.ui.lastQueueSize !== 0) {
+ return false
+ }
+ return true
+}
diff --git a/util.py b/util.py
new file mode 100644
index 0000000..cc986b3
--- /dev/null
+++ b/util.py
@@ -0,0 +1,5 @@
+# Hack: string type that is always equal in not equal comparisons
+# Thanks to https://github.com/pythongosssss/ComfyUI-Custom-Scripts
+class ComfyAnyType(str):
+ def __ne__(self, __value: object) -> bool:
+ return False