Skip to content

Commit 633c005

Browse files
committed
added nostr support for notifications
1 parent 659cba2 commit 633c005

4 files changed

Lines changed: 122 additions & 28 deletions

File tree

app.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
import threading
44
import time
55
import os
6+
import queue
67
from flask import Flask, render_template, request, jsonify
78
from scanner import scan_network
9+
from pynostr.key import PrivateKey
10+
from pynostr.relay_manager import RelayManager
11+
from pynostr.encrypted_dm import EncryptedDirectMessage
812

913
app = Flask(__name__)
1014

1115
DATA_FILE = "miners.json"
1216
SETTINGS_FILE = "settings.json"
1317
UPDATE_EVENT = threading.Event()
18+
NOSTR_QUEUE = queue.Queue()
1419

1520
def load_db():
1621
if os.path.exists(DATA_FILE):
@@ -38,7 +43,10 @@ def load_settings():
3843
"ntfy_timeout": 30,
3944
"notify_offline": True,
4045
"notify_blocks": True,
41-
"notify_tuning": True
46+
"notify_tuning": True,
47+
"nostr_privkey": "",
48+
"nostr_recipient_pubkey": "",
49+
"nostr_relays": ["wss://nostr.mom/", "wss://nostrelites.org/", "wss://relay.damus.io/", "wss://wot.nostr.net/"]
4250
}
4351
if os.path.exists(SETTINGS_FILE):
4452
try:
@@ -75,6 +83,33 @@ def save_settings(settings):
7583
OFFLINE_TRACKER = {}
7684
LAST_BLOCK_COUNT = 0
7785

86+
def nostr_queue_worker():
87+
while True:
88+
try:
89+
item = NOSTR_QUEUE.get()
90+
if item is None: break
91+
message, priv, pub, relays = item
92+
try:
93+
pk = PrivateKey.from_hex(priv)
94+
dm = EncryptedDirectMessage()
95+
dm.encrypt(pk.hex(), recipient_pubkey=pub, cleartext_content=message)
96+
event = dm.to_event()
97+
event.pubkey = pk.public_key.hex()
98+
event.add_tag("p", pub)
99+
event.sign(pk.hex())
100+
rm = RelayManager(timeout=6)
101+
for r in relays: rm.add_relay(r.strip())
102+
time.sleep(1.5)
103+
rm.publish_event(event)
104+
rm.run_sync()
105+
time.sleep(2)
106+
rm.close_connections()
107+
except Exception as e: print(f"Nostr Error: {e}")
108+
NOSTR_QUEUE.task_done()
109+
except: time.sleep(1)
110+
111+
threading.Thread(target=nostr_queue_worker, daemon=True).start()
112+
78113
def send_ntfy(message, title="OpenAxe Alert", tags="cpu"):
79114
topic = DB['settings'].get('ntfy_topic', '').strip()
80115
server = DB['settings'].get('ntfy_server', 'https://ntfy.sh').strip().rstrip('/')
@@ -88,6 +123,12 @@ def send_ntfy(message, title="OpenAxe Alert", tags="cpu"):
88123
except:
89124
pass
90125

126+
def send_nostr(message):
127+
p = DB['settings'].get('nostr_privkey', '').strip()
128+
r_pub = DB['settings'].get('nostr_recipient_pubkey', '').strip()
129+
relays = DB['settings'].get('nostr_relays', [])
130+
if p and r_pub: NOSTR_QUEUE.put((message, p, r_pub, relays))
131+
91132
def fetch_market_data():
92133
global MARKET_DATA
93134
while True:
@@ -179,11 +220,15 @@ def gentle_miner_poller():
179220
if threshold <= downtime < threshold + 25:
180221
if DB['settings'].get('notify_offline'):
181222
name = m.get('custom_name') or m.get('name') or ip
182-
send_ntfy(f"Miner {name} offline for {int(downtime)}s", "MINER DOWN", "warning,skull")
223+
msg = f"Miner {name} offline for {int(downtime)}s"
224+
send_ntfy(msg, "MINER DOWN", "warning,skull")
225+
send_nostr(msg)
183226
OFFLINE_TRACKER[ip] = now + 86400
184227
time.sleep(0.5)
185228
if DB['settings'].get('notify_blocks') and LAST_BLOCK_COUNT > 0 and current_total_blocks > LAST_BLOCK_COUNT:
186-
send_ntfy(f"Fleet found a new block! Total: {current_total_blocks}", "BLOCK FOUND!", "moneybag,tada")
229+
msg = f"Fleet found a new block! Total: {current_total_blocks}"
230+
send_ntfy(msg, "BLOCK FOUND!", "moneybag,tada")
231+
send_nostr(msg)
187232
LAST_BLOCK_COUNT = current_total_blocks
188233
time.sleep(20)
189234

