add nip-07 back with proper error toast (#408)

This commit is contained in:
BlowaterNostr 2024-03-16 01:23:54 +08:00 committed by GitHub
parent df1a425910
commit f1787eef9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 99 deletions

View File

@ -11,44 +11,29 @@ import { LocalPrivateKeyController } from "./sign-in.ts";
type NIP07 = {
getPublicKey(): Promise<string>;
signEvent(event: UnsignedNostrEvent): Promise<NostrEvent>;
signEvent<T extends NostrKind>(event: UnsignedNostrEvent<T>): Promise<NostrEvent<T>>;
getRelays(): { [url: string]: { read: boolean; write: boolean } };
nip04: {
encrypt: (pubkey: string, plaintext: string) => Promise<string | Error>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string | Error>;
};
enabled: boolean;
enable: () => Promise<{ enabled: boolean }>;
nip44: {
encrypt: (pubkey: string, plaintext: string) => Promise<string | Error>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string | Error>;
};
};
export class Nip7ExtensionContext implements NostrAccountContext {
private readonly operationChan = chan<
{
op: "encrypt";
pubkey: string;
plaintext: string;
} | {
op: "signEvent";
event: UnsignedNostrEvent;
}
>();
private readonly encryptChan = chan<string | Error>();
// private readonly decryptChan = chan<string | Error>();
private readonly signEventChan = chan<NostrEvent>();
static async New(): Promise<Nip7ExtensionContext | Error | undefined> {
async function getExtensionObject(): Promise<NIP07 | undefined> {
// wait for alby or nos2x init
await sleep(20);
if ("nostr" in window) {
return window.nostr as NIP07;
}
return undefined;
}
const ext = await getExtensionObject();
if (ext === undefined) {
// wait for nip-07 extension init
await sleep(20);
let ext;
if ("nostr" in window) {
ext = window.nostr as NIP07;
} else {
return undefined;
}
let pubkey: string | undefined;
try {
pubkey = await ext.getPublicKey();
@ -63,53 +48,41 @@ export class Nip7ExtensionContext implements NostrAccountContext {
}
private constructor(
private alby: NIP07,
private nip07: NIP07,
public publicKey: PublicKey,
) {
console.log(alby);
(async () => {
for await (const op of this.operationChan) {
if (op.op == "encrypt") {
try {
const res = await alby.nip04.encrypt(op.pubkey, op.plaintext);
await this.encryptChan.put(res);
} catch (e) {
await this.encryptChan.put(e as Error);
}
} else if (op.op === "signEvent") {
const res = await alby.signEvent(op.event);
await this.signEventChan.put(res);
}
}
})();
console.log(nip07);
}
async signEvent<T extends NostrKind = NostrKind>(event: UnsignedNostrEvent<T>): Promise<NostrEvent<T>> {
await this.operationChan.put({ op: "signEvent", event: event });
const res = await this.signEventChan.pop();
if (res === closed) {
throw new Error("unreachable");
}
// @ts-ignore
return res;
async signEvent<T extends NostrKind = NostrKind>(event: UnsignedNostrEvent<T>) {
return this.nip07.signEvent(event);
}
encrypt = async (pubkey: string, plaintext: string) => {
await this.operationChan.put({
op: "encrypt",
pubkey,
plaintext,
});
const res = await this.encryptChan.pop();
if (res === closed) {
throw new Error("unreachable");
if (!("nip44" in this.nip07)) {
return new Error(
"This NIP-07 extension does not implement NIP-44, please use a NIP-44 compatible one",
);
}
try {
return this.nip07.nip44.encrypt(pubkey, plaintext);
} catch (e) {
return e as Error;
}
return res;
};
decrypt = async (pubkey: string, ciphertext: string) => {
try {
const res = await this.alby.nip04.decrypt(pubkey, ciphertext);
return res;
if (ciphertext.includes("?iv")) {
return await this.nip07.nip04.decrypt(pubkey, ciphertext);
} else {
if (!("nip44" in this.nip07)) {
return new Error(
"This NIP-07 extension does not implement NIP-44, please use a NIP-44 compatible one",
);
}
return await this.nip07.nip44.decrypt(pubkey, ciphertext);
}
} catch (e) {
return e as Error;
}

View File

@ -1,7 +1,7 @@
/** @jsx h */
import { h, render, VNode } from "https://esm.sh/preact@10.17.1";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { InvalidKey, PublicKey } from "../../libs/nostr.ts/key.ts";
import { NostrAccountContext, NostrEvent, NostrKind } from "../../libs/nostr.ts/nostr.ts";
import { ConnectionPool } from "../../libs/nostr.ts/relay-pool.ts";
import { Database_View } from "../database.ts";
@ -203,7 +203,9 @@ export class App {
});
if (error instanceof Error) {
console.error(error.message);
await args.database.remove(e.id);
if (error instanceof InvalidKey) {
await args.database.remove(e.id);
}
}
} else {
continue;

View File

@ -14,6 +14,7 @@ import { SignInEvent } from "./sign-in.ts";
import { SaveProfile } from "./edit-profile.tsx";
import { setState } from "./_helper.ts";
import { robohash } from "./relay-detail.tsx";
import { Nip7ExtensionContext } from "./account-context.ts";
interface Props {
emit: emitFunc<SaveProfile | SignInEvent>;
@ -109,7 +110,24 @@ export class SignIn extends Component<Props, State> {
onClick={() => this.setState({ step: { type: "signin", private_key: "" } })}
class={`w-full p-3 rounded-lg flex items-center justify-center bg-[#4d4f57] hover:bg-[#6c6f77]`}
>
Login
Login with private key
</button>
<div class="my-1"></div>
<button
onClick={async () => {
const nip07 = await Nip7ExtensionContext.New();
if (nip07 instanceof Error || nip07 == undefined) {
console.error(nip07);
return;
}
this.props.emit({
type: "SignInEvent",
ctx: nip07,
});
}}
class={`w-full p-3 rounded-lg flex items-center justify-center bg-[#4d4f57] hover:bg-[#6c6f77]`}
>
Login with NIP-07 extensions
</button>
</Fragment>
);

View File

@ -1,10 +1,18 @@
import { prepareEncryptedNostrEvent } from "../../libs/nostr.ts/event.ts";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { InvalidKey, PublicKey } from "../../libs/nostr.ts/key.ts";
import { NostrAccountContext, NostrEvent, NostrKind } from "../../libs/nostr.ts/nostr.ts";
import { ConnectionPool } from "../../libs/nostr.ts/relay-pool.ts";
import { DirectMessageGetter } from "../UI/app_update.tsx";
import { ChatMessage, parseContent } from "../UI/message.ts";
import { compare, getTags, Parsed_Event, prepareNostrImageEvent, Tag, Tags } from "../nostr.ts";
import {
compare,
DirectedMessage_Event,
getTags,
Parsed_Event,
prepareNostrImageEvent,
Tag,
Tags,
} from "../nostr.ts";
import { EventSender } from "../../libs/nostr.ts/relay.interface.ts";
import {
@ -203,7 +211,7 @@ export class DirectedMessageController implements DirectMessageGetter {
} else {
parsedTags = getTags(event);
}
let publicKey;
let publicKey: PublicKey | InvalidKey;
if ("publicKey" in event) {
publicKey = event.publicKey;
} else {
@ -212,7 +220,7 @@ export class DirectedMessageController implements DirectMessageGetter {
return publicKey;
}
}
let dmEvent = await parseDM(
const result = await parseDM(
{
...event,
kind,
@ -221,40 +229,43 @@ export class DirectedMessageController implements DirectMessageGetter {
parsedTags,
publicKey,
);
if ("type" in dmEvent) {
if (dmEvent.type == "Other") {
return dmEvent.error;
} else if (dmEvent.type == "NotMyMessage") {
return; // ignore
}
if (result.type == "Other") {
return result;
} else if (result.type == "NotMyMessage") {
return result;
} else if (result.type == "error") {
return result;
}
if (dmEvent instanceof Error) {
return dmEvent;
}
const isImage = dmEvent.parsedTags.image;
const dm_event = result.event;
const isImage = dm_event.parsedTags.image;
let chatMessage: ChatMessage;
if (isImage) {
const imageBase64 = dmEvent.decryptedContent;
const imageBase64 = dm_event.decryptedContent;
chatMessage = {
event: dmEvent,
author: dmEvent.publicKey,
event: dm_event,
author: dm_event.publicKey,
content: imageBase64,
type: "image",
created_at: new Date(dmEvent.created_at * 1000),
lamport: dmEvent.parsedTags.lamport_timestamp,
created_at: new Date(dm_event.created_at * 1000),
lamport: dm_event.parsedTags.lamport_timestamp,
};
} else {
chatMessage = {
event: dmEvent,
author: dmEvent.publicKey,
content: dmEvent.decryptedContent,
event: dm_event,
author: dm_event.publicKey,
content: dm_event.decryptedContent,
type: "text",
created_at: new Date(dmEvent.created_at * 1000),
lamport: dmEvent.parsedTags.lamport_timestamp,
created_at: new Date(dm_event.created_at * 1000),
lamport: dm_event.parsedTags.lamport_timestamp,
};
}
this.directed_messages.set(event.id, chatMessage);
/* do not await */ this.new_message_chan.put(chatMessage);
return {
type: true,
};
}
onChange() {
@ -277,22 +288,42 @@ async function parseDM(
ctx: NostrAccountContext,
parsedTags: Tags,
publicKey: PublicKey,
) {
): Promise<
{
type: "NotMyMessage";
error: Error;
} | {
type: "Other";
error: Error;
} | {
type: "error";
error: Error;
} | {
type: "event";
event: DirectedMessage_Event;
}
> {
const theOther = whoIamTalkingTo(event, ctx.publicKey);
if (theOther.type != true) {
return theOther;
}
const decrypted = await ctx.decrypt(theOther.talkingTo, event.content);
if (decrypted instanceof Error) {
return decrypted;
return {
type: "error",
error: decrypted,
};
}
return {
...event,
kind: event.kind,
parsedTags,
publicKey,
decryptedContent: decrypted,
parsedContentItems: Array.from(parseContent(decrypted)),
type: "event",
event: {
...event,
kind: event.kind,
parsedTags,
publicKey,
decryptedContent: decrypted,
parsedContentItems: Array.from(parseContent(decrypted)),
},
};
}