1+ import json
2+ import logging
3+ import uuid
4+ from datetime import datetime , timezone
15from secrets import token_urlsafe
26
37from decouple import config
812from api .utils import get_session
913from api .tasks import nostr_send_notification_event
1014
15+ logger = logging .getLogger ("api.notifications" )
16+
1117
1218class Notifications :
1319 """Simple telegram messages using TG's API"""
@@ -16,7 +22,7 @@ class Notifications:
1622 site = config ("HOST_NAME" )
1723
1824 def get_context (user ):
19- """returns context needed to enable TG notifications"""
25+ """returns context needed to enable TG and webhook notifications"""
2026 context = {}
2127 if user .robot .telegram_enabled :
2228 context ["tg_enabled" ] = True
@@ -30,17 +36,24 @@ def get_context(user):
3036 context ["tg_token" ] = user .robot .telegram_token
3137 context ["tg_bot_name" ] = config ("TELEGRAM_BOT_NAME" )
3238
39+ context ["webhook_enabled" ] = user .robot .webhook_enabled
40+ context ["webhook_url" ] = user .robot .webhook_url or ""
41+
3342 return context
3443
35- def send_message (self , order , robot , title , description = "" ):
36- """Save a message for a user and sends it to Telegram and/or Nostr"""
44+ def send_message (
45+ self , order , robot , title , description = "" , event_type = "notification"
46+ ):
47+ """Save a message for a user and sends it to Telegram, Nostr, and/or Webhook"""
3748 self .save_message (order , robot , title , description )
3849 if robot .nostr_pubkey :
3950 nostr_send_notification_event .delay (
4051 robot_id = robot .id , order_id = order .id , text = title
4152 )
4253 if robot .telegram_enabled :
4354 self .send_telegram_message (robot .telegram_chat_id , title , description )
55+ if robot .webhook_enabled :
56+ self .send_webhook_message (order , robot , title , description , event_type )
4457
4558 def save_message (self , order , robot , title , description = "" ):
4659 """Save a message for a user"""
@@ -62,6 +75,104 @@ def send_telegram_message(self, chat_id, title, description=""):
6275 except Exception :
6376 pass
6477
78+ def send_webhook_message (
79+ self , order , robot , title , description = "" , event_type = "notification"
80+ ):
81+ """Sends a webhook notification to the user's custom HTTP endpoint (Tor .onion only)"""
82+ from api .models import Robot
83+
84+ webhook_url = robot .webhook_url
85+ if not Robot .is_valid_onion_url (webhook_url ):
86+ logger .warning (
87+ f"Webhook URL rejected: not a .onion address for robot { robot .id } "
88+ )
89+ return
90+ payload = {
91+ "event_type" : event_type ,
92+ "event_id" : str (uuid .uuid4 ()),
93+ "timestamp" : datetime .now (timezone .utc ).isoformat (),
94+ "robot_hash_id" : robot .hash_id ,
95+ "order" : {
96+ "id" : order .id ,
97+ "type" : "BUY" if order .type == Order .Types .BUY else "SELL" ,
98+ "status" : order .status ,
99+ },
100+ "message" : {
101+ "title" : title ,
102+ "description" : description ,
103+ },
104+ "metadata" : {
105+ "coordinator" : config ("COORDINATOR_ALIAS" , cast = str , default = "Unknown" ),
106+ "platform_version" : config ("VERSION" , cast = str , default = "Unknown" ),
107+ },
108+ }
109+
110+ headers = {
111+ "Content-Type" : "application/json" ,
112+ }
113+
114+ if robot .webhook_api_key :
115+ headers ["X-API-Key" ] = robot .webhook_api_key
116+
117+ try :
118+ response = self .session .post (
119+ webhook_url ,
120+ data = json .dumps (payload ),
121+ headers = headers ,
122+ timeout = 60 ,
123+ )
124+ response .raise_for_status ()
125+ logger .info (f"Webhook sent successfully to robot { robot .id } " )
126+ except Exception as e :
127+ logger .error (f"Webhook failed for robot { robot .id } : { e } " )
128+
129+ def send_webhook_test (self , robot ):
130+ """Sends a test webhook notification when webhook is first configured"""
131+ from api .models import Robot
132+
133+ webhook_url = robot .webhook_url
134+ if not Robot .is_valid_onion_url (webhook_url ):
135+ logger .warning (
136+ f"Webhook test rejected: not a .onion address for robot { robot .id } "
137+ )
138+ return False
139+
140+ payload = {
141+ "event_type" : "webhook_test" ,
142+ "event_id" : str (uuid .uuid4 ()),
143+ "timestamp" : datetime .now (timezone .utc ).isoformat (),
144+ "robot_hash_id" : robot .hash_id ,
145+ "message" : {
146+ "title" : f"🔔 Hey { robot .user .username } , your webhook is configured!" ,
147+ "description" : "You will receive notifications about your RoboSats orders." ,
148+ },
149+ "metadata" : {
150+ "coordinator" : config ("COORDINATOR_ALIAS" , cast = str , default = "Unknown" ),
151+ "platform_version" : config ("VERSION" , cast = str , default = "Unknown" ),
152+ },
153+ }
154+
155+ headers = {
156+ "Content-Type" : "application/json" ,
157+ }
158+
159+ if robot .webhook_api_key :
160+ headers ["X-API-Key" ] = robot .webhook_api_key
161+
162+ try :
163+ response = self .session .post (
164+ webhook_url ,
165+ data = json .dumps (payload ),
166+ headers = headers ,
167+ timeout = 60 ,
168+ )
169+ response .raise_for_status ()
170+ logger .info (f"Webhook test sent successfully to robot { robot .id } " )
171+ return True
172+ except Exception as e :
173+ logger .error (f"Webhook test failed for robot { robot .id } : { e } " )
174+ return False
175+
65176 def welcome (self , user ):
66177 """User enabled Telegram Notifications"""
67178 lang = user .robot .telegram_lang_code
0 commit comments