From 19484971aff699d93ecd3df3c58ca500e9376284 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 17:58:28 +0530 Subject: [PATCH 01/10] feat: added nostr forwarding fields to Robot model --- .../0058_add_nostr_forward_fields.py | 28 +++++++++++++++++++ api/models/robot.py | 5 ++++ 2 files changed, 33 insertions(+) create mode 100644 api/migrations/0058_add_nostr_forward_fields.py diff --git a/api/migrations/0058_add_nostr_forward_fields.py b/api/migrations/0058_add_nostr_forward_fields.py new file mode 100644 index 000000000..cda213837 --- /dev/null +++ b/api/migrations/0058_add_nostr_forward_fields.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.15 on 2026-02-05 12:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_robot_webhook_enabled_alter_order_escrow_duration'), + ] + + operations = [ + migrations.AddField( + model_name='robot', + name='nostr_forward_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='robot', + name='nostr_forward_pubkey', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AddField( + model_name='robot', + name='nostr_forward_relay', + field=models.URLField(blank=True, max_length=500, null=True), + ), + ] diff --git a/api/models/robot.py b/api/models/robot.py index da94591e5..7b2bb90da 100644 --- a/api/models/robot.py +++ b/api/models/robot.py @@ -49,6 +49,11 @@ class Robot(models.Model): webhook_api_key = models.CharField(max_length=256, null=True, blank=True) webhook_enabled = models.BooleanField(default=False, null=False) + # Nostr forwarding to main account + nostr_forward_pubkey = models.CharField(max_length=64, null=True, blank=True) + nostr_forward_relay = models.URLField(max_length=500, null=True, blank=True) + nostr_forward_enabled = models.BooleanField(default=False, null=False) + # Claimable rewards earned_rewards = models.PositiveIntegerField(null=False, default=0) # Total claimed rewards From 9b532002c10a198603345ff8a2cca1aa72b7eb32 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 18:32:05 +0530 Subject: [PATCH 02/10] feat: added nostr fwd API endpoints and test notifs --- api/nostr.py | 30 ++++++++++++++++++++++++++++++ api/notifications.py | 28 ++++++++++++++++++++++++++++ api/serializers.py | 13 +++++++++++++ api/tasks.py | 14 ++++++++++++++ api/views.py | 26 +++++++++++++++++++++++++- 5 files changed, 110 insertions(+), 1 deletion(-) diff --git a/api/nostr.py b/api/nostr.py index c0c437813..66afb032f 100644 --- a/api/nostr.py +++ b/api/nostr.py @@ -64,6 +64,36 @@ async def send_notification_event(self, robot, order, text): await client.send_private_msg(PublicKey.parse(robot.nostr_pubkey), text, tags) print("Nostr NOTIFICATION event sent") + async def send_forward_test(self, robot): + """Sends a test notification to user's main nostr account via their .onion relay""" + from api.models import Robot + + if config("NOSTR_NSEC", cast=str, default="") == "": + return + + if not robot.nostr_forward_pubkey or not robot.nostr_forward_relay: + return + + if not Robot.is_valid_onion_url(robot.nostr_forward_relay): + return + + print(f"Sending nostr FORWARD TEST to {robot.nostr_forward_relay}") + + keys = Keys.parse(config("NOSTR_NSEC", cast=str)) + signer = NostrSigner.keys(keys) + client = Client(signer) + + await client.add_relay(robot.nostr_forward_relay) + await client.connect() + + coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="RoboSats") + text = f"🔔 Hey {robot.user.username}, your Nostr forwarding is configured! You will receive order notifications from {coordinator_alias}." + + await client.send_private_msg( + PublicKey.parse(robot.nostr_forward_pubkey), text, [] + ) + print("Nostr FORWARD TEST event sent") + async def initialize_client(self, keys): # Initialize with coordinator Keys signer = NostrSigner.keys(keys) diff --git a/api/notifications.py b/api/notifications.py index 9a4fdb0a7..9d87ad49e 100644 --- a/api/notifications.py +++ b/api/notifications.py @@ -39,6 +39,10 @@ def get_context(user): context["webhook_enabled"] = user.robot.webhook_enabled context["webhook_url"] = user.robot.webhook_url or "" + context["nostr_forward_enabled"] = user.robot.nostr_forward_enabled + context["nostr_forward_relay"] = user.robot.nostr_forward_relay or "" + context["nostr_forward_pubkey"] = user.robot.nostr_forward_pubkey or "" + return context def send_message( @@ -173,6 +177,30 @@ def send_webhook_test(self, robot): logger.error(f"Webhook test failed for robot {robot.id}: {e}") return False + def send_nostr_forward_test(self, robot): + """Sends a test nostr notification to user's main account via their .onion relay""" + from api.models import Robot + from api.tasks import nostr_send_forward_test + + relay_url = robot.nostr_forward_relay + pubkey = robot.nostr_forward_pubkey + + if not relay_url or not pubkey: + logger.warning( + f"Nostr forward test rejected: missing relay or pubkey for robot {robot.id}" + ) + return False + + if not Robot.is_valid_onion_url(relay_url): + logger.warning( + f"Nostr forward test rejected: not a .onion address for robot {robot.id}" + ) + return False + + nostr_send_forward_test.delay(robot_id=robot.id) + logger.info(f"Nostr forward test queued for robot {robot.id}") + return True + def welcome(self, user): """User enabled Telegram Notifications""" lang = user.robot.telegram_lang_code diff --git a/api/serializers.py b/api/serializers.py index 220d7bb5b..70bdc2dd0 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -737,11 +737,17 @@ class Meta: "webhook_url", "webhook_enabled", "webhook_api_key", + "nostr_forward_pubkey", + "nostr_forward_relay", + "nostr_forward_enabled", ) extra_kwargs = { "webhook_url": {"required": False, "allow_null": True}, "webhook_enabled": {"required": False}, "webhook_api_key": {"required": False, "allow_null": True}, + "nostr_forward_pubkey": {"required": False, "allow_null": True}, + "nostr_forward_relay": {"required": False, "allow_null": True}, + "nostr_forward_enabled": {"required": False}, } def validate_webhook_url(self, value): @@ -750,3 +756,10 @@ def validate_webhook_url(self, value): "Webhook URL must be a Tor .onion address" ) return value + + def validate_nostr_forward_relay(self, value): + if value and not Robot.is_valid_onion_url(value): + raise serializers.ValidationError( + "Nostr relay must be a Tor .onion address" + ) + return value diff --git a/api/tasks.py b/api/tasks.py index 0f5d97fcc..2787702f2 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -282,6 +282,20 @@ def nostr_send_notification_event(robot_id=None, order_id=None, text=None): return +@shared_task(name="nostr_send_forward_test", ignore_result=True, time_limit=120) +def nostr_send_forward_test(robot_id=None): + """Send a test notification to user's main nostr account via their .onion relay""" + if robot_id: + from api.models import Robot + from api.nostr import Nostr + + robot = Robot.objects.get(id=robot_id) + nostr = Nostr() + async_to_sync(nostr.send_forward_test)(robot) + + return + + @shared_task(name="send_notification", ignore_result=True, time_limit=120) def send_notification(order_id=None, chat_message_id=None, message=None): if order_id: diff --git a/api/views.py b/api/views.py index 6b1719b2d..ea854402c 100644 --- a/api/views.py +++ b/api/views.py @@ -676,6 +676,10 @@ def get(self, request, format=None): context["webhook_enabled"] = user.robot.webhook_enabled context["webhook_api_key"] = user.robot.webhook_api_key + context["nostr_forward_pubkey"] = user.robot.nostr_forward_pubkey + context["nostr_forward_relay"] = user.robot.nostr_forward_relay + context["nostr_forward_enabled"] = user.robot.nostr_forward_enabled + context["last_login"] = user.last_login # Adds/generate telegram token and whether it is enabled @@ -703,11 +707,13 @@ def get(self, request, format=None): @extend_schema(**RobotViewSchema.put) def put(self, request, format=None): """ - Update robot's webhook settings. + Update robot's webhook and nostr forward settings. """ robot = request.user.robot old_webhook_url = robot.webhook_url old_webhook_enabled = robot.webhook_enabled + old_nostr_forward_relay = robot.nostr_forward_relay + old_nostr_forward_enabled = robot.nostr_forward_enabled serializer = UpdateRobotSerializer(robot, data=request.data, partial=True) if not serializer.is_valid(): @@ -715,6 +721,7 @@ def put(self, request, format=None): serializer.save() + # Handle webhook test notification new_webhook_url = request.data.get("webhook_url") new_webhook_enabled = serializer.validated_data.get( "webhook_enabled", old_webhook_enabled @@ -728,6 +735,23 @@ def put(self, request, format=None): Notifications().send_webhook_test(robot) + # Handle nostr forward test notification + new_nostr_forward_relay = request.data.get("nostr_forward_relay") + new_nostr_forward_enabled = serializer.validated_data.get( + "nostr_forward_enabled", old_nostr_forward_enabled + ) + + relay_changed = ( + new_nostr_forward_relay + and new_nostr_forward_relay != old_nostr_forward_relay + ) + nostr_just_enabled = new_nostr_forward_enabled and not old_nostr_forward_enabled + + if relay_changed or nostr_just_enabled: + from api.notifications import Notifications + + Notifications().send_nostr_forward_test(robot) + return Response(serializer.data, status=status.HTTP_200_OK) From 2e38257e3344b2b8334f1265f9885ddadd0bc494 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 18:43:41 +0530 Subject: [PATCH 03/10] feat: added nostr notif fwd to main acc --- api/nostr.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/api/nostr.py b/api/nostr.py index 66afb032f..08f3c8e9a 100644 --- a/api/nostr.py +++ b/api/nostr.py @@ -64,6 +64,43 @@ async def send_notification_event(self, robot, order, text): await client.send_private_msg(PublicKey.parse(robot.nostr_pubkey), text, tags) print("Nostr NOTIFICATION event sent") + await self.send_forward_notification(robot, order, text) + + async def send_forward_notification(self, robot, order, text): + """Sends notification to user's main account via their .onion relay""" + from api.models import Robot + + if not robot.nostr_forward_enabled: + return + if not robot.nostr_forward_pubkey or not robot.nostr_forward_relay: + return + if not Robot.is_valid_onion_url(robot.nostr_forward_relay): + return + + print(f"Forwarding nostr notification to {robot.nostr_forward_relay}") + + keys = Keys.parse(config("NOSTR_NSEC", cast=str)) + signer = NostrSigner.keys(keys) + client = Client(signer) + + await client.add_relay(robot.nostr_forward_relay) + await client.connect() + + tags = [ + Tag.parse( + [ + "order_id", + f"{config('COORDINATOR_ALIAS', cast=str).lower()}/{order.id}", + ] + ), + Tag.parse(["status", str(order.status)]), + ] + + await client.send_private_msg( + PublicKey.parse(robot.nostr_forward_pubkey), text, tags + ) + print("Nostr FORWARD notification sent") + async def send_forward_test(self, robot): """Sends a test notification to user's main nostr account via their .onion relay""" from api.models import Robot From d410f4032e70d29cc0404f8d98e36b012c226539 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 18:57:11 +0530 Subject: [PATCH 04/10] feat: added nostr fwd fields to frontend Robot model --- frontend/src/models/Robot.model.ts | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/models/Robot.model.ts b/frontend/src/models/Robot.model.ts index 1ec02e29e..08f0534f7 100644 --- a/frontend/src/models/Robot.model.ts +++ b/frontend/src/models/Robot.model.ts @@ -30,6 +30,10 @@ class Robot { public webhookEnabled: boolean = false; public webhookApiKey: string = ''; + public nostrForwardPubkey: string = ''; + public nostrForwardRelay: string = ''; + public nostrForwardEnabled: boolean = false; + update = (attributes: object): void => { Object.assign(this, attributes); }; @@ -83,6 +87,9 @@ class Robot { webhookUrl: data.webhook_url ?? '', webhookEnabled: data.webhook_enabled ?? false, webhookApiKey: data.webhook_api_key ?? '', + nostrForwardPubkey: data.nostr_forward_pubkey ?? '', + nostrForwardRelay: data.nostr_forward_relay ?? '', + nostrForwardEnabled: data.nostr_forward_enabled ?? false, }); }) .catch((e) => { @@ -160,6 +167,33 @@ class Robot { }); }; + fetchNostrForward = async ( + federation: Federation, + settings: { + nostr_forward_pubkey?: string; + nostr_forward_relay?: string; + nostr_forward_enabled?: boolean; + }, + ): Promise => { + if (!federation) return; + + const coordinator = federation.getCoordinator(this.shortAlias); + await apiClient + .put(coordinator.url, '/api/robot/', settings, { tokenSHA256: this.tokenSHA256 }) + .then((data) => { + if (data) { + this.update({ + nostrForwardPubkey: data.nostr_forward_pubkey ?? this.nostrForwardPubkey, + nostrForwardRelay: data.nostr_forward_relay ?? this.nostrForwardRelay, + nostrForwardEnabled: data.nostr_forward_enabled ?? this.nostrForwardEnabled, + }); + } + }) + .catch((e) => { + console.log(e); + }); + }; + loadReviewToken = ( federation: Federation, onDataLoad: (token: string) => void = () => {}, From 473c964c1b934a8caa3993602fff05855f790ac8 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 19:32:51 +0530 Subject: [PATCH 05/10] feat: added nostr fwd settings UI --- frontend/src/components/RobotInfo/index.tsx | 128 +++++++++++++++++++- frontend/static/locales/ca.json | 7 ++ frontend/static/locales/cs.json | 7 ++ frontend/static/locales/de.json | 7 ++ frontend/static/locales/en.json | 7 ++ frontend/static/locales/es.json | 7 ++ frontend/static/locales/eu.json | 7 ++ frontend/static/locales/fr.json | 7 ++ frontend/static/locales/it.json | 7 ++ frontend/static/locales/ja.json | 7 ++ frontend/static/locales/pl.json | 7 ++ frontend/static/locales/pt.json | 7 ++ frontend/static/locales/ru.json | 7 ++ frontend/static/locales/sv.json | 7 ++ frontend/static/locales/sw.json | 7 ++ frontend/static/locales/th.json | 7 ++ frontend/static/locales/zh-SI.json | 7 ++ frontend/static/locales/zh-TR.json | 7 ++ 18 files changed, 246 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/RobotInfo/index.tsx b/frontend/src/components/RobotInfo/index.tsx index dfe8e6671..28e8946ac 100644 --- a/frontend/src/components/RobotInfo/index.tsx +++ b/frontend/src/components/RobotInfo/index.tsx @@ -21,7 +21,7 @@ import { DialogContent, DialogActions, } from '@mui/material'; -import { Numbers, Send, EmojiEvents, Webhook } from '@mui/icons-material'; +import { Numbers, Send, EmojiEvents, Webhook, Key } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; import { Robot, type Coordinator } from '../../models'; import { useTranslation } from 'react-i18next'; @@ -63,6 +63,12 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { const [webhookEnabled, setWebhookEnabled] = useState(false); const [webhookSaving, setWebhookSaving] = useState(false); const [webhookUrlError, setWebhookUrlError] = useState(''); + const [openNostrForwardSettings, setOpenNostrForwardSettings] = useState(false); + const [nostrForwardPubkey, setNostrForwardPubkey] = useState(''); + const [nostrForwardRelay, setNostrForwardRelay] = useState(''); + const [nostrForwardEnabled, setNostrForwardEnabled] = useState(false); + const [nostrForwardSaving, setNostrForwardSaving] = useState(false); + const [nostrForwardRelayError, setNostrForwardRelayError] = useState(''); const isValidOnionUrl = (url: string): boolean => { if (!url) return true; @@ -81,6 +87,9 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { setWebhookUrl(robot.webhookUrl ?? ''); setWebhookApiKey(robot.webhookApiKey ?? ''); setWebhookEnabled(robot.webhookEnabled ?? false); + setNostrForwardPubkey(robot.nostrForwardPubkey ?? ''); + setNostrForwardRelay(robot.nostrForwardRelay ?? ''); + setNostrForwardEnabled(robot.nostrForwardEnabled ?? false); } }, [slotUpdatedAt]); @@ -130,6 +139,25 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { setOpenWebhookSettings(false); }; + const handleSaveNostrForwardSettings = async (): Promise => { + if (!robot) return; + + if (nostrForwardRelay && !isValidOnionUrl(nostrForwardRelay)) { + setNostrForwardRelayError(t('URL must be a valid .onion address')); + return; + } + setNostrForwardRelayError(''); + + setNostrForwardSaving(true); + await robot.fetchNostrForward(federation, { + nostr_forward_pubkey: nostrForwardPubkey || undefined, + nostr_forward_relay: nostrForwardRelay || undefined, + nostr_forward_enabled: nostrForwardEnabled, + }); + setNostrForwardSaving(false); + setOpenNostrForwardSettings(false); + }; + return ( <> setOpenOptions(true)}> @@ -367,6 +395,104 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { + {/* Nostr Forward Settings */} + + + + + + + {robot?.nostrForwardEnabled ? ( + + {t('Nostr forwarding enabled')} + + ) : ( + + )} + {robot?.nostrForwardEnabled && ( + + )} + + + + setOpenNostrForwardSettings(false)} + > + + + {t('Nostr Forwarding')} + + + {t('Forward notifications to your main Nostr account via your .onion relay.')} + + + + { + setNostrForwardPubkey(e.target.value); + }} + size='small' + /> + + + { + setNostrForwardRelay(e.target.value); + setNostrForwardRelayError(''); + }} + size='small' + error={Boolean(nostrForwardRelayError)} + helperText={nostrForwardRelayError} + /> + + + setNostrForwardEnabled(e.target.checked)} + /> + } + /> + + + + + + + + + diff --git a/frontend/static/locales/ca.json b/frontend/static/locales/ca.json index 6afc4b18b..a1cd266bf 100644 --- a/frontend/static/locales/ca.json +++ b/frontend/static/locales/ca.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Active order!", "Claim": "Retirar", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Habilita notificacions a Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Ordre inactiva", "Invoice for {{amountSats}} Sats": "Factura per {{amountSats}} Sats", "No active orders": "No hi ha ordres actives", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Anar a ordre activa #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Enviar", diff --git a/frontend/static/locales/cs.json b/frontend/static/locales/cs.json index 1096ec8e1..ff80ebc5d 100644 --- a/frontend/static/locales/cs.json +++ b/frontend/static/locales/cs.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Aktivní objednávka!", "Claim": "Vybrat", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Povolit Telegram notifikace", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Dokončená objednávka", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Neaktivní objednávka", "Invoice for {{amountSats}} Sats": "Faktura pro {{amountSats}} Satů", "No active orders": "Žádné aktivní objednávky", "No orders found": "Nenalezeny žádné objednávky", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Jedna aktivní objednávka #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Odeslat", diff --git a/frontend/static/locales/de.json b/frontend/static/locales/de.json index a6e9c33f5..e0cc77c58 100644 --- a/frontend/static/locales/de.json +++ b/frontend/static/locales/de.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Aktive Bestellung!", "Claim": "Erhalten", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Telegram-Benachrichtigungen aktivieren", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Inaktive Bestellung", "Invoice for {{amountSats}} Sats": "Rechnung für {{amountSats}} Sats", "No active orders": "Keine aktive Bestellung", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Eine aktive Bestellung #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Bestätigen", diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index d3918be1c..c95b0986a 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Active order!", "Claim": "Claim", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Enable Telegram Notifications", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Inactive order", "Invoice for {{amountSats}} Sats": "Invoice for {{amountSats}} Sats", "No active orders": "No active orders", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "One active order #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Submit", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index 9b1a8d8d4..dd410eced 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "¡Orden activa!", "Claim": "Reclamar", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Activar Notificaciones de Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Orden inactiva", "Invoice for {{amountSats}} Sats": "Factura de {{amountSats}} Sats", "No active orders": "No hay órdenes activas", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Una orden activa #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Enviar", diff --git a/frontend/static/locales/eu.json b/frontend/static/locales/eu.json index dd00247e2..d69a9de19 100644 --- a/frontend/static/locales/eu.json +++ b/frontend/static/locales/eu.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Eskaera aktiboa!", "Claim": "Eskatu", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Baimendu Telegram Jakinarazpenak", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Eskaera ez aktiboa", "Invoice for {{amountSats}} Sats": "{{amountSats}} Sateko faktura", "No active orders": "Ez dago eskaera aktiboak", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Eskaera aktiboa #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Bidali", diff --git a/frontend/static/locales/fr.json b/frontend/static/locales/fr.json index afd2396fe..70a555959 100644 --- a/frontend/static/locales/fr.json +++ b/frontend/static/locales/fr.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Ordre actif!", "Claim": "Réclamer", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Activer les notifications Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Ordre inactif", "Invoice for {{amountSats}} Sats": "Facture pour {{amountSats}} Sats", "No active orders": "Aucun ordre actif", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Un ordre actif #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Soumettre", diff --git a/frontend/static/locales/it.json b/frontend/static/locales/it.json index 4e4d40ee3..7aa1e0fab 100644 --- a/frontend/static/locales/it.json +++ b/frontend/static/locales/it.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Ordine attivo!", "Claim": "Richiedi", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Abilita Notifiche Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Ordine inattivo", "Invoice for {{amountSats}} Sats": "Fattura per {{amountSats}} Sats", "No active orders": "Nessun ordine attivo", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Un ordine attivo #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Invia", diff --git a/frontend/static/locales/ja.json b/frontend/static/locales/ja.json index 0a8c9ba38..e07d9da21 100644 --- a/frontend/static/locales/ja.json +++ b/frontend/static/locales/ja.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "アクティブな注文!", "Claim": "請求する", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Telegram通知を有効にする", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "非アクティブなオーダー", "Invoice for {{amountSats}} Sats": " {{amountSats}} Satsのインボイス", "No active orders": "アクティブなオーダーはありません", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "アクティブなオーダー #{{orderID}} に進む", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "送信", diff --git a/frontend/static/locales/pl.json b/frontend/static/locales/pl.json index 56cded3eb..c8f88029f 100644 --- a/frontend/static/locales/pl.json +++ b/frontend/static/locales/pl.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Aktywne zamówienie!", "Claim": "Odebrać", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Włącz powiadomienia Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Nieaktywne zamówienie", "Invoice for {{amountSats}} Sats": "Faktura za {{amountSats}} Sats", "No active orders": "Brak aktywnych zamówień", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Jedno aktywne zamówienie #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Prześlij", diff --git a/frontend/static/locales/pt.json b/frontend/static/locales/pt.json index b4ac1194a..f55294081 100644 --- a/frontend/static/locales/pt.json +++ b/frontend/static/locales/pt.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Ordem ativa!", "Claim": "Reivindicar", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Habilitar notificações do Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Ordem inativa", "Invoice for {{amountSats}} Sats": "Fatura para {{amountSats}} Sats", "No active orders": "Nenhuma ordem ativa", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Uma ordem ativa #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Enviar", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index 71e1a97cd..1f0c33bb5 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Активный ордер!", "Claim": "Запросить", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Включить уведомления Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Неактивный ордер", "Invoice for {{amountSats}} Sats": "Инвойс на {{amountSats}} Сатоши", "No active orders": "Нет активных ордеров", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Один активный ордер #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Отправить", diff --git a/frontend/static/locales/sv.json b/frontend/static/locales/sv.json index af1367887..c1e2fedfc 100644 --- a/frontend/static/locales/sv.json +++ b/frontend/static/locales/sv.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Aktiv order!", "Claim": "Anspråk", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Aktivera Telegram-aviseringar", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Inaktiv order", "Invoice for {{amountSats}} Sats": "Faktura för {{amountSats}} sats", "No active orders": "Inga aktiva ordrar", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "En aktiv order #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Skicka", diff --git a/frontend/static/locales/sw.json b/frontend/static/locales/sw.json index a23f5775c..734a829db 100644 --- a/frontend/static/locales/sw.json +++ b/frontend/static/locales/sw.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "Agizo la Kazi!", "Claim": "Dai", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "Washa Arifa za Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "Agizo lisilo hai", "Invoice for {{amountSats}} Sats": "Ankara kwa {{amountSats}} Sats", "No active orders": "Hakuna maagizo yanayofanya kazi", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Agizo moja la kazi #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "Wasilisha", diff --git a/frontend/static/locales/th.json b/frontend/static/locales/th.json index 528098038..133bd7f22 100644 --- a/frontend/static/locales/th.json +++ b/frontend/static/locales/th.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "คำสั่งซื้อที่ใช้งานอยู่!", "Claim": "รับรางวัล", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "เปิดใช้การแจ้งเตือน Telegram", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "คำสั่งซื้อที่ไม่ใช้งาน", "Invoice for {{amountSats}} Sats": "ใบแจ้งหนี้สำหรับ {{amountSats}} Sats", "No active orders": "ไม่มีคำสั่งซื้อที่ใช้งานอยู่", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "มี 1 คำสั่งซื้อที่กำลังดำเนินการอยู่ #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "ส่งข้อมูล", diff --git a/frontend/static/locales/zh-SI.json b/frontend/static/locales/zh-SI.json index fe80fb54b..31ce950f3 100644 --- a/frontend/static/locales/zh-SI.json +++ b/frontend/static/locales/zh-SI.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "活跃订单!", "Claim": "领取", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "开启电报通知", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "不活跃订单", "Invoice for {{amountSats}} Sats": "{{amountSats}}聪的发票", "No active orders": "没有活跃的订单", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "一个活跃的订单#{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "提交", diff --git a/frontend/static/locales/zh-TR.json b/frontend/static/locales/zh-TR.json index 79790f3a3..9679e2b9e 100644 --- a/frontend/static/locales/zh-TR.json +++ b/frontend/static/locales/zh-TR.json @@ -514,15 +514,22 @@ "API Key (optional)": "API Key (optional)", "Active order!": "活躍訂單!", "Claim": "索取", + "Configure Nostr Forwarding": "Configure Nostr Forwarding", "Configure Webhook": "Configure Webhook", "Edit": "Edit", + "Enable Nostr forwarding": "Enable Nostr forwarding", "Enable Telegram Notifications": "啟用 Telegram 通知", "Enable webhook notifications": "Enable webhook notifications", "Finished order": "Finished order", + "Forward notifications to your main Nostr account via your .onion relay.": "Forward notifications to your main Nostr account via your .onion relay.", + "Forward pubkey (npub or hex)": "Forward pubkey (npub or hex)", + "Forward relay (.onion only)": "Forward relay (.onion only)", "Inactive order": "不活躍的訂單", "Invoice for {{amountSats}} Sats": "{{amountSats}} 聰的發票", "No active orders": "沒有活躍的訂單", "No orders found": "No orders found", + "Nostr Forwarding": "Nostr Forwarding", + "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "一個活躍的訂單 #{{orderID}}", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", "Submit": "提交", From dca81d2c140cb8f61c676196bdeb8a7f0f461e55 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Thu, 5 Feb 2026 20:35:13 +0530 Subject: [PATCH 06/10] feat: added forwarding tests and fix relay field type --- .../0058_add_nostr_forward_fields.py | 4 +- api/models/robot.py | 2 +- api/oas_schemas.py | 56 ++++++ docs/assets/schemas/api-latest.yaml | 47 +++++ tests/test_api_robot_nostr_forward.py | 163 ++++++++++++++++++ 5 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 tests/test_api_robot_nostr_forward.py diff --git a/api/migrations/0058_add_nostr_forward_fields.py b/api/migrations/0058_add_nostr_forward_fields.py index cda213837..e88427d7f 100644 --- a/api/migrations/0058_add_nostr_forward_fields.py +++ b/api/migrations/0058_add_nostr_forward_fields.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.15 on 2026-02-05 12:20 +# Generated by Django 5.1.15 on 2026-02-05 14:55 from django.db import migrations, models @@ -23,6 +23,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='robot', name='nostr_forward_relay', - field=models.URLField(blank=True, max_length=500, null=True), + field=models.CharField(blank=True, max_length=500, null=True), ), ] diff --git a/api/models/robot.py b/api/models/robot.py index 7b2bb90da..310f68749 100644 --- a/api/models/robot.py +++ b/api/models/robot.py @@ -51,7 +51,7 @@ class Robot(models.Model): # Nostr forwarding to main account nostr_forward_pubkey = models.CharField(max_length=64, null=True, blank=True) - nostr_forward_relay = models.URLField(max_length=500, null=True, blank=True) + nostr_forward_relay = models.CharField(max_length=500, null=True, blank=True) nostr_forward_enabled = models.BooleanField(default=False, null=False) # Claimable rewards diff --git a/api/oas_schemas.py b/api/oas_schemas.py index 0e561f460..7a028088f 100644 --- a/api/oas_schemas.py +++ b/api/oas_schemas.py @@ -559,6 +559,21 @@ class RobotViewSchema: "nullable": True, "description": "API key sent in X-API-Key header for webhook authentication", }, + "nostr_forward_pubkey": { + "type": "string", + "nullable": True, + "description": "Nostr public key (hex) for forwarding notifications", + }, + "nostr_forward_relay": { + "type": "string", + "nullable": True, + "description": "Nostr relay URL for forwarding notifications (.onion only)", + }, + "nostr_forward_enabled": { + "type": "boolean", + "default": False, + "description": "Whether Nostr forwarding notifications are enabled", + }, }, }, }, @@ -599,6 +614,12 @@ class RobotViewSchema: **Note:** Webhook is enabled automatically when a valid .onion URL is set. A test notification will be sent when the webhook URL is configured. + + ### Nostr Forwarding + + You can also configure Nostr direct message forwarding. Provide your main Nostr + public key (hex format) and a .onion relay URL to receive trade notifications + as encrypted DMs from the coordinator's Nostr identity. """ ), "responses": { @@ -619,6 +640,20 @@ class RobotViewSchema: "nullable": True, "description": "API key sent in X-API-Key header", }, + "nostr_forward_pubkey": { + "type": "string", + "nullable": True, + "description": "Nostr public key (hex) for forwarding notifications", + }, + "nostr_forward_relay": { + "type": "string", + "nullable": True, + "description": "Nostr relay URL for forwarding notifications (.onion only)", + }, + "nostr_forward_enabled": { + "type": "boolean", + "description": "Whether Nostr forwarding notifications are enabled", + }, }, }, 400: { @@ -629,6 +664,11 @@ class RobotViewSchema: "items": {"type": "string"}, "description": "Validation errors for webhook_url field", }, + "nostr_forward_relay": { + "type": "array", + "items": {"type": "string"}, + "description": "Validation errors for nostr_forward_relay field", + }, }, }, }, @@ -642,6 +682,15 @@ class RobotViewSchema: }, status_codes=[200], ), + OpenApiExample( + "Successfully updated Nostr forwarding settings", + value={ + "nostr_forward_pubkey": "abcd1234...", + "nostr_forward_relay": "ws://relay.onion/", + "nostr_forward_enabled": True, + }, + status_codes=[200], + ), OpenApiExample( "Invalid URL (not .onion)", value={ @@ -649,6 +698,13 @@ class RobotViewSchema: }, status_codes=[400], ), + OpenApiExample( + "Invalid relay URL (not .onion)", + value={ + "nostr_forward_relay": ["Nostr relay must be a Tor .onion address"], + }, + status_codes=[400], + ), ], } diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index e61b5ace6..ecf298450 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -925,6 +925,19 @@ paths: type: string nullable: true description: API key sent in X-API-Key header for webhook authentication + nostr_forward_pubkey: + type: string + nullable: true + description: Nostr public key (hex) for forwarding notifications + nostr_forward_relay: + type: string + nullable: true + description: Nostr relay URL for forwarding notifications (.onion + only) + nostr_forward_enabled: + type: boolean + default: false + description: Whether Nostr forwarding notifications are enabled examples: SuccessfullyRetrievedRobot: value: @@ -965,6 +978,12 @@ paths: **Note:** Webhook is enabled automatically when a valid .onion URL is set. A test notification will be sent when the webhook URL is configured. + + ### Nostr Forwarding + + You can also configure Nostr direct message forwarding. Provide your main Nostr + public key (hex format) and a .onion relay URL to receive trade notifications + as encrypted DMs from the coordinator's Nostr identity. summary: Update robot webhook settings tags: - robot @@ -988,6 +1007,18 @@ paths: type: string nullable: true description: API key sent in X-API-Key header + nostr_forward_pubkey: + type: string + nullable: true + description: Nostr public key (hex) for forwarding notifications + nostr_forward_relay: + type: string + nullable: true + description: Nostr relay URL for forwarding notifications (.onion + only) + nostr_forward_enabled: + type: boolean + description: Whether Nostr forwarding notifications are enabled examples: SuccessfullyUpdatedWebhookSettings: value: @@ -995,6 +1026,12 @@ paths: webhook_enabled: true webhook_api_key: my-secret-key summary: Successfully updated webhook settings + SuccessfullyUpdatedNostrForwardingSettings: + value: + nostr_forward_pubkey: abcd1234... + nostr_forward_relay: ws://relay.onion/ + nostr_forward_enabled: true + summary: Successfully updated Nostr forwarding settings description: '' '400': content: @@ -1007,12 +1044,22 @@ paths: items: type: string description: Validation errors for webhook_url field + nostr_forward_relay: + type: array + items: + type: string + description: Validation errors for nostr_forward_relay field examples: InvalidURL(not.onion): value: webhook_url: - Webhook URL must be a Tor .onion address summary: Invalid URL (not .onion) + InvalidRelayURL(not.onion): + value: + nostr_forward_relay: + - Nostr relay must be a Tor .onion address + summary: Invalid relay URL (not .onion) description: '' /api/stealth/: post: diff --git a/tests/test_api_robot_nostr_forward.py b/tests/test_api_robot_nostr_forward.py new file mode 100644 index 000000000..09dd404be --- /dev/null +++ b/tests/test_api_robot_nostr_forward.py @@ -0,0 +1,163 @@ +""" +Tests for Robot Nostr Forward API endpoints. +Tests the Nostr forwarding configuration functionality: +- GET /api/robot/ returns nostr_forward fields +- PUT /api/robot/ updates nostr forward settings +- Validation: nostr_forward_relay must be .onion +""" + +from unittest.mock import patch + +from django.urls import reverse + +from tests.test_api import BaseAPITestCase + + +def read_file(file_path): + """Read a file and return its content.""" + with open(file_path, "r") as file: + return file.read() + + +class RobotNostrForwardAPITest(BaseAPITestCase): + """Test Nostr forwarding configuration via Robot API endpoints.""" + + robot_index = 1 # Use pre-generated test robot + + def get_robot_auth(self): + """ + Create an AUTH header using pre-generated test robot credentials. + """ + b91_token = read_file(f"tests/robots/{self.robot_index}/b91_token") + pub_key = read_file(f"tests/robots/{self.robot_index}/pub_key") + enc_priv_key = read_file(f"tests/robots/{self.robot_index}/enc_priv_key") + nostr_pubkey = read_file(f"tests/robots/{self.robot_index}/nostr_pubkey") + + return { + "HTTP_AUTHORIZATION": f"Token {b91_token} | Public {pub_key} | Private {enc_priv_key} | Nostr {nostr_pubkey}" + } + + def test_robot_get_includes_nostr_forward_fields(self): + """Test that GET /api/robot/ returns nostr forward configuration fields.""" + path = reverse("robot") + headers = self.get_robot_auth() + + response = self.client.get(path, **headers) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + # Verify nostr forward fields are present in response + self.assertIn("nostr_forward_pubkey", data) + self.assertIn("nostr_forward_relay", data) + self.assertIn("nostr_forward_enabled", data) + + @patch("api.notifications.Notifications.send_nostr_forward_test") + def test_robot_put_update_nostr_forward_settings(self, mock_send_test): + """Test that PUT /api/robot/ updates nostr forward settings.""" + mock_send_test.return_value = True + + path = reverse("robot") + headers = self.get_robot_auth() + + update_data = { + "nostr_forward_pubkey": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", + "nostr_forward_relay": "ws://testrelay123abc.onion/", + "nostr_forward_enabled": True, + } + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + data = response.json() + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + self.assertEqual( + data["nostr_forward_pubkey"], + "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", + ) + self.assertEqual(data["nostr_forward_relay"], "ws://testrelay123abc.onion/") + self.assertEqual(data["nostr_forward_enabled"], True) + + def test_robot_put_partial_update_nostr_forward(self): + """Test that PUT /api/robot/ allows partial updates (pubkey only).""" + path = reverse("robot") + headers = self.get_robot_auth() + + update_data = { + "nostr_forward_pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + } + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + self.assertEqual( + response.json()["nostr_forward_pubkey"], + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + ) + + def test_robot_put_rejects_non_onion_relay(self): + """Test that PUT /api/robot/ rejects non-.onion relay URLs.""" + path = reverse("robot") + headers = self.get_robot_auth() + + update_data = {"nostr_forward_relay": "wss://relay.damus.io"} + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("nostr_forward_relay", response.json()) + + @patch("api.notifications.Notifications.send_nostr_forward_test") + def test_robot_put_accepts_valid_onion_relay(self, mock_send_test): + """Test that PUT /api/robot/ accepts valid .onion relay URLs.""" + mock_send_test.return_value = True + + path = reverse("robot") + headers = self.get_robot_auth() + + test_relay = "ws://testrelay.onion/" + update_data = { + "nostr_forward_relay": test_relay, + "nostr_forward_enabled": True, + } + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + + def test_robot_put_toggle_nostr_forward_enabled(self): + """Test that PUT /api/robot/ can toggle nostr_forward_enabled.""" + path = reverse("robot") + headers = self.get_robot_auth() + + # Enable + response = self.client.put( + path, + data={"nostr_forward_enabled": True}, + content_type="application/json", + **headers, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["nostr_forward_enabled"], True) + + # Disable + response = self.client.put( + path, + data={"nostr_forward_enabled": False}, + content_type="application/json", + **headers, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["nostr_forward_enabled"], False) From b04f0c546effd96da7ed7f6c439e82f10b90ac47 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Sun, 26 Apr 2026 16:21:42 +0530 Subject: [PATCH 07/10] fix(android): send robot settings PUT requests --- frontend/src/services/api/ApiAndroidClient/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/services/api/ApiAndroidClient/index.ts b/frontend/src/services/api/ApiAndroidClient/index.ts index d83ef710c..d9cd4f309 100644 --- a/frontend/src/services/api/ApiAndroidClient/index.ts +++ b/frontend/src/services/api/ApiAndroidClient/index.ts @@ -66,10 +66,11 @@ class ApiAndroidClient implements ApiClient { body: object, auth?: Auth, silent?: boolean, - ) => Promise = async (_baseUrl, _path, _body, _auth, _silent) => { - return await new Promise((resolve, _reject) => { - resolve({}); - }); + ) => Promise = async (baseUrl, path, body, auth, silent = false) => { + const jsonHeaders = JSON.stringify(this.getHeaders(auth)); + const jsonBody = JSON.stringify(body); + + return await this.request('PUT', baseUrl, path, jsonHeaders, jsonBody, silent); }; public delete: ( From 912ac356f685fd686cb29f0fcfa3f4a0b8a50f3f Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Sun, 26 Apr 2026 16:34:18 +0530 Subject: [PATCH 08/10] fix(nostr): validate forwarding settings --- api/models/robot.py | 13 ++++ api/notifications.py | 4 +- api/oas_schemas.py | 28 +++++-- api/serializers.py | 15 +++- api/tasks.py | 1 + api/views.py | 13 +++- docs/assets/schemas/api-latest.yaml | 26 +++++-- frontend/src/components/RobotInfo/index.tsx | 36 ++++++++- frontend/static/locales/ca.json | 2 + frontend/static/locales/cs.json | 2 + frontend/static/locales/de.json | 2 + frontend/static/locales/en.json | 2 + frontend/static/locales/es.json | 2 + frontend/static/locales/eu.json | 2 + frontend/static/locales/fr.json | 2 + frontend/static/locales/it.json | 2 + frontend/static/locales/ja.json | 2 + frontend/static/locales/pl.json | 2 + frontend/static/locales/pt.json | 2 + frontend/static/locales/ru.json | 2 + frontend/static/locales/sv.json | 2 + frontend/static/locales/sw.json | 2 + frontend/static/locales/th.json | 2 + frontend/static/locales/zh-SI.json | 2 + frontend/static/locales/zh-TR.json | 2 + tests/test_api_robot_nostr_forward.py | 81 +++++++++++++++++++++ 26 files changed, 230 insertions(+), 21 deletions(-) diff --git a/api/models/robot.py b/api/models/robot.py index 310f68749..605679e6e 100644 --- a/api/models/robot.py +++ b/api/models/robot.py @@ -110,5 +110,18 @@ def is_valid_onion_url(url): except Exception: return False + @staticmethod + def is_valid_onion_relay_url(url): + """Validates that the URL is a websocket .onion relay.""" + if not Robot.is_valid_onion_url(url): + return False + try: + from urllib.parse import urlparse + + parsed = urlparse(url) + return parsed.scheme in ["ws", "wss"] + except Exception: + return False + def __str__(self): return self.user.username diff --git a/api/notifications.py b/api/notifications.py index 9d87ad49e..601a96bd4 100644 --- a/api/notifications.py +++ b/api/notifications.py @@ -191,9 +191,9 @@ def send_nostr_forward_test(self, robot): ) return False - if not Robot.is_valid_onion_url(relay_url): + if not Robot.is_valid_onion_relay_url(relay_url): logger.warning( - f"Nostr forward test rejected: not a .onion address for robot {robot.id}" + f"Nostr forward test rejected: not a websocket .onion relay for robot {robot.id}" ) return False diff --git a/api/oas_schemas.py b/api/oas_schemas.py index 7a028088f..9ce71a394 100644 --- a/api/oas_schemas.py +++ b/api/oas_schemas.py @@ -562,12 +562,12 @@ class RobotViewSchema: "nostr_forward_pubkey": { "type": "string", "nullable": True, - "description": "Nostr public key (hex) for forwarding notifications", + "description": "Nostr public key (hex or npub) for forwarding notifications", }, "nostr_forward_relay": { "type": "string", "nullable": True, - "description": "Nostr relay URL for forwarding notifications (.onion only)", + "description": "Nostr relay websocket URL for forwarding notifications (.onion only)", }, "nostr_forward_enabled": { "type": "boolean", @@ -618,7 +618,7 @@ class RobotViewSchema: ### Nostr Forwarding You can also configure Nostr direct message forwarding. Provide your main Nostr - public key (hex format) and a .onion relay URL to receive trade notifications + public key (hex or npub) and a .onion relay websocket URL to receive trade notifications as encrypted DMs from the coordinator's Nostr identity. """ ), @@ -643,12 +643,12 @@ class RobotViewSchema: "nostr_forward_pubkey": { "type": "string", "nullable": True, - "description": "Nostr public key (hex) for forwarding notifications", + "description": "Nostr public key (hex or npub) for forwarding notifications", }, "nostr_forward_relay": { "type": "string", "nullable": True, - "description": "Nostr relay URL for forwarding notifications (.onion only)", + "description": "Nostr relay websocket URL for forwarding notifications (.onion only)", }, "nostr_forward_enabled": { "type": "boolean", @@ -669,6 +669,11 @@ class RobotViewSchema: "items": {"type": "string"}, "description": "Validation errors for nostr_forward_relay field", }, + "nostr_forward_pubkey": { + "type": "array", + "items": {"type": "string"}, + "description": "Validation errors for nostr_forward_pubkey field", + }, }, }, }, @@ -701,7 +706,18 @@ class RobotViewSchema: OpenApiExample( "Invalid relay URL (not .onion)", value={ - "nostr_forward_relay": ["Nostr relay must be a Tor .onion address"], + "nostr_forward_relay": [ + "Nostr relay must be a Tor .onion websocket URL" + ], + }, + status_codes=[400], + ), + OpenApiExample( + "Invalid Nostr forward pubkey", + value={ + "nostr_forward_pubkey": [ + "Nostr forward pubkey must be a valid hex or npub public key" + ], }, status_codes=[400], ), diff --git a/api/serializers.py b/api/serializers.py index 70bdc2dd0..5a87b5095 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,7 @@ from decouple import config from decimal import Decimal from rest_framework import serializers +from nostr_sdk import PublicKey from .models import MarketTick, Order, Notification, Robot @@ -758,8 +759,18 @@ def validate_webhook_url(self, value): return value def validate_nostr_forward_relay(self, value): - if value and not Robot.is_valid_onion_url(value): + if value and not Robot.is_valid_onion_relay_url(value): raise serializers.ValidationError( - "Nostr relay must be a Tor .onion address" + "Nostr relay must be a Tor .onion websocket URL" ) return value + + def validate_nostr_forward_pubkey(self, value): + if value: + try: + PublicKey.parse(value) + except Exception: + raise serializers.ValidationError( + "Nostr forward pubkey must be a valid hex or npub public key" + ) + return value diff --git a/api/tasks.py b/api/tasks.py index 2787702f2..f4bfb1e04 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -31,6 +31,7 @@ def users_cleansing(): or user.robot.claimed_rewards > 0 or user.robot.telegram_enabled is True or user.robot.webhook_enabled is True + or user.robot.nostr_forward_enabled is True ): continue if not user.robot.total_contracts == 0: diff --git a/api/views.py b/api/views.py index ea854402c..c43b9bfc5 100644 --- a/api/views.py +++ b/api/views.py @@ -712,6 +712,7 @@ def put(self, request, format=None): robot = request.user.robot old_webhook_url = robot.webhook_url old_webhook_enabled = robot.webhook_enabled + old_nostr_forward_pubkey = robot.nostr_forward_pubkey old_nostr_forward_relay = robot.nostr_forward_relay old_nostr_forward_enabled = robot.nostr_forward_enabled serializer = UpdateRobotSerializer(robot, data=request.data, partial=True) @@ -736,18 +737,28 @@ def put(self, request, format=None): Notifications().send_webhook_test(robot) # Handle nostr forward test notification + new_nostr_forward_pubkey = request.data.get("nostr_forward_pubkey") new_nostr_forward_relay = request.data.get("nostr_forward_relay") new_nostr_forward_enabled = serializer.validated_data.get( "nostr_forward_enabled", old_nostr_forward_enabled ) + pubkey_changed = ( + new_nostr_forward_pubkey + and new_nostr_forward_pubkey != old_nostr_forward_pubkey + ) relay_changed = ( new_nostr_forward_relay and new_nostr_forward_relay != old_nostr_forward_relay ) nostr_just_enabled = new_nostr_forward_enabled and not old_nostr_forward_enabled + has_nostr_forward_target = ( + robot.nostr_forward_pubkey and robot.nostr_forward_relay + ) - if relay_changed or nostr_just_enabled: + if has_nostr_forward_target and ( + pubkey_changed or relay_changed or nostr_just_enabled + ): from api.notifications import Notifications Notifications().send_nostr_forward_test(robot) diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index ecf298450..29521e9cf 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -928,12 +928,12 @@ paths: nostr_forward_pubkey: type: string nullable: true - description: Nostr public key (hex) for forwarding notifications + description: Nostr public key (hex or npub) for forwarding notifications nostr_forward_relay: type: string nullable: true - description: Nostr relay URL for forwarding notifications (.onion - only) + description: Nostr relay websocket URL for forwarding notifications + (.onion only) nostr_forward_enabled: type: boolean default: false @@ -982,7 +982,7 @@ paths: ### Nostr Forwarding You can also configure Nostr direct message forwarding. Provide your main Nostr - public key (hex format) and a .onion relay URL to receive trade notifications + public key (hex or npub) and a .onion relay websocket URL to receive trade notifications as encrypted DMs from the coordinator's Nostr identity. summary: Update robot webhook settings tags: @@ -1010,12 +1010,12 @@ paths: nostr_forward_pubkey: type: string nullable: true - description: Nostr public key (hex) for forwarding notifications + description: Nostr public key (hex or npub) for forwarding notifications nostr_forward_relay: type: string nullable: true - description: Nostr relay URL for forwarding notifications (.onion - only) + description: Nostr relay websocket URL for forwarding notifications + (.onion only) nostr_forward_enabled: type: boolean description: Whether Nostr forwarding notifications are enabled @@ -1049,6 +1049,11 @@ paths: items: type: string description: Validation errors for nostr_forward_relay field + nostr_forward_pubkey: + type: array + items: + type: string + description: Validation errors for nostr_forward_pubkey field examples: InvalidURL(not.onion): value: @@ -1058,8 +1063,13 @@ paths: InvalidRelayURL(not.onion): value: nostr_forward_relay: - - Nostr relay must be a Tor .onion address + - Nostr relay must be a Tor .onion websocket URL summary: Invalid relay URL (not .onion) + InvalidNostrForwardPubkey: + value: + nostr_forward_pubkey: + - Nostr forward pubkey must be a valid hex or npub public key + summary: Invalid Nostr forward pubkey description: '' /api/stealth/: post: diff --git a/frontend/src/components/RobotInfo/index.tsx b/frontend/src/components/RobotInfo/index.tsx index 28e8946ac..84e74310b 100644 --- a/frontend/src/components/RobotInfo/index.tsx +++ b/frontend/src/components/RobotInfo/index.tsx @@ -23,6 +23,7 @@ import { } from '@mui/material'; import { Numbers, Send, EmojiEvents, Webhook, Key } from '@mui/icons-material'; import { useNavigate } from 'react-router-dom'; +import { nip19 } from 'nostr-tools'; import { Robot, type Coordinator } from '../../models'; import { useTranslation } from 'react-i18next'; import { EnableTelegramDialog } from '../Dialogs'; @@ -68,6 +69,7 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { const [nostrForwardRelay, setNostrForwardRelay] = useState(''); const [nostrForwardEnabled, setNostrForwardEnabled] = useState(false); const [nostrForwardSaving, setNostrForwardSaving] = useState(false); + const [nostrForwardPubkeyError, setNostrForwardPubkeyError] = useState(''); const [nostrForwardRelayError, setNostrForwardRelayError] = useState(''); const isValidOnionUrl = (url: string): boolean => { @@ -80,6 +82,27 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { } }; + const isValidOnionRelayUrl = (url: string): boolean => { + if (!url) return true; + try { + const parsed = new URL(url); + return parsed.hostname.endsWith('.onion') && ['ws:', 'wss:'].includes(parsed.protocol); + } catch { + return false; + } + }; + + const isValidNostrPubkey = (pubkey: string): boolean => { + if (!pubkey) return true; + if (/^[0-9a-fA-F]{64}$/.test(pubkey)) return true; + + try { + return nip19.decode(pubkey).type === 'npub'; + } catch { + return false; + } + }; + useEffect(() => { const robot = garage.getSlot()?.getRobot(coordinator.shortAlias) ?? null; setRobot(robot); @@ -142,8 +165,14 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { const handleSaveNostrForwardSettings = async (): Promise => { if (!robot) return; - if (nostrForwardRelay && !isValidOnionUrl(nostrForwardRelay)) { - setNostrForwardRelayError(t('URL must be a valid .onion address')); + if (nostrForwardPubkey && !isValidNostrPubkey(nostrForwardPubkey)) { + setNostrForwardPubkeyError(t('Pubkey must be valid hex or npub')); + return; + } + setNostrForwardPubkeyError(''); + + if (nostrForwardRelay && !isValidOnionRelayUrl(nostrForwardRelay)) { + setNostrForwardRelayError(t('Relay must be a valid ws:// or wss:// .onion address')); return; } setNostrForwardRelayError(''); @@ -449,8 +478,11 @@ const RobotInfo: React.FC = ({ coordinator, onClose }: Props) => { value={nostrForwardPubkey} onChange={(e) => { setNostrForwardPubkey(e.target.value); + setNostrForwardPubkeyError(''); }} size='small' + error={Boolean(nostrForwardPubkeyError)} + helperText={nostrForwardPubkeyError} /> diff --git a/frontend/static/locales/ca.json b/frontend/static/locales/ca.json index a1cd266bf..5d6a11888 100644 --- a/frontend/static/locales/ca.json +++ b/frontend/static/locales/ca.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Anar a ordre activa #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Enviar", "Telegram enabled": "Telegram activat", "There it goes!": "Aquí va!", diff --git a/frontend/static/locales/cs.json b/frontend/static/locales/cs.json index ff80ebc5d..f490eaaeb 100644 --- a/frontend/static/locales/cs.json +++ b/frontend/static/locales/cs.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Jedna aktivní objednávka #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Odeslat", "Telegram enabled": "Telegram povolen", "There it goes!": "A je to!", diff --git a/frontend/static/locales/de.json b/frontend/static/locales/de.json index e0cc77c58..72f544c59 100644 --- a/frontend/static/locales/de.json +++ b/frontend/static/locales/de.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Eine aktive Bestellung #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Bestätigen", "Telegram enabled": "Telegram aktiviert", "There it goes!": "Da geht es hin!", diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index c95b0986a..726f33a49 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "One active order #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Submit", "Telegram enabled": "Telegram enabled", "There it goes!": "There it goes!", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index dd410eced..f76948185 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Una orden activa #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Enviar", "Telegram enabled": "Telegram activado", "There it goes!": "¡Ahí va!", diff --git a/frontend/static/locales/eu.json b/frontend/static/locales/eu.json index d69a9de19..3a2bcaa24 100644 --- a/frontend/static/locales/eu.json +++ b/frontend/static/locales/eu.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Eskaera aktiboa #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Bidali", "Telegram enabled": "Telegram baimendua", "There it goes!": "Hementxe doa!", diff --git a/frontend/static/locales/fr.json b/frontend/static/locales/fr.json index 70a555959..2981b9814 100644 --- a/frontend/static/locales/fr.json +++ b/frontend/static/locales/fr.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Un ordre actif #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Soumettre", "Telegram enabled": "Telegram activé", "There it goes!": "Là ça va!", diff --git a/frontend/static/locales/it.json b/frontend/static/locales/it.json index 7aa1e0fab..7281b7199 100644 --- a/frontend/static/locales/it.json +++ b/frontend/static/locales/it.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Un ordine attivo #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Invia", "Telegram enabled": "Telegram abilitato", "There it goes!": "Ecco!", diff --git a/frontend/static/locales/ja.json b/frontend/static/locales/ja.json index e07d9da21..a6300ed83 100644 --- a/frontend/static/locales/ja.json +++ b/frontend/static/locales/ja.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "アクティブなオーダー #{{orderID}} に進む", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "送信", "Telegram enabled": "Telegramが有効になりました", "There it goes!": "そこにあります!", diff --git a/frontend/static/locales/pl.json b/frontend/static/locales/pl.json index c8f88029f..7e7a3e0b6 100644 --- a/frontend/static/locales/pl.json +++ b/frontend/static/locales/pl.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Jedno aktywne zamówienie #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Prześlij", "Telegram enabled": "Telegram włączony", "There it goes!": "Oto idzie!", diff --git a/frontend/static/locales/pt.json b/frontend/static/locales/pt.json index f55294081..b4ab990d1 100644 --- a/frontend/static/locales/pt.json +++ b/frontend/static/locales/pt.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Uma ordem ativa #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Enviar", "Telegram enabled": "Telegram ativado", "There it goes!": "Lá vai!", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index 1f0c33bb5..46c6419b7 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Один активный ордер #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Отправить", "Telegram enabled": "Telegram включен", "There it goes!": "Вот оно идет!", diff --git a/frontend/static/locales/sv.json b/frontend/static/locales/sv.json index c1e2fedfc..23e7ee9f3 100644 --- a/frontend/static/locales/sv.json +++ b/frontend/static/locales/sv.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "En aktiv order #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Skicka", "Telegram enabled": "Telegram aktiverat", "There it goes!": "Där går det!", diff --git a/frontend/static/locales/sw.json b/frontend/static/locales/sw.json index 734a829db..90b62c24a 100644 --- a/frontend/static/locales/sw.json +++ b/frontend/static/locales/sw.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "Agizo moja la kazi #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "Wasilisha", "Telegram enabled": "Telegram imewezeshwa", "There it goes!": "Hapo mwenda!", diff --git a/frontend/static/locales/th.json b/frontend/static/locales/th.json index 133bd7f22..b58e378ee 100644 --- a/frontend/static/locales/th.json +++ b/frontend/static/locales/th.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "มี 1 คำสั่งซื้อที่กำลังดำเนินการอยู่ #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "ส่งข้อมูล", "Telegram enabled": "เปิดใช้ Telegram แล้ว", "There it goes!": "นั่นไง!", diff --git a/frontend/static/locales/zh-SI.json b/frontend/static/locales/zh-SI.json index 31ce950f3..c5cae8c22 100644 --- a/frontend/static/locales/zh-SI.json +++ b/frontend/static/locales/zh-SI.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "一个活跃的订单#{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "提交", "Telegram enabled": "电报已启用", "There it goes!": "那就这样!", diff --git a/frontend/static/locales/zh-TR.json b/frontend/static/locales/zh-TR.json index 9679e2b9e..785a01b21 100644 --- a/frontend/static/locales/zh-TR.json +++ b/frontend/static/locales/zh-TR.json @@ -531,7 +531,9 @@ "Nostr Forwarding": "Nostr Forwarding", "Nostr forwarding enabled": "Nostr forwarding enabled", "One active order #{{orderID}}": "一個活躍的訂單 #{{orderID}}", + "Pubkey must be valid hex or npub": "Pubkey must be valid hex or npub", "Receive notifications via HTTP POST to your own .onion server.": "Receive notifications via HTTP POST to your own .onion server.", + "Relay must be a valid ws:// or wss:// .onion address": "Relay must be a valid ws:// or wss:// .onion address", "Submit": "提交", "Telegram enabled": "Telegram 已啟用", "There it goes!": "這就行了!", diff --git a/tests/test_api_robot_nostr_forward.py b/tests/test_api_robot_nostr_forward.py index 09dd404be..a8e4b126d 100644 --- a/tests/test_api_robot_nostr_forward.py +++ b/tests/test_api_robot_nostr_forward.py @@ -8,6 +8,7 @@ from unittest.mock import patch +from nostr_sdk import PublicKey from django.urls import reverse from tests.test_api import BaseAPITestCase @@ -82,6 +83,40 @@ def test_robot_put_update_nostr_forward_settings(self, mock_send_test): self.assertEqual(data["nostr_forward_relay"], "ws://testrelay123abc.onion/") self.assertEqual(data["nostr_forward_enabled"], True) + @patch("api.notifications.Notifications.send_nostr_forward_test") + def test_robot_put_sends_test_when_forward_pubkey_changes(self, mock_send_test): + """Test that PUT /api/robot/ sends a test when the forward pubkey changes.""" + mock_send_test.return_value = True + + path = reverse("robot") + headers = self.get_robot_auth() + + response = self.client.put( + path, + data={ + "nostr_forward_pubkey": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", + "nostr_forward_relay": "ws://testrelay123abc.onion/", + "nostr_forward_enabled": True, + }, + content_type="application/json", + **headers, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(mock_send_test.call_count, 1) + + response = self.client.put( + path, + data={ + "nostr_forward_pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + content_type="application/json", + **headers, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(mock_send_test.call_count, 2) + def test_robot_put_partial_update_nostr_forward(self): """Test that PUT /api/robot/ allows partial updates (pubkey only).""" path = reverse("robot") @@ -116,6 +151,52 @@ def test_robot_put_rejects_non_onion_relay(self): self.assertEqual(response.status_code, 400) self.assertIn("nostr_forward_relay", response.json()) + def test_robot_put_rejects_non_websocket_onion_relay(self): + """Test that PUT /api/robot/ rejects non-websocket .onion relay URLs.""" + path = reverse("robot") + headers = self.get_robot_auth() + + update_data = {"nostr_forward_relay": "http://testrelay.onion/"} + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("nostr_forward_relay", response.json()) + + def test_robot_put_rejects_invalid_nostr_forward_pubkey(self): + """Test that PUT /api/robot/ rejects invalid forward pubkeys.""" + path = reverse("robot") + headers = self.get_robot_auth() + + update_data = {"nostr_forward_pubkey": "not-a-valid-pubkey"} + + response = self.client.put( + path, data=update_data, content_type="application/json", **headers + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("nostr_forward_pubkey", response.json()) + + def test_robot_put_accepts_npub_forward_pubkey(self): + """Test that PUT /api/robot/ accepts npub forward pubkeys.""" + path = reverse("robot") + headers = self.get_robot_auth() + hex_pubkey = read_file(f"tests/robots/{self.robot_index}/nostr_pubkey").strip() + npub = PublicKey.parse(hex_pubkey).to_bech32() + + response = self.client.put( + path, + data={"nostr_forward_pubkey": npub}, + content_type="application/json", + **headers, + ) + + self.assertEqual(response.status_code, 200) + self.assertResponse(response) + self.assertEqual(response.json()["nostr_forward_pubkey"], npub) + @patch("api.notifications.Notifications.send_nostr_forward_test") def test_robot_put_accepts_valid_onion_relay(self, mock_send_test): """Test that PUT /api/robot/ accepts valid .onion relay URLs.""" From 343f3ae5cb4bf37f038675a59d420c4c6da8269c Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Sun, 26 Apr 2026 16:35:22 +0530 Subject: [PATCH 09/10] fix(nostr): route forward relays through Tor --- api/nostr.py | 55 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/api/nostr.py b/api/nostr.py index 08f3c8e9a..7bd1a2331 100644 --- a/api/nostr.py +++ b/api/nostr.py @@ -4,8 +4,22 @@ from secp256k1 import PrivateKey from asgiref.sync import sync_to_async -from nostr_sdk import Keys, Client, EventBuilder, NostrSigner, Kind, Tag, PublicKey +from nostr_sdk import ( + Keys, + Client, + ClientBuilder, + Connection, + ConnectionMode, + ConnectionTarget, + EventBuilder, + NostrSigner, + Options, + Kind, + Tag, + PublicKey, +) from api.models import Order +from api.utils import TOR_PROXY, USE_TOR from decouple import config @@ -64,7 +78,10 @@ async def send_notification_event(self, robot, order, text): await client.send_private_msg(PublicKey.parse(robot.nostr_pubkey), text, tags) print("Nostr NOTIFICATION event sent") - await self.send_forward_notification(robot, order, text) + try: + await self.send_forward_notification(robot, order, text) + except Exception as e: + print(f"Nostr FORWARD notification failed: {e}") async def send_forward_notification(self, robot, order, text): """Sends notification to user's main account via their .onion relay""" @@ -74,14 +91,13 @@ async def send_forward_notification(self, robot, order, text): return if not robot.nostr_forward_pubkey or not robot.nostr_forward_relay: return - if not Robot.is_valid_onion_url(robot.nostr_forward_relay): + if not Robot.is_valid_onion_relay_url(robot.nostr_forward_relay): return print(f"Forwarding nostr notification to {robot.nostr_forward_relay}") keys = Keys.parse(config("NOSTR_NSEC", cast=str)) - signer = NostrSigner.keys(keys) - client = Client(signer) + client = self.initialize_onion_client(keys) await client.add_relay(robot.nostr_forward_relay) await client.connect() @@ -96,9 +112,11 @@ async def send_forward_notification(self, robot, order, text): Tag.parse(["status", str(order.status)]), ] - await client.send_private_msg( + output = await client.send_private_msg( PublicKey.parse(robot.nostr_forward_pubkey), text, tags ) + if robot.nostr_forward_relay not in output.success: + raise Exception(output.failed.get(robot.nostr_forward_relay, "relay failed")) print("Nostr FORWARD notification sent") async def send_forward_test(self, robot): @@ -111,14 +129,13 @@ async def send_forward_test(self, robot): if not robot.nostr_forward_pubkey or not robot.nostr_forward_relay: return - if not Robot.is_valid_onion_url(robot.nostr_forward_relay): + if not Robot.is_valid_onion_relay_url(robot.nostr_forward_relay): return print(f"Sending nostr FORWARD TEST to {robot.nostr_forward_relay}") keys = Keys.parse(config("NOSTR_NSEC", cast=str)) - signer = NostrSigner.keys(keys) - client = Client(signer) + client = self.initialize_onion_client(keys) await client.add_relay(robot.nostr_forward_relay) await client.connect() @@ -126,9 +143,11 @@ async def send_forward_test(self, robot): coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="RoboSats") text = f"🔔 Hey {robot.user.username}, your Nostr forwarding is configured! You will receive order notifications from {coordinator_alias}." - await client.send_private_msg( + output = await client.send_private_msg( PublicKey.parse(robot.nostr_forward_pubkey), text, [] ) + if robot.nostr_forward_relay not in output.success: + raise Exception(output.failed.get(robot.nostr_forward_relay, "relay failed")) print("Nostr FORWARD TEST event sent") async def initialize_client(self, keys): @@ -144,6 +163,22 @@ async def initialize_client(self, keys): return client + def initialize_onion_client(self, keys): + signer = NostrSigner.keys(keys) + + if not USE_TOR: + return Client(signer) + + host, port = TOR_PROXY.rsplit(":", 1) + connection = ( + Connection() + .mode(ConnectionMode.PROXY(host, int(port))) + .target(ConnectionTarget.ONION) + ) + options = Options().connection(connection) + + return ClientBuilder().signer(signer).opts(options).build() + @sync_to_async def get_user_name(self, order): return order.maker.username From 1056240f2c9ce3e7f86f50add1ae769f5b020074 Mon Sep 17 00:00:00 2001 From: keshav0479 Date: Sun, 26 Apr 2026 16:43:47 +0530 Subject: [PATCH 10/10] docs(api): update robot notification summary --- api/oas_schemas.py | 4 ++-- docs/assets/schemas/api-latest.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/oas_schemas.py b/api/oas_schemas.py index 9ce71a394..19141e60a 100644 --- a/api/oas_schemas.py +++ b/api/oas_schemas.py @@ -594,10 +594,10 @@ class RobotViewSchema: } put = { - "summary": "Update robot webhook settings", + "summary": "Update robot notification settings", "description": textwrap.dedent( """ - Update the robot's webhook notification settings. + Update the robot's webhook and Nostr forwarding notification settings. Webhooks allow you to receive HTTP POST notifications to your own server when order events occur. **Only `.onion` URLs are accepted** for privacy. diff --git a/docs/assets/schemas/api-latest.yaml b/docs/assets/schemas/api-latest.yaml index 29521e9cf..f748a1dc2 100644 --- a/docs/assets/schemas/api-latest.yaml +++ b/docs/assets/schemas/api-latest.yaml @@ -961,7 +961,7 @@ paths: operationId: robot_update description: |2 - Update the robot's webhook notification settings. + Update the robot's webhook and Nostr forwarding notification settings. Webhooks allow you to receive HTTP POST notifications to your own server when order events occur. **Only `.onion` URLs are accepted** for privacy. @@ -984,7 +984,7 @@ paths: You can also configure Nostr direct message forwarding. Provide your main Nostr public key (hex or npub) and a .onion relay websocket URL to receive trade notifications as encrypted DMs from the coordinator's Nostr identity. - summary: Update robot webhook settings + summary: Update robot notification settings tags: - robot security: