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