From e8def92afba04ff66fd935f66519928f11ee2fca Mon Sep 17 00:00:00 2001 From: Believethehype Date: Sat, 2 Dec 2023 17:01:29 +0100 Subject: [PATCH] added advanced search for notes --- bot/bot.py | 5 + tasks/advanced_search.py | 200 +++++++++++++++++++++++++++++++++++++++ utils/definitions.py | 4 + utils/output_utils.py | 4 +- 4 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 tasks/advanced_search.py diff --git a/bot/bot.py b/bot/bot.py index 1d43ab5..9f911db 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -456,6 +456,8 @@ class Bot: relaylist.append(relay) relays_tag = Tag.parse(relaylist) tags.append(relays_tag) + output_tag = Tag.parse(["output", "text/plain"]) + tags.append(output_tag) remaining_text = command.replace(input, "") print(remaining_text) @@ -475,6 +477,9 @@ class Bot: if param == "cashu": tag = Tag.parse([param, value]) else: + if param == "user": + if value.startswith("@") or value.startswith("nostr:") or value.startswith("npub"): + value = PublicKey.from_bech32(value.replace("@","").replace("nostr:","")).to_hex() tag = Tag.parse(["param", param, value]) tags.append(tag) print("Added params: " + str(tag.as_vec())) diff --git a/tasks/advanced_search.py b/tasks/advanced_search.py new file mode 100644 index 0000000..57fcb23 --- /dev/null +++ b/tasks/advanced_search.py @@ -0,0 +1,200 @@ +import json +import os +import re +from datetime import timedelta +from pathlib import Path +from threading import Thread + +import dotenv +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, Alphabet, SecretKey + +from interfaces.dvmtaskinterface import DVMTaskInterface +from tasks.convert_media import MediaConverter +from utils.admin_utils import AdminConfig +from utils.backend_utils import keep_alive +from utils.definitions import EventDefinitions +from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config, check_and_set_d_tag +from utils.nostr_utils import get_event_by_id, check_and_set_private_key +from utils.output_utils import post_process_list_to_users, post_process_list_to_events + +""" +This File contains a Module to find inactive follows for a user on nostr + +Accepted Inputs: None needed +Outputs: A list of users that have been inactive +Params: None +""" + + +class AdvancedSearch(DVMTaskInterface): + KIND: int = EventDefinitions.KIND_NIP90_CONTENT_SEARCH + TASK: str = "search-content" + FIX_COST: float = 0 + dvm_config: DVMConfig + + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + super().__init__(name, dvm_config, nip89config, admin_config, options) + + def is_input_supported(self, tags): + # no input required + return True + + def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): + self.dvm_config = dvm_config + print(self.dvm_config.PRIVATE_KEY) + + request_form = {"jobID": event.id().to_hex()} + + # default values + user = "" + since_days = 1 #days ago + until_days = 0 #days ago + search = "" + max_results = 20 + + for tag in event.tags(): + if tag.as_vec()[0] == 'i': + input_type = tag.as_vec()[2] + if input_type == "text": + search = tag.as_vec()[1] + elif tag.as_vec()[0] == 'param': + param = tag.as_vec()[1] + if param == "user": # check for param type + user = tag.as_vec()[2] + if 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]) + elif param == "max_results": # check for param type + max_results = int(tag.as_vec()[2]) + + options = { + "search": search, + "user": user, + "since": since_days, + "until": until_days, + "max_results": max_results + } + request_form['options'] = json.dumps(options) + return request_form + + def process(self, request_form): + from nostr_sdk import Filter + options = DVMTaskInterface.set_options(request_form) + + opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))) + sk = SecretKey.from_hex(self.dvm_config.PRIVATE_KEY) + keys = Keys.from_sk_str(sk.to_hex()) + cli = Client.with_opts(keys, opts) + cli.add_relay("wss://relay.nostr.band") + cli.connect() + + search_since_seconds = int(options["since"]) * 24 * 60 * 60 + dif = Timestamp.now().as_secs() - search_since_seconds + search_since = Timestamp.from_secs(dif) + + search_until_seconds = int(options["until"]) * 24 * 60 * 60 + dif = Timestamp.now().as_secs() - search_until_seconds + search_until = Timestamp.from_secs(dif) + + if options["user"] == "": + notes_filter = Filter().kind(1).search(options["search"]).since(search_since).until(search_until) + elif options["search"] == "": + notes_filter = Filter().kind(1).author(PublicKey.from_hex(options["user"])).since(search_since).until(search_until) + else: + notes_filter = Filter().kind(1).author(PublicKey.from_hex(options["user"])).search(options["search"]).since(search_since).until(search_until) + + events = cli.get_events_of([notes_filter], timedelta(seconds=5)) + + result_list = [] + if len(events) > 0: + i = 0 + for event in events: + if i < int(options["max_results"]): + i = i+1 + e_tag = Tag.parse(["e", event.id().to_hex()]) + print(e_tag.as_vec()) + result_list.append(e_tag.as_vec()) + + return json.dumps(result_list) + + + + def post_process(self, result, event): + """Overwrite the interface function to return a social client readable format, if requested""" + for tag in event.tags(): + if tag.as_vec()[0] == 'output': + format = tag.as_vec()[1] + if format == "text/plain": # check for output type + result = post_process_list_to_events(result) + + # if not text/plain, don't post-process + return result + + +# We build an example here that we can call by either calling this file directly from the main directory, +# or by adding it to our playground. You can call the example and adjust it to your needs or redefine it in the +# playground or elsewhere +def build_example(name, identifier, admin_config): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + # Add NIP89 + nip90params = { + "user": { + "required": False, + "values": [], + "description": "Do the task for another user" + }, + "since": { + "required": False, + "values": [], + "description": "The number of days in the past from now the search should include" + }, + "until": { + "required": False, + "values": [], + "description": "The number of days in the past from now the search should include up to" + }, + "max_results": { + "required": False, + "values": [], + "description": "The number of maximum results to return (default currently 20)" + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I search notes", + "nip90Params": nip90params + } + + nip89config = NIP89Config() + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) + + nip89config.CONTENT = json.dumps(nip89info) + return AdvancedSearch(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config) + + +if __name__ == '__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} ') + + admin_config = AdminConfig() + admin_config.REBROADCAST_NIP89 = False + admin_config.UPDATE_PROFILE = False + admin_config.LUD16 = "" + + dvm = build_example("advanced_search", "discovery_content_search", admin_config) + dvm.run() + + keep_alive() diff --git a/utils/definitions.py b/utils/definitions.py index 96a3d73..723ea67 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -23,6 +23,9 @@ class EventDefinitions: KIND_NIP90_CONTENT_DISCOVERY = 5300 KIND_NIP90_RESULT_CONTENT_DISCOVERY = 6300 KIND_NIP90_PEOPLE_DISCOVERY = 5301 + KIND_NIP90_RESULT_PEOPLE_DISCOVERY = 6301 + KIND_NIP90_CONTENT_SEARCH = 5302 + KIND_NIP90_RESULTS_CONTENT_SEARCH = 6302 KIND_NIP90_GENERIC = 5999 KIND_NIP90_RESULT_GENERIC = 6999 ANY_RESULT = [KIND_NIP90_RESULT_EXTRACT_TEXT, @@ -34,6 +37,7 @@ class EventDefinitions: KIND_NIP90_PEOPLE_DISCOVERY, KIND_NIP90_RESULT_CONVERT_VIDEO, KIND_NIP90_RESULT_CONTENT_DISCOVERY, + KIND_NIP90_RESULT_PEOPLE_DISCOVERY, KIND_NIP90_RESULT_GENERIC] diff --git a/utils/output_utils.py b/utils/output_utils.py index 515cff5..7a3c336 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -98,9 +98,9 @@ def post_process_list_to_events(result): result_list = json.loads(result) result_str = "" for tag in result_list: - p_tag = Tag.parse(tag) + e_tag = Tag.parse(tag) result_str = result_str + "nostr:" + EventId.from_hex( - p_tag.as_vec()[1]).to_bech32() + "\n" + e_tag.as_vec()[1]).to_bech32() + "\n" return result_str