diff --git a/nostr_dvm/tasks/advanced_search.py b/nostr_dvm/tasks/advanced_search.py
index 94cafec..09610d5 100644
--- a/nostr_dvm/tasks/advanced_search.py
+++ b/nostr_dvm/tasks/advanced_search.py
@@ -47,6 +47,7 @@ class AdvancedSearch(DVMTaskInterface):
# default values
user = ""
+ users = []
since_days = 800 # days ago
until_days = 0 # days ago
search = ""
@@ -61,7 +62,9 @@ class AdvancedSearch(DVMTaskInterface):
param = tag.as_vec()[1]
if param == "user": # check for param type
user = tag.as_vec()[2]
- if param == "since": # check for param type
+ elif param == "users": # check for param type
+ user = json.loads(tag.as_vec()[2])
+ elif param == "since": # check for param type
since_days = int(tag.as_vec()[2])
elif param == "until": # check for param type
until_days = int(tag.as_vec()[2])
@@ -71,6 +74,7 @@ class AdvancedSearch(DVMTaskInterface):
options = {
"search": search,
"user": user,
+ "users": users,
"since": since_days,
"until": until_days,
"max_results": max_results
@@ -98,16 +102,37 @@ class AdvancedSearch(DVMTaskInterface):
search_until_seconds = int(options["until"]) * 24 * 60 * 60
dif = Timestamp.now().as_secs() - search_until_seconds
search_until = Timestamp.from_secs(dif)
+ userkeys = []
+ for user in options["users"]:
+ user = user.as_json()[1]
+ user = str(user).lstrip("@")
+ if str(user).startswith('npub'):
+ userkey = PublicKey.from_bech32(user)
+ elif str(user).startswith("nostr:npub"):
+ userkey = PublicKey.from_nostr_uri(user)
+ else:
+ userkey = PublicKey.from_hex(user)
- if options["user"] == "":
+ userkeys.append(userkey)
+
+ if not options["users"] and options["user"] == "":
notes_filter = Filter().kind(1).search(options["search"]).since(search_since).until(search_until).limit(
options["max_results"])
elif options["search"] == "":
- notes_filter = Filter().kind(1).author(PublicKey.from_hex(options["user"])).since(search_since).until(
- search_until).limit(options["max_results"])
+ if options["users"]:
+ notes_filter = Filter().kind(1).authors(userkeys).since(search_since).until(
+ search_until).limit(options["max_results"])
+ else:
+ notes_filter = Filter().kind(1).authors([PublicKey.from_hex(options["user"])]).since(search_since).until(
+ search_until).limit(options["max_results"])
else:
- notes_filter = Filter().kind(1).author(PublicKey.from_hex(options["user"])).search(options["search"]).since(
- search_since).until(search_until).limit(options["max_results"])
+ if options["users"]:
+ notes_filter = Filter().kind(1).authors(userkeys).search(options["search"]).since(
+ search_since).until(search_until).limit(options["max_results"])
+ else:
+ notes_filter = Filter().kind(1).authors([PublicKey.from_hex(options["user"])]).search(options["search"]).since(
+ search_since).until(search_until).limit(options["max_results"])
+
events = cli.get_events_of([notes_filter], timedelta(seconds=5))
diff --git a/nostr_dvm/utils/database_utils.py b/nostr_dvm/utils/database_utils.py
index 5180f36..0572647 100644
--- a/nostr_dvm/utils/database_utils.py
+++ b/nostr_dvm/utils/database_utils.py
@@ -217,7 +217,7 @@ def fetch_user_metadata(npub, client):
pk = PublicKey.from_hex(npub)
print(f"\nGetting profile metadata for {pk.to_bech32()}...")
profile_filter = Filter().kind(0).author(pk).limit(1)
- events = client.get_events_of([profile_filter], timedelta(seconds=5))
+ events = client.get_events_of([profile_filter], timedelta(seconds=1))
if len(events) > 0:
latest_entry = events[0]
latest_time = 0
diff --git a/nostr_dvm/utils/nostr_utils.py b/nostr_dvm/utils/nostr_utils.py
index e8d5db0..f0a1af0 100644
--- a/nostr_dvm/utils/nostr_utils.py
+++ b/nostr_dvm/utils/nostr_utils.py
@@ -2,6 +2,7 @@ import json
import os
from datetime import timedelta
from pathlib import Path
+from typing import List
import dotenv
from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt, Metadata, Options, \
@@ -36,6 +37,15 @@ def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None:
return None
+def get_events_by_id(event_ids: list, client: Client, config=None) -> list[Event] | None:
+ id_filter = Filter().ids(event_ids)
+ events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT))
+ if len(events) > 0:
+ return events
+ else:
+ return None
+
+
def get_referenced_event_by_id(event_id, client, dvm_config, kinds) -> Event | None:
if kinds is None:
kinds = []
@@ -161,7 +171,6 @@ def update_profile(dvm_config, client, lud16=""):
about = nip89content.get("about")
image = nip89content.get("image")
-
# Set metadata
metadata = Metadata() \
.set_name(name) \
@@ -170,7 +179,7 @@ def update_profile(dvm_config, client, lud16=""):
.set_picture(image) \
.set_lud16(lud16) \
.set_nip05(lud16)
- # .set_banner("https://example.com/banner.png") \
+ # .set_banner("https://example.com/banner.png") \
print(f"Setting profile metadata for {keys.public_key().to_bech32()}...")
print(metadata.as_json())
@@ -192,4 +201,3 @@ def add_pk_to_env_file(dtag, oskey):
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
dotenv.set_key(env_path, dtag, oskey)
-
diff --git a/tests/nostrAI_search_client.py b/tests/nostrAI_search_client.py
new file mode 100644
index 0000000..cb94eda
--- /dev/null
+++ b/tests/nostrAI_search_client.py
@@ -0,0 +1,200 @@
+import asyncio
+import json
+import time
+from datetime import timedelta
+from pathlib import Path
+from nicegui import run, ui
+import dotenv
+from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, nip04_decrypt, \
+ nip04_encrypt, Options, Timestamp, ZapRequestData, ClientSigner, EventId, Nip19Event, PublicKey
+
+from nostr_dvm.utils import dvmconfig
+from nostr_dvm.utils.database_utils import fetch_user_metadata
+from nostr_dvm.utils.dvmconfig import DVMConfig
+from nostr_dvm.utils.nip89_utils import nip89_fetch_events_pubkey
+from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key, get_event_by_id, get_events_by_id
+from nostr_dvm.utils.definitions import EventDefinitions
+
+keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
+opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=2))
+ .skip_disconnected_relays(True))
+
+signer = ClientSigner.KEYS(keys)
+client = Client.with_opts(signer, opts)
+relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", "wss://nostr-pub.wellorder.net"]
+
+for relay in relay_list:
+ client.add_relay(relay)
+client.connect()
+
+dvm_filter = (Filter().pubkey(keys.public_key()).kinds([EventDefinitions.KIND_NIP90_RESULTS_CONTENT_SEARCH,
+ EventDefinitions.KIND_FEEDBACK])) # public events
+client.subscribe([dvm_filter])
+
+
+def nostr_client_test_search(prompt, users=None, since="", until=""):
+ if users is None:
+ users = []
+
+ iTag = Tag.parse(["i", prompt, "text"])
+ # outTag = Tag.parse(["output", "text/plain"])
+ userTag = Tag.parse(['param', 'users', json.dumps(users)])
+ sinceTag = Tag.parse(['param', 'since', since])
+ untilTag = Tag.parse(['param', 'until', until])
+ maxResultsTag = Tag.parse(['param', 'max_results', "100"])
+
+ relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
+ "wss://nostr-pub.wellorder.net"])
+ alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to search content"])
+
+ tags = [iTag, relaysTag, alttag, maxResultsTag]
+ if users:
+ tags.append(userTag)
+ if since != "":
+ tags.append(sinceTag)
+ if until != "":
+ tags.append(untilTag)
+ event = EventBuilder(EventDefinitions.KIND_NIP90_CONTENT_SEARCH, str("Search.."),
+ tags).to_event(keys)
+
+ config = DVMConfig
+ config.RELAY_LIST = relay_list
+ send_event(event, client=client, dvm_config=config)
+ return event.as_json()
+
+
+def handledvm(now):
+ response = False
+ feedbackfilter = Filter().pubkey(keys.public_key()).kinds(
+ [EventDefinitions.KIND_NIP90_RESULTS_CONTENT_SEARCH]).since(now)
+ feedbackfilter2 = Filter().pubkey(keys.public_key()).kinds(
+ [EventDefinitions.KIND_FEEDBACK]).since(now)
+ events = []
+ fevents = []
+ while not response:
+ events = client.get_events_of([feedbackfilter], timedelta(seconds=3))
+ fevents = client.get_events_of([feedbackfilter2], timedelta(seconds=3))
+ if len(fevents) > 0:
+ print(fevents[0].content())
+ # ui.notify(fevents[0].content())
+ if len(events) == 0:
+ response = False
+ time.sleep(1.0)
+ continue
+ else:
+ event_etags = json.loads(events[0].content())
+ event_ids = []
+ for etag in event_etags:
+ eventidob = EventId.from_hex(etag[1])
+ event_ids.append(eventidob)
+
+ config = DVMConfig()
+ events = get_events_by_id(event_ids, client, config)
+ listui = []
+ for event in events:
+ nip19event = Nip19Event(event.id(), event.pubkey(), dvmconfig.DVMConfig.RELAY_LIST)
+ nip19eventid = nip19event.to_bech32()
+ new = {'result': event.content(), 'author': event.pubkey().to_hex(),
+ 'eventid': str(event.id().to_hex()),
+ 'time': str(event.created_at().to_human_datetime()),
+ 'njump': "https://njump.me/" + nip19eventid,
+ 'highlighter': "https://highlighter.com/a/" + nip19eventid,
+ 'nostrudel': "https://nostrudel.ninja/#/n/" + nip19eventid
+ }
+ listui.append(new)
+ print(event.as_json())
+ # ui.update(table)
+ return listui
+
+async def search():
+ data.clear()
+ table.clear()
+ table.visible = False
+ now = Timestamp.now()
+ taggedusersfrom = [str(word).lstrip('from:@') for word in prompt.value.split() if word.startswith('from:@')]
+ taggedusersto = [str(word).lstrip('to:@') for word in prompt.value.split() if word.startswith('to:@')]
+
+ search = prompt.value
+ tags = []
+ for word in taggedusersfrom:
+ search = str(search).replace(word, "")
+ user_pubkey = PublicKey.from_bech32(word).to_hex()
+ pTag = ["p", user_pubkey]
+ tags.append(pTag)
+ search = str(search).replace("from:@", "").replace("to:@", "").lstrip().rstrip()
+
+ ev = nostr_client_test_search(search, tags)
+ ui.notify('Request sent to DVM, awaiting results..')
+
+ print("Sent: " + ev)
+ listui = []
+ print(str(now.to_human_datetime()))
+ listui = await run.cpu_bound(handledvm, now)
+ ui.notify("Received results from DVM")
+
+ for element in listui:
+ table.add_rows(element)
+
+ table.visible = True
+ ui.update(table)
+ return
+
+
+if __name__ in {"__main__", "__mp_main__"}:
+ env_path = Path('.env')
+ if env_path.is_file():
+ print(f'loading environment from {env_path.resolve()}')
+ dotenv.load_dotenv(env_path, verbose=True, override=True)
+ else:
+ raise FileNotFoundError(f'.env file not found at {env_path} ')
+
+ with ui.row().style('gap:10em').classes("row-1"):
+ with ui.column().classes("col-1"):
+ ui.label('NostrAI Search Page').classes('text-2xl')
+ prompt = ui.input('Search').style('width: 20em')
+ ui.button('Search', on_click=search).style('width: 15em')
+ # image = ui.image().style('width: 60em')
+ columns = [
+ {'name': 'result', 'label': 'result', 'field': 'result', 'sortable': True, 'align': 'left', },
+ {'name': 'time', 'label': 'time', 'field': 'time', 'sortable': True, 'align': 'left'},
+ # {'name': 'eventid', 'label': 'eventid', 'field': 'eventid', 'sortable': True, 'align': 'left'},
+ ]
+ data = []
+
+ # table = ui.table(columns, rows=data).classes('w-full bordered')
+ table = ui.table(columns=columns, rows=data, row_key='result',
+ pagination={'rowsPerPage': 10, 'sortBy': 'time', 'descending': True, 'page': 1}).style(
+ 'width: 80em')
+ table.add_slot('header', r'''
+
+
+
+ {{ col.label }}
+
+
+ ''')
+ table.add_slot('body', r'''
+
+
+
+
+
+ {{ col.value }}
+
+
+
+
+ Njump
+ Highlighter
+ NoStrudel
+
+
+ ''')
+
+ table.on('action', lambda msg: print(msg))
+ table.visible = False
+
+ # t1 = threading.Thread(target=nostr_client).start()
+ ui.run(reload=True, port=1234)
diff --git a/tests/test_events.py b/tests/test_events.py
index ceb42d6..5d661c0 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -46,13 +46,13 @@ def test_referred_events(event_id, kinds=None):
return None
-def test_all_reposts_by_user_since_days(pubkey, days):
+def test_search_by_user_since_days(pubkey, days, prompt):
since_seconds = int(days) * 24 * 60 * 60
dif = Timestamp.now().as_secs() - since_seconds
since = Timestamp.from_secs(dif)
- filter = Filter().author(PublicKey.from_hex(pubkey)).kinds([6]).since(since)
- events = client.get_events_of([filter], timedelta(seconds=5))
+ filterts = Filter().search(prompt).author(pubkey).kinds([1]).since(since)
+ events = client.get_events_of([filterts], timedelta(seconds=5))
if len(events) > 0:
for event in events:
@@ -87,4 +87,6 @@ if __name__ == '__main__':
nostruri = EventId.from_hex("5635e5dd930b3c831f6ab1e348bb488f3c9aca2f13190e93ab5e5e1e1ba1835e").to_nostr_uri()
print(nostruri)
+ test_search_by_user_since_days(PublicKey.from_bech32("npub1nxa4tywfz9nqp7z9zp7nr7d4nchhclsf58lcqt5y782rmf2hefjquaa6q8"), 60, "Bitcoin")
+