mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-11-19 07:07:02 +01:00
599 lines
18 KiB
Vue
599 lines
18 KiB
Vue
<script setup>
|
|
|
|
|
|
import {
|
|
Client,
|
|
Filter,
|
|
Timestamp,
|
|
Event,
|
|
Metadata,
|
|
PublicKey,
|
|
EventBuilder,
|
|
Tag,
|
|
EventId,
|
|
Nip19Event,
|
|
Alphabet,
|
|
ClientBuilder,
|
|
Keys,
|
|
NostrDatabase,
|
|
NegentropyOptions,
|
|
NegentropyDirection,
|
|
Duration
|
|
} from "@rust-nostr/nostr-sdk";
|
|
import store from '../store';
|
|
import miniToastr from "mini-toastr";
|
|
import VueNotifications from "vue-notifications";
|
|
import {computed, onMounted, ref} from "vue";
|
|
import deadnip89s from "@/components/data/deadnip89s.json";
|
|
import amberSignerService from "./android-signer/AndroidSigner";
|
|
import VueDatePicker from '@vuepic/vue-datepicker';
|
|
import '@vuepic/vue-datepicker/dist/main.css'
|
|
|
|
let items = []
|
|
let dvms =[]
|
|
let listener = false
|
|
let searching = false
|
|
|
|
const message = ref("");
|
|
const fromuser = ref("");
|
|
|
|
|
|
let usernames = []
|
|
|
|
|
|
const datefrom = ref(new Date().setFullYear(new Date().getFullYear() - 1));
|
|
const dateto = ref(Date.now());
|
|
|
|
|
|
onMounted(async () => {
|
|
let urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has('q')) {
|
|
message.value = urlParams.get('q')
|
|
await sleep(1000)
|
|
await send_search_request(message.value)
|
|
}
|
|
|
|
await sleep(2000)
|
|
|
|
})
|
|
|
|
|
|
// console.log(urlParams.has('search')); // true
|
|
// console.log(urlParams.get('search')); // "MyParam"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sleep = (ms) => {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|
|
|
|
|
|
|
|
|
|
async function send_search_request(msg) {
|
|
try {
|
|
if (msg === undefined){
|
|
msg = "Nostr"
|
|
}
|
|
|
|
if(store.state.pubkey === undefined){
|
|
miniToastr.showMessage("Please login first", "No pubkey set", VueNotifications.types.warn)
|
|
return
|
|
}
|
|
items = []
|
|
dvms =[]
|
|
store.commit('set_search_results', items)
|
|
let client = store.state.client
|
|
|
|
let users = [];
|
|
|
|
const taggedUsersFrom = msg.split(' ')
|
|
.filter(word => word.startsWith('from:'))
|
|
.map(word => word.replace('from:', ''));
|
|
|
|
// search
|
|
let search = msg;
|
|
for (let word of taggedUsersFrom) {
|
|
search = search.replace(word, "");
|
|
if(word === "me"){
|
|
word = store.state.pubkey.toBech32()
|
|
|
|
}
|
|
const userPubkey = PublicKey.fromBech32(word.replace("@", "")).toHex()
|
|
const pTag = Tag.parse(["p", userPubkey]);
|
|
users.push(pTag.asVec());
|
|
}
|
|
|
|
if (fromuser.value !== ""){
|
|
const userPubkey = PublicKey.fromBech32(fromuser.value.replace("@", "")).toHex()
|
|
const pTag = Tag.parse(["p", userPubkey]);
|
|
users.push(pTag.asVec());
|
|
}
|
|
|
|
msg = search.replace(/from:|to:|@/g, '').trim();
|
|
console.log(search);
|
|
|
|
let content = "NIP 90 Search request"
|
|
let kind = 5302
|
|
let tags = [
|
|
["i", msg, "text"],
|
|
["param", "max_results", "150"],
|
|
["param", "since", ((datefrom.value/1000).toFixed(0))],
|
|
["param", "until", ((dateto.value/1000).toFixed(0))],
|
|
['param', 'users', JSON.stringify(users)]
|
|
]
|
|
|
|
let res;
|
|
let requestid;
|
|
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
|
|
let draft = {
|
|
content: content,
|
|
kind: kind,
|
|
pubkey: store.state.pubkey.toHex(),
|
|
tags: tags,
|
|
createdAt: Date.now()
|
|
};
|
|
|
|
res = await amberSignerService.signEvent(draft)
|
|
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
|
|
requestid = res.id;
|
|
res = res.id;
|
|
}
|
|
|
|
else {
|
|
let tags_t = []
|
|
for (let tag of tags){
|
|
tags_t.push(Tag.parse(tag))
|
|
}
|
|
let evt = new EventBuilder(kind, content, tags_t)
|
|
try{
|
|
res = await client.sendEventBuilder(evt)
|
|
requestid = res.toHex()
|
|
}
|
|
catch(error){
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
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()
|
|
store.commit('set_hasEventListener', true)
|
|
}
|
|
else{
|
|
console.log("Already has event listener")
|
|
}
|
|
console.log(res)
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
}
|
|
|
|
|
|
async function getEvents(eventids) {
|
|
const event_filter = new Filter().ids(eventids)
|
|
let client = store.state.client
|
|
return await client.getEventsOf([event_filter], Duration.fromSecs(5))
|
|
}
|
|
|
|
|
|
async function get_user_infos(pubkeys){
|
|
let profiles = []
|
|
let client = store.state.client
|
|
const profile_filter = new Filter().kind(0).authors(pubkeys)
|
|
let evts = await client.getEventsOf([profile_filter], Duration.fromSecs(10))
|
|
|
|
for (const entry of evts){
|
|
try{
|
|
let contentjson = JSON.parse(entry.content)
|
|
//console.log(contentjson)
|
|
profiles.push({profile: contentjson, author: entry.author.toHex(), createdAt: entry.createdAt});
|
|
}
|
|
catch(error){
|
|
console.log("error")
|
|
}
|
|
|
|
}
|
|
|
|
return profiles
|
|
|
|
}
|
|
|
|
async function listen() {
|
|
listener = true
|
|
let client = store.state.client
|
|
let pubkey = store.state.pubkey
|
|
let originale = [store.state.requestidSearch]
|
|
|
|
const filter = new Filter().kinds([7000, 6302]).pubkey(pubkey).since(Timestamp.now());
|
|
await client.subscribe([filter]);
|
|
|
|
const handle = {
|
|
// Handle event
|
|
handleEvent: async (relayUrl, event) => {
|
|
if (store.state.hasEventListener === false){
|
|
return true
|
|
}
|
|
//const dvmname = getNamefromId(event.author.toHex())
|
|
console.log("Received new event from", relayUrl);
|
|
let resonsetorequest = false
|
|
|
|
sleep(1200).then(async () => {
|
|
|
|
for (let tag in event.tags) {
|
|
if (event.tags[tag].asVec()[0] === "e") {
|
|
//console.log("SEARCH ETAG: " + event.tags[tag].asVec()[1])
|
|
// console.log("SEARCH LISTEN TO : " + store.state.requestidSearch)
|
|
if (event.tags[tag].asVec()[1] === store.state.requestidSearch) {
|
|
resonsetorequest = true
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (resonsetorequest) {
|
|
|
|
if (event.kind === 7000) {
|
|
try {
|
|
console.log("7000: ", event.content);
|
|
console.log("DVM: " + event.author.toHex())
|
|
searching = false
|
|
//miniToastr.showMessage("DVM: " + dvmname, event.content, VueNotifications.types.info)
|
|
|
|
let status = "unknown"
|
|
let jsonentry = {
|
|
id: event.author.toHex(),
|
|
kind: "",
|
|
status: status,
|
|
result: "",
|
|
name: event.author.toBech32(),
|
|
about: "",
|
|
image: "",
|
|
amount: 0,
|
|
bolt11: ""
|
|
}
|
|
|
|
for (const tag in event.tags) {
|
|
if (event.tags[tag].asVec()[0] === "status") {
|
|
status = event.tags[tag].asVec()[1]
|
|
if (event.tags[tag].asVec().length > 2) {
|
|
jsonentry.about = event.tags[tag].asVec()[2]
|
|
}
|
|
}
|
|
|
|
if (event.tags[tag].asVec()[0] === "amount") {
|
|
jsonentry.amount = event.tags[tag].asVec()[1]
|
|
if (event.tags[tag].asVec().length > 2) {
|
|
jsonentry.bolt11 = event.tags[tag].asVec()[2]
|
|
}
|
|
// TODO else request invoice
|
|
}
|
|
}
|
|
|
|
//let dvm = store.state.nip89dvms.find(x => JSON.parse(x.event).pubkey === event.author.toHex())
|
|
for (const el of store.state.nip89dvms) {
|
|
if (JSON.parse(el.event).pubkey === event.author.toHex().toString()) {
|
|
jsonentry.name = el.name
|
|
jsonentry.about = event.content
|
|
jsonentry.image = el.image
|
|
console.log(jsonentry)
|
|
|
|
}
|
|
}
|
|
if (dvms.filter(i => i.id === jsonentry.id).length === 0) {
|
|
dvms.push(jsonentry)
|
|
}
|
|
|
|
dvms.find(i => i.id === jsonentry.id).status = status
|
|
if (status === "error") {
|
|
const index = dvms.indexOf((dvms.find(i => i.id === event.author.toHex())));
|
|
if (index > -1) {
|
|
dvms.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
store.commit('set_active_search_dvms', dvms)
|
|
console.log(store.state.activesearchdvms)
|
|
|
|
} catch (error) {
|
|
console.log("Error: ", error);
|
|
}
|
|
}
|
|
|
|
else if (event.kind === 6302) {
|
|
let entries = []
|
|
console.log("6302:", event.content);
|
|
|
|
//miniToastr.showMessage("DVM: " + dvmname, "Received Results", VueNotifications.types.success)
|
|
let event_etags = JSON.parse(event.content)
|
|
if (event_etags.length > 0) {
|
|
for (let etag of event_etags) {
|
|
const eventid = EventId.fromHex(etag[1])
|
|
entries.push(eventid)
|
|
}
|
|
const events = await getEvents(entries)
|
|
let authors = []
|
|
for (const evt of events) {
|
|
authors.push(evt.author)
|
|
}
|
|
if (authors.length > 0) {
|
|
let profiles = await get_user_infos(authors)
|
|
for (const evt of events) {
|
|
let p = profiles.find(record => record.author === evt.author.toHex())
|
|
let bech32id = evt.id.toBech32()
|
|
let nip19 = new Nip19Event(evt.id, evt.author, store.state.relays)
|
|
let nip19bech32 = nip19.toBech32()
|
|
let picture = p === undefined ? "../assets/nostr-purple.svg" : p["profile"]["picture"]
|
|
let name = p === undefined ? bech32id : p["profile"]["name"]
|
|
let highlighterurl = "https://highlighter.com/e/" + nip19bech32
|
|
let njumpurl = "https://njump.me/" + nip19bech32
|
|
let nostrudelurl = "https://nostrudel.ninja/#/n/" + bech32id
|
|
let uri = "nostr:" + bech32id // nip19.toNostrUri()
|
|
|
|
if (items.find(e => e.id.toHex() === evt.id.toHex()) === undefined) {
|
|
items.push({
|
|
id: evt.id,
|
|
content: evt.content,
|
|
author: name,
|
|
authorurl: "https://njump.me/" + evt.author.toBech32(),
|
|
links: {
|
|
"uri": uri,
|
|
"highlighter": highlighterurl,
|
|
"njump": njumpurl,
|
|
"nostrudel": nostrudelurl
|
|
},
|
|
avatar: picture,
|
|
indicator: {"time": evt.createdAt.toHumanDatetime()}
|
|
})
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const index = dvms.indexOf((dvms.find(i => i.id === event.author.toHex())));
|
|
if (index > -1) {
|
|
dvms.splice(index, 1);
|
|
}
|
|
|
|
store.commit('set_active_search_dvms', dvms)
|
|
console.log("Events from" + event.author.toHex())
|
|
store.commit('set_search_results', items)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
// Handle relay message
|
|
handleMsg: async (relayUrl, message) => {
|
|
//console.log(`Received message from ${relayUrl} ${message.asJson()}`);
|
|
}
|
|
};
|
|
|
|
client.handleNotifications(handle);
|
|
}
|
|
|
|
|
|
function getNamefromId(id){
|
|
let elements = searchdvms.filter(i => i.id === id)
|
|
if (elements.length === 0){
|
|
return id
|
|
}
|
|
else return elements[0].name
|
|
}
|
|
|
|
function nextInput(e) {
|
|
const next = e.currentTarget.nextElementSibling;
|
|
if (next) {
|
|
next.focus();
|
|
|
|
}
|
|
}
|
|
|
|
async function checkuser(msg){
|
|
usernames = []
|
|
let profiles = await get_user_from_search(msg)
|
|
for (let profile of profiles){
|
|
usernames.push(profile)
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
async function get_user_from_search(name){
|
|
name = "\"name\":" + name
|
|
|
|
if (store.state.dbclient.database === undefined){
|
|
console.log("not logged in, not getting profile suggestions")
|
|
return []
|
|
}
|
|
let dbclient = store.state.dbclient
|
|
let profiles = []
|
|
let filter1 = new Filter().kind(0).search(name)
|
|
let evts = await dbclient.database.query([filter1])
|
|
|
|
|
|
|
|
//const profile_filter = new Filter().kind(0).search(name)
|
|
//let evts = await client.getEventsOf([profile_filter], 3)
|
|
|
|
for (const entry of evts){
|
|
try{
|
|
let contentjson = JSON.parse(entry.content)
|
|
//console.log(entry.content)
|
|
profiles.push({profile: contentjson, author: entry.author.toBech32(), createdAt: entry.createdAt});
|
|
}
|
|
catch(error){
|
|
console.log(error)
|
|
}
|
|
|
|
}
|
|
|
|
return profiles
|
|
|
|
}
|
|
|
|
defineProps({
|
|
msg: {
|
|
type: String,
|
|
required: false
|
|
},
|
|
})
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
|
<div class="greetings">
|
|
<img alt="Nostr logo" class="logo" src="../assets/nostr-purple.svg" />
|
|
<br>
|
|
<h1 class="text-7xl font-black tracking-wide">Noogle</h1>
|
|
<h2 class="text-base-200-content text-center tracking-wide text-2xl">
|
|
Search the Nostr with Data Vending Machines</h2>
|
|
<h3>
|
|
<br>
|
|
|
|
<input class="c-Input" type="search" name="s" autofocus placeholder="Search..." v-model="message" @keyup.enter="send_search_request(message)" @keydown.enter="nextInput">
|
|
|
|
|
|
|
|
<button class="v-Button" @click="send_search_request(message)">Search the Nostr</button>
|
|
</h3>
|
|
|
|
<details class="collapse bg-base " className="advanced" >
|
|
<summary class="collapse-title font-thin bg">Advanced Options</summary>
|
|
<div class="collapse-content font-size-0" className="z-10" id="collapse-settings">
|
|
|
|
<div>
|
|
<h4 className="inline-flex flex-none font-thin">by: </h4>
|
|
<div className="inline-flex flex-none" style="width: 10px;"></div>
|
|
<input list="users" id="user" class="u-Input" style="margin-left: 10px" type="search" name="user" autofocus placeholder="npub..." v-model="fromuser" @input="checkuser(fromuser)">
|
|
|
|
<datalist id="users">
|
|
<option v-for="profile in usernames" :value="profile.author">
|
|
|
|
{{profile.profile.name + ' (' + profile.profile.nip05 + ')'}}
|
|
|
|
</option>
|
|
</datalist>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="inline-flex flex-none" style="width: 20px;"></div>
|
|
|
|
<div>
|
|
<h4 className="inline-flex flex-none font-thin">from:</h4>
|
|
<div className="inline-flex flex-none" style="width: 10px;"></div>
|
|
<VueDatePicker :teleport="true" :dark="true" position="left" className="bg-base-200 inline-flex flex-none" style="width: 220px;" v-model="datefrom"></VueDatePicker>
|
|
</div>
|
|
|
|
<div className="inline-flex flex-none" style="width: 20px;"></div>
|
|
<div>
|
|
<h4 className="inline-flex font-thin ">until: </h4>
|
|
<div className="inline-flex flex-none" style="width: 10px;"></div>
|
|
<VueDatePicker :teleport="true" :dark="true" position="left" className="bg-base-200 inline-flex flex-none" style="width: 220px;" v-model="dateto"></VueDatePicker>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="max-w-5xl relative space-y-3">
|
|
<div class="grid grid-cols-1 gap-6">
|
|
<div className="card w-30 h-60 bg-base-100 shadow-xl" v-for="dvm in store.state.activesearchdvms"
|
|
:key="dvm.name">
|
|
<div className="card-body">
|
|
<div class="grid grid-cols-1 gap-6">
|
|
|
|
<div className="col-end-1">
|
|
<h2 className="card-title">{{ dvm.name }}</h2>
|
|
<figure v-if="dvm.image!==''" className="w-40"><img className="h-30" :src="dvm.image" alt="DVM Picture" /></figure>
|
|
</div>
|
|
|
|
<div className="col-end-2 w-auto card-body">
|
|
<p>{{ dvm.about }}</p>
|
|
<div><br>
|
|
<span className="loading loading-dots loading-lg" ></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</template>
|
|
|
|
<style scoped>
|
|
|
|
.v-Button {
|
|
@apply bg-nostr hover:bg-nostr2 focus:ring-white mb-2 inline-flex flex-none items-center rounded-lg border border-black px-3 py-1.5 text-sm leading-4 text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
|
|
height: 48px;
|
|
margin: 5px;
|
|
}
|
|
|
|
.c-Input {
|
|
@apply bg-base-200 text-accent dark:bg-black dark:text-white focus:ring-white mb-2 inline-flex flex-none items-center rounded-lg border border-transparent px-3 py-1.5 text-sm leading-4 text-accent-content transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
|
|
|
|
width: 350px;
|
|
height: 48px;
|
|
|
|
}
|
|
|
|
.u-Input {
|
|
@apply bg-base-200 text-accent dark:bg-base-200 dark:text-white focus:ring-white border border-transparent px-3 py-1.5 text-sm leading-4 text-accent-content transition-colors duration-300 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
|
|
|
|
width: 220px;
|
|
height: 35px;
|
|
|
|
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
width:100%;
|
|
height:125px;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
h4 {
|
|
font-size: 1.0rem;
|
|
}
|
|
|
|
.greetings h1,
|
|
.greetings h3 {
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
@media (min-width: 1000px) {
|
|
|
|
.greetings h1,
|
|
.greetings h3 {
|
|
text-align: center;
|
|
}
|
|
}
|
|
</style>
|