<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel rdf:about="https://let.hatelabo.jp/Nikola/rss">
    <link>https://let.hatelabo.jp/Nikola/rss</link>
    <description></description>
    <title>Bookmarklets from Nikola</title>
    <items>
      <rdf:Seq>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kO23t_SCgcAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/k56d8sbogqAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/k4HPq9jOgYAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/ku6Php3ygoAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kuj34OLcgsAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/ktDU2MuegsAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kqyujdzOgKAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kO26wurQgcAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kc6ngIXCgqAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/kbSl9ra-gqAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/karR3IrkgOAA"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/gYC-yLSpueTJcQ"/>
        <rdf:li rdf:resource="https://let.hatelabo.jp/Nikola/let/gYC-y5SY84WKVg"/>
      </rdf:Seq>
    </items>
  </channel>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kO23t_SCgcAA">
    <link>https://let.hatelabo.jp/Nikola/let/kO23t_SCgcAA</link>
    <dc:date>2025-08-20T23:18:27Z</dc:date>
    <description>リレーからkind10000イベントを取得してRabbitにミュートリストをインポートする</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] import mute list to Rabbit</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkO23t_SCgcAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;import mute list to Rabbit&lt;/a&gt;&lt;pre&gt;/*
 * @title import mute list to Rabbit
 * @description リレーからkind10000イベントを取得してRabbitにミュートリストをインポートする
 * @include https://rabbit.syusui.net/*
 * @license CC0 1.0
 * @require
 */

(async () =&amp;gt; {
	if (typeof window === 'undefined') {
		await import('websocket-polyfill');
	}

	let relayUrl = 'wss://relay-jp.nostr.wirednet.jp';

	const getPubkey = async () =&amp;gt; {
		let pubkey; //Node.jsでデバッグする際はここに初期値を入れてください。無ければ最新の誰かのイベントで代用します。
		const nostr = globalThis.window?.nostr;
		if (nostr?.getPublicKey) {
			try {
				pubkey = await nostr.getPublicKey();
			} catch (error) {
				console.warn(error);
			}
		}
		return pubkey;
	};

	const getEvent10000 = async (pubkey) =&amp;gt; {
		return new Promise((resolve) =&amp;gt; {
			const ws = new WebSocket(relayUrl);
			const subscription_id = 'getmutelistinrabbit';
			let res;
			ws.onopen = () =&amp;gt; {
				const req = ['REQ', subscription_id, pubkey ? { kinds: [10000], authors: [pubkey] } : { kinds: [10000], limit: 1 }];
				ws.send(JSON.stringify(req));
			};
			ws.onmessage = (e) =&amp;gt; {
				const msg = JSON.parse(e.data);
				switch (msg[0]) {
					case 'EVENT':
						res = msg[2];
						break;
					case 'EOSE':
						ws.send(JSON.stringify(['CLOSE', subscription_id]));
						ws.close();
						resolve(res);
						break;
					default:
						console.log(msg);
						break;
				}
			};
			ws.onerror = () =&amp;gt; {
				console.error('failed to connect');
			};
		});
	};

	const getDecrypt = (content) =&amp;gt; {
		const nostr = globalThis.window?.nostr;
		const isNIP04 = content.includes('?iv=');
		if (isNIP04 &amp;amp;&amp;amp; nostr?.nip04?.decrypt !== undefined) {
			return nostr.nip04.decrypt;
		} else if (nostr?.nip44?.decrypt !== undefined) {
			return nostr.nip44.decrypt;
		}
		return null;
	};

	const getMuteList = async (event, pubkey) =&amp;gt; {
		let mutedPubkeys = event.tags.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'p').map((tag) =&amp;gt; tag[1]) ?? [];
		let mutedChannels = event.tags.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'e').map((tag) =&amp;gt; tag[1]) ?? [];
		let mutedWords = event.tags.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'word').map((tag) =&amp;gt; tag[1]) ?? [];
		let mutedHashtags = event.tags.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 't').map((tag) =&amp;gt; tag[1]) ?? [];
		if (event.content.length &amp;gt; 0) {
			const decrypt = getDecrypt(event.content);
			if (decrypt !== null) {
				try {
					const content = await decrypt(pubkey, event.content);
					const list = JSON.parse(content);
					mutedPubkeys = mutedPubkeys.concat(list.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'p').map((tag) =&amp;gt; tag[1]));
					mutedChannels = mutedChannels.concat(list.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'e').map((tag) =&amp;gt; tag[1]));
					mutedWords = mutedWords.concat(list.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'word').map((tag) =&amp;gt; tag[1]));
					mutedHashtags = mutedHashtags.concat(list.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 't').map((tag) =&amp;gt; tag[1]));
				} catch (error) {
					console.warn(error);
				}
			}
		}
		return [mutedPubkeys, Array.from(new Set(mutedWords.concat(mutedHashtags.map((t) =&amp;gt; '#' + t)))), mutedChannels];
	};

	const main = async () =&amp;gt; {
		relayUrl = window.prompt('Input relay URL.', relayUrl);
		if (!URL.canParse(relayUrl)) {
			console.warn(`Invalid URL: ${relayUrl}`);
			return;
		}
		let pubkey;
		try {
			pubkey = await getPubkey();
		} catch (error) {
			console.warn(error);
			return;
		}
		if (globalThis.window &amp;amp;&amp;amp; !pubkey) {
			console.warn('pubkey is empty.');
			return;
		}
		let ev10000;
		try {
			ev10000 = await getEvent10000(pubkey);
		} catch (error) {
			console.warn(error);
			return;
		}
		const [mutedPubkeysList, mutedKeywordsList, mutedChannelList] = await getMuteList(ev10000, pubkey);
		if (globalThis.window) {
			const config = JSON.parse(localStorage['RabbitConfig']);
			config.mutedPubkeys = Array.from(new Set([...config.mutedPubkeys, ...mutedPubkeysList]));
			config.mutedKeywords = Array.from(new Set([...config.mutedKeywords, ...mutedKeywordsList]));
			config.mutedThreads = Array.from(new Set([...config.mutedThreads, ...mutedChannelList]));
			localStorage['RabbitConfig'] = JSON.stringify(config);
			alert('Complete. Please reload by yourself.');
		} else {
			console.log({ mutedPubkeysList, mutedKeywordsList });
			console.log('Complete.');
		}
	};

	main();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/k56d8sbogqAA">
    <link>https://let.hatelabo.jp/Nikola/let/k56d8sbogqAA</link>
    <dc:date>2025-06-21T22:43:13Z</dc:date>
    <description>現在のWebページをNostrでブックマークするためにKUCHIYOSEに遷移</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] go KUCHIYOSE</title>
    <content:encoded>&lt;a href="javascript:%28%28%29%3D%3E%7Bconst%20website%3D%27https%3A%2F%2Fkuchiyose.vercel.app%2F%27%3Bconst%20url%3Dnew%20URL%28location.href%29%3Burl.search%3D%27%27%3Burl.hash%3D%27%27%3Bconst%20identifier%3Durl.href.replace%28%2F%23%24%2F%2C%27%27%29.replace%28%2F%5C%3F%24%2F%2C%27%27%29.replace%28%2F%5Ehttps%3F%3A%5C%2F%5C%2F%2F%2C%27%27%29%3Bconst%20title%3Ddocument.title%3Bconst%20usp%3Dnew%20URLSearchParams%28%7Bd%3Aidentifier%2Ctitle%3Atitle%7D%29%3Bconst%20urlToGo%3D%60%24%7Bwebsite%7Dentry%2F%24%7Bidentifier%7D%3F%24%7Busp.toString%28%29%7D%60%3Blocation.href%3DurlToGo%7D%29%28%29%3B"&gt;go KUCHIYOSE&lt;/a&gt;&lt;pre&gt;/*
 * @title go KUCHIYOSE
 * @description 現在のWebページをNostrでブックマークするためにKUCHIYOSEに遷移
 * @include http://*
 * @license CC0 1.0
 * @javascript_url
 */

