ok, ik heb nu toegang. bedankt!
top! heel erg bedankt !
ik wil graag meedoen aan de nederlandstalige relay lang.relays.land/nl
I have thought about Switzerland’s strategy. AKAIK and from speaking with ArmaSuisse, there is everything except oil. It would run out in a few days.
I often thought making ethanol and wood alcohol for fuel would be a solution. Works with gas engines
I like the goal of getting as many people exposed as many books as possible.
I understand that some people also want the Project Gutenberg version of The Iliad in Mobi format etc., I would focus on ease of exposure
Eg these classics are all perfect in markdown https://github.com/mlschmitt/classic-books-markdown turning one of those into a NIP-23 works perfectly, and they can be read in any client
Form over function?
It’s text, no need to turn it into a binary blob
Markdown because it is a format that works with NIP-23. The vast majority of books need only the most basic markdown (if that). No extension or deviation from the spec is needed.
Anything with images is a different matter, comic idk much about, but they are normally specific formats right?
re: libgen, disappointing, libgen+ is available
re: books over nostr: I have been thinking about this a lot:
- NIP-23 markdown is enough for most books
- converion to epub can be done using Calibre, pandoc, Google Docs
I'm working on an app series that renders markdown similar to the Apple Books app 
the downloading to a PC is gone, the kindles are actually unchanged. There is a jailbreak btw that works for kindles since about 2 months `winterbreak`
Ever wanted a privacy-focused nostr web client? http://nostr.spa (https://paulmillr.com/demos/nostr) got you covered! It’s simple, open-source, and does not require a private key. You can even send messages, pre-signed somewhere else.
This sounds great, but is offline ATM:
Not found :(
Sorry, but the page you were trying to view does not exist.
It looks like this was the result of either:
a mistyped address
an out-of-date link
Go back to the main page 
Nostrings: Standalone HTML-Javscript client with no dependencies (382 lines):
https://github.com/bquast/nostrings
#asknostr
body {
font-family: sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 0 10px;
}
h1, h2 {
color: #333;
}
#events {
border: 1px solid #ccc;
height: 300px;
overflow-y: auto;
padding: 5px;
background: #fafafa;
}
input, textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
box-sizing: border-box;
}
button {
padding: 10px 20px;
font-size: 1em;
}
.section {
margin-bottom: 20px;
}
Nostrings
Connect to a Relay
id="relayUrl"
type="text"
placeholder="Relay URL (e.g., wss://relay.damus.io)"
value="wss://relay.damus.io"
/>
Subscribe to Kind 1 (Text Note) Events
Events
Publish an Event
// -- secp256k1 parameters (all as BigInt) --
const F = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");
const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
const Gx = BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
const Gy = BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8");
const G = { x: Gx, y: Gy };
// -- modular arithmetic --
function mod(a, m) {
const result = a % m;
return result >= 0n ? result : result + m;
}
function modInv(a, m) {
let [lm, hm] = [1n, 0n];
let [low, high] = [mod(a, m), m];
while (low > 1n) {
const r = high / low;
[lm, hm] = [hm - lm * r, lm];
[low, high] = [high - low * r, low];
}
return mod(lm, m);
}
// -- elliptic curve point operations --
// pepresent points as {x: BigInt, y: BigInt}, null for infinity
function pointAdd(P, Q) {
if (P === null) return Q;
if (Q === null) return P;
if (P.x === Q.x && P.y !== Q.y) return null;
let m;
if (P.x === Q.x && P.y === Q.y) {
m = mod(3n * P.x * P.x * modInv(2n * P.y, F), F);
} else {
m = mod((Q.y - P.y) * modInv(Q.x - P.x, F), F);
}
const rx = mod(m * m - P.x - Q.x, F);
const ry = mod(m * (P.x - rx) - P.y, F);
return { x: rx, y: ry };
}
function scalarMultiply(k, point) {
let result = null;
let addend = point;
while (k > 0n) {
if (k & 1n) result = pointAdd(result, addend);
addend = pointAdd(addend, addend);
k >>= 1n;
}
return result;
}
// -- helper functions --
function bigIntToHex(n, length) {
let hex = n.toString(16);
while (hex.length < length) hex = "0" + hex;
return hex;
}
// convert an ArrayBuffer or Uint8Array to hex
function bufferToHex(buffer) {
if (buffer instanceof ArrayBuffer) buffer = new Uint8Array(buffer);
return Array.from(buffer)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// convert a hex string to a Uint8Array
function hexToBytes(hex) {
if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
// convert a Uint8Array to BigInt
function bytesToBigInt(bytes) {
return BigInt("0x" + bufferToHex(bytes));
}
// SHA-256 hash (returns hex string)
async function sha256(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return bufferToHex(hashBuffer);
}
// -- tagged hash per BIP-340 --
// computes sha256(sha256(tag)||sha256(tag)||...msgs concatenated)
async function taggedHash(tag, ...msgs) {
const encoder = new TextEncoder();
const tagBytes = encoder.encode(tag);
const tagHashBuffer = await crypto.subtle.digest("SHA-256", tagBytes);
const tagHash = new Uint8Array(tagHashBuffer);
let totalLength = tagHash.length * 2;
const arrays = [];
for (const msg of msgs) {
if (typeof msg === "string" && /^[0-9a-fA-F]+$/.test(msg) && msg.length % 2 === 0) {
// assume hex string representing binary data
const b = hexToBytes(msg);
arrays.push(b);
totalLength += b.length;
} else if (msg instanceof Uint8Array) {
arrays.push(msg);
totalLength += msg.length;
} else if (typeof msg === "string") {
const b = encoder.encode(msg);
arrays.push(b);
totalLength += b.length;
} else {
throw new Error("Unsupported type in taggedHash");
}
}
const buffer = new Uint8Array(totalLength);
buffer.set(tagHash, 0);
buffer.set(tagHash, tagHash.length);
let offset = tagHash.length * 2;
for (const arr of arrays) {
buffer.set(arr, offset);
offset += arr.length;
}
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
return new Uint8Array(hashBuffer);
}
// -- high-level secp256k1 functions for Nostr --
// derive public key point (full point) from a private key (hex)
function getPublicKey(privKeyHex) {
const d = BigInt("0x" + privKeyHex);
return scalarMultiply(d, G);
}
// -- Schnorr signing (BIP-340 style) --
// implementation uses tagged hashes for nonce and challenge
async function schnorrSign(privKeyHex, message) {
// Convert private key to BigInt and bytes.
const d = BigInt("0x" + privKeyHex);
const dBytes = hexToBytes(privKeyHex); // 32 bytes
// derive the x-only public key
const pubPoint = getPublicKey(privKeyHex);
const pubKeyX = bigIntToHex(pubPoint.x, 64); // 32 bytes as hex
// --- nonce derivation ---
// per BIP-340: k = int(taggedHash("BIP0340/nonce", d_bytes, message)) mod N
const nonceHash = await taggedHash("BIP0340/nonce", bufferToHex(dBytes), message);
let k = mod(bytesToBigInt(nonceHash), N);
if (k === 0n) throw new Error("Invalid nonce derived");
let R = scalarMultiply(k, G);
if (R === null) throw new Error("R is infinity, bad nonce");
// If R.y is odd, set k = N - k.
if (R.y % 2n !== 0n) {
k = N - k;
R = scalarMultiply(k, G);
}
const rHex = bigIntToHex(R.x, 64); // 32-byte x-coordinate
// --- challenge derivation ---
// e = int(taggedHash("BIP0340/challenge", r, pubKeyX, message)) mod N
const challengeHash = await taggedHash("BIP0340/challenge", rHex, pubKeyX, message);
const e = mod(bytesToBigInt(challengeHash), N);
// --- signature calculation ---
const s = mod(k + e * d, N);
return rHex + bigIntToHex(s, 64);
}
let ws;
let subId = null;
// DOM elements
const relayUrlInput = document.getElementById("relayUrl");
const connectBtn = document.getElementById("connectBtn");
const statusDiv = document.getElementById("status");
const subscribeBtn = document.getElementById("subscribeBtn");
const eventsDiv = document.getElementById("events");
const sendBtn = document.getElementById("sendBtn");
// connect to a relay
connectBtn.addEventListener("click", () => {
const url = relayUrlInput.value.trim();
if (!url) {
alert("Please enter a relay URL.");
return;
}
ws = new WebSocket(url);
ws.onopen = () => { statusDiv.textContent = "Connected to " + url; };
ws.onmessage = (message) => {
try {
const data = JSON.parse(message.data);
if (data[0] === "EVENT") {
const event = data[2];
displayEvent(event);
} else if (data[0] === "NOTICE") {
console.log("Notice: " + data[1]);
} else if (data[0] === "OK") {
console.log("OK response: ", data);
}
} catch (e) {
console.error("Error parsing message:", e);
}
};
ws.onerror = (e) => {
statusDiv.textContent = "Error connecting to relay.";
console.error(e);
};
ws.onclose = () => { statusDiv.textContent = "Disconnected."; };
});
// subscribe to kind 1 events
subscribeBtn.addEventListener("click", () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert("Please connect to a relay first.");
return;
}
subId = generateSubId();
const req = ["REQ", subId, { kinds: [1] }];
ws.send(JSON.stringify(req));
appendLog("Subscribed with id: " + subId);
});
// publish an event
sendBtn.addEventListener("click", async () => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert("Please connect to a relay first.");
return;
}
const privKey = document.getElementById("privateKey").value.trim();
if (!privKey) {
alert("Please enter your private key.");
return;
}
const content = document.getElementById("content").value.trim();
if (!content) {
alert("Please enter content for your event.");
return;
}
try {
// derive x-only public key
const pubPoint = getPublicKey(privKey);
const pubKeyX = bigIntToHex(pubPoint.x, 64);
console.log("pubKey:", pubKeyX, "length:", pubKeyX.length);
// build the event (NIP-01 format)
const event = {
pubkey: pubKeyX,
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: content,
};
// serialize the event
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content,
]);
const id = await sha256(serialized);
event.id = id;
console.log("Event ID:", id, "length:", id.length);
// sign event ID
const sig = await schnorrSign(privKey, id);
console.log("Signature:", sig, "length:", sig.length);
event.sig = sig;
// publish event
const msg = ["EVENT", event];
ws.send(JSON.stringify(msg));
appendLog("Event sent: " + JSON.stringify(event));
} catch (e) {
console.error(e);
alert("Error sending event: " + e);
}
});
// helper functions for UI
function displayEvent(event) {
const div = document.createElement("div");
div.style.borderBottom = "1px solid #eee";
div.style.padding = "5px";
div.textContent =
"[" + event.created_at + "] " +
event.pubkey.substring(0, 8) +
"...: " + event.content;
eventsDiv.prepend(div);
}
function appendLog(message) {
const p = document.createElement("p");
p.textContent = message;
eventsDiv.prepend(p);
}
function generateSubId() {
return Math.random().toString(36).substr(2, 9);
}
how about when ADHD means you hate managing other people?
I end up cleaning, washing, everything myself, because I think the help is more trouble than it is worth
# Page 1

## Welcome to Stories
This is a new way to tell stories on nostr.
##
Some content in the middle of the page.
## Read More
Swipe left to continue...
# Page 2

## Top Info
This text appears at the top of the page.
## Important Point
This content is centered in the middle.
## Bottom Line
And this wraps it up at the bottom.
# Page 3

##
##
## The End
Thanks for reading! This text appears at the bottom.
# Page: cover
background: #000000
template: vertical
# Welcome to My Story
This is an amazing story.
# Page: page1
background-image: 
template: vertical
## First Page
Content goes here
Ancient Apocalypse (Graham Hancock):
> sea levels 120m lower a mere 20,000 years ago
I bet a lot of people don't know our world is very much not static
What is the best way to enroll non-technical people on #nostr ?
