Merge pull request #6 from ursuscamp/event-log

Event log
This commit is contained in:
Ryan Breen
2023-02-16 08:43:17 -05:00
committed by GitHub
10 changed files with 376 additions and 5 deletions

View File

@@ -89,6 +89,12 @@
944A6DED299682EF0032C2E3 /* experimental.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DE8299682EF0032C2E3 /* experimental.js */; };
944A6DEE299682EF0032C2E3 /* experimental.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DE9299682EF0032C2E3 /* experimental.build.js */; };
944A6DEF299682EF0032C2E3 /* experimental.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DE9299682EF0032C2E3 /* experimental.build.js */; };
944A6DF3299975A70032C2E3 /* event_log.html in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF0299975A70032C2E3 /* event_log.html */; };
944A6DF4299975A70032C2E3 /* event_log.html in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF0299975A70032C2E3 /* event_log.html */; };
944A6DF5299975A70032C2E3 /* event_log.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF1299975A70032C2E3 /* event_log.build.js */; };
944A6DF6299975A70032C2E3 /* event_log.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF1299975A70032C2E3 /* event_log.build.js */; };
944A6DF7299975A70032C2E3 /* event_log.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF2299975A70032C2E3 /* event_log.js */; };
944A6DF8299975A70032C2E3 /* event_log.js in Resources */ = {isa = PBXBuildFile; fileRef = 944A6DF2299975A70032C2E3 /* event_log.js */; };
948C69D9297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; };
948C69DA297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; };
948C69DD297F88A200FB3574 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DB297F88A200FB3574 /* options.css */; };
@@ -198,6 +204,9 @@
944A6DE7299682EF0032C2E3 /* experimental.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = experimental.html; sourceTree = "<group>"; };
944A6DE8299682EF0032C2E3 /* experimental.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = experimental.js; sourceTree = "<group>"; };
944A6DE9299682EF0032C2E3 /* experimental.build.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = experimental.build.js; sourceTree = "<group>"; };
944A6DF0299975A70032C2E3 /* event_log.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = event_log.html; sourceTree = "<group>"; };
944A6DF1299975A70032C2E3 /* event_log.build.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = event_log.build.js; sourceTree = "<group>"; };
944A6DF2299975A70032C2E3 /* event_log.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = event_log.js; sourceTree = "<group>"; };
948C69D8297F887600FB3574 /* options.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = options.html; sourceTree = "<group>"; };
948C69DB297F88A200FB3574 /* options.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = options.css; sourceTree = "<group>"; };
948C69DC297F88A200FB3574 /* options.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = options.js; sourceTree = "<group>"; };
@@ -285,6 +294,9 @@
941B03A2296FA90400CA291E /* Resources */ = {
isa = PBXGroup;
children = (
944A6DF1299975A70032C2E3 /* event_log.build.js */,
944A6DF0299975A70032C2E3 /* event_log.html */,
944A6DF2299975A70032C2E3 /* event_log.js */,
944A6DE9299682EF0032C2E3 /* experimental.build.js */,
944A6DE7299682EF0032C2E3 /* experimental.html */,
944A6DE8299682EF0032C2E3 /* experimental.js */,
@@ -548,17 +560,20 @@
948C69E2297F891F00FB3574 /* options.build.js in Resources */,
941B03F8296FA90400CA291E /* popup.css in Resources */,
941B04292977A28700CA291E /* Icon-128.png in Resources */,
944A6DF3299975A70032C2E3 /* event_log.html in Resources */,
941B04182971138F00CA291E /* nostr.build.js in Resources */,
941B042C2978CD8E00CA291E /* iOS-Icon-1024.png in Resources */,
941B03F6296FA90400CA291E /* popup.html in Resources */,
941B040D296FAD6900CA291E /* nostr.js in Resources */,
944A6DE02991DFC60032C2E3 /* delegation_wizard.build.js in Resources */,
944A6DF5299975A70032C2E3 /* event_log.build.js in Resources */,
941B03EE296FA90400CA291E /* images in Resources */,
941B03F0296FA90400CA291E /* manifest.json in Resources */,
944A6DD62988BD230032C2E3 /* permission.js in Resources */,
941B04312978CDF900CA291E /* Icon-32.png in Resources */,
941B041A2971139000CA291E /* content.build.js in Resources */,
941B041C2971139000CA291E /* popup.build.js in Resources */,
944A6DF7299975A70032C2E3 /* event_log.js in Resources */,
944A6DEC299682EF0032C2E3 /* experimental.js in Resources */,
941B03EC296FA90400CA291E /* _locales in Resources */,
941B04222977A25700CA291E /* Icon-512.png in Resources */,
@@ -591,17 +606,20 @@
948C69E3297F891F00FB3574 /* options.build.js in Resources */,
941B03F9296FA90400CA291E /* popup.css in Resources */,
941B042A2977A28700CA291E /* Icon-128.png in Resources */,
944A6DF4299975A70032C2E3 /* event_log.html in Resources */,
941B04192971138F00CA291E /* nostr.build.js in Resources */,
941B042D2978CD8E00CA291E /* iOS-Icon-1024.png in Resources */,
941B03F7296FA90400CA291E /* popup.html in Resources */,
941B040E296FAD6900CA291E /* nostr.js in Resources */,
944A6DE12991DFC60032C2E3 /* delegation_wizard.build.js in Resources */,
944A6DF6299975A70032C2E3 /* event_log.build.js in Resources */,
941B03EF296FA90400CA291E /* images in Resources */,
941B03F1296FA90400CA291E /* manifest.json in Resources */,
944A6DD72988BD230032C2E3 /* permission.js in Resources */,
941B04322978CDF900CA291E /* Icon-32.png in Resources */,
941B041B2971139000CA291E /* content.build.js in Resources */,
941B041D2971139000CA291E /* popup.build.js in Resources */,
944A6DF8299975A70032C2E3 /* event_log.js in Resources */,
944A6DED299682EF0032C2E3 /* experimental.js in Resources */,
941B03ED296FA90400CA291E /* _locales in Resources */,
941B04232977A25700CA291E /* Icon-512.png in Resources */,

View File

@@ -7,12 +7,12 @@
<key>Nostore (iOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>0</integer>
</dict>
<key>Nostore (macOS).xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
<key>nostore (iOS).xcscheme_^#shared#^_</key>
<dict>

View File

@@ -16,6 +16,7 @@ import {
setPermission,
feature,
} from './utils';
import { saveEvent } from './db';
const storage = browser.storage.local;
const log = msg => console.log('Background: ', msg);
@@ -139,7 +140,7 @@ function complete({ payload, origKind, event, remember, host }) {
});
break;
case 'signEvent':
signEvent_(event).then(e => sendResponse(e));
signEvent_(event, host).then(e => sendResponse(e));
break;
case 'nip04.encrypt':
nip04Encrypt(event).then(e => sendResponse(e));
@@ -213,14 +214,17 @@ async function currentProfile() {
return profiles[index];
}
async function signEvent_(event) {
async function signEvent_(event, host) {
event = JSON.parse(JSON.stringify(event));
let privKey = await getPrivKey();
let pubKey = getPublicKey(privKey);
event.pubkey = pubKey;
event.id = getEventHash(event);
event.sig = signEvent(event, privKey);
console.log(JSON.stringify(event));
saveEvent({
event,
metadata: { host, signed_at: Math.round(Date.now() / 1000) },
});
return event;
}

View File

@@ -0,0 +1,69 @@
import { openDB } from 'idb';
async function openEventsDb() {
return await openDB('events', 1, {
upgrade(db) {
const events = db.createObjectStore('events', {
keyPath: 'event.id',
});
events.createIndex('pubkey', 'event.pubkey');
events.createIndex('created_at', 'event.created_at');
events.createIndex('kind', 'event.kind');
events.createIndex('host', 'metadata.host');
},
});
}
export async function saveEvent(event) {
let db = await openEventsDb();
return db.put('events', event);
}
export async function sortByIndex(index, query, asc, max) {
let db = await openEventsDb();
let events = [];
let cursor = await db
.transaction('events')
.store.index(index)
.openCursor(query, asc ? 'next' : 'prev');
while (cursor) {
events.push(cursor.value);
if (events.length >= max) {
break;
}
cursor = await cursor.continue();
}
return events;
}
export async function getHosts() {
let db = await openEventsDb();
let hosts = new Set();
let cursor = await db.transaction('events').store.openCursor();
while (cursor) {
hosts.add(cursor.value.metadata.host);
cursor = await cursor.continue();
}
return [...hosts];
}
export async function downloadAllContents() {
let db = await openEventsDb();
let events = [];
let cursor = await db.transaction('events').store.openCursor();
while (cursor) {
events.push(cursor.value.event);
cursor = await cursor.continue();
}
events = events.map(e => JSON.stringify(e));
events = events.join('\n');
console.log(events);
const file = new File([events], 'events.jsonl', {
type: 'application/octet-stream',
});
const blob = new Blob([events], { type: 'plain/text' });
return blob;
}

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer src="event_log.build.js"></script>
<link rel="stylesheet" href="options.build.css">
<title>Event Log</title>
<style>
label {
display: block;
}
</style>
</head>
<body class="text-fuchsia-900 p-3.5 lg:p-32" x-data="eventLog">
<p>
<a href="options.html" class="border-none hover:underline">← Back</a>
</p>
<h1 class="section-header">Event Log</h1>
<div class="section">
<div class="section-header">Filters</div>
<div class="grid grid-cols-2 xl:grid-cols-4 gap-4">
<div>
<label for="view">View</label>
<select id="view" class="input" x-model="view" @change="reload">
<option value="created_at">created_at</option>
<option value="kind">kind</option>
<option value="host">host</option>
<option value="pubkey">pubkey</option>
</select>
</div>
<div>
<label for="sort">Order</label>
<select id="sort" x-model="sort" class="input" @change="reload">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
<div>
<label for="max">Max</label>
<input type="number" x-model.number="max" @input.debounce.750ms="reload" class="input" min="10">
</div>
<div></div>
<div x-show="view === 'created_at'">
<label for="fromCreatedAt">From</label>
<input type="date" id="fromCreatedAt" x-model="fromCreatedAt" class="input" @change="reload">
</div>
<div x-show="view === 'created_at'">
<label for="toCreatedAt">To</label>
<input type="date" id="toCreatedAt" x-model="toCreatedAt" class="input" @change="reload">
</div>
<div x-show="view === 'kind'">
<label for="kindShortcut">Quick Select</label>
<select id="kindShortcut" class="input" @change="quickKindSelect" x-model="quickKind">
<option></option>
<template x-for="k in kinds">
<option :value="k[0]" x-text="k[1]"></option>
</template>
</select>
</div>
<div x-show="view === 'kind'">
<label for="fromKind">From</label>
<input type="number" id="fromKind" x-model.number="fromKind" class="input" @change="reload">
</div>
<div x-show="view === 'kind'">
<label for="toKind">To</label>
<input type="number" id="toKind" x-model.number="toKind" class="input" @change="reload">
</div>
<div x-show="view === 'host'">
<label for="host">Host</label>
<select id="host" class="input" x-model="host" @change="reload">
<option value=""></option>
<template x-for="h in allHosts">
<option :value="h" x-text="h"></option>
</template>
</select>
</div>
<div x-show="view === 'pubkey'">
<label for="profiles">Profiles</label>
<select id="profiles" class="input" x-model="profile" @change="pkFromProfile">
<option value=""></option>
<template x-for="p in profileNames">
<option :value="p" x-text="p"></option>
</template>
</select>
</div>
<div x-show="view === 'pubkey'">
<label for="pubkey">Pubkey</label>
<input type="text" class="input" x-model="pubkey" @input.debounce="reload">
</div>
</div>
<div>
<button class="button mt-3" @click="saveAll">Save all</button>
<button class="button mt-3" @click="deleteAll">Delete all</button>
</div>
</div>
<template x-for="(event, index) in events">
<div class="mt-3 border-solid border border-fuchsia-700 rounded-lg">
<div class="select-none flex cursor-pointer text-sm md:text-xl"
@click="selected = selected === index ? null : index">
<div class="flex-none w-14 p-4 font-extrabold" x-text="selected === index ? '-' : '+'"></div>
<div class="flex-1 w-64 p-4" x-text="formatDate(event.metadata.signed_at)"></div>
<div class="flex-1 w-64 p-4" x-text="event.metadata.host"></div>
<div class="flex-1 w-64 p-4" x-text="formatKind(event.event.kind)"></div>
</div>
<pre class="rounded-b-lg bg-slate-200 text-sm md:text-xl" x-html="highlight(event)" x-show="selected === index"
x-transition:enter.opacity.delay.75ms x-transition:leave.opacity>
</pre>
</div>
</template>
</body>
</html>

View File

@@ -0,0 +1,135 @@
import Alpine from 'alpinejs';
import { deleteDB } from 'idb';
import jsonFormatHighlight from 'json-format-highlight';
import { getPublicKey } from 'nostr-tools';
import { downloadAllContents, getHosts, sortByIndex } from './db';
import { getProfiles, KINDS } from './utils';
const TOMORROW = new Date();
TOMORROW.setDate(TOMORROW.getDate() + 1);
Alpine.data('eventLog', () => ({
kinds: KINDS,
events: [],
view: 'created_at',
max: 100,
sort: 'asc',
allHosts: [],
host: '',
allProfiles: [],
profile: '',
pubkey: '',
selected: null,
// date view
fromCreatedAt: '2008-10-31',
toCreatedAt: TOMORROW.toISOString().split('T')[0],
// kind view
quickKind: '',
fromKind: 0,
toKind: 50000,
async init() {
await this.reload();
},
async reload() {
let events = await sortByIndex(
this.view,
this.keyRange,
this.sort === 'asc',
this.max
);
this.events = events;
getHosts().then(hosts => (this.allHosts = hosts));
const profiles = await getProfiles();
console.log(profiles);
this.allProfiles = profiles.map(profile => ({
name: profile.name,
pubkey: getPublicKey(profile.privKey),
}));
},
async saveAll() {
const file = await downloadAllContents();
browser.tabs.create({
url: URL.createObjectURL(file),
active: true,
});
},
async deleteAll() {
if (confirm('Are you sure you want to delete ALL events?')) {
await deleteDB('events');
await this.reload();
}
},
quickKindSelect() {
if (this.quickKind === '') return;
const i = parseInt(this.quickKind);
this.fromKind = i;
this.toKind = i;
this.reload();
},
pkFromProfile() {
this.pubkey = this.allProfiles.find(
({ name }) => name === this.profile
).pubkey;
this.reload();
},
highlight(event) {
return jsonFormatHighlight(event);
},
formatDate(epochSeconds) {
return new Date(epochSeconds * 1000).toUTCString();
},
formatKind(kind) {
const k = KINDS.find(([kNum, _]) => kNum === kind);
return k ? `${k[1]} (${kind})` : `Unknown (${kind})`;
},
// Properties
get fromTime() {
let dt = new Date(this.fromCreatedAt);
return Math.floor(dt.getTime() / 1000);
},
get toTime() {
let dt = new Date(this.toCreatedAt);
return Math.floor(dt.getTime() / 1000);
},
get profileNames() {
return this.allProfiles.map(p => p.name);
},
get keyRange() {
switch (this.view) {
case 'created_at':
return IDBKeyRange.bound(this.fromTime, this.toTime);
case 'kind':
return IDBKeyRange.bound(this.fromKind, this.toKind);
case 'host':
if (this.host.length === 0) return null;
return IDBKeyRange.only(this.host);
case 'pubkey':
if (this.pubkey.length === 0) return null;
return IDBKeyRange.only(this.pubkey);
default:
return null;
}
},
}));
Alpine.start();

View File

@@ -162,6 +162,8 @@
<div class="mt-6">
<a href="experimental.html" class="border-none hover:underline">Experimental features →</a>
<br>
<a href="event_log.html" class="border-none hover:underline">Event log →</a>
</div>
</body>

View File

@@ -23,6 +23,7 @@ require('esbuild')
'./Shared (Extension)/Resources/experimental.js',
'delegation_wizard.build':
'./Shared (Extension)/Resources/delegation_wizard.js',
'event_log.build': './Shared (Extension)/Resources/event_log.js',
},
outdir: './Shared (Extension)/Resources',
sourcemap: 'inline',

6
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"alpinejs": "^3.10.5",
"async-mutex": "^0.4.0",
"idb": "^7.1.1",
"json-format-highlight": "^1.0.4",
"nostr-tools": "^1.3.0"
},
@@ -835,6 +836,11 @@
"node": ">= 0.4.0"
}
},
"node_modules/idb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
"integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",

View File

@@ -19,6 +19,7 @@
"dependencies": {
"alpinejs": "^3.10.5",
"async-mutex": "^0.4.0",
"idb": "^7.1.1",
"json-format-highlight": "^1.0.4",
"nostr-tools": "^1.3.0"
},