nostr bookmark

    @@ -75,67 +75,69 @@ ); }; - const showDialog = (identifier, title, tTags, content) => { - const escapeSpecialChars = (str) => { - return str - .replace(/&/g, '&amp;') - .replace(/</g, '&lt;') - .replace(/>/g, '&gt;') - .replace(/"/g, '&quot;') - .replace(/'/g, '&#039;'); - }; - const escapeHTML = (strings, ...values) => { - return strings.reduce((result, str, i) => { - const value = values[i - 1]; - if (typeof value === 'string') { - return result + escapeSpecialChars(value) + str; - } else { - return result + String(value) + str; - } - }); - }; + const escapeSpecialChars = (str) => { + return str + .replace(/&/g, '&amp;') + .replace(/</g, '&lt;') + .replace(/>/g, '&gt;') + .replace(/"/g, '&quot;') + .replace(/'/g, '&#039;'); + }; + + const escapeHTML = (strings, ...values) => { + return strings.reduce((result, str, i) => { + const value = values[i - 1]; + if (typeof value === 'string') { + return result + escapeSpecialChars(value) + str; + } else { + return result + String(value) + str; + } + }); + }; + + const showDialogBookmark = (identifier, title, tTags, content) => { const view = escapeHTML` - <dialog id="nostr-web-bookmark-trend-dialog"> - <form> - <dl> - <dt>d-tag</dt> - <dd>${identifier}</dd> - <dt>title-tag</dt> - <dd>${title}</dd> - <dt> - <label for="nostr-web-bookmark-trend-tag-input">t-tag</label> - <button id="nostr-web-bookmark-trend-clear" type="button">Clear</button> - <span id="nostr-web-bookmark-trend-tags"></span> - </dt> - <dd> - <input id="nostr-web-bookmark-trend-tag-input" type="text" pattern="[^\\s#]+" /> - <button - id="nostr-web-bookmark-trend-tag-add" - type="button" - title="add" - >+</button> - </dd> - <dt><label for="nostr-web-bookmark-trend-content">content</label></dt> - <dd> - <textarea id="nostr-web-bookmark-trend-content"></textarea> - </dd> - <dt>Submit</dt> - <dd> - <button value="cancel" formmethod="dialog">Cancel</button> - <button id="nostr-web-bookmark-trend-confirm" value="default">Submit</button> - </dd> - </dl> - </form> - </dialog> + <dialog id="nostr-web-bookmark-trend-dialog-bookmark"> + <form> + <dl> + <dt>d-tag</dt> + <dd>${identifier}</dd> + <dt>title-tag</dt> + <dd>${title}</dd> + <dt> + <label for="nostr-web-bookmark-trend-tag-input">t-tag</label> + <button id="nostr-web-bookmark-trend-clear" type="button">Clear</button> + <span id="nostr-web-bookmark-trend-tags"></span> + </dt> + <dd> + <input id="nostr-web-bookmark-trend-tag-input" type="text" pattern="[^\\s#]+" /> + <button + id="nostr-web-bookmark-trend-tag-add" + type="button" + title="add" + >+</button> + </dd> + <dt><label for="nostr-web-bookmark-trend-content">content</label></dt> + <dd> + <textarea id="nostr-web-bookmark-trend-content"></textarea> + </dd> + <dt>Submit</dt> + <dd> + <button value="cancel" formmethod="dialog">Cancel</button> + <button id="nostr-web-bookmark-trend-confirm" value="default">Submit</button> + </dd> + </dl> + </form> + </dialog> `; const div = document.createElement('div'); div.innerHTML = view; document.body.append(div); const css = ` - dialog#nostr-web-bookmark-trend-dialog * { + dialog#nostr-web-bookmark-trend-dialog-bookmark * { all: revert; } - dialog#nostr-web-bookmark-trend-dialog { + dialog#nostr-web-bookmark-trend-dialog-bookmark { margin: auto; padding: 10px; text-align: left; @@ -152,7 +154,7 @@ style.textContent = css; document.head.appendChild(style); - const dialogId = 'nostr-web-bookmark-trend-dialog'; + const dialogId = 'nostr-web-bookmark-trend-dialog-bookmark'; const clearId = 'nostr-web-bookmark-trend-clear'; const tagsId = 'nostr-web-bookmark-trend-tags'; const tagInputId = 'nostr-web-bookmark-trend-tag-input'; @@ -217,6 +219,48 @@ }); }; + const showDialogResult = (relays, url) => { + const view = escapeHTML` + <dialog id="nostr-web-bookmark-trend-dialog-result"> + <p>以下のリレーに送信しました。</p> + <p>${relays.join('\n')}</p> + <p><a href="${url}" target="_blank" rel="noopener noreferrer">ブックマークページを開く</a></p> + <form> + <button value="close" formmethod="dialog">Close</button> + </form> + </dialog> + `; + const div = document.createElement('div'); + div.innerHTML = view; + document.body.append(div); + const css = ` + dialog#nostr-web-bookmark-trend-dialog-result * { + all: revert; + } + dialog#nostr-web-bookmark-trend-dialog-result { + margin: auto; + padding: 10px; + text-align: left; + } + dialog#nostr-web-bookmark-trend-dialog-result > p { + white-space: pre-line; + } + `; + const style = document.createElement('style'); + style.textContent = css; + document.head.appendChild(style); + + const dialogId = 'nostr-web-bookmark-trend-dialog-result'; + const dialog = document.getElementById(dialogId); + dialog.showModal(); + + return new Promise((resolve) => { + dialog.addEventListener('close', (e) => { + resolve('close'); + }); + }); + }; + const main = async () => { const pubkey = await getPubkey(); if (pubkey === undefined) { @@ -254,7 +298,7 @@ } const tTagsOld = event39701?.tags.filter((tag) => tag[0] === 't').map((tag) => tag[1].toLowerCase()) ?? []; - const resDialog = await showDialog( + const resDialog = await showDialogBookmark( identifier, document.title, tTagsOld, @@ -274,15 +318,7 @@ created_at: now }; const signedEvent = await window.nostr.signEvent(eventTemplate); - if ( - !window.confirm(`以下のリレーに送信します。よろしいですか?\n${relaysToWrite.join('\n')}`) - ) { - return; - } await Promise.any(pool.publish(relaysToWrite, signedEvent)); - if (!window.confirm('送信完了しました。ブックマークページを開きますか?')) { - return; - } const naddr = window.NostrTools.nip19.naddrEncode({ identifier, kind, @@ -290,7 +326,7 @@ relays: relaysToWrite }); const urlToOpen = `https://nostr-web-bookmark-trend.vercel.app/${naddr}`; - location.href = urlToOpen; + await showDialogResult(relaysToWrite, urlToOpen); }; affixScriptToHead(nostrToolsUrl, main);
  • /*
     * @title nostr bookmark
     * @description 現在のWebページをNostrでブックマーク
     * @include http://*
     * @license CC0 1.0
     * @require
     */
    
    (() => {
    	const nostrToolsUrl = 'https://unpkg.com/nostr-tools/lib/nostr.bundle.js';
    	const profileRelay = 'wss://directory.yabu.me/';
    	const now = Math.floor(Date.now() / 1000);
    
    	const getPubkey = async () => {
    		let pubkey;
    		if (window.nostr?.getPublicKey) {
    			try {
    				pubkey = await window.nostr.getPublicKey();
    			} catch (error) {
    				console.warn(error);
    			}
    		}
    		return pubkey;
    	};
    
    	const getReplaceableEvent = (pool, relays, filter) => {
    		return new Promise((resolve) => {
    			let event;
    			const sub = pool.subscribe(relays, filter, {
    				onevent(ev) {
    					if (event === undefined || event.created_at < ev.created_at) {
    						event = ev;
    					}
    				},
    				oneose() {
    					sub.close();
    					resolve(event);
    				}
    			});
    		});
    	};
    
    	const getRelaysToUseFromKind10002Event = (event) => {
    		const newRelays = {};
    		for (const tag of event?.tags.filter(
    			(tag) => tag.length >= 2 && tag[0] === 'r' && URL.canParse(tag[1])
    		) ?? []) {
    			const url = window.NostrTools.utils.normalizeURL(tag[1]);
    			const isRead = tag.length === 2 || tag[2] === 'read';
    			const isWrite = tag.length === 2 || tag[2] === 'write';
    			if (newRelays[url] === undefined) {
    				newRelays[url] = {
    					read: isRead,
    					write: isWrite
    				};
    			} else {
    				if (isRead) {
    					newRelays[url].read = true;
    				}
    				if (isWrite) {
    					newRelays[url].write = true;
    				}
    			}
    		}
    		return newRelays;
    	};
    
    	const getRelays = (relayRecord, relayType) => {
    		return Array.from(
    			new Set(
    				Object.entries(relayRecord)
    					.filter(([_, obj]) => obj[relayType])
    					.map(([relay, _]) => relay)
    			)
    		);
    	};
    
    	const escapeSpecialChars = (str) => {
    		return str
    			.replace(/&/g, '&amp;')
    			.replace(/</g, '&lt;')
    			.replace(/>/g, '&gt;')
    			.replace(/"/g, '&quot;')
    			.replace(/'/g, '&#039;');
    	};
    
    	const escapeHTML = (strings, ...values) => {
    		return strings.reduce((result, str, i) => {
    			const value = values[i - 1];
    			if (typeof value === 'string') {
    				return result + escapeSpecialChars(value) + str;
    			} else {
    				return result + String(value) + str;
    			}
    		});
    	};
    
    	const showDialogBookmark = (identifier, title, tTags, content) => {
    		const view = escapeHTML`
    			<dialog id="nostr-web-bookmark-trend-dialog-bookmark">
    				<form>
    					<dl>
    						<dt>d-tag</dt>
    						<dd>${identifier}</dd>
    						<dt>title-tag</dt>
    						<dd>${title}</dd>
    						<dt>
    							<label for="nostr-web-bookmark-trend-tag-input">t-tag</label>
    							<button id="nostr-web-bookmark-trend-clear" type="button">Clear</button>
    							<span id="nostr-web-bookmark-trend-tags"></span>
    						</dt>
    						<dd>
    							<input id="nostr-web-bookmark-trend-tag-input" type="text" pattern="[^\\s#]+" />
    							<button
    								id="nostr-web-bookmark-trend-tag-add"
    								type="button"
    								title="add"
    							>+</button>
    						</dd>
    						<dt><label for="nostr-web-bookmark-trend-content">content</label></dt>
    						<dd>
    							<textarea id="nostr-web-bookmark-trend-content"></textarea>
    						</dd>
    						<dt>Submit</dt>
    						<dd>
    							<button value="cancel" formmethod="dialog">Cancel</button>
    							<button id="nostr-web-bookmark-trend-confirm" value="default">Submit</button>
    						</dd>
    					</dl>
    				</form>
    			</dialog>
    		`;
    		const div = document.createElement('div');
    		div.innerHTML = view;
    		document.body.append(div);
    		const css = `
    			dialog#nostr-web-bookmark-trend-dialog-bookmark * {
    				all: revert;
    			}
    			dialog#nostr-web-bookmark-trend-dialog-bookmark {
    				margin: auto;
    				padding: 10px;
    				text-align: left;
    			}
    			input#nostr-web-bookmark-trend-tag-input:invalid {
    				outline: 2px solid red;
    			}
    			textarea#nostr-web-bookmark-trend-content {
    				width: 30em;
    				height: 5em;
    			}
    		`;
    		const style = document.createElement('style');
    		style.textContent = css;
    		document.head.appendChild(style);
    
    		const dialogId = 'nostr-web-bookmark-trend-dialog-bookmark';
    		const clearId = 'nostr-web-bookmark-trend-clear';
    		const tagsId = 'nostr-web-bookmark-trend-tags';
    		const tagInputId = 'nostr-web-bookmark-trend-tag-input';
    		const tagAddId = 'nostr-web-bookmark-trend-tag-add';
    		const contentId = 'nostr-web-bookmark-trend-content';
    		const confirmId = 'nostr-web-bookmark-trend-confirm';
    
    		const dialog = document.getElementById(dialogId);
    		const form = dialog.querySelector('form');
    		const clearButton = dialog.querySelector(`#${clearId}`);
    		const tags = dialog.querySelector(`#${tagsId}`);
    		const tagInput = dialog.querySelector(`#${tagInputId}`);
    		const tagAddButton = dialog.querySelector(`#${tagAddId}`);
    		const contentEl = dialog.querySelector(`#${contentId}`);
    		const confirmButton = dialog.querySelector(`#${confirmId}`);
    
    		form.addEventListener('keypress', (e) => {
    			if (tagInput === document.activeElement && e.key === 'Enter') {
    				e.preventDefault();
    			}
    		});
    
    		clearButton.addEventListener('click', () => {
    			tTags.length = 0;
    			tagInput.value = '';
    			tagInput.focus();
    			tags.textContent = '';
    		});
    
    		tagAddButton.addEventListener('click', () => {
    			if (tagInput.value.length === 0 || tagInput.validity.patternMismatch) {
    				return;
    			}
    			const v = tagInput.value.toLowerCase();
    			if (tTags.includes(v)) {
    				return;
    			}
    			tTags.push(v);
    			tagInput.value = '';
    			tagInput.focus();
    			tags.textContent = tTags.map((t) => `#${t}`).join(' ');
    		});
    
    		confirmButton.addEventListener('click', (e) => {
    			e.preventDefault();
    			const res = {
    				tTags,
    				content: contentEl.value
    			};
    			dialog.close(JSON.stringify(res));
    		});
    
    		tags.textContent = tTags.map((t) => `#${t}`).join(' ');
    		contentEl.value = content;
    		dialog.showModal();
    
    		return new Promise((resolve) => {
    			dialog.addEventListener('close', (e) => {
    				const r = dialog.returnValue === 'cancel' ? null : dialog.returnValue;
    				resolve(r);
    			});
    		});
    	};
    
    	const showDialogResult = (relays, url) => {
    		const view = escapeHTML`
    			<dialog id="nostr-web-bookmark-trend-dialog-result">
    				<p>以下のリレーに送信しました。</p>
    				<p>${relays.join('\n')}</p>
    				<p><a href="${url}" target="_blank" rel="noopener noreferrer">ブックマークページを開く</a></p>
    				<form>
    					<button value="close" formmethod="dialog">Close</button>
    				</form>
    			</dialog>
    		`;
    		const div = document.createElement('div');
    		div.innerHTML = view;
    		document.body.append(div);
    		const css = `
    			dialog#nostr-web-bookmark-trend-dialog-result * {
    				all: revert;
    			}
    			dialog#nostr-web-bookmark-trend-dialog-result {
    				margin: auto;
    				padding: 10px;
    				text-align: left;
    			}
    			dialog#nostr-web-bookmark-trend-dialog-result > p {
    				white-space: pre-line;
    			}
    		`;
    		const style = document.createElement('style');
    		style.textContent = css;
    		document.head.appendChild(style);
    
    		const dialogId = 'nostr-web-bookmark-trend-dialog-result';
    		const dialog = document.getElementById(dialogId);
    		dialog.showModal();
    
    		return new Promise((resolve) => {
    			dialog.addEventListener('close', (e) => {
    				resolve('close');
    			});
    		});
    	};
    
    	const main = async () => {
    		const pubkey = await getPubkey();
    		if (pubkey === undefined) {
    			return;
    		}
    		const url = new URL(location.href);
    		url.search = '';
    		url.hash = '';
    		const identifier = url.href
    			.replace(/#$/, '')
    			.replace(/\?$/, '')
    			.replace(/^https?:\/\//, '');
    		const pool = new window.NostrTools.SimplePool();
    		const event10002 = await getReplaceableEvent(pool, [profileRelay], {
    			kinds: [10002],
    			authors: [pubkey],
    			until: now
    		});
    		const rr = getRelaysToUseFromKind10002Event(event10002);
    		const relaysToRead = getRelays(rr, 'read');
    		const relaysToWrite = getRelays(rr, 'write');
    		const kind = 39701;
    		const event39701 = await getReplaceableEvent(pool, relaysToRead, {
    			kinds: [kind],
    			authors: [pubkey],
    			'#d': [identifier],
    			until: now
    		});
    		const tags = event39701?.tags.filter((tag) => !['t', 'title'].includes(tag[0])) ?? [
    			['d', identifier],
    			['published_at', String(now)]
    		];
    		if (document.title.length > 0) {
    			tags.push(['title', document.title]);
    		}
    		const tTagsOld =
    			event39701?.tags.filter((tag) => tag[0] === 't').map((tag) => tag[1].toLowerCase()) ?? [];
    		const resDialog = await showDialogBookmark(
    			identifier,
    			document.title,
    			tTagsOld,
    			event39701?.content ?? ''
    		);
    		if (resDialog === null) {
    			return;
    		}
    		const { content, tTags } = JSON.parse(resDialog);
    		for (const t of tTags) {
    			tags.push(['t', t]);
    		}
    		const eventTemplate = {
    			content,
    			kind,
    			tags,
    			created_at: now
    		};
    		const signedEvent = await window.nostr.signEvent(eventTemplate);
    		await Promise.any(pool.publish(relaysToWrite, signedEvent));
    		const naddr = window.NostrTools.nip19.naddrEncode({
    			identifier,
    			kind,
    			pubkey,
    			relays: relaysToWrite
    		});
    		const urlToOpen = `https://nostr-web-bookmark-trend.vercel.app/${naddr}`;
    		await showDialogResult(relaysToWrite, urlToOpen);
    	};
    
    	affixScriptToHead(nostrToolsUrl, main);
    
    	//https://developer.mozilla.org/ja/docs/Web/API/HTMLScriptElement のコピペ
    	function loadError(oError) {
    		throw new URIError(`スクリプト ${oError.target.src} は正しく読み込まれませんでした。`);
    	}
    
    	function affixScriptToHead(url, onloadFunction) {
    		const newScript = document.createElement('script');
    		newScript.onerror = loadError;
    		if (onloadFunction) {
    			newScript.onload = onloadFunction;
    		}
    		document.head.appendChild(newScript);
    		newScript.src = url;
    	}
    })();
    
  • Permalink
    このページへの個別リンクです。
    RAW
    書かれたコードへの直接のリンクです。
    Packed
    文字列が圧縮された書かれたコードへのリンクです。
    Userscript
    Greasemonkey 等で利用する場合の .user.js へのリンクです。
    Loader
    @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
    Metadata
    コード中にコメントで @xxx と書かれたメタデータの JSON です。