From 3e4022e1d57c496431a36d67932aeb3e07870da4 Mon Sep 17 00:00:00 2001 From: KenwoodFox Date: Wed, 10 Dec 2025 22:15:06 -0500 Subject: [PATCH] Add webhook trigger --- Pipfile | 1 + Pipfile.lock | 154 +++++++++++++++++++- quotes/discord_utils.py | 89 +++++++++++ quotes/email_utils.py | 92 +++++++++++- requirements.txt | 1 + seduttomachineworks_project/quote_submit.py | 15 +- static/css/quote_upload.css | 2 +- 7 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 quotes/discord_utils.py diff --git a/Pipfile b/Pipfile index c43b36d..7924fe1 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] django = "<5.0,>=4.2" whitenoise = "<7.0,>=6.0" +requests = "<3.0,>=2.31" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 25ecfca..41aa0d8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "faf9d6b8d73f937fa430b92dadbef77a6921f9675d5e8e3131b663391e308135" + "sha256": "7a4ed5cb168d39157da8b0d77840ffb125ecbdf2c71eb5cdc25a4548c9047bb8" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,133 @@ "markers": "python_version >= '3.9'", "version": "==3.11.0" }, + "certifi": { + "hashes": [ + "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", + "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316" + ], + "markers": "python_version >= '3.7'", + "version": "==2025.11.12" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, "django": { "hashes": [ "sha256:b865fbe0f4a3d1ee36594c5efa42b20db3c8bbb10dff0736face1c6e4bda5b92", @@ -33,6 +160,23 @@ "markers": "python_version >= '3.8'", "version": "==4.2.27" }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "requests": { + "hashes": [ + "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", + "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.32.5" + }, "sqlparse": { "hashes": [ "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e", @@ -41,6 +185,14 @@ "markers": "python_version >= '3.8'", "version": "==0.5.4" }, + "urllib3": { + "hashes": [ + "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", + "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b" + ], + "markers": "python_version >= '3.9'", + "version": "==2.6.1" + }, "whitenoise": { "hashes": [ "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", diff --git a/quotes/discord_utils.py b/quotes/discord_utils.py new file mode 100644 index 0000000..6bdabef --- /dev/null +++ b/quotes/discord_utils.py @@ -0,0 +1,89 @@ +""" +Discord webhook utilities for sending submission notifications. +""" + +import os +import json +import requests +from django.conf import settings + + +def send_discord_webhook(submission): + """ + Sends a discord webhook notification if DISCORD_WEBHOOK is configured. + + Joe wrote this! + """ + + webhook_url = os.environ.get("DISCORD_WEBHOOK") + if not webhook_url: + return False + + try: + files = submission.files.all() + file_urls = [] + + # Get file URLs + from .email_utils import get_file_urls_with_base_url + + file_url_data = get_file_urls_with_base_url(files) + for item in file_url_data: + file_urls.append( + { + "name": item["file"].original_filename, + "url": item["url"], + } + ) + + # Build description with file links + description_parts = [] + if submission.description: + description_parts.append(f"**Description:** {submission.description}") + + if file_urls: + description_parts.append("\n**Uploaded Files:**") + for file_info in file_urls: + description_parts.append(f"- [{file_info['name']}]({file_info['url']})") + + description = ( + "\n".join(description_parts) + if description_parts + else "No description or files provided." + ) + + # Discord webhook payload + payload = { + "content": "Somebody submitted a quote!", + "embeds": [ + { + "title": f"New Quote Request - Submission #{submission.id}", + "color": 3447003, # Blue color + "fields": [ + { + "name": "Email", + "value": submission.email, + "inline": True, + }, + { + "name": "Submitted At", + "value": submission.submitted_at.strftime( + "%Y-%m-%d %H:%M:%S" + ), + "inline": True, + }, + ], + "description": description, + "footer": { + "text": f"Submission ID: #{submission.id}", + }, + } + ], + } + + response = requests.post(webhook_url, json=payload, timeout=10) + response.raise_for_status() + return True + + except Exception as e: + print(f"Error sending Discord webhook: {e}") + return False diff --git a/quotes/email_utils.py b/quotes/email_utils.py index b808c0f..eeda26e 100644 --- a/quotes/email_utils.py +++ b/quotes/email_utils.py @@ -2,10 +2,86 @@ Email utilities for sending submission notifications. """ +import signal +import threading +import logging from django.core.mail import send_mail, EmailMultiAlternatives from django.template.loader import render_to_string from django.conf import settings +logger = logging.getLogger(__name__) + + +class TimeoutError(Exception): + """Custom timeout exception""" + + pass + + +def timeout_handler(signum, frame): + """Signal handler for timeout""" + raise TimeoutError("Operation timed out") + + +def send_email_with_timeout(msg, timeout_seconds=5): + """ + Send email with a timeout to prevent hanging. + Uses signal-based timeout on Unix, threading fallback otherwise. + + Args: + msg: EmailMultiAlternatives instance + timeout_seconds: Maximum time to wait for email to send + + Returns: + bool: True if sent successfully, False otherwise + """ + result = [None] # Use list to allow modification in nested function + exception = [None] + + def send_email(): + try: + msg.send() + result[0] = True + except Exception as e: + exception[0] = e + result[0] = False + + # Try signal-based timeout first (Unix/Linux) + try: + signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(timeout_seconds) + try: + msg.send() + signal.alarm(0) # Cancel the alarm + return True + except TimeoutError: + logger.error(f"Email sending timed out after {timeout_seconds} seconds") + signal.alarm(0) # Cancel the alarm + return False + except Exception as e: + signal.alarm(0) # Cancel the alarm + logger.error(f"Error sending email: {e}", exc_info=True) + return False + except (AttributeError, ValueError, OSError): + # Windows doesn't support SIGALRM, or signal already in use + # Use threading-based timeout as fallback + email_thread = threading.Thread(target=send_email) + email_thread.daemon = True + email_thread.start() + email_thread.join(timeout=timeout_seconds) + + if email_thread.is_alive(): + logger.error( + f"Email sending timed out after {timeout_seconds} seconds (threading)" + ) + return False + + if exception[0]: + logger.error(f"Error sending email: {exception[0]}", exc_info=True) + return False + + return result[0] if result[0] is not None else False + def get_file_urls_with_base_url(files): """ @@ -120,10 +196,12 @@ Files uploaded: {files.count()} to=[owner_email], ) msg.attach_alternative(html_message, "text/html") - msg.send() - owner_sent = True + owner_sent = send_email_with_timeout(msg, timeout_seconds=30) + if not owner_sent: + logger.error(f"Failed to send owner email to {owner_email}") except Exception as e: - print(f"Error sending owner email: {e}") + logger.error(f"Error sending owner email: {e}", exc_info=True) + owner_sent = False # Email 2: Confirmation to submitter try: @@ -168,9 +246,11 @@ Files uploaded: {files.count()} to=[submission.email], ) msg.attach_alternative(html_message, "text/html") - msg.send() - submitter_sent = True + submitter_sent = send_email_with_timeout(msg, timeout_seconds=30) + if not submitter_sent: + logger.error(f"Failed to send submitter email to {submission.email}") except Exception as e: - print(f"Error sending submitter email: {e}") + logger.error(f"Error sending submitter email: {e}", exc_info=True) + submitter_sent = False return (owner_sent, submitter_sent) diff --git a/requirements.txt b/requirements.txt index 98b1283..138817c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django>=4.2,<5.0 whitenoise>=6.0,<7.0 +requests>=2.31,<3.0 diff --git a/seduttomachineworks_project/quote_submit.py b/seduttomachineworks_project/quote_submit.py index 2627303..76fc909 100644 --- a/seduttomachineworks_project/quote_submit.py +++ b/seduttomachineworks_project/quote_submit.py @@ -91,15 +91,17 @@ def submit_quote(request): yield send_progress_update(3, "Files saved...", 50) - # Step 4: Send emails + # Step 4: Send webook if config + from quotes.discord_utils import send_discord_webhook + + yield send_progress_update(4, "Triggering hooks...", 60) + send_discord_webhook(submission) + + # Step 5: Send emails from quotes.email_utils import send_submission_emails - yield send_progress_update(4, "Sending notification email...", 70) + yield send_progress_update(4, "Sending emails...", 70) owner_sent, submitter_sent = send_submission_emails(submission) - time.sleep(0.3) - - yield send_progress_update(4, "Sending confirmation email...", 75) - time.sleep(0.3) # Verify emails were sent if not owner_sent and getattr(settings, "OWNER_EMAIL", ""): @@ -113,6 +115,7 @@ def submit_quote(request): "warning", "Warning: Confirmation email may not have been sent", 75 ) + time.sleep(0.1) # Complete yield send_progress_update( 5, diff --git a/static/css/quote_upload.css b/static/css/quote_upload.css index 289ed6d..ce3fa96 100644 --- a/static/css/quote_upload.css +++ b/static/css/quote_upload.css @@ -18,7 +18,7 @@ body { .upload-box { width: 100%; - min-height: 100vh; + min-height: auto; background: white; padding: 16px; padding-bottom: 200px; /* Reserve space for progress bar and files */