(() =&amp;gt; {
	const website = 'https://kuchiyose.vercel.app/';
	const url = new URL(location.href);
	url.search = '';
	url.hash = '';
	const identifier = url.href
		.replace(/#$/, '')
		.replace(/\?$/, '')
		.replace(/^https?:\/\//, '');
	const title = document.title;
	const usp = new URLSearchParams({
		d: identifier,
		title
	});
	const urlToGo = `${website}entry/${identifier}?${usp.toString()}`;
	location.href = urlToGo;
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/k4HPq9jOgYAA">
    <link>https://let.hatelabo.jp/Nikola/let/k4HPq9jOgYAA</link>
    <dc:date>2025-06-06T06:28:55Z</dc:date>
    <description>現在のWebページをNostrでブックマーク</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] nostr bookmark</title>
    <content:encoded>&lt;a href="javascript:%28%28%29%3D%3E%7Bconst%20website%3D%27https%3A%2F%2Fkuchiyose.vercel.app%2F%27%3Bconst%20nostrToolsUrl%3D%27https%3A%2F%2Funpkg.com%2Fnostr-tools%2Flib%2Fnostr.bundle.js%27%3Bconst%20indexerRelays%3D%5B%27wss%3A%2F%2Fdirectory.yabu.me%2F%27%2C%27wss%3A%2F%2Fpurplepag.es%2F%27%2C%27wss%3A%2F%2Findexer.coracle.social%2F%27%5D%3Bconst%20now%3DMath.floor%28Date.now%28%29%2F1e3%29%3Bconst%20getPubkey%3Dasync%28%29%3D%3E%7Blet%20pubkey%3Bif%28window.nostr%3F.getPublicKey%29%7Btry%7Bpubkey%3Dawait%20window.nostr.getPublicKey%28%29%7Dcatch%28error%29%7Bconsole.warn%28error%29%7D%7Dreturn%20pubkey%7D%3Bconst%20getReplaceableEvent%3D%28pool%2Crelays%2Cfilter%29%3D%3Enew%20Promise%28%28resolve%3D%3E%7Blet%20event%3Bconst%20sub%3Dpool.subscribe%28relays%2Cfilter%2C%7Bonevent%28ev%29%7Bif%28event%3D%3D%3Dundefined%7C%7Cevent.created_at%3Cev.created_at%29%7Bevent%3Dev%7D%7D%2Coneose%28%29%7Bsub.close%28%29%3Bresolve%28event%29%7D%7D%29%7D%29%29%3Bconst%20getRelaysToUseFromKind10002Event%3Devent%3D%3E%7Bconst%20newRelays%3D%7B%7D%3Bfor%28const%20tag%20of%20event%3F.tags.filter%28%28tag%3D%3Etag.length%3E%3D2%26%26tag%5B0%5D%3D%3D%3D%27r%27%26%26URL.canParse%28tag%5B1%5D%29%29%29%3F%3F%5B%5D%29%7Bconst%20url%3Dwindow.NostrTools.utils.normalizeURL%28tag%5B1%5D%29%3Bconst%20isRead%3Dtag.length%3D%3D%3D2%7C%7Ctag%5B2%5D%3D%3D%3D%27read%27%3Bconst%20isWrite%3Dtag.length%3D%3D%3D2%7C%7Ctag%5B2%5D%3D%3D%3D%27write%27%3Bif%28newRelays%5Burl%5D%3D%3D%3Dundefined%29%7BnewRelays%5Burl%5D%3D%7Bread%3AisRead%2Cwrite%3AisWrite%7D%7Delse%7Bif%28isRead%29%7BnewRelays%5Burl%5D.read%3Dtrue%7Dif%28isWrite%29%7BnewRelays%5Burl%5D.write%3Dtrue%7D%7D%7Dreturn%20newRelays%7D%3Bconst%20getRelays%3D%28relayRecord%2CrelayType%29%3D%3EArray.from%28new%20Set%28Object.entries%28relayRecord%29.filter%28%28%28%5B_%2Cobj%5D%29%3D%3Eobj%5BrelayType%5D%29%29.map%28%28%28%5Brelay%2C_%5D%29%3D%3Erelay%29%29%29%29%3Bconst%20escapeSpecialChars%3Dstr%3D%3Estr.replace%28%2F%26%2Fg%2C%27%26amp%3B%27%29.replace%28%2F%3C%2Fg%2C%27%26lt%3B%27%29.replace%28%2F%3E%2Fg%2C%27%26gt%3B%27%29.replace%28%2F%22%2Fg%2C%27%26quot%3B%27%29.replace%28%2F%27%2Fg%2C%27%26%23039%3B%27%29%3Bconst%20escapeHTML%3D%28strings%2C...values%29%3D%3Estrings.reduce%28%28%28result%2Cstr%2Ci%29%3D%3E%7Bconst%20value%3Dvalues%5Bi-1%5D%3Bif%28typeof%20value%3D%3D%3D%27string%27%29%7Breturn%20result%2BescapeSpecialChars%28value%29%2Bstr%7Delse%7Breturn%20result%2BString%28value%29%2Bstr%7D%7D%29%29%3Bconst%20showDialogBookmark%3D%28identifier%2Ctitle%2CtTags%2Ccontent%2CisError%29%3D%3E%7Bconst%20usp%3Dnew%20URLSearchParams%28%7Bd%3Aidentifier%2Ctitle%3Atitle%2Ccontent%3Acontent%7D%29%3Bfor%28const%20t%20of%20tTags%29%7Busp.append%28%27t%27%2Ct%29%7Dconst%20url%3D%60%24%7Bwebsite%7Dentry%2F%24%7Bidentifier%7D%3F%24%7Busp.toString%28%29%7D%60%3Bconst%20view%3DescapeHTML%60%0A%09%09%09%3Cdialog%20id%3D%22nostr-web-bookmark-trend-dialog-bookmark%22%3E%0A%09%09%09%09%3Cform%3E%0A%09%09%09%09%09%3Cdiv%3E%0A%09%09%09%09%09%09%3Cp%3E%24%7BisError%3F%27%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E4%B8%8A%E3%81%AE%E7%90%86%E7%94%B1%E3%81%A7%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E4%B8%8B%E8%A8%98%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%8B%E3%82%89%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%97%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%F0%9F%99%87%27%3A%27%27%7D%3C%2Fp%3E%0A%09%09%09%09%09%09%3Cp%3E%3Ca%20href%3D%22%24%7Burl%7D%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3E%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E9%96%8B%E3%81%8F%3C%2Fa%3E%3C%2Fp%3E%0A%09%09%09%09%09%3C%2Fdiv%3E%0A%09%09%09%09%09%3Cdl%3E%0A%09%09%09%09%09%09%3Cdt%3Ed-tag%3C%2Fdt%3E%0A%09%09%09%09%09%09%3Cdd%3E%24%7Bidentifier%7D%3C%2Fdd%3E%0A%09%09%09%09%09%09%3Cdt%3Etitle-tag%3C%2Fdt%3E%0A%09%09%09%09%09%09%3Cdd%3E%24%7Btitle%7D%3C%2Fdd%3E%0A%09%09%09%09%09%09%3Cdt%3E%0A%09%09%09%09%09%09%09%3Clabel%20for%3D%22nostr-web-bookmark-trend-tag-input%22%3Et-tag%3C%2Flabel%3E%0A%09%09%09%09%09%09%09%3Cbutton%20id%3D%22nostr-web-bookmark-trend-clear%22%20type%3D%22button%22%3EClear%3C%2Fbutton%3E%0A%09%09%09%09%09%09%09%3Cspan%20id%3D%22nostr-web-bookmark-trend-tags%22%3E%3C%2Fspan%3E%0A%09%09%09%09%09%09%3C%2Fdt%3E%0A%09%09%09%09%09%09%3Cdd%3E%0A%09%09%09%09%09%09%09%3Cinput%20id%3D%22nostr-web-bookmark-trend-tag-input%22%20type%3D%22text%22%20pattern%3D%22%5B%5E%5C%5Cs%23%5D%2B%22%20%2F%3E%0A%09%09%09%09%09%09%09%3Cbutton%0A%09%09%09%09%09%09%09%09id%3D%22nostr-web-bookmark-trend-tag-add%22%0A%09%09%09%09%09%09%09%09type%3D%22button%22%0A%09%09%09%09%09%09%09%09title%3D%22add%22%0A%09%09%09%09%09%09%09%3E%2B%3C%2Fbutton%3E%0A%09%09%09%09%09%09%3C%2Fdd%3E%0A%09%09%09%09%09%09%3Cdt%3E%3Clabel%20for%3D%22nostr-web-bookmark-trend-content%22%3Econtent%3C%2Flabel%3E%3C%2Fdt%3E%0A%09%09%09%09%09%09%3Cdd%3E%0A%09%09%09%09%09%09%09%3Ctextarea%20id%3D%22nostr-web-bookmark-trend-content%22%3E%3C%2Ftextarea%3E%0A%09%09%09%09%09%09%3C%2Fdd%3E%0A%09%09%09%09%09%09%3Cdt%3ESubmit%3C%2Fdt%3E%0A%09%09%09%09%09%09%3Cdd%3E%0A%09%09%09%09%09%09%09%3Cbutton%20value%3D%22cancel%22%20formmethod%3D%22dialog%22%3ECancel%3C%2Fbutton%3E%0A%09%09%09%09%09%09%09%3Cbutton%20id%3D%22nostr-web-bookmark-trend-confirm%22%20value%3D%22default%22%3ESubmit%3C%2Fbutton%3E%0A%09%09%09%09%09%09%3C%2Fdd%3E%0A%09%09%09%09%09%3C%2Fdl%3E%0A%09%09%09%09%3C%2Fform%3E%0A%09%09%09%3C%2Fdialog%3E%0A%09%09%60%3Bconst%20div%3Ddocument.createElement%28%27div%27%29%3Bdiv.innerHTML%3Dview%3Bdocument.body.append%28div%29%3Bconst%20css%3D%60%5Cn%5Ct%5Ct%5Ctdialog%23nostr-web-bookmark-trend-dialog-bookmark%20%2A%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctall%3A%20revert%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%5Ctdialog%23nostr-web-bookmark-trend-dialog-bookmark%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctmargin%3A%20auto%3B%5Cn%5Ct%5Ct%5Ct%5Ctpadding%3A%2010px%3B%5Cn%5Ct%5Ct%5Ct%5Cttext-align%3A%20left%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%5Ctinput%23nostr-web-bookmark-trend-tag-input%3Ainvalid%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctoutline%3A%202px%20solid%20red%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%5Cttextarea%23nostr-web-bookmark-trend-content%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctwidth%3A%2030em%3B%5Cn%5Ct%5Ct%5Ct%5Ctheight%3A%205em%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%60%3Bconst%20style%3Ddocument.createElement%28%27style%27%29%3Bstyle.textContent%3Dcss%3Bdocument.head.appendChild%28style%29%3Bconst%20dialogId%3D%27nostr-web-bookmark-trend-dialog-bookmark%27%3Bconst%20clearId%3D%27nostr-web-bookmark-trend-clear%27%3Bconst%20tagsId%3D%27nostr-web-bookmark-trend-tags%27%3Bconst%20tagInputId%3D%27nostr-web-bookmark-trend-tag-input%27%3Bconst%20tagAddId%3D%27nostr-web-bookmark-trend-tag-add%27%3Bconst%20contentId%3D%27nostr-web-bookmark-trend-content%27%3Bconst%20confirmId%3D%27nostr-web-bookmark-trend-confirm%27%3Bconst%20dialog%3Ddocument.getElementById%28dialogId%29%3Bconst%20form%3Ddialog.querySelector%28%27form%27%29%3Bconst%20clearButton%3Ddialog.querySelector%28%60%23%24%7BclearId%7D%60%29%3Bconst%20tags%3Ddialog.querySelector%28%60%23%24%7BtagsId%7D%60%29%3Bconst%20tagInput%3Ddialog.querySelector%28%60%23%24%7BtagInputId%7D%60%29%3Bconst%20tagAddButton%3Ddialog.querySelector%28%60%23%24%7BtagAddId%7D%60%29%3Bconst%20contentEl%3Ddialog.querySelector%28%60%23%24%7BcontentId%7D%60%29%3Bconst%20confirmButton%3Ddialog.querySelector%28%60%23%24%7BconfirmId%7D%60%29%3Bif%28isError%29%7BtagInput.disabled%3Dtrue%3BtagAddButton.disabled%3Dtrue%3BcontentEl.readOnly%3Dtrue%3BconfirmButton.disabled%3Dtrue%7Dform.addEventListener%28%27keypress%27%2C%28e%3D%3E%7Bif%28tagInput%3D%3D%3Ddocument.activeElement%26%26e.key%3D%3D%3D%27Enter%27%29%7Be.preventDefault%28%29%7D%7D%29%29%3BclearButton.addEventListener%28%27click%27%2C%28%28%29%3D%3E%7BtTags.length%3D0%3BtagInput.value%3D%27%27%3BtagInput.focus%28%29%3Btags.textContent%3D%27%27%7D%29%29%3BtagAddButton.addEventListener%28%27click%27%2C%28%28%29%3D%3E%7Bif%28tagInput.value.length%3D%3D%3D0%7C%7CtagInput.validity.patternMismatch%29%7Breturn%7Dconst%20v%3DtagInput.value.toLowerCase%28%29%3Bif%28tTags.includes%28v%29%29%7Breturn%7DtTags.push%28v%29%3BtagInput.value%3D%27%27%3BtagInput.focus%28%29%3Btags.textContent%3DtTags.map%28%28t%3D%3E%60%23%24%7Bt%7D%60%29%29.join%28%27%20%27%29%7D%29%29%3BconfirmButton.addEventListener%28%27click%27%2C%28e%3D%3E%7Be.preventDefault%28%29%3Bconst%20res%3D%7BtTags%3AtTags%2Ccontent%3AcontentEl.value%7D%3Bdialog.close%28JSON.stringify%28res%29%29%7D%29%29%3Btags.textContent%3DtTags.map%28%28t%3D%3E%60%23%24%7Bt%7D%60%29%29.join%28%27%20%27%29%3BcontentEl.value%3Dcontent%3Bdialog.showModal%28%29%3Breturn%20new%20Promise%28%28resolve%3D%3E%7Bdialog.addEventListener%28%27close%27%2C%28e%3D%3E%7Bconst%20r%3Ddialog.returnValue%3D%3D%3D%27cancel%27%3Fnull%3Adialog.returnValue%3Bresolve%28r%29%7D%29%29%7D%29%29%7D%3Bconst%20showDialogResult%3D%28relays%2Curl%29%3D%3E%7Bconst%20view%3DescapeHTML%60%0A%09%09%09%3Cdialog%20id%3D%22nostr-web-bookmark-trend-dialog-result%22%3E%0A%09%09%09%09%3Cp%3E%E4%BB%A5%E4%B8%8B%E3%81%AE%E3%83%AA%E3%83%AC%E3%83%BC%E3%81%AB%E9%80%81%E4%BF%A1%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82%3C%2Fp%3E%0A%09%09%09%09%3Cp%3E%24%7Brelays.join%28%27%5Cn%27%29%7D%3C%2Fp%3E%0A%09%09%09%09%3Cp%3E%3Ca%20href%3D%22%24%7Burl%7D%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3E%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E9%96%8B%E3%81%8F%3C%2Fa%3E%3C%2Fp%3E%0A%09%09%09%09%3Cform%3E%0A%09%09%09%09%09%3Cbutton%20value%3D%22close%22%20formmethod%3D%22dialog%22%3EClose%3C%2Fbutton%3E%0A%09%09%09%09%3C%2Fform%3E%0A%09%09%09%3C%2Fdialog%3E%0A%09%09%60%3Bconst%20div%3Ddocument.createElement%28%27div%27%29%3Bdiv.innerHTML%3Dview%3Bdocument.body.append%28div%29%3Bconst%20css%3D%60%5Cn%5Ct%5Ct%5Ctdialog%23nostr-web-bookmark-trend-dialog-result%20%2A%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctall%3A%20revert%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%5Ctdialog%23nostr-web-bookmark-trend-dialog-result%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctmargin%3A%20auto%3B%5Cn%5Ct%5Ct%5Ct%5Ctpadding%3A%2010px%3B%5Cn%5Ct%5Ct%5Ct%5Cttext-align%3A%20left%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%5Ctdialog%23nostr-web-bookmark-trend-dialog-result%20%3E%20p%20%7B%5Cn%5Ct%5Ct%5Ct%5Ctwhite-space%3A%20pre-line%3B%5Cn%5Ct%5Ct%5Ct%7D%5Cn%5Ct%5Ct%60%3Bconst%20style%3Ddocument.createElement%28%27style%27%29%3Bstyle.textContent%3Dcss%3Bdocument.head.appendChild%28style%29%3Bconst%20dialogId%3D%27nostr-web-bookmark-trend-dialog-result%27%3Bconst%20dialog%3Ddocument.getElementById%28dialogId%29%3Bdialog.showModal%28%29%3Breturn%20new%20Promise%28%28resolve%3D%3E%7Bdialog.addEventListener%28%27close%27%2C%28e%3D%3E%7Bresolve%28%27close%27%29%7D%29%29%7D%29%29%7D%3Bconst%20indexOfFirstUnmatchingCloseParen%3D%28url%2Cleft%2Cright%29%3D%3E%7Blet%20nest%3D0%3Bfor%28let%20i%3D0%3Bi%3Curl.length%3Bi%2B%2B%29%7Bconst%20c%3Durl.charAt%28i%29%3Bif%28c%3D%3D%3Dleft%29%7Bnest%2B%2B%7Delse%20if%28c%3D%3D%3Dright%29%7Bif%28nest%3C%3D0%29%7Breturn%20i%7Dnest--%7D%7Dreturn-1%7D%3Bconst%20urlLinkString%3Durl%3D%3E%7Bfor%28const%5Bleft%2Cright%5Dof%5B%5B%27%28%27%2C%27%29%27%5D%2C%5B%27%5B%27%2C%27%5D%27%5D%5D%29%7Bconst%20splitIdx%3DindexOfFirstUnmatchingCloseParen%28url%2Cleft%2Cright%29%3Bif%28splitIdx%3E%3D0%29%7Breturn%5Burl.substring%280%2CsplitIdx%29%2Curl.substring%28splitIdx%29%5D%7D%7Dreturn%5Burl%2C%27%27%5D%7D%3Bconst%20getTagsForContent%3Dcontent%3D%3E%7Bconst%20tags%3D%5B%5D%3Bconst%20ppMap%3Dnew%20Map%3Bconst%20epMap%3Dnew%20Map%3Bconst%20apMap%3Dnew%20Map%3Bconst%20matchesIteratorId%3Dcontent.matchAll%28%2F%28%5E%7C%5CW%7C%5Cb%29%28nostr%3A%28note1%5Cw%7B58%7D%7Cnevent1%5Cw%2B%7Cnaddr1%5Cw%2B%29%29%28%24%7C%5CW%7C%5Cb%29%2Fg%29%3Bfor%28const%20match%20of%20matchesIteratorId%29%7Blet%20d%3Btry%7Bd%3Dwindow.NostrTools.nip19.decode%28match%5B3%5D%29%7Dcatch%28error%29%7Bcontinue%7Dif%28d.type%3D%3D%3D%27note%27%29%7BepMap.set%28d.data%2C%7Bid%3Ad.data%7D%29%7Delse%20if%28d.type%3D%3D%3D%27nevent%27%29%7BepMap.set%28d.data.id%2Cd.data%29%3Bif%28d.data.author%21%3D%3Dundefined%29%7BppMap.set%28d.data.author%2C%7Bpubkey%3Ad.data.author%7D%29%7D%7Delse%20if%28d.type%3D%3D%3D%27naddr%27%29%7BapMap.set%28%60%24%7Bd.data.kind%7D%3A%24%7Bd.data.pubkey%7D%3A%24%7Bd.data.identifier%7D%60%2Cd.data%29%3BppMap.set%28d.data.pubkey%2C%7Bpubkey%3Ad.data.pubkey%7D%29%7D%7Dconst%20matchesIteratorPubkey%3Dcontent.matchAll%28%2F%28%5E%7C%5CW%7C%5Cb%29%28nostr%3A%28npub1%5Cw%7B58%7D%7Cnprofile1%5Cw%2B%29%29%28%24%7C%5CW%7C%5Cb%29%2Fg%29%3Bfor%28const%20match%20of%20matchesIteratorPubkey%29%7Blet%20d%3Btry%7Bd%3Dwindow.NostrTools.nip19.decode%28match%5B3%5D%29%7Dcatch%28error%29%7Bcontinue%7Dif%28d.type%3D%3D%3D%27npub%27%29%7BppMap.set%28d.data%2C%7Bpubkey%3Ad.data%7D%29%7Delse%20if%28d.type%3D%3D%3D%27nprofile%27%29%7BppMap.set%28d.data.pubkey%2Cd.data%29%7D%7Dconst%20matchesIteratorLink%3Dcontent.matchAll%28%2Fhttps%3F%3A%5C%2F%5C%2F%5B%5Cw%21%3F%2F%3D%2B%5C-_~%3A%3B.%2C%2A%26%40%23%24%25%28%29%5B%5C%5D%5D%2B%2Fg%29%3Bconst%20links%3Dnew%20Set%3Bfor%28const%20match%20of%20matchesIteratorLink%29%7Blinks.add%28urlLinkString%28match%5B0%5D%29%5B0%5D%29%7Dfor%28const%5Bid%2Cep%5Dof%20epMap%29%7Bconst%20qTag%3D%5B%27q%27%2Cid%5D%3Bconst%20recommendedRelayForQuote%3Dep.relays%3F.filter%28%28relay%3D%3Erelay.startsWith%28%27wss%3A%2F%2F%27%29%29%29.at%280%29%3Bif%28recommendedRelayForQuote%21%3D%3Dundefined%29%7BqTag.push%28recommendedRelayForQuote%29%7Dtags.push%28qTag%29%7Dfor%28const%5Ba%2Cap%5Dof%20apMap%29%7Bconst%20qTag%3D%5B%27q%27%2Ca%5D%3Bconst%20recommendedRelayForQuote%3Dap.relays%3F.filter%28%28relay%3D%3Erelay.startsWith%28%27wss%3A%2F%2F%27%29%29%29.at%280%29%3Bif%28recommendedRelayForQuote%21%3D%3Dundefined%29%7BqTag.push%28recommendedRelayForQuote%29%7Dtags.push%28qTag%29%3BppMap.set%28ap.pubkey%2C%7Bpubkey%3Aap.pubkey%7D%29%7Dfor%28const%5Bp%2Cpp%5Dof%20ppMap%29%7Bconst%20pTag%3D%5B%27p%27%2Cp%5D%3Bconst%20recommendedRelayForPubkey%3Dpp.relays%3F.filter%28%28relay%3D%3Erelay.startsWith%28%27wss%3A%2F%2F%27%29%29%29.at%280%29%3Bif%28recommendedRelayForPubkey%21%3D%3Dundefined%29%7BpTag.push%28recommendedRelayForPubkey%29%7Dtags.push%28pTag%29%7Dfor%28const%20r%20of%20links%29%7Btags.push%28%5B%27r%27%2Cr%5D%29%7Dreturn%20tags%7D%3Bconst%20main%3Dasync%28%29%3D%3E%7Bconst%20pubkey%3Dawait%20getPubkey%28%29%3Bif%28pubkey%3D%3D%3Dundefined%29%7Breturn%7Dconst%20url%3Dnew%20URL%28location.href%29%3Burl.search%3D%27%27%3Burl.hash%3D%27%27%3Bconst%20identifier%3Durl.href.replace%28%2F%23%24%2F%2C%27%27%29.replace%28%2F%5C%3F%24%2F%2C%27%27%29.replace%28%2F%5Ehttps%3F%3A%5C%2F%5C%2F%2F%2C%27%27%29%3Bif%28window.NostrTools%3D%3D%3Dundefined%29%7Bawait%20showDialogBookmark%28identifier%2Cdocument.title%2C%5B%5D%2C%27%27%2Ctrue%29%3Breturn%7Dconst%20pool%3Dnew%20window.NostrTools.SimplePool%3Bconst%20event10002%3Dawait%20getReplaceableEvent%28pool%2CindexerRelays%2C%7Bkinds%3A%5B10002%5D%2Cauthors%3A%5Bpubkey%5D%2Cuntil%3Anow%7D%29%3Bconst%20rr%3DgetRelaysToUseFromKind10002Event%28event10002%29%3Bconst%20relaysToWrite%3DgetRelays%28rr%2C%27write%27%29%3Bconst%20kind%3D39701%3Bconst%20event39701%3Dawait%20getReplaceableEvent%28pool%2CrelaysToWrite%2C%7Bkinds%3A%5Bkind%5D%2Cauthors%3A%5Bpubkey%5D%2C%27%23d%27%3A%5Bidentifier%5D%2Cuntil%3Anow%7D%29%3Bconst%20tags%3Devent39701%3F.tags.filter%28%28tag%3D%3E%21%5B%27t%27%2C%27title%27%5D.includes%28tag%5B0%5D%29%29%29%3F%3F%5B%5B%27d%27%2Cidentifier%5D%2C%5B%27published_at%27%2CString%28now%29%5D%5D%3Bif%28document.title.length%3E0%29%7Btags.push%28%5B%27title%27%2Cdocument.title%5D%29%7Dconst%20tTagsOld%3Devent39701%3F.tags.filter%28%28tag%3D%3Etag%5B0%5D%3D%3D%3D%27t%27%29%29.map%28%28tag%3D%3Etag%5B1%5D.toLowerCase%28%29%29%29%3F%3F%5B%5D%3Bconst%20resDialog%3Dawait%20showDialogBookmark%28identifier%2Cdocument.title%2CtTagsOld%2Cevent39701%3F.content%3F%3F%27%27%2Cfalse%29%3Bif%28resDialog%3D%3D%3Dnull%29%7Breturn%7Dconst%7Bcontent%3Acontent%2CtTags%3AtTags%7D%3DJSON.parse%28resDialog%29%3Bfor%28const%20t%20of%20tTags%29%7Btags.push%28%5B%27t%27%2Ct%5D%29%7Dfor%28const%20tag%20of%20getTagsForContent%28content%29%29%7Btags.push%28tag%29%7Dconst%20eventTemplate%3D%7Bcontent%3Acontent%2Ckind%3Akind%2Ctags%3Atags%2Ccreated_at%3Anow%7D%3Bconst%20signedEvent%3Dawait%20window.nostr.signEvent%28eventTemplate%29%3Bawait%20Promise.any%28pool.publish%28relaysToWrite%2CsignedEvent%29%29%3Bconst%20naddr%3Dwindow.NostrTools.nip19.naddrEncode%28%7Bidentifier%3Aidentifier%2Ckind%3Akind%2Cpubkey%3Apubkey%2Crelays%3ArelaysToWrite%7D%29%3Bconst%20urlToOpen%3D%60%24%7Bwebsite%7D%24%7Bnaddr%7D%60%3Bawait%20showDialogResult%28relaysToWrite%2CurlToOpen%29%7D%3BaffixScriptToHead%28nostrToolsUrl%2Cmain%29%3Bfunction%20loadError%28oError%29%7Bmain%28%29%7Dfunction%20affixScriptToHead%28url%2ConloadFunction%29%7Bconst%20newScript%3Ddocument.createElement%28%27script%27%29%3BnewScript.onerror%3DloadError%3Bif%28onloadFunction%29%7BnewScript.onload%3DonloadFunction%7Ddocument.head.appendChild%28newScript%29%3BnewScript.src%3Durl%7D%7D%29%28%29%3B"&gt;nostr bookmark&lt;/a&gt;&lt;pre&gt;/*
 * @title nostr bookmark
 * @description 現在のWebページをNostrでブックマーク
 * @include http://*
 * @license CC0 1.0
 * @javascript_url
 */

(() =&amp;gt; {
	const website = 'https://kuchiyose.vercel.app/';
	const nostrToolsUrl = 'https://unpkg.com/nostr-tools/lib/nostr.bundle.js';
	const indexerRelays = [
		'wss://directory.yabu.me/',
		'wss://purplepag.es/',
		'wss://indexer.coracle.social/'
	];
	const now = Math.floor(Date.now() / 1000);

	const getPubkey = async () =&amp;gt; {
		let pubkey;
		if (window.nostr?.getPublicKey) {
			try {
				pubkey = await window.nostr.getPublicKey();
			} catch (error) {
				console.warn(error);
			}
		}
		return pubkey;
	};

	const getReplaceableEvent = (pool, relays, filter) =&amp;gt; {
		return new Promise((resolve) =&amp;gt; {
			let event;
			const sub = pool.subscribe(relays, filter, {
				onevent(ev) {
					if (event === undefined || event.created_at &amp;lt; ev.created_at) {
						event = ev;
					}
				},
				oneose() {
					sub.close();
					resolve(event);
				}
			});
		});
	};

	const getRelaysToUseFromKind10002Event = (event) =&amp;gt; {
		const newRelays = {};
		for (const tag of event?.tags.filter(
			(tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'r' &amp;amp;&amp;amp; 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) =&amp;gt; {
		return Array.from(
			new Set(
				Object.entries(relayRecord)
					.filter(([_, obj]) =&amp;gt; obj[relayType])
					.map(([relay, _]) =&amp;gt; relay)
			)
		);
	};

	const escapeSpecialChars = (str) =&amp;gt; {
		return str
			.replace(/&amp;amp;/g, '&amp;amp;amp;')
			.replace(/&amp;lt;/g, '&amp;amp;lt;')
			.replace(/&amp;gt;/g, '&amp;amp;gt;')
			.replace(/&amp;quot;/g, '&amp;amp;quot;')
			.replace(/'/g, '&amp;amp;#039;');
	};

	const escapeHTML = (strings, ...values) =&amp;gt; {
		return strings.reduce((result, str, i) =&amp;gt; {
			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, isError) =&amp;gt; {
		const usp = new URLSearchParams({
			d: identifier,
			title,
			content
		});
		for (const t of tTags) {
			usp.append('t', t);
		}
		const url = `${website}entry/${identifier}?${usp.toString()}`;
		const view = escapeHTML`
			&amp;lt;dialog id=&amp;quot;nostr-web-bookmark-trend-dialog-bookmark&amp;quot;&amp;gt;
				&amp;lt;form&amp;gt;
					&amp;lt;div&amp;gt;
						&amp;lt;p&amp;gt;${isError ? 'セキュリティ上の理由でブックマークできないため下記のページからブックマークしてください🙇' : ''}&amp;lt;/p&amp;gt;
						&amp;lt;p&amp;gt;&amp;lt;a href=&amp;quot;${url}&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;ブックマークページを開く&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
					&amp;lt;/div&amp;gt;
					&amp;lt;dl&amp;gt;
						&amp;lt;dt&amp;gt;d-tag&amp;lt;/dt&amp;gt;
						&amp;lt;dd&amp;gt;${identifier}&amp;lt;/dd&amp;gt;
						&amp;lt;dt&amp;gt;title-tag&amp;lt;/dt&amp;gt;
						&amp;lt;dd&amp;gt;${title}&amp;lt;/dd&amp;gt;
						&amp;lt;dt&amp;gt;
							&amp;lt;label for=&amp;quot;nostr-web-bookmark-trend-tag-input&amp;quot;&amp;gt;t-tag&amp;lt;/label&amp;gt;
							&amp;lt;button id=&amp;quot;nostr-web-bookmark-trend-clear&amp;quot; type=&amp;quot;button&amp;quot;&amp;gt;Clear&amp;lt;/button&amp;gt;
							&amp;lt;span id=&amp;quot;nostr-web-bookmark-trend-tags&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;
						&amp;lt;/dt&amp;gt;
						&amp;lt;dd&amp;gt;
							&amp;lt;input id=&amp;quot;nostr-web-bookmark-trend-tag-input&amp;quot; type=&amp;quot;text&amp;quot; pattern=&amp;quot;[^\\s#]+&amp;quot; /&amp;gt;
							&amp;lt;button
								id=&amp;quot;nostr-web-bookmark-trend-tag-add&amp;quot;
								type=&amp;quot;button&amp;quot;
								title=&amp;quot;add&amp;quot;
							&amp;gt;+&amp;lt;/button&amp;gt;
						&amp;lt;/dd&amp;gt;
						&amp;lt;dt&amp;gt;&amp;lt;label for=&amp;quot;nostr-web-bookmark-trend-content&amp;quot;&amp;gt;content&amp;lt;/label&amp;gt;&amp;lt;/dt&amp;gt;
						&amp;lt;dd&amp;gt;
							&amp;lt;textarea id=&amp;quot;nostr-web-bookmark-trend-content&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;
						&amp;lt;/dd&amp;gt;
						&amp;lt;dt&amp;gt;Submit&amp;lt;/dt&amp;gt;
						&amp;lt;dd&amp;gt;
							&amp;lt;button value=&amp;quot;cancel&amp;quot; formmethod=&amp;quot;dialog&amp;quot;&amp;gt;Cancel&amp;lt;/button&amp;gt;
							&amp;lt;button id=&amp;quot;nostr-web-bookmark-trend-confirm&amp;quot; value=&amp;quot;default&amp;quot;&amp;gt;Submit&amp;lt;/button&amp;gt;
						&amp;lt;/dd&amp;gt;
					&amp;lt;/dl&amp;gt;
				&amp;lt;/form&amp;gt;
			&amp;lt;/dialog&amp;gt;
		`;
		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}`);
		if (isError) {
			tagInput.disabled = true;
			tagAddButton.disabled = true;
			contentEl.readOnly = true;
			confirmButton.disabled = true;
		}

		form.addEventListener('keypress', (e) =&amp;gt; {
			if (tagInput === document.activeElement &amp;amp;&amp;amp; e.key === 'Enter') {
				e.preventDefault();
			}
		});

		clearButton.addEventListener('click', () =&amp;gt; {
			tTags.length = 0;
			tagInput.value = '';
			tagInput.focus();
			tags.textContent = '';
		});

		tagAddButton.addEventListener('click', () =&amp;gt; {
			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) =&amp;gt; `#${t}`).join(' ');
		});

		confirmButton.addEventListener('click', (e) =&amp;gt; {
			e.preventDefault();
			const res = {
				tTags,
				content: contentEl.value
			};
			dialog.close(JSON.stringify(res));
		});

		tags.textContent = tTags.map((t) =&amp;gt; `#${t}`).join(' ');
		contentEl.value = content;
		dialog.showModal();

		return new Promise((resolve) =&amp;gt; {
			dialog.addEventListener('close', (e) =&amp;gt; {
				const r = dialog.returnValue === 'cancel' ? null : dialog.returnValue;
				resolve(r);
			});
		});
	};

	const showDialogResult = (relays, url) =&amp;gt; {
		const view = escapeHTML`
			&amp;lt;dialog id=&amp;quot;nostr-web-bookmark-trend-dialog-result&amp;quot;&amp;gt;
				&amp;lt;p&amp;gt;以下のリレーに送信しました。&amp;lt;/p&amp;gt;
				&amp;lt;p&amp;gt;${relays.join('\n')}&amp;lt;/p&amp;gt;
				&amp;lt;p&amp;gt;&amp;lt;a href=&amp;quot;${url}&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;ブックマークページを開く&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
				&amp;lt;form&amp;gt;
					&amp;lt;button value=&amp;quot;close&amp;quot; formmethod=&amp;quot;dialog&amp;quot;&amp;gt;Close&amp;lt;/button&amp;gt;
				&amp;lt;/form&amp;gt;
			&amp;lt;/dialog&amp;gt;
		`;
		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 &amp;gt; 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) =&amp;gt; {
			dialog.addEventListener('close', (e) =&amp;gt; {
				resolve('close');
			});
		});
	};

	const indexOfFirstUnmatchingCloseParen = (url, left, right) =&amp;gt; {
		let nest = 0;
		for (let i = 0; i &amp;lt; url.length; i++) {
			const c = url.charAt(i);
			if (c === left) {
				nest++;
			} else if (c === right) {
				if (nest &amp;lt;= 0) {
					return i;
				}
				nest--;
			}
		}
		return -1;
	};

	//https://github.com/jiftechnify/motherfucking-nostr-client
	const urlLinkString = (url) =&amp;gt; {
		for (const [left, right] of [
			['(', ')'],
			['[', ']']
		]) {
			const splitIdx = indexOfFirstUnmatchingCloseParen(url, left, right);
			if (splitIdx &amp;gt;= 0) {
				return [url.substring(0, splitIdx), url.substring(splitIdx)];
			}
		}
		return [url, ''];
	};

	const getTagsForContent = (content) =&amp;gt; {
		const tags = [];
		const ppMap = new Map();
		const epMap = new Map();
		const apMap = new Map();
		const matchesIteratorId = content.matchAll(
			/(^|\W|\b)(nostr:(note1\w{58}|nevent1\w+|naddr1\w+))($|\W|\b)/g
		);
		for (const match of matchesIteratorId) {
			let d;
			try {
				d = window.NostrTools.nip19.decode(match[3]);
			} catch (error) {
				continue;
			}
			if (d.type === 'note') {
				epMap.set(d.data, { id: d.data });
			} else if (d.type === 'nevent') {
				epMap.set(d.data.id, d.data);
				if (d.data.author !== undefined) {
					ppMap.set(d.data.author, { pubkey: d.data.author });
				}
			} else if (d.type === 'naddr') {
				apMap.set(`${d.data.kind}:${d.data.pubkey}:${d.data.identifier}`, d.data);
				ppMap.set(d.data.pubkey, { pubkey: d.data.pubkey });
			}
		}
		const matchesIteratorPubkey = content.matchAll(
			/(^|\W|\b)(nostr:(npub1\w{58}|nprofile1\w+))($|\W|\b)/g
		);
		for (const match of matchesIteratorPubkey) {
			let d;
			try {
				d = window.NostrTools.nip19.decode(match[3]);
			} catch (error) {
				continue;
			}
			if (d.type === 'npub') {
				ppMap.set(d.data, { pubkey: d.data });
			} else if (d.type === 'nprofile') {
				ppMap.set(d.data.pubkey, d.data);
			}
		}
		const matchesIteratorLink = content.matchAll(/https?:\/\/[\w!?/=+\-_~:;.,*&amp;amp;@#$%()[\]]+/g);
		const links = new Set();
		for (const match of matchesIteratorLink) {
			links.add(urlLinkString(match[0])[0]);
		}
		for (const [id, ep] of epMap) {
			const qTag = ['q', id];
			const recommendedRelayForQuote = ep.relays
				?.filter((relay) =&amp;gt; relay.startsWith('wss://'))
				.at(0);
			if (recommendedRelayForQuote !== undefined) {
				qTag.push(recommendedRelayForQuote);
			}
			tags.push(qTag);
		}
		for (const [a, ap] of apMap) {
			const qTag = ['q', a];
			const recommendedRelayForQuote = ap.relays
				?.filter((relay) =&amp;gt; relay.startsWith('wss://'))
				.at(0);
			if (recommendedRelayForQuote !== undefined) {
				qTag.push(recommendedRelayForQuote);
			}
			tags.push(qTag);
			ppMap.set(ap.pubkey, { pubkey: ap.pubkey });
		}
		for (const [p, pp] of ppMap) {
			const pTag = ['p', p];
			const recommendedRelayForPubkey = pp.relays
				?.filter((relay) =&amp;gt; relay.startsWith('wss://'))
				.at(0);
			if (recommendedRelayForPubkey !== undefined) {
				pTag.push(recommendedRelayForPubkey);
			}
			tags.push(pTag);
		}
		for (const r of links) {
			tags.push(['r', r]);
		}
		return tags;
	};

	const main = async () =&amp;gt; {
		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?:\/\//, '');
		if (window.NostrTools === undefined) {
			await showDialogBookmark(identifier, document.title, [], '', true);
			return;
		}
		const pool = new window.NostrTools.SimplePool();
		const event10002 = await getReplaceableEvent(pool, indexerRelays, {
			kinds: [10002],
			authors: [pubkey],
			until: now
		});
		const rr = getRelaysToUseFromKind10002Event(event10002);
		const relaysToWrite = getRelays(rr, 'write');
		const kind = 39701;
		const event39701 = await getReplaceableEvent(pool, relaysToWrite, {
			kinds: [kind],
			authors: [pubkey],
			'#d': [identifier],
			until: now
		});
		const tags = event39701?.tags.filter((tag) =&amp;gt; !['t', 'title'].includes(tag[0])) ?? [
			['d', identifier],
			['published_at', String(now)]
		];
		if (document.title.length &amp;gt; 0) {
			tags.push(['title', document.title]);
		}
		const tTagsOld =
			event39701?.tags.filter((tag) =&amp;gt; tag[0] === 't').map((tag) =&amp;gt; tag[1].toLowerCase()) ?? [];
		const resDialog = await showDialogBookmark(
			identifier,
			document.title,
			tTagsOld,
			event39701?.content ?? '',
			false
		);
		if (resDialog === null) {
			return;
		}
		const { content, tTags } = JSON.parse(resDialog);
		for (const t of tTags) {
			tags.push(['t', t]);
		}
		for (const tag of getTagsForContent(content)) {
			tags.push(tag);
		}
		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 = `${website}${naddr}`;
		await showDialogResult(relaysToWrite, urlToOpen);
	};

	affixScriptToHead(nostrToolsUrl, main);

	//https://developer.mozilla.org/ja/docs/Web/API/HTMLScriptElement のコピペ
	function loadError(oError) {
		main();
	}

	function affixScriptToHead(url, onloadFunction) {
		const newScript = document.createElement('script');
		newScript.onerror = loadError;
		if (onloadFunction) {
			newScript.onload = onloadFunction;
		}
		document.head.appendChild(newScript);
		newScript.src = url;
	}
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/ku6Php3ygoAA">
    <link>https://let.hatelabo.jp/Nikola/let/ku6Php3ygoAA</link>
    <dc:date>2025-04-08T04:49:35Z</dc:date>
    <description>どこでもnostr-login</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] nostr-login anywhere</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2Fku6Php3ygoAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;nostr-login anywhere&lt;/a&gt;&lt;pre&gt;/*
 * @title nostr-login anywhere
 * @description どこでもnostr-login
 * @include http://*
 * @license CC0 1.0
 * @require
 */

(() =&amp;gt; {
  const url = 'https://www.unpkg.com/nostr-login@latest/dist/unpkg.js';
  affixScriptToHead(url, () =&amp;gt; {});

  //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;
  }
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kuj34OLcgsAA">
    <link>https://let.hatelabo.jp/Nikola/let/kuj34OLcgsAA</link>
    <dc:date>2025-03-31T03:38:11Z</dc:date>
    <description>nostalkに喋らせる</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] call nostalk</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2Fkuj34OLcgsAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;call nostalk&lt;/a&gt;&lt;pre&gt;/*
 * @title call nostalk
 * @description nostalkに喋らせる
 * @include https://*
 * @license CC0 1.0
 * @require
 */

(async () =&amp;gt; {
  const ifghost = 'ノス民,none';
  const message = '\\0\\_q＼ｵｱｰｯ!!／\\e';
  const sspServerURL = 'http://localhost:9801';
  const callNostalk = async () =&amp;gt; {
    const mes = [
      'NOTIFY SSTP/1.1',
      'Charset: UTF-8',
      'Sender: SSTP-bookmarklet',
      'SecurityLevel: external',
      'Event: OnNostrCallNostalk',
      'Option: nobreak',
      `IfGhost: ${ifghost}`,
      `Script: ${message}`,
      '',
      ''
    ].join('\n');
    const res = await postData(sspServerURL + '/api/sstp/v1', mes);
    console.log(mes, '----------\n', res, '----------\n');
  };
  const postData = async (url = '', data = '') =&amp;gt; {
    const param = {
      method: 'POST',
      headers: {
        'Content-Type': 'text/plain',
        Origin: sspServerURL
      },
      body: data
    };
    try {
      const response = await fetch(url, param);
      return response.text();
    } catch (error) {
      console.log(error);
      return '';
    }
  };
  callNostalk();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/ktDU2MuegsAA">
    <link>https://let.hatelabo.jp/Nikola/let/ktDU2MuegsAA</link>
    <dc:date>2025-02-21T10:38:22Z</dc:date>
    <description>リレーからkind30002イベントを取得してRabbitにリレーセットをインポートする</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] import relay set to Rabbit</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FktDU2MuegsAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;import relay set to Rabbit&lt;/a&gt;&lt;pre&gt;/*
 * @title import relay set to Rabbit
 * @description リレーからkind30002イベントを取得してRabbitにリレーセットをインポートする
 * @include https://rabbit.syusui.net/*
 * @license CC0 1.0
 * @require
 */

(async () =&amp;gt; {
	let relayUrl = 'wss://relay-jp.nostr.wirednet.jp';

	const getPubkey = async () =&amp;gt; {
		let pubkey; //Node.jsでデバッグする際はここに初期値を入れてください。無ければ最新の誰かのイベントで代用します。
		const nostr = globalThis.window?.nostr;
		if (nostr?.getPublicKey) {
			try {
				pubkey = await nostr.getPublicKey();
			} catch (error) {
				console.warn(error);
			}
		}
		return pubkey;
	};

	const getEvent30002 = async (pubkey) =&amp;gt; {
		return new Promise((resolve) =&amp;gt; {
			const ws = new WebSocket(relayUrl);
			const subscription_id = 'getrelaysetinrabbit';
			let res = [];
			ws.onopen = () =&amp;gt; {
				const req = [
					'REQ',
					subscription_id,
					pubkey ? { kinds: [30002], authors: [pubkey] } : { kinds: [30002], limit: 1 }
				];
				ws.send(JSON.stringify(req));
			};
			ws.onmessage = (e) =&amp;gt; {
				const msg = JSON.parse(e.data);
				switch (msg[0]) {
					case 'EVENT':
						res.push(msg[2]);
						break;
					case 'EOSE':
						ws.send(JSON.stringify(['CLOSE', subscription_id]));
						ws.close();
						resolve(res);
						break;
					default:
						console.log(msg);
						break;
				}
			};
			ws.onerror = () =&amp;gt; {
				console.error('failed to connect');
			};
		});
	};

	const main = async () =&amp;gt; {
		relayUrl = window.prompt('Input relay URL.', relayUrl);
		if (!URL.canParse(relayUrl)) {
			console.warn(`Invalid URL: ${relayUrl}`);
			return;
		}
		let pubkey;
		try {
			pubkey = await getPubkey();
		} catch (error) {
			console.warn(error);
			return;
		}
		if (globalThis.window &amp;amp;&amp;amp; !pubkey) {
			console.warn('pubkey is empty.');
			return;
		}
		let ev30002s;
		try {
			ev30002s = await getEvent30002(pubkey);
		} catch (error) {
			console.warn(error);
			return;
		}
		let relays;
		for (const ev of ev30002s) {
			const d = ev.tags.find((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'd')?.at(1) ?? '';
			const rs = ev.tags
				.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'relay' &amp;amp;&amp;amp; URL.canParse(tag[1]))
				.map((tag) =&amp;gt; tag[1]);
			if (window.confirm(`Do you want to use &amp;quot;${d}&amp;quot; ?\n\n` + rs.join('\n'))) {
				relays = rs;
				break;
			}
		}
		if (relays === undefined) {
			return;
		}
		if (globalThis.window) {
			const config = JSON.parse(localStorage['RabbitConfig']);
			config.relayUrls = Array.from(new Set([...config.relayUrls, ...relays]));
			localStorage['RabbitConfig'] = JSON.stringify(config);
			alert('Complete. Please reload by yourself.');
		} else {
			console.log({ relays });
			console.log('Complete.');
		}
	};

	main();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kqyujdzOgKAA">
    <link>https://let.hatelabo.jp/Nikola/let/kqyujdzOgKAA</link>
    <dc:date>2024-12-31T09:07:47Z</dc:date>
    <description>このページに言及しているNostrのコメントを表示する</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] show comment on this URL</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkqyujdzOgKAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;show comment on this URL&lt;/a&gt;&lt;pre&gt;/*
 * @title show comment on this URL
 * @description このページに言及しているNostrのコメントを表示する
 * @include https://*
 * @license CC0 1.0
 * @require
 */

(() =&amp;gt; {
	const relayUrl = 'wss://yabu.me/';

	const getEvent = (filter) =&amp;gt; {
		return new Promise((resolve) =&amp;gt; {
			const ws = new WebSocket(relayUrl);
			const subscription_id = 'getcommentofthispage';
			let res = [];
			ws.onopen = () =&amp;gt; {
				const req = ['REQ', subscription_id, filter];
				ws.send(JSON.stringify(req));
			};
			ws.onmessage = (e) =&amp;gt; {
				const msg = JSON.parse(e.data);
				switch (msg[0]) {
					case 'EVENT':
						res.push(msg[2]);
						break;
					case 'EOSE':
						ws.send(JSON.stringify(['CLOSE', subscription_id]));
						ws.close();
						resolve(res);
						break;
					default:
						console.log(msg);
						break;
				}
			};
			ws.onerror = () =&amp;gt; {
				console.error('failed to connect');
			};
		});
	};

	const tag = (name, props = {}, children = []) =&amp;gt; {
		const e = Object.assign(document.createElement(name), props);
		if (typeof props.style === 'object') Object.assign(e.style, props.style);
		(children.forEach ? children : [children]).forEach((c) =&amp;gt; e.appendChild(c));
		return e;
	};

	const main = async () =&amp;gt; {
		const url = location.href.replace(/#.*$/, '');
		const filter1 = { kinds: [1], '#r': [url], limit: 10, until: Math.floor(Date.now() / 1000) };
		let events1;
		try {
			events1 = await getEvent(filter1);
		} catch (error) {
			console.warn(error);
			return;
		}
		const pubkeys = events1.map((ev) =&amp;gt; ev.pubkey);
		const filter0 = { kinds: [0], authors: pubkeys, until: Math.floor(Date.now() / 1000) };
		let events0;
		try {
			events0 = await getEvent(filter0);
		} catch (error) {
			console.warn(error);
			return;
		}
		const profs = new Map();
		for (const ev0 of events0) {
			let prof;
			try {
				prof = JSON.parse(ev0.content);
			} catch (error) {
				console.warn(error);
				continue;
			}
			profs.set(ev0.pubkey, prof);
		}
		const div = document.createElement('div');
		div.style.position = 'fixed';
		div.style.top = '10px';
		div.style.right = '10px';
		div.style.whiteSpace = 'pre-wrap';
		div.style.marginLeft = '10px';
		div.style.paddingBottom = '100px';
		div.style.maxHeight = '100%';
		div.style.overflowY = 'scroll';
		div.addEventListener('click', () =&amp;gt; {
			div.style.display = 'none';
		});
		document.body.append(div);
		for (const ev of events1) {
			const prof = profs.get(ev.pubkey);
			const img = document.createElement('img');
			img.src = prof.picture ?? '';
			img.width = 32;
			img.height = 32;
			const time = new Date(1000 * ev.created_at).toLocaleString();
			const name = document.createTextNode(`${prof.display_name} @${prof.name} ${time}`);
			const span = document.createElement('span');
			span.append(img);
			span.append(name);
			div.append(
				tag(
					'div',
					{
						style:
							'max-width: 30em; margin-bottom: 2px; padding: 5px; background-color: rgba(50, 0, 50, 0.7); color: #fff; border-radius: 10px;'
					},
					[span]
				)
			);
			const text = document.createTextNode(ev.content);
			div.append(
				tag(
					'div',
					{
						style:
							'max-width: 30em; margin-bottom: 2px; padding: 5px; background-color: rgba(0, 0, 0, 0.7); color: #fff; border-radius: 10px;'
					},
					[text]
				)
			);
		}
	};

	main();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kO26wurQgcAA">
    <link>https://let.hatelabo.jp/Nikola/let/kO26wurQgcAA</link>
    <dc:date>2024-12-19T21:26:28Z</dc:date>
    <description>リレーからkind10030,kind30030イベントを取得してRabbitにカスタム絵文字リストをインポートする</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] import emoji list to Rabbit</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkO26wurQgcAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;import emoji list to Rabbit&lt;/a&gt;&lt;pre&gt;/*
 * @title import emoji list to Rabbit
 * @description リレーからkind10030,kind30030イベントを取得してRabbitにカスタム絵文字リストをインポートする
 * @include https://rabbit.syusui.net/*
 * @license CC0 1.0
 * @require
 */

(async () =&amp;gt; {
	if (typeof window === 'undefined') {
		await import('websocket-polyfill');
	}

	let relayUrl = 'wss://relay-jp.nostr.wirednet.jp';

	const getPubkey = async () =&amp;gt; {
		let pubkey; //Node.jsでデバッグする際はここに初期値を入れてください。無ければ最新の誰かのイベントで代用します。
		const nostr = globalThis.window?.nostr;
		if (nostr?.getPublicKey) {
			try {
				pubkey = await nostr.getPublicKey();
			} catch (error) {
				console.warn(error);
			}
		}
		return pubkey;
	};

	const getEvent10030 = async (pubkey) =&amp;gt; {
		return new Promise((resolve) =&amp;gt; {
			const ws = new WebSocket(relayUrl);
			const subscription_id = 'getemojilistinrabbit';
			let res;
			ws.onopen = () =&amp;gt; {
				const req = [
					'REQ',
					subscription_id,
					pubkey ? { kinds: [10030], authors: [pubkey] } : { kinds: [10030], limit: 1 }
				];
				ws.send(JSON.stringify(req));
			};
			ws.onmessage = (e) =&amp;gt; {
				const msg = JSON.parse(e.data);
				switch (msg[0]) {
					case 'EVENT':
						res = msg[2];
						break;
					case 'EOSE':
						ws.send(JSON.stringify(['CLOSE', subscription_id]));
						ws.close();
						resolve(res);
						break;
					default:
						console.log(msg);
						break;
				}
			};
			ws.onerror = () =&amp;gt; {
				console.error('failed to connect');
			};
		});
	};

	const getEmojiList = async (ev10030) =&amp;gt; {
		const atags = ev10030.tags
			.filter((tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'a')
			.map((tag) =&amp;gt; tag[1]);
		if (atags.length === 0) {
			return [];
		}
		const filters = [];
		for (const atag of atags) {
			const ary = atag.split(':');
			filters.push({ kinds: [parseInt(ary[0])], authors: [ary[1]], '#d': [ary[2]] });
		}
		const sliceByNumber = (array, number) =&amp;gt; {
			const length = Math.ceil(array.length / number);
			return new Array(length)
				.fill(undefined)
				.map((_, i) =&amp;gt; array.slice(i * number, (i + 1) * number));
		};
		let emojiMap = new Map();
		for (const filterGroup of sliceByNumber(filters, 10)) {
			await getEvent30030(filterGroup);
		}
		return emojiMap;
		async function getEvent30030(filters) {
			return new Promise((resolve) =&amp;gt; {
				const ws = new WebSocket(relayUrl);
				const subscription_id = 'getemojiinrabbit';
				ws.onopen = () =&amp;gt; {
					const req = ['REQ', subscription_id, ...filters];
					ws.send(JSON.stringify(req));
				};
				ws.onmessage = (e) =&amp;gt; {
					const msg = JSON.parse(e.data);
					switch (msg[0]) {
						case 'EVENT':
							for (const tag of msg[2].tags.filter(
								(tag) =&amp;gt; tag.length &amp;gt;= 2 &amp;amp;&amp;amp; tag[0] === 'emoji'
							)) {
								let key = tag[1];
								while (emojiMap.has(key)) {
									key += '_';
								}
								key = key.replaceAll('-', '_');
								if (/\W/.test(key)) continue;
								emojiMap.set(key, tag[2]);
							}
							break;
						case 'EOSE':
							ws.send(JSON.stringify(['CLOSE', subscription_id]));
							ws.close();
							resolve();
							break;
						default:
							console.log(msg);
							break;
					}
				};
				ws.onerror = () =&amp;gt; {
					console.error('failed to connect');
				};
			});
		}
	};

	const main = async () =&amp;gt; {
		relayUrl = window.prompt('Input relay URL.', relayUrl);
		if (!URL.canParse(relayUrl)) {
			console.warn(`Invalid URL: ${relayUrl}`);
			return;
		}
		let pubkey;
		try {
			pubkey = await getPubkey();
		} catch (error) {
			console.warn(error);
			return;
		}
		if (globalThis.window &amp;amp;&amp;amp; !pubkey) {
			console.warn('pubkey is empty.');
			return;
		}
		let ev10030;
		try {
			ev10030 = await getEvent10030(pubkey);
		} catch (error) {
			console.warn(error);
			return;
		}
		if (!ev10030) {
			return;
		}
		const emojiMap = await getEmojiList(ev10030);
		if (globalThis.window) {
			const config = JSON.parse(localStorage['RabbitConfig']);
			for (const [key, value] of emojiMap) {
				if (config.customEmojis[key] === undefined) {
					config.customEmojis[key] = { shortcode: key, url: value };
				} else if (
					config.customEmojis[key].shortcode === key &amp;amp;&amp;amp;
					config.customEmojis[key].url !== value
				) {
					let newkey = key;
					while (
						config.customEmojis[newkey] !== undefined &amp;amp;&amp;amp;
						config.customEmojis[newkey].shortcode === newkey &amp;amp;&amp;amp;
						config.customEmojis[newkey].url !== value
					) {
						newkey += '_';
					}
					config.customEmojis[newkey] = { shortcode: newkey, url: value };
				}
			}
			localStorage['RabbitConfig'] = JSON.stringify(config);
			alert('Complete. Please reload by yourself.');
		} else {
			console.log(emojiMap);
			console.log('Complete.');
		}
	};

	main();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kc6ngIXCgqAA">
    <link>https://let.hatelabo.jp/Nikola/let/kc6ngIXCgqAA</link>
    <dc:date>2024-11-27T11:46:50Z</dc:date>
    <description>どこでもマキビシ</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] MAKIBISHI anywhere</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2Fkc6ngIXCgqAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;MAKIBISHI anywhere&lt;/a&gt;&lt;pre&gt;/*
 * @title MAKIBISHI anywhere
 * @description どこでもマキビシ
 * @include http://*
 * @license CC0 1.0
 * @require
 */

(() =&amp;gt; {
  const url = 'https://cdn.jsdelivr.net/npm/@nikolat/makibishi';
  const elms = document.querySelectorAll('.makibishi');
  //設置済みの場合、強制的に剥がして最新版で読み直し
  if (elms.length &amp;gt; 0) {
    //MAKIBISHIを動的に読み込む
    affixScriptToHead(url, () =&amp;gt; {
      for (const elm of elms) {
        elm.innerHTML = '';
        elm.dataset.allowToDeleteReaction = 'true';
        window.makibishi.initTarget(elm);
      }
    });
    return;
  }
  //&amp;lt;span class=&amp;quot;makibishi&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;をh1またはh2の末尾に追加
  const h1 = document.querySelector('h1');
  const h2 = document.querySelector('h2');
  const span = document.createElement('span');
  span.classList.add('makibishi');
  span.dataset.allowToDeleteReaction = 'true';
  if (h1) h1.appendChild(span);
  else if (h2) h2.appendChild(span);
  else return;
  //MAKIBISHIを動的に読み込む
  affixScriptToHead(url, () =&amp;gt; {
    window.makibishi.initTarget(span);
  });

  //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;
  }
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/kbSl9ra-gqAA">
    <link>https://let.hatelabo.jp/Nikola/let/kbSl9ra-gqAA</link>
    <dc:date>2024-06-23T12:54:25Z</dc:date>
    <description>Zapされたらｵｱｰｯ!ってなる</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] zap oaa</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkbSl9ra-gqAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;zap oaa&lt;/a&gt;&lt;pre&gt;/*
 * @title zap oaa
 * @description Zapされたらｵｱｰｯ!ってなる
 * @include https://*
 * @license CC0 1.0
 * @require 
 */

(async () =&amp;gt; {
	const relayUrl = 'wss://yabu.me/';//リレーは決め打ちです。すみません。

	const getPubkey = async () =&amp;gt; {
		let pubkey;
		const nostr = window.nostr;
		if (nostr?.getPublicKey) {
			try {
				pubkey = await nostr.getPublicKey();
			} catch (error) {
				console.warn(error);
			}
		}
		return pubkey;
	};

	const watchEvent9735 = (pubkey, callback) =&amp;gt; {
		const ws = new WebSocket(relayUrl);
		const subscription_id = 'getzap';
		ws.onopen = () =&amp;gt; {
			const req = [
				'REQ',
				subscription_id,
				{ 'kinds': [9735], '#p': [pubkey], 'since': Math.floor(Date.now() / 1000) }
			];
			ws.send(JSON.stringify(req));
		};
		ws.onmessage = (e) =&amp;gt; {
			const msg = JSON.parse(e.data);
			switch (msg[0]) {
			case 'EVENT':
				callback();
				break;
			case 'EOSE':
				break;
			default:
				console.log(msg);
				break;
			}
		};
		ws.onerror = () =&amp;gt; {
			console.error('failed to connect');
		};
	};

	const main = async () =&amp;gt; {
		let pubkey;
		try {
			pubkey = await getPubkey();
		} catch (error) {
			console.warn(error);
			return;
		}
		if (!pubkey) {
			console.warn('pubkey is empty.');
			return;
		}
		watchEvent9735(pubkey, () =&amp;gt; {
			const audioId = 'nostr_zap_oaa';
			const elementAudio = document.getElementById(audioId);
			if (elementAudio) {
				elementAudio.play();
				return;
			}
			const oaadiv = document.createElement('div');
			oaadiv.style.display = 'none';
			oaadiv.id = 'nostr_zap_oaa_div';
			const oaaaudio = document.createElement('audio');
			oaaaudio.id = audioId;
			oaaaudio.src = 'https://soundeffect-lab.info/sound/voice/mp3/people/people-shout-oo2.mp3';//(c) https://soundeffect-lab.info/sound/voice/people.html
			oaadiv.appendChild(oaaaudio);
			document.body.appendChild(oaadiv);
			oaaaudio.play();
		});
	};

	main();
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/karR3IrkgOAA">
    <link>https://let.hatelabo.jp/Nikola/let/karR3IrkgOAA</link>
    <dc:date>2024-06-08T12:50:09Z</dc:date>
    <description>nostter に SSTP over HTTP で喋らせるボタンを生やす</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] Nostr - SSTP over HTTP で喋らせるボタンを生やすやつ</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkarR3IrkgOAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;Nostr - SSTP over HTTP で喋らせるボタンを生やすやつ&lt;/a&gt;&lt;pre&gt;/*
 * @title Nostr - SSTP over HTTP で喋らせるボタンを生やすやつ
 * @description nostter に SSTP over HTTP で喋らせるボタンを生やす
 * @include https://nostter.app/*
 * @require 
 */

(function() {
    'use strict';

    const tag = (name, props = {}, children = []) =&amp;gt; {
        const e = Object.assign(document.createElement(name), props);
        if (typeof props.style === &amp;quot;object&amp;quot;) Object.assign(e.style, props.style);
        (children.forEach ? children : [children]).forEach(c =&amp;gt; e.appendChild(c));
        return e;
    };

    const postData = async (url = '', data = '') =&amp;gt; {
        const param = {
            method: 'POST',
            headers: {
                'Content-Type': 'text/plain',
                'Origin': sspServerURL,
            },
            body: data,
        };
        try {
            const response = await fetch(url, param);
            return response.text();
        } catch (error) {
            return '';
        }
    };
    const sspServerURL = 'http://localhost:9801';
    const sendSSTP = async (script, type, content, name, display_name, picture) =&amp;gt; {
        const protocol_version = 'Nostr/0.3';
        const mes = [
            'NOTIFY SSTP/1.1',
            'Charset: UTF-8',
            'SecurityLevel: external',
            'Sender: ぶらうざのゆーざーすくりぷと',
            'Event: OnNostr',
            `Reference0: ${protocol_version}`,
            `Reference1: ${type}`,
            `Reference2: ${content}`,
            `Reference3: ${name}`,
            `Reference4: ${display_name}`,
            `Reference5: ${picture}`,
            'Option: notranslate,nobreak',
            `Script: ${script}`,
            '',
            '',
        ].join('\n');
        const res = await postData(sspServerURL + '/api/sstp/v1', mes);
    };

    const onClick = async e =&amp;gt; {
        let root;
        let avatar_url;
        let name;
        let display_name;
        let acct;
        let body;
        switch (document.domain) {
            //nostter
            case 'nostter.app':
                root = e.target.closest('main div &amp;gt; article');
                avatar_url = root.querySelector('div &amp;gt; a &amp;gt; img.picture').src;
                name = root.querySelector('.name').textContent.trim().replace('@', '');
                display_name = root.querySelector('.display_name').textContent.trim();
                acct = root.querySelector('.name').textContent.trim();
                body = root.querySelector('.content').textContent.trim().replace(/\n/g, &amp;quot;\\n&amp;quot;);
                break;
            default:
                break;
        }

        let script = &amp;quot;&amp;quot;;
        script += `\\![set,balloonwait,0]\\0\\_l[@4,]${display_name}\\n\\_l[@36,]${acct}\\_l[0,36]`;
        script += `\\![set,balloonwait]${body}`;
        script += &amp;quot;\\e&amp;quot;;

        sendSSTP(script, 'note', body, name, display_name, avatar_url);
        
    };

    new MutationObserver(() =&amp;gt; {
        let q;
        switch (document.domain) {
            //nostter
            case 'nostter.app':
                q = '.action-menu:not(.__ukabutton)';
                break;
            default:
                break;
        }
        let timeoutID;
        for (const el of document.querySelectorAll(q)) {
            el.classList.add('__ukabutton');
            const e = tag('button', {
                className: 'icon-button',
                style: &amp;quot;width: 18px; height: 18px; background: no-repeat center/18px url(https://ukadon.shillest.net/favicon.ico); opacity: 0.5;&amp;quot;,
                onclick: onClick
            });
            el.append(e);
            if (timeoutID) {
                clearTimeout(timeoutID);
            }
            timeoutID = setTimeout(() =&amp;gt; { e.dispatchEvent(new PointerEvent('click')); }, 100);
        }
    }).observe(document.body, {childList: 1, subtree:1});
})();
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/gYC-yLSpueTJcQ">
    <link>https://let.hatelabo.jp/Nikola/let/gYC-yLSpueTJcQ</link>
    <dc:date>2011-08-07T04:54:04Z</dc:date>
    <description>Fix links on RakugakiHB</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] RakugakiHB Reader</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FgYC-yLSpueTJcQ.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;RakugakiHB Reader&lt;/a&gt;&lt;pre&gt;/*
 * @title RakugakiHB Reader
 * @description Fix links on RakugakiHB
 * @include http://rakgakihb.x.fc2.com/ts/*
 * @include http://rakgakihb.x.fc2.com/jidou/*
 * @license MIT License
 * @require 
 */

var target = document.querySelector('.box3 a:nth-child(3)');
if (target.href.match(/(ts|jidou)x\.html?(\d\d)/)) {
	target.href = RegExp.$1 + RegExp.$2 + '.html';
}
&lt;/pre&gt;</content:encoded>
  </item>
  <item rdf:about="https://let.hatelabo.jp/Nikola/let/gYC-y5SY84WKVg">
    <link>https://let.hatelabo.jp/Nikola/let/gYC-y5SY84WKVg</link>
    <dc:date>2010-12-01T10:50:51Z</dc:date>
    <description>mixiの日記削除のチェックボックス全選択</description>
    <dc:creator>Nikola</dc:creator>
    <title>[Let] mixi reset</title>
    <content:encoded>&lt;a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FgYC-y5SY84WKVg.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B"&gt;mixi reset&lt;/a&gt;&lt;pre&gt;/*
 * @title mixi reset
 * @description mixiの日記削除のチェックボックス全選択
 * @include http://mixi.jp/*
 * @license MIT License
 * @require 
 */

var targets = document.querySelectorAll('#bodyMainAreaMain input[type=&amp;quot;checkbox&amp;quot;]');
for (var i=0, l=targets.length; i&amp;lt;l; i++) {
	targets[i].checked = true;
}
&lt;/pre&gt;</content:encoded>
  </item>
</rdf:RDF>
