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") +