@@ -287,7 +332,9 @@ def overclock():
287332
payload = {"frequency": int(d['freq']), "coreVoltage": int(d['volt'])}
288333
requests.patch(f"http://{d['ip']}/api/system", json=payload, timeout=5)
289334
if DB['settings'].get('notify_tuning'):
290-
send_ntfy(f"Tuning applied to {d['ip']}: {d['freq']}MHz / {d['volt']}mV", "TUNING SUCCESS", "zap")
335+
msg = f"Tuning applied to {d['ip']}: {d['freq']}MHz / {d['volt']}mV"
336+
send_ntfy(msg, "TUNING SUCCESS", "zap")
337+
send_nostr(msg)
291338
return jsonify({"status": "ok"})
292339
except:
293340
try:
@@ -324,5 +371,13 @@ def test_ntfy():
324371
except:
325372
return jsonify({"status": "error"}), 500
326373

374+
@app.route('/api/test_nostr', methods=['POST'])
375+
def test_nostr():
376+
try:
377+
send_nostr("Testing Nostr notification system. If you see this, it works!")
378+
return jsonify({"status": "ok"})
379+
except Exception as e:
380+
return jsonify({"status": "error", "msg": str(e)}), 500
381+
327382
if __name__ == '__main__':
328383
app.run(host='0.0.0.0', port=5055, debug=True)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
flask
22
requests
3+
pynostr

static/js/dashboard.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,14 @@ function closeDetail() {
238238
}
239239

240240
function formatBigNum(num, unit='') {
241-
if (num >= 1e24) return (num / 1e24).toFixed(2) + " Y" + unit; // Yottahash
242-
if (num >= 1e21) return (num / 1e21).toFixed(2) + " Z" + unit; // Zettahash
243-
if (num >= 1e18) return (num / 1e18).toFixed(2) + " E" + unit; // Exahash
244-
if (num >= 1e15) return (num / 1e15).toFixed(2) + " P" + unit; // Petahash
245-
if (num >= 1e12) return (num / 1e12).toFixed(2) + " T" + unit; // Terahash
246-
if (num >= 1e9) return (num / 1e9).toFixed(2) + " G" + unit; // Gigahash
247-
if (num >= 1e6) return (num / 1e6).toFixed(2) + " M" + unit; // Megahash
248-
if (num >= 1e3) return (num / 1e3).toFixed(2) + " k" + unit; // Kilohash
241+
if (num >= 1e24) return (num / 1e24).toFixed(2) + " Y" + unit;
242+
if (num >= 1e21) return (num / 1e21).toFixed(2) + " Z" + unit;
243+
if (num >= 1e18) return (num / 1e18).toFixed(2) + " E" + unit;
244+
if (num >= 1e15) return (num / 1e15).toFixed(2) + " P" + unit;
245+
if (num >= 1e12) return (num / 1e12).toFixed(2) + " T" + unit;
246+
if (num >= 1e9) return (num / 1e9).toFixed(2) + " G" + unit;
247+
if (num >= 1e6) return (num / 1e6).toFixed(2) + " M" + unit;
248+
if (num >= 1e3) return (num / 1e3).toFixed(2) + " k" + unit;
249249
return num.toLocaleString() + " " + unit;
250250
}
251251

@@ -327,11 +327,22 @@ async function testNtfy() {
327327
} catch (e) { alert("Failed: " + e.message); } finally { btn.disabled = false; btn.innerHTML = originalHtml; }
328328
}
329329

330+
async function testNostr() {
331+
const btn = document.querySelector('button[onclick="testNostr()"]'), originalHtml = btn.innerHTML;
332+
btn.disabled = true; btn.innerHTML = '<i class="ph-bold ph-circle-notched animate-spin"></i> Sending...';
333+
try {
334+
const res = await fetch('/api/test_nostr', { method: 'POST' }), data = await res.json();
335+
alert(data.status === "ok" ? "Nostr test notification fired!" : "Error: " + data.msg);
336+
} catch (e) { alert("Failed: " + e.message); } finally { btn.disabled = false; btn.innerHTML = originalHtml; }
337+
}
338+
330339
async function saveNotificationSettings() {
331340
const config = {
332341
ntfy_server: document.getElementById('ntfyServer').value,
333342
ntfy_topic: document.getElementById('ntfyTopic').value,
334343
ntfy_timeout: parseInt(document.getElementById('ntfyTimeout').value),
344+
nostr_privkey: document.getElementById('nostrPrivKey').value,
345+
nostr_recipient_pubkey: document.getElementById('nostrPubKey').value,
335346
notify_offline: document.getElementById('notifyOffline').checked,
336347
notify_blocks: document.getElementById('notifyBlocks').checked,
337348
notify_tuning: document.getElementById('notifyTuning').checked

templates/dashboard.html

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,23 +112,47 @@ <h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4"><i cl
112112
<button onclick="resetSystem()" class="w-full border border-red-500 text-red-500 hover:bg-red-500/20 py-2 rounded text-sm font-bold transition">Factory Reset</button>
113113
</div>
114114
</div>
115+
115116
<div class="theme-card p-6 rounded-xl shadow-sm border border-slate-700/50 md:col-span-2">
116-
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4"><i class="ph-bold ph-bell"></i> Notification Engine</h3>
117-
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
118-
<div>
119-
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Ntfy Server</label>
120-
<input type="text" id="ntfyServer" value="{{ settings.ntfy_server }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm" placeholder="https://ntfy.sh">
121-
</div>
122-
<div>
123-
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Topic</label>
124-
<input type="text" id="ntfyTopic" value="{{ settings.ntfy_topic }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm font-mono">
117+
<h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-6 flex items-center gap-2">
118+
<i class="ph-bold ph-bell"></i> Notification Engine
119+
</h3>
120+
121+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-6">
122+
<div class="space-y-4">
123+
<p class="text-[10px] font-bold text-indigo-400 uppercase border-b border-slate-700/50 pb-1">Ntfy.sh</p>
124+
<div class="grid grid-cols-1 gap-4">
125+
<div class="grid grid-cols-2 gap-4">
126+
<div>
127+
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Server</label>
128+
<input type="text" id="ntfyServer" value="{{ settings.ntfy_server }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm" placeholder="https://ntfy.sh">
129+
</div>
130+
<div>
131+
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Delay (Sec)</label>
132+
<input type="number" id="ntfyTimeout" value="{{ settings.ntfy_timeout }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm">
133+
</div>
134+
</div>
135+
<div>
136+
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Topic</label>
137+
<input type="text" id="ntfyTopic" value="{{ settings.ntfy_topic }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm font-mono">
138+
</div>
139+
</div>
125140
</div>
126-
<div>
127-
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Delay (Seconds)</label>
128-
<input type="number" id="ntfyTimeout" value="{{ settings.ntfy_timeout }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm">
141+
142+
<div class="space-y-4">
143+
<p class="text-[10px] font-bold text-purple-400 uppercase border-b border-slate-700/50 pb-1">Nostr (Direct Message)</p>
144+
<div>
145+
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">App Private Key (nsec/hex)</label>
146+
<input type="password" id="nostrPrivKey" value="{{ settings.nostr_privkey }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm font-mono" placeholder="Your bot's private key">
147+
</div>
148+
<div>
149+
<label class="block text-[10px] font-bold text-slate-500 uppercase mb-1">Your Public Key (npub/hex)</label>
150+
<input type="text" id="nostrPubKey" value="{{ settings.nostr_recipient_pubkey }}" class="w-full bg-slate-900 border border-slate-700 text-white p-2 rounded text-sm font-mono" placeholder="Your main account pubkey">
151+
</div>
129152
</div>
130153
</div>
131-
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4 border-t border-slate-700/50 pt-4">
154+
155+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 border-t border-slate-700/50 pt-4">
132156
<label class="flex items-center gap-3 cursor-pointer group">
133157
<input type="checkbox" id="notifyOffline" {% if settings.notify_offline %}checked{% endif %} class="w-4 h-4 rounded border-slate-700 bg-slate-900 text-indigo-600 focus:ring-indigo-500">
134158
<span class="text-xs font-bold text-slate-400 group-hover:text-slate-200 transition uppercase">Miner Offline</span>
@@ -142,11 +166,14 @@ <h3 class="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4"><i cl
142166
<span class="text-xs font-bold text-slate-400 group-hover:text-slate-200 transition uppercase">Tuning Success</span>
143167
</label>
144168
</div>
145-
<div class="flex gap-3 mt-4">
146-
<button onclick="testNtfy()" class="w-1/3 bg-slate-700 hover:bg-slate-600 text-slate-300 py-2 rounded font-bold transition flex items-center justify-center gap-2"><i class="ph-bold ph-paper-plane-tilt"></i> Test</button>
147-
<button onclick="saveNotificationSettings()" class="w-2/3 bg-indigo-600 hover:bg-indigo-700 text-white py-2 rounded font-bold transition">Save Config</button>
169+
170+
<div class="flex gap-3 mt-6">
171+
<button onclick="testNtfy()" class="w-1/4 bg-slate-700 hover:bg-slate-600 text-slate-300 py-2 rounded font-bold transition flex items-center justify-center gap-2"><i class="ph-bold ph-paper-plane-tilt"></i> Test Ntfy</button>
172+
<button onclick="testNostr()" class="w-1/4 bg-purple-900/50 hover:bg-purple-800 text-purple-200 border border-purple-700/50 py-2 rounded font-bold transition flex items-center justify-center gap-2"><i class="ph-bold ph-broadcast"></i> Test Nostr</button>
173+
<button onclick="saveNotificationSettings()" class="w-2/4 bg-indigo-600 hover:bg-indigo-700 text-white py-2 rounded font-bold transition">Save All Config</button>
148174
</div>
149175
</div>
176+
150177
<div class="theme-card p-6 rounded-xl shadow-sm border border-slate-700/50 md:col-span-2">
151178
<h2 class="text-xl font-bold flex items-center gap-2 text-white"><i class="ph-fill ph-radar text-green-500"></i> Auto-Discovery</h2>
152179
<div class="flex gap-4 mt-4">

0 commit comments

Comments
 (0)