mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
update
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
margin: 20px;
|
||||
max-width: 600px;
|
||||
color: #fff;
|
||||
background: transparent; /* transparent background */
|
||||
background: transparent;
|
||||
}
|
||||
label {
|
||||
display: inline-block;
|
||||
@@ -24,8 +24,8 @@
|
||||
padding: 6px;
|
||||
margin-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: #234566; /* dark blue */
|
||||
color: #fff; /* white text */
|
||||
background-color: #234566;
|
||||
color: #fff;
|
||||
border: 1px solid #888;
|
||||
}
|
||||
button {
|
||||
@@ -50,6 +50,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bitvid Content Appeals Form</h1>
|
||||
|
||||
<form id="appeal-form">
|
||||
<h3>1. User Information</h3>
|
||||
<label for="npub">Nostr Public Key (npub)</label>
|
||||
@@ -175,6 +176,7 @@
|
||||
|
||||
<button type="submit">Submit Appeal</button>
|
||||
</form>
|
||||
|
||||
<p>
|
||||
<strong>Processing Time:</strong> Appeals will be reviewed within 7-14
|
||||
days.
|
||||
@@ -183,11 +185,478 @@
|
||||
<div id="status-message"></div>
|
||||
|
||||
<script>
|
||||
/***********************************
|
||||
* Minimal "nostr-tools" subset
|
||||
***********************************/
|
||||
/********************************************************************
|
||||
* Minimal single-file solution that:
|
||||
* 1) Generates ephemeral secp256k1 keys (sender).
|
||||
* 2) Uses standard NIP-04 encryption (AES-256-CBC, base64).
|
||||
* 3) Publishes kind=4 events to chosen relays.
|
||||
********************************************************************/
|
||||
|
||||
// Simple bech32 decoding for npub => hex
|
||||
/********************************************************************
|
||||
* Noble Secp256k1 - stripped-down code from https://github.com/paulmillr/noble-secp256k1
|
||||
* for ECDH and signing. This is a partial inline library (not full).
|
||||
********************************************************************/
|
||||
(function () {
|
||||
// We'll store the main methods we need in an object:
|
||||
window.nobleSecp256k1 = {};
|
||||
|
||||
const _0n = BigInt(0);
|
||||
const _1n = BigInt(1);
|
||||
const _2n = BigInt(2);
|
||||
const _3n = BigInt(3);
|
||||
const _8n = BigInt(8);
|
||||
const CURVE = {
|
||||
P: BigInt(
|
||||
"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
|
||||
),
|
||||
n: BigInt(
|
||||
"0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
|
||||
),
|
||||
Gx: BigInt(
|
||||
"55066263022277343669578718895168534326250603453777594175500187360389116729240"
|
||||
),
|
||||
Gy: BigInt(
|
||||
"32670510020758816978083085130507043184471273380659243275938904335757337482424"
|
||||
),
|
||||
};
|
||||
const pow2_256 = _1n << BigInt(256);
|
||||
|
||||
function mod(a, b = CURVE.P) {
|
||||
const result = a % b;
|
||||
return result >= _0n ? result : b + result;
|
||||
}
|
||||
function invert(number, modulo = CURVE.P) {
|
||||
// Binary Extended Euclidian Algorithm
|
||||
// https://brilliant.org/wiki/extended-euclidean-algorithm/
|
||||
// returns x where (x*number)%modulo==1
|
||||
if (number === _0n || modulo <= _0n) {
|
||||
throw new Error("invert: wrong inputs");
|
||||
}
|
||||
let a = mod(number, modulo);
|
||||
let b = modulo;
|
||||
// prettier-ignore
|
||||
let x0 = _0n, x1 = _1n;
|
||||
while (a !== _0n) {
|
||||
const q = b / a;
|
||||
const r = b % a;
|
||||
const x2 = x0 - q * x1;
|
||||
b = a;
|
||||
a = r;
|
||||
x0 = x1;
|
||||
x1 = x2;
|
||||
}
|
||||
if (b !== _1n) throw new Error("invert: does not exist");
|
||||
return mod(x0, modulo);
|
||||
}
|
||||
function bytesToNumber(bytes) {
|
||||
return BigInt(
|
||||
"0x" +
|
||||
[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
);
|
||||
}
|
||||
function numberTo32Bytes(num) {
|
||||
const hex = num.toString(16).padStart(64, "0");
|
||||
const len = hex.length / 2;
|
||||
const u8 = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
u8[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return u8;
|
||||
}
|
||||
function isWithinCurveOrder(num) {
|
||||
return _0n < num && num < CURVE.n;
|
||||
}
|
||||
// Simplified point addition/EC math
|
||||
class Point {
|
||||
constructor(px, py) {
|
||||
this.x = px;
|
||||
this.y = py;
|
||||
}
|
||||
static fromPrivateKey(privKey) {
|
||||
const p = bytesToNumber(privKey);
|
||||
if (!isWithinCurveOrder(p)) throw new Error("Invalid private key");
|
||||
return Point.BASE.multiply(p);
|
||||
}
|
||||
// secp256k1 scalar multiplication
|
||||
multiply(scalar) {
|
||||
if (!isWithinCurveOrder(scalar)) {
|
||||
throw new Error("Point#multiply: invalid scalar");
|
||||
}
|
||||
let n = scalar;
|
||||
if (n === _0n) return Point.ZERO;
|
||||
|
||||
let p = Point.ZERO;
|
||||
let d = this;
|
||||
while (n > _0n) {
|
||||
if ((n & _1n) === _1n) p = p.add(d);
|
||||
d = d.double();
|
||||
n >>= _1n;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
double() {
|
||||
const X1 = this.x;
|
||||
const Y1 = this.y;
|
||||
const a = mod(_3n * X1 * X1);
|
||||
const inv = invert(_2n * Y1, CURVE.P);
|
||||
const x3 = mod(a * a * inv * inv - _2n * X1);
|
||||
const y3 = mod(a * (X1 - x3) * inv - Y1);
|
||||
return new Point(x3, y3);
|
||||
}
|
||||
add(other) {
|
||||
if (this.equals(Point.ZERO)) return other;
|
||||
if (other.equals(Point.ZERO)) return this;
|
||||
if (this.x === other.x) {
|
||||
if (this.y !== other.y) return Point.ZERO;
|
||||
return this.double();
|
||||
}
|
||||
const inv = invert(other.x - this.x, CURVE.P);
|
||||
const slope = mod((other.y - this.y) * inv);
|
||||
const x3 = mod(slope * slope - this.x - other.x);
|
||||
const y3 = mod(slope * (this.x - x3) - this.y);
|
||||
return new Point(x3, y3);
|
||||
}
|
||||
equals(other) {
|
||||
return this.x === other.x && this.y === other.y;
|
||||
}
|
||||
toRawBytes(isCompressed = false) {
|
||||
if (!isCompressed) {
|
||||
// 0x04 + x + y
|
||||
return new Uint8Array([
|
||||
4,
|
||||
...numberTo32Bytes(this.x),
|
||||
...numberTo32Bytes(this.y),
|
||||
]);
|
||||
}
|
||||
// 0x02 || 0x03 + x
|
||||
const header = this.y & _1n ? 0x03 : 0x02;
|
||||
return new Uint8Array([header, ...numberTo32Bytes(this.x)]);
|
||||
}
|
||||
static fromXY(x, y) {
|
||||
return new Point(x, y);
|
||||
}
|
||||
}
|
||||
Point.BASE = (function () {
|
||||
return new Point(CURVE.Gx, CURVE.Gy);
|
||||
})();
|
||||
Point.ZERO = new Point(_0n, _0n);
|
||||
|
||||
// getSharedSecret and sign
|
||||
function getSharedSecret(privKey, pubBytes) {
|
||||
const px = pubBytesToPoint(pubBytes);
|
||||
const ib = bytesToNumber(privKey);
|
||||
const hash = px.multiply(ib);
|
||||
return numberTo32Bytes(hash.x);
|
||||
}
|
||||
function pubBytesToPoint(bytes) {
|
||||
if (bytes[0] === 0x04 && bytes.length === 65) {
|
||||
const x = bytesToNumber(bytes.slice(1, 33));
|
||||
const y = bytesToNumber(bytes.slice(33, 65));
|
||||
return new Point(x, y);
|
||||
} else if (
|
||||
(bytes[0] === 0x02 || bytes[0] === 0x03) &&
|
||||
bytes.length === 33
|
||||
) {
|
||||
const x = bytesToNumber(bytes.slice(1));
|
||||
// We find y via formula y^2 = x^3 + 7 mod p
|
||||
const y2 = mod(x * x * x + _7n, CURVE.P);
|
||||
let y = powMod(y2, (CURVE.P + _1n) / _4n, CURVE.P);
|
||||
const isOdd = (y & _1n) === _1n;
|
||||
const wantOdd = bytes[0] === 0x03;
|
||||
if (isOdd !== wantOdd) y = mod(-y, CURVE.P);
|
||||
return new Point(x, y);
|
||||
}
|
||||
throw new Error("Unsupported compressed pubkey format");
|
||||
}
|
||||
// exponent
|
||||
const _7n = BigInt(7);
|
||||
const _4n = BigInt(4);
|
||||
function powMod(a, e, m) {
|
||||
let r = _1n;
|
||||
let b = a;
|
||||
while (e > 0) {
|
||||
if (e & _1n) r = mod(r * b, m);
|
||||
b = mod(b * b, m);
|
||||
e >>= _1n;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
async function sign(msgHash, privKey) {
|
||||
// We won't implement a full RFC6979 here for brevity
|
||||
// We'll do a simple ephemeral k = random approach. For production, use RFC6979 or a stable approach.
|
||||
const d0 = bytesToNumber(privKey);
|
||||
if (!isWithinCurveOrder(d0)) {
|
||||
throw new Error("sign: invalid privkey");
|
||||
}
|
||||
// parse msgHash
|
||||
const e = bytesToNumber(msgHash);
|
||||
// ephemeral k
|
||||
let k = bytesToNumber(await randomBytes(32));
|
||||
while (!isWithinCurveOrder(k)) {
|
||||
k = bytesToNumber(await randomBytes(32));
|
||||
}
|
||||
const R = Point.BASE.multiply(k);
|
||||
const r = mod(R.x, CURVE.n);
|
||||
if (r === _0n) return null;
|
||||
const s = mod(invert(k, CURVE.n) * (e + r * d0), CURVE.n);
|
||||
if (s === _0n) return null;
|
||||
const sig = new Uint8Array(64);
|
||||
// r, s each 32 bytes
|
||||
sig.set(numberTo32Bytes(r), 0);
|
||||
sig.set(numberTo32Bytes(s), 32);
|
||||
return sig;
|
||||
}
|
||||
|
||||
async function randomBytes(length) {
|
||||
const arr = new Uint8Array(length);
|
||||
crypto.getRandomValues(arr);
|
||||
return arr;
|
||||
}
|
||||
|
||||
function getPublicKey(privKey, compressed = true) {
|
||||
const p = Point.fromPrivateKey(privKey);
|
||||
return p.toRawBytes(compressed);
|
||||
}
|
||||
|
||||
// Expose minimal API
|
||||
window.nobleSecp256k1.getSharedSecret = getSharedSecret;
|
||||
window.nobleSecp256k1.sign = sign;
|
||||
window.nobleSecp256k1.getPublicKey = getPublicKey;
|
||||
window.nobleSecp256k1.randomBytes = randomBytes;
|
||||
})();
|
||||
|
||||
/********************************************************************
|
||||
* NIP-04 encryption with ephemeral ephemeral keys:
|
||||
* We'll do AES-256-CBC with random IV, base64-encode "ciphertext?iv".
|
||||
********************************************************************/
|
||||
async function nip04Encrypt(privKey, recipientPubKeyHex, plaintext) {
|
||||
// 1) convert recipient hex => raw pubkey bytes
|
||||
const recPubBin = hexToBytes(recipientPubKeyHex);
|
||||
// 2) ECDH => 32-byte shared secret
|
||||
const sharedKey = window.nobleSecp256k1.getSharedSecret(
|
||||
privKey,
|
||||
recPubBin
|
||||
);
|
||||
// remove leading 0 byte
|
||||
const shReduced = sharedKey.slice(1);
|
||||
|
||||
// 3) import as AES key
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
shReduced,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
|
||||
// 4) create random IV
|
||||
const iv = new Uint8Array(16);
|
||||
crypto.getRandomValues(iv);
|
||||
|
||||
// 5) AES-CBC
|
||||
const enc = new TextEncoder();
|
||||
const cipherBuffer = await crypto.subtle.encrypt(
|
||||
{ name: "AES-CBC", iv },
|
||||
key,
|
||||
enc.encode(plaintext)
|
||||
);
|
||||
|
||||
const cipherBytes = new Uint8Array(cipherBuffer);
|
||||
const cipherBase64 = b64encode(cipherBytes);
|
||||
const ivBase64 = b64encode(iv);
|
||||
|
||||
// final string is "cipher?iv"
|
||||
return `${cipherBase64}?${ivBase64}`;
|
||||
}
|
||||
|
||||
function hexToBytes(hex) {
|
||||
if (hex.startsWith("0x")) hex = hex.slice(2);
|
||||
const len = hex.length / 2;
|
||||
const out = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function bytesToHex(uint8a) {
|
||||
let hex = "";
|
||||
for (let i = 0; i < uint8a.length; i++) {
|
||||
hex += uint8a[i].toString(16).padStart(2, "0");
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
function b64encode(data) {
|
||||
return btoa(String.fromCharCode(...data));
|
||||
}
|
||||
|
||||
/********************************************************************
|
||||
* getEventHash: standard Nostr ID for an event
|
||||
********************************************************************/
|
||||
async function getEventHash(evt) {
|
||||
// We'll build the array per NIP-01
|
||||
const enc = new TextEncoder();
|
||||
const payload = JSON.stringify([
|
||||
0,
|
||||
evt.pubkey,
|
||||
evt.created_at,
|
||||
evt.kind,
|
||||
evt.tags,
|
||||
evt.content,
|
||||
]);
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
enc.encode(payload)
|
||||
);
|
||||
return bytesToHex(new Uint8Array(digest));
|
||||
}
|
||||
|
||||
/********************************************************************
|
||||
* signEvent: sign with ephemeral privkey
|
||||
********************************************************************/
|
||||
async function signEvent(evt, privKey) {
|
||||
const idHex = await getEventHash(evt);
|
||||
const idBytes = hexToBytes(idHex);
|
||||
const signature = await window.nobleSecp256k1.sign(idBytes, privKey);
|
||||
evt.id = idHex; // store the final ID
|
||||
evt.sig = bytesToHex(signature);
|
||||
}
|
||||
|
||||
/********************************************************************
|
||||
* Minimal "SimplePool": open websockets to each relay, send event
|
||||
********************************************************************/
|
||||
class SimplePool {
|
||||
constructor() {
|
||||
this.conns = {};
|
||||
}
|
||||
async connectRelay(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (
|
||||
this.conns[url] &&
|
||||
this.conns[url].readyState === WebSocket.OPEN
|
||||
) {
|
||||
resolve(this.conns[url]);
|
||||
return;
|
||||
}
|
||||
const ws = new WebSocket(url);
|
||||
ws.onopen = () => {
|
||||
this.conns[url] = ws;
|
||||
resolve(ws);
|
||||
};
|
||||
ws.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
async publish(urls, event) {
|
||||
return Promise.all(
|
||||
urls.map(async (url) => {
|
||||
const ws = await this.connectRelay(url);
|
||||
ws.send(JSON.stringify(["EVENT", event]));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************************************
|
||||
* On form submit:
|
||||
* 1) generate ephemeral key
|
||||
* 2) for each target npub => do standard nip04 => build kind=4 => sign => publish
|
||||
********************************************************************/
|
||||
const targetNpubs = [
|
||||
// Example moderators:
|
||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
|
||||
];
|
||||
const relayUrls = {
|
||||
"wss://relay.snort.social": true,
|
||||
"wss://relay.damus.io": true,
|
||||
"wss://relay.primal.net": true,
|
||||
};
|
||||
|
||||
const form = document.getElementById("appeal-form");
|
||||
const statusEl = document.getElementById("status-message");
|
||||
const pool = new SimplePool();
|
||||
|
||||
form.addEventListener("submit", async (evt) => {
|
||||
evt.preventDefault();
|
||||
statusEl.textContent = "";
|
||||
|
||||
// Gather form fields
|
||||
const formData = new FormData(form);
|
||||
const dataObject = {};
|
||||
formData.forEach((val, key) => {
|
||||
dataObject[key] = val.trim();
|
||||
});
|
||||
|
||||
// 1) Generate ephemeral keys
|
||||
const ephemeralPriv = await window.nobleSecp256k1.randomBytes(32);
|
||||
// Public key in compressed form
|
||||
const ephemeralPubBytes = window.nobleSecp256k1.getPublicKey(
|
||||
ephemeralPriv,
|
||||
true
|
||||
);
|
||||
const ephemeralPubHex = bytesToHex(ephemeralPubBytes);
|
||||
|
||||
let overallSuccess = false;
|
||||
|
||||
// convert entire form data to text
|
||||
const contentText = JSON.stringify(dataObject, null, 2);
|
||||
|
||||
// For each mod npub, build a new event
|
||||
for (const modNpub of targetNpubs) {
|
||||
try {
|
||||
// decode modNpub => hex
|
||||
const modHex = decodeNpubToHex(modNpub);
|
||||
// do nip04 encryption with ephemeral => mod
|
||||
const ciphertext = await nip04Encrypt(
|
||||
ephemeralPriv,
|
||||
modHex,
|
||||
contentText
|
||||
);
|
||||
|
||||
// build event
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const event = {
|
||||
kind: 4, // DM
|
||||
pubkey: ephemeralPubHex, // ephemeral pubkey in hex
|
||||
created_at: now,
|
||||
tags: [["p", modHex]],
|
||||
content: ciphertext,
|
||||
};
|
||||
|
||||
// sign
|
||||
await signEvent(event, ephemeralPriv);
|
||||
|
||||
// publish
|
||||
try {
|
||||
await pool.publish(Object.keys(relayUrls), event);
|
||||
overallSuccess = true;
|
||||
statusEl.textContent += `Published to ${modNpub}\n`;
|
||||
} catch (pubErr) {
|
||||
statusEl.textContent += `Failed to publish to ${modNpub}: ${pubErr}\n`;
|
||||
}
|
||||
} catch (err) {
|
||||
statusEl.textContent += `Error sending to ${modNpub}: ${err}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (overallSuccess) {
|
||||
alert("Appeal submitted successfully.");
|
||||
form.reset();
|
||||
} else {
|
||||
alert("Submission encountered errors. Check status messages above.");
|
||||
}
|
||||
});
|
||||
|
||||
/********************************************************************
|
||||
* decodeNpubToHex: minimal bech32 decode from your prior snippet
|
||||
********************************************************************/
|
||||
function decodeNpubToHex(npub) {
|
||||
const { hrp, data } = bech32Decode(npub);
|
||||
if (hrp !== "npub") throw new Error("Not an npub");
|
||||
const converted = convertBits(data, 5, 8, false);
|
||||
return bytesToHex(new Uint8Array(converted));
|
||||
}
|
||||
const ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
function polymod(values) {
|
||||
let chk = 1;
|
||||
@@ -247,8 +716,8 @@
|
||||
function convertBits(data, inBits, outBits, pad = true) {
|
||||
let value = 0;
|
||||
let bits = 0;
|
||||
const result = [];
|
||||
const maxV = (1 << outBits) - 1;
|
||||
const result = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
value = (value << inBits) | data[i];
|
||||
bits += inBits;
|
||||
@@ -268,230 +737,6 @@
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function decodeNpubToHex(npub) {
|
||||
const { hrp, data } = bech32Decode(npub);
|
||||
if (hrp !== "npub") {
|
||||
throw new Error("Not an npub");
|
||||
}
|
||||
const converted = convertBits(data, 5, 8, false);
|
||||
return bytesToHex(new Uint8Array(converted));
|
||||
}
|
||||
function bytesToHex(uint8a) {
|
||||
let hex = "";
|
||||
for (let i = 0; i < uint8a.length; i++) {
|
||||
const b = uint8a[i];
|
||||
hex += b.toString(16).padStart(2, "0");
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// Simple SHA-256 for event hashing
|
||||
// Here we'll rely on browser's crypto.subtle
|
||||
async function sha256Hex(msgUint8) {
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
|
||||
const hashArray = new Uint8Array(hashBuffer);
|
||||
let hex = "";
|
||||
for (let i = 0; i < hashArray.length; i++) {
|
||||
hex += hashArray[i].toString(16).padStart(2, "0");
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
// Helper to build event.id
|
||||
// This is the "serialize" approach in NIP-01
|
||||
async function getEventHash(evt) {
|
||||
const { kind, pubkey, created_at, tags, content } = evt;
|
||||
const payload = JSON.stringify([
|
||||
0,
|
||||
pubkey,
|
||||
created_at,
|
||||
kind,
|
||||
tags,
|
||||
content,
|
||||
]);
|
||||
const enc = new TextEncoder();
|
||||
const encodedPayload = enc.encode(payload);
|
||||
return await sha256Hex(encodedPayload);
|
||||
}
|
||||
|
||||
// Minimal "SimplePool" that can publish events
|
||||
class SimplePool {
|
||||
constructor() {
|
||||
this.conns = {};
|
||||
}
|
||||
async connectRelay(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (
|
||||
this.conns[url] &&
|
||||
this.conns[url].readyState === WebSocket.OPEN
|
||||
) {
|
||||
resolve(this.conns[url]);
|
||||
return;
|
||||
}
|
||||
const ws = new WebSocket(url);
|
||||
ws.onopen = () => {
|
||||
this.conns[url] = ws;
|
||||
resolve(ws);
|
||||
};
|
||||
ws.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
async publish(urls, event) {
|
||||
// For each URL, ensure a WS connection, then send
|
||||
return Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
const ws = await this.connectRelay(url);
|
||||
const msg = ["EVENT", event];
|
||||
ws.send(JSON.stringify(msg));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to publish to ${url}: ${err.message}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************
|
||||
* Our "fakeEncrypt" to match your app
|
||||
******************************************/
|
||||
function fakeEncrypt(str) {
|
||||
return str.split("").reverse().join("");
|
||||
}
|
||||
|
||||
/******************************************
|
||||
* NostrClient - like in your app
|
||||
******************************************/
|
||||
class NostrClient {
|
||||
constructor(relayUrls) {
|
||||
this.relayUrls = relayUrls;
|
||||
this.pool = new SimplePool();
|
||||
this.pubkey = null; // will store the extension's pubkey
|
||||
}
|
||||
|
||||
// Connect is trivial here; we do it on publish instead
|
||||
async init() {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
// Attempt to get pubkey from extension
|
||||
async login() {
|
||||
if (!window.nostr) {
|
||||
throw new Error(
|
||||
"No window.nostr found. Please install a Nostr extension."
|
||||
);
|
||||
}
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
this.pubkey = pubkey;
|
||||
return pubkey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a kind=4 DM using "fake encryption".
|
||||
* This is not secure. Replace with actual encryption if needed.
|
||||
*/
|
||||
async sendAppealDM(targetNpub, formDataObj) {
|
||||
if (!this.pubkey) {
|
||||
throw new Error("You must login first (no pubkey).");
|
||||
}
|
||||
// Convert target npub => hex
|
||||
let targetHex;
|
||||
try {
|
||||
targetHex = decodeNpubToHex(targetNpub);
|
||||
} catch (err) {
|
||||
throw new Error("Invalid target npub: " + err.message);
|
||||
}
|
||||
|
||||
// Fake-encrypt the entire JSON
|
||||
const jsonStr = JSON.stringify(formDataObj, null, 2);
|
||||
const ciphertext = fakeEncrypt(jsonStr);
|
||||
|
||||
// Build the event
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const event = {
|
||||
kind: 4,
|
||||
pubkey: this.pubkey,
|
||||
created_at: now,
|
||||
tags: [["p", targetHex]],
|
||||
content: ciphertext,
|
||||
};
|
||||
|
||||
// We need to hash the event, then sign with extension
|
||||
event.id = await getEventHash(event);
|
||||
event.sig = await window.nostr.signEvent(event);
|
||||
|
||||
// Publish to all configured relays
|
||||
await this.pool.publish(Object.keys(this.relayUrls), event);
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************
|
||||
* Main code
|
||||
******************************************/
|
||||
// Configure your target npubs and relays:
|
||||
const targetNpubs = [
|
||||
// e.g. Moderation accounts:
|
||||
"npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe",
|
||||
];
|
||||
const relayUrls = {
|
||||
"wss://relay.snort.social": true,
|
||||
"wss://relay.damus.io": true,
|
||||
"wss://relay.primal.net": true,
|
||||
};
|
||||
|
||||
const form = document.getElementById("appeal-form");
|
||||
const statusEl = document.getElementById("status-message");
|
||||
|
||||
// Create a local client
|
||||
const nostrClient = new NostrClient(relayUrls);
|
||||
// We won't call .init() for now, since we connect on publish if needed.
|
||||
|
||||
form.addEventListener("submit", async (evt) => {
|
||||
evt.preventDefault();
|
||||
statusEl.textContent = "";
|
||||
|
||||
// Gather form fields
|
||||
const data = new FormData(form);
|
||||
const appealData = {};
|
||||
for (const [key, val] of data.entries()) {
|
||||
appealData[key] = val.trim();
|
||||
}
|
||||
|
||||
// If user is not logged in, attempt login
|
||||
if (!nostrClient.pubkey) {
|
||||
try {
|
||||
await nostrClient.login();
|
||||
} catch (err) {
|
||||
statusEl.textContent =
|
||||
"Failed to login via extension: " + err.message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let anySuccess = false;
|
||||
// For each target mod npub, send the DM
|
||||
for (const modNpub of targetNpubs) {
|
||||
try {
|
||||
await nostrClient.sendAppealDM(modNpub, appealData);
|
||||
statusEl.textContent += `Sent DM to ${modNpub}\n`;
|
||||
anySuccess = true;
|
||||
} catch (err) {
|
||||
const msg = `Failed to send DM to ${modNpub}: ${err.message}`;
|
||||
console.error(msg);
|
||||
statusEl.textContent += msg + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (anySuccess) {
|
||||
alert("Appeal submitted successfully to at least one mod npub.");
|
||||
form.reset();
|
||||
} else {
|
||||
alert("Could not submit to any target moderators. Check logs above.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user