mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-11-18 12:08:00 +01:00
@@ -5,15 +5,18 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rust-nostr/nostr-sdk": "^0.10.0",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"bech32": "^2.0.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"daisyui": "^4.6.0",
|
||||
"mini-toastr": "^0.8.1",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-notifications": "^1.0.2",
|
||||
"vue3-easy-data-table": "^1.5.47",
|
||||
@@ -26,8 +29,10 @@
|
||||
"postcss": "^8.4.33",
|
||||
"sass": "^1.70.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||
|
||||
@@ -9,7 +9,6 @@ export default {
|
||||
components: {Donate, Nip07, ResultsTable, Search}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -23,6 +23,7 @@ import deadnip89s from "@/components/data/deadnip89s.json";
|
||||
import {data} from "autoprefixer";
|
||||
import {requestProvider} from "webln";
|
||||
import Newnote from "@/components/Newnote.vue";
|
||||
import amberSignerService from "./android-signer/AndroidSigner";
|
||||
|
||||
let dvms =[]
|
||||
let searching = false
|
||||
@@ -41,8 +42,19 @@ const sleep = (ms) => {
|
||||
|
||||
async function post_note(note){
|
||||
let client = store.state.client
|
||||
await client.publishTextNote(note, []);
|
||||
|
||||
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
|
||||
const draft = {
|
||||
content: note,
|
||||
kind: 1,
|
||||
pubkey: store.state.pubkey.toHex(),
|
||||
tags: [],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
const eventJson = await amberSignerService.signEvent(draft);
|
||||
await client.sendEvent(Event.fromJson(JSON.stringify(eventJson)));
|
||||
} else {
|
||||
await client.publishTextNote(note, []);
|
||||
}
|
||||
}
|
||||
async function generate_image(message) {
|
||||
|
||||
@@ -64,14 +76,36 @@ async function generate_image(message) {
|
||||
tags.push(Tag.parse(["i", message, "text"]))
|
||||
|
||||
let evt = new EventBuilder(5100, "NIP 90 Image Generation request", tags)
|
||||
let res = await client.sendEventBuilder(evt)
|
||||
store.commit('set_current_request_id_image', res.toHex())
|
||||
console.log("IMAGE EVENT SENT: " + res.toHex())
|
||||
let res;
|
||||
let requestid;
|
||||
|
||||
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
|
||||
let draft = {
|
||||
content: "NIP 90 Image Generation request",
|
||||
kind: 5100,
|
||||
pubkey: store.state.pubkey.toHex(),
|
||||
tags: [
|
||||
["i", message, "text"]
|
||||
],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
res = await amberSignerService.signEvent(draft)
|
||||
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
|
||||
requestid = res.id;
|
||||
res = res.id;
|
||||
} else {
|
||||
res = await client.sendEventBuilder(evt);
|
||||
requestid = res.toHex();
|
||||
}
|
||||
|
||||
store.commit('set_current_request_id_image', requestid)
|
||||
//console.log("IMAGE EVENT SENT: " + res.toHex())
|
||||
|
||||
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
|
||||
searching = true
|
||||
if (!store.state.imagehasEventListener){
|
||||
listen()
|
||||
listen()
|
||||
store.commit('set_imagehasEventListener', true)
|
||||
}
|
||||
else{
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<h3 className="card-title">Nip07 Login</h3>
|
||||
<p>Use a Browser Nip07 Extension like getalby or nos2x to login</p>
|
||||
<button className="btn" @click="sign_in_nip07()">Browser Extension</button>
|
||||
<template v-if="supports_android_signer">
|
||||
<button className="btn" @click="sign_in_amber()">Amber Sign in</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,13 +54,14 @@ import {
|
||||
Filter,
|
||||
initLogger,
|
||||
LogLevel,
|
||||
Timestamp, Keys, NostrDatabase, ClientBuilder, ClientZapper, Alphabet, SingleLetterTag, Options, Duration
|
||||
Timestamp, Keys, NostrDatabase, ClientBuilder, ClientZapper, Alphabet, SingleLetterTag, Options, Duration, PublicKey
|
||||
} from "@rust-nostr/nostr-sdk";
|
||||
import VueNotifications from "vue-notifications";
|
||||
import store from '../store';
|
||||
import Nip89 from "@/components/Nip89.vue";
|
||||
import miniToastr from "mini-toastr";
|
||||
import deadnip89s from "@/components/data/deadnip89s.json";
|
||||
import amberSignerService from "./android-signer/AndroidSigner";
|
||||
import {useDark, useToggle} from "@vueuse/core";
|
||||
const isDark = useDark();
|
||||
//const toggleDark = useToggle(isDark);
|
||||
@@ -72,12 +76,15 @@ export default {
|
||||
current_user: "",
|
||||
avatar: "",
|
||||
signer: "",
|
||||
|
||||
supports_android_signer: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
try{
|
||||
if (amberSignerService.supported) {
|
||||
this.supports_android_signer = true;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('nostr-key-method') === 'nip07')
|
||||
{
|
||||
await this.sign_in_nip07()
|
||||
@@ -91,7 +98,6 @@ export default {
|
||||
catch (error){
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -214,6 +220,47 @@ export default {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
async sign_in_amber() {
|
||||
try {
|
||||
|
||||
await loadWasmAsync();
|
||||
|
||||
if(logger){
|
||||
try {
|
||||
initLogger(LogLevel.debug());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!amberSignerService.supported) {
|
||||
alert("android signer not supported")
|
||||
return;
|
||||
}
|
||||
|
||||
const hexKey = await amberSignerService.getPublicKey();
|
||||
let publicKey = PublicKey.fromHex(hexKey);
|
||||
let keys = Keys.fromPublicKey(publicKey)
|
||||
this.signer = ClientSigner.keys(keys)
|
||||
let opts = new Options().waitForSend(false).connectionTimeout(Duration.fromSecs(5));
|
||||
let client = new ClientBuilder().signer(this.signer).opts(opts).build()
|
||||
for (const relay of store.state.relays){
|
||||
await client.addRelay(relay);
|
||||
}
|
||||
await client.connect();
|
||||
store.commit('set_client', client)
|
||||
store.commit('set_pubkey', publicKey)
|
||||
store.commit('set_hasEventListener', false)
|
||||
localStorage.setItem('nostr-key-method', "android-signer")
|
||||
localStorage.setItem('nostr-key', "")
|
||||
await this.get_user_info(publicKey)
|
||||
|
||||
//miniToastr.showMessage("Login successful!", "Logged in as " + publicKey.toHex(), VueNotifications.types.success)
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
async getnip89s(){
|
||||
|
||||
//let keys = Keys.generate()
|
||||
|
||||
@@ -19,6 +19,7 @@ import {computed, onMounted, ref} from "vue";
|
||||
import countries from "@/components/data/countries.json";
|
||||
import deadnip89s from "@/components/data/deadnip89s.json";
|
||||
import Nip07 from "@/components/Nip07.vue";
|
||||
import amberSignerService from "./android-signer/AndroidSigner";
|
||||
|
||||
let items = []
|
||||
let dvms =[]
|
||||
@@ -92,26 +93,43 @@ async function send_search_request(msg) {
|
||||
tags.push(Tag.parse(['param', 'users', JSON.stringify(users)]))
|
||||
|
||||
let evt = new EventBuilder(5302, "NIP 90 Search request", tags)
|
||||
let res;
|
||||
let requestid;
|
||||
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
|
||||
let draft = {
|
||||
content: "NIP 90 Search request",
|
||||
kind: 5302,
|
||||
pubkey: store.state.pubkey.toHex(),
|
||||
tags: [
|
||||
["i", msg, "text"],
|
||||
["param", "max_results", "150"],
|
||||
['param', 'users', JSON.stringify(users)]
|
||||
],
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
let res = await client.sendEventBuilder(evt)
|
||||
let requestid = res.toHex()
|
||||
res = await amberSignerService.signEvent(draft)
|
||||
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
|
||||
requestid = res.id;
|
||||
res = res.id;
|
||||
} else {
|
||||
res = await client.sendEventBuilder(evt)
|
||||
requestid = res.toHex()
|
||||
}
|
||||
|
||||
console.log("STORE: " +store.state.requestidSearch)
|
||||
store.commit('set_current_request_id_search', requestid)
|
||||
console.log("STORE AFTER: " + store.state.requestidSearch)
|
||||
|
||||
|
||||
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
|
||||
if (!store.state.hasEventListener){
|
||||
listen()
|
||||
listen()
|
||||
store.commit('set_hasEventListener', true)
|
||||
}
|
||||
else{
|
||||
console.log("Already has event listener")
|
||||
}
|
||||
|
||||
console.log(res)
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -313,7 +331,7 @@ async function listen() {
|
||||
},
|
||||
// Handle relay message
|
||||
handleMsg: async (relayUrl, message) => {
|
||||
//console.log("Received message from", relayUrl, message.asJson());
|
||||
//console.log(`Received message from ${relayUrl} ${message.asJson()}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
80
ui/noogle/src/components/android-signer/AndroidSigner.ts
Normal file
80
ui/noogle/src/components/android-signer/AndroidSigner.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// taken from https://github.com/hzrd149/nostrudel
|
||||
|
||||
import { nip19, verifySignature } from "nostr-tools";
|
||||
import createDefer, { Deferred } from "./classes/deffered";
|
||||
import { getPubkeyFromDecodeResult, isHexKey } from "./helpers/nip19";
|
||||
import { NostrEvent } from "./types/nostr-event";
|
||||
|
||||
export function createGetPublicKeyIntent() {
|
||||
return `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end`;
|
||||
}
|
||||
export function createSignEventIntent(draft) {
|
||||
return `intent:${encodeURIComponent(
|
||||
JSON.stringify(draft),
|
||||
)}#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=event;S.type=sign_event;end`;
|
||||
}
|
||||
|
||||
let pendingRequest: Deferred<string> | null = null;
|
||||
|
||||
function rejectPending() {
|
||||
if (pendingRequest) {
|
||||
pendingRequest.reject("Canceled");
|
||||
pendingRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibilityChange() {
|
||||
if (document.visibilityState === "visible") {
|
||||
if (!pendingRequest || !navigator.clipboard) return;
|
||||
|
||||
// read the result from the clipboard
|
||||
setTimeout(() => {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then((result) => pendingRequest?.resolve(result))
|
||||
.catch((e) => pendingRequest?.reject(e));
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
|
||||
async function intentRequest(intent: string) {
|
||||
rejectPending();
|
||||
const request = createDefer<string>();
|
||||
window.open(intent, "_blank");
|
||||
// NOTE: wait 500ms before setting the pending request since the visibilitychange event fires as soon as window.open is called
|
||||
setTimeout(() => {
|
||||
pendingRequest = request;
|
||||
}, 500);
|
||||
const result = await request;
|
||||
if (result.length === 0) throw new Error("Empty clipboard");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getPublicKey() {
|
||||
const result = await intentRequest(createGetPublicKeyIntent());
|
||||
if (isHexKey(result)) return result;
|
||||
else if (result.startsWith("npub") || result.startsWith("nprofile")) {
|
||||
const decode = nip19.decode(result);
|
||||
const pubkey = getPubkeyFromDecodeResult(decode);
|
||||
if (!pubkey) throw new Error("Expected npub from clipboard");
|
||||
return pubkey;
|
||||
}
|
||||
throw new Error("Expected clipboard to have pubkey");
|
||||
}
|
||||
|
||||
async function signEvent(draft): Promise<NostrEvent> {
|
||||
const signedEventJson = await intentRequest(createSignEventIntent(draft));
|
||||
const signedEvent = JSON.parse(signedEventJson) as NostrEvent;
|
||||
|
||||
if (!verifySignature(signedEvent)) throw new Error("Invalid signature");
|
||||
return signedEvent;
|
||||
}
|
||||
|
||||
const amberSignerService = {
|
||||
supported: navigator.userAgent.includes("Android") && navigator.clipboard,
|
||||
getPublicKey,
|
||||
signEvent
|
||||
};
|
||||
|
||||
export default amberSignerService;
|
||||
21
ui/noogle/src/components/android-signer/classes/deffered.ts
Normal file
21
ui/noogle/src/components/android-signer/classes/deffered.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type Deferred<T> = Promise<T> & {
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
|
||||
export default function createDefer<T>() {
|
||||
let _resolve: (value?: T | PromiseLike<T>) => void;
|
||||
let _reject: (reason?: any) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
_resolve = resolve;
|
||||
_reject = reject;
|
||||
}) as Deferred<T>;
|
||||
|
||||
// @ts-ignore
|
||||
promise.resolve = _resolve;
|
||||
// @ts-ignore
|
||||
promise.reject = _reject;
|
||||
|
||||
return promise;
|
||||
}
|
||||
19
ui/noogle/src/components/android-signer/helpers/nip19.ts
Normal file
19
ui/noogle/src/components/android-signer/helpers/nip19.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
|
||||
export function isHexKey(key?: string) {
|
||||
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
|
||||
if (!result) return;
|
||||
switch (result.type) {
|
||||
case "naddr":
|
||||
case "nprofile":
|
||||
return result.data.pubkey;
|
||||
case "npub":
|
||||
return result.data;
|
||||
case "nsec":
|
||||
return getPublicKey(result.data);
|
||||
}
|
||||
}
|
||||
54
ui/noogle/src/components/android-signer/types/nostr-event.ts
Normal file
54
ui/noogle/src/components/android-signer/types/nostr-event.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
|
||||
export type ATag = ["a", string] | ["a", string, string];
|
||||
export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string];
|
||||
export type RTag = ["r", string] | ["r", string, string];
|
||||
export type DTag = ["d"] | ["d", string];
|
||||
export type EmojiTag = ["emoji", string, string];
|
||||
export type Tag = string[] | ETag | PTag | RTag | DTag | ATag;
|
||||
|
||||
export type NostrEvent = {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
created_at: number;
|
||||
kind: number;
|
||||
tags: Tag[];
|
||||
content: string;
|
||||
sig: string;
|
||||
};
|
||||
export type CountResponse = {
|
||||
count: number;
|
||||
approximate?: boolean;
|
||||
};
|
||||
|
||||
export type DraftNostrEvent = Omit<NostrEvent, "pubkey" | "id" | "sig"> & { pubkey?: string; id?: string };
|
||||
|
||||
export type RawIncomingEvent = ["EVENT", string, NostrEvent];
|
||||
export type RawIncomingNotice = ["NOTICE", string];
|
||||
export type RawIncomingCount = ["COUNT", string, CountResponse];
|
||||
export type RawIncomingEOSE = ["EOSE", string];
|
||||
export type RawIncomingCommandResult = ["OK", string, boolean, string];
|
||||
export type RawIncomingNostrEvent =
|
||||
| RawIncomingEvent
|
||||
| RawIncomingNotice
|
||||
| RawIncomingCount
|
||||
| RawIncomingEOSE
|
||||
| RawIncomingCommandResult;
|
||||
|
||||
export function isETag(tag: Tag): tag is ETag {
|
||||
return tag[0] === "e" && tag[1] !== undefined;
|
||||
}
|
||||
export function isPTag(tag: Tag): tag is PTag {
|
||||
return tag[0] === "p" && tag[1] !== undefined;
|
||||
}
|
||||
export function isRTag(tag: Tag): tag is RTag {
|
||||
return tag[0] === "r" && tag[1] !== undefined;
|
||||
}
|
||||
export function isDTag(tag: Tag): tag is DTag {
|
||||
return tag[0] === "d";
|
||||
}
|
||||
export function isATag(tag: Tag): tag is ATag {
|
||||
return tag[0] === "a" && tag[1] !== undefined;
|
||||
}
|
||||
export function isEmojiTag(tag: Tag): tag is EmojiTag {
|
||||
return tag[0] === "emoji" && tag[1] !== undefined && tag[2] !== undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user