mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 23:18:43 +00:00
update
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
margin: 20px;
|
margin: 20px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: transparent; /* transparent background */
|
background: transparent;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #234566; /* dark blue */
|
background-color: #234566;
|
||||||
color: #fff; /* white text */
|
color: #fff;
|
||||||
border: 1px solid #888;
|
border: 1px solid #888;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Bitvid Content Appeals Form</h1>
|
<h1>Bitvid Content Appeals Form</h1>
|
||||||
|
|
||||||
<form id="appeal-form">
|
<form id="appeal-form">
|
||||||
<h3>1. User Information</h3>
|
<h3>1. User Information</h3>
|
||||||
<label for="npub">Nostr Public Key (npub)</label>
|
<label for="npub">Nostr Public Key (npub)</label>
|
||||||
@@ -175,6 +176,7 @@
|
|||||||
|
|
||||||
<button type="submit">Submit Appeal</button>
|
<button type="submit">Submit Appeal</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Processing Time:</strong> Appeals will be reviewed within 7-14
|
<strong>Processing Time:</strong> Appeals will be reviewed within 7-14
|
||||||
days.
|
days.
|
||||||
@@ -183,11 +185,478 @@
|
|||||||
<div id="status-message"></div>
|
<div id="status-message"></div>
|
||||||
|
|
||||||
<script>
|
<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";
|
const ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
function polymod(values) {
|
function polymod(values) {
|
||||||
let chk = 1;
|
let chk = 1;
|
||||||
@@ -247,8 +716,8 @@
|
|||||||
function convertBits(data, inBits, outBits, pad = true) {
|
function convertBits(data, inBits, outBits, pad = true) {
|
||||||
let value = 0;
|
let value = 0;
|
||||||
let bits = 0;
|
let bits = 0;
|
||||||
const result = [];
|
|
||||||
const maxV = (1 << outBits) - 1;
|
const maxV = (1 << outBits) - 1;
|
||||||
|
const result = [];
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
value = (value << inBits) | data[i];
|
value = (value << inBits) | data[i];
|
||||||
bits += inBits;
|
bits += inBits;
|
||||||
@@ -268,230 +737,6 @@
|
|||||||
}
|
}
|
||||||
return result;
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Reference in New Issue
Block a user