From fd7f051c47c69355015130c823d234a288973189 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 5 Feb 2024 14:43:09 +0100 Subject: [PATCH] fixes, first attempt to find inactive followers (false positives) --- main.py | 8 +- nostr_dvm/tasks/discovery_inactive_follows.py | 2 +- nostr_dvm/tasks/discovery_nonfollowers.py | 218 ++++++++++++++++++ 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 nostr_dvm/tasks/discovery_nonfollowers.py diff --git a/main.py b/main.py index b23b124..36d220f 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,8 @@ from nostr_dvm.bot import Bot from nostr_dvm.tasks import videogeneration_replicate_svd, imagegeneration_replicate_sdxl, textgeneration_llmlite, \ trending_notes_nostrband, discovery_inactive_follows, translation_google, textextraction_pdf, \ translation_libretranslate, textextraction_google, convert_media, imagegeneration_openai_dalle, texttospeech, \ - imagegeneration_sd21_mlx, advanced_search, textgeneration_huggingchat, summarization_huggingchat + imagegeneration_sd21_mlx, advanced_search, textgeneration_huggingchat, summarization_huggingchat, \ + discovery_nonfollowers from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.backend_utils import keep_alive from nostr_dvm.utils.definitions import EventDefinitions @@ -128,6 +129,11 @@ def playground(): bot_config.SUPPORTED_DVMS.append(discover_inactive) discover_inactive.run() + discover_nonfollowers = discovery_nonfollowers.build_example("Not Refollowing", "discovery_non_followers", + admin_config) + bot_config.SUPPORTED_DVMS.append(discover_nonfollowers) + discover_nonfollowers.run() + trending = trending_notes_nostrband.build_example("Trending Notes on nostr.band", "trending_notes_nostrband", admin_config) bot_config.SUPPORTED_DVMS.append(trending) trending.run() diff --git a/nostr_dvm/tasks/discovery_inactive_follows.py b/nostr_dvm/tasks/discovery_inactive_follows.py index 13a17d8..66bc81b 100644 --- a/nostr_dvm/tasks/discovery_inactive_follows.py +++ b/nostr_dvm/tasks/discovery_inactive_follows.py @@ -122,7 +122,7 @@ class DiscoverInactiveFollows(DVMTaskInterface): filters.append(filter1) event_from_authors = cli.get_events_of(filters, timedelta(seconds=10)) for author in event_from_authors: - instance.dic[author.pubkey().to_hex()] = "True" + instance.dic[author.author().to_hex()] = "True" print(str(i) + "/" + str(len(users))) cli.disconnect() diff --git a/nostr_dvm/tasks/discovery_nonfollowers.py b/nostr_dvm/tasks/discovery_nonfollowers.py new file mode 100644 index 0000000..5e9bb0c --- /dev/null +++ b/nostr_dvm/tasks/discovery_nonfollowers.py @@ -0,0 +1,218 @@ +import json +import os +from datetime import timedelta +from threading import Thread + +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, ClientSigner + +from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv +from nostr_dvm.utils.admin_utils import AdminConfig +from nostr_dvm.utils.definitions import EventDefinitions +from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag +from nostr_dvm.utils.output_utils import post_process_list_to_users + +""" +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 DiscoverNonFollowers(DVMTaskInterface): + KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY + TASK: str = "non-followers" + FIX_COST: float = 50 + client: Client + dvm_config: DVMConfig + + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + dvm_config.SCRIPT = os.path.abspath(__file__) + super().__init__(name, dvm_config, nip89config, admin_config, options) + + def is_input_supported(self, tags, client=None, dvm_config=None): + # no input required + return True + + def create_request_from_nostr_event(self, event, client=None, dvm_config=None): + self.dvm_config = dvm_config + + request_form = {"jobID": event.id().to_hex()} + + # default values + user = event.author().to_hex() + for tag in event.tags(): + if tag.as_vec()[0] == 'param': + param = tag.as_vec()[1] + if param == "user": # check for param type + user = tag.as_vec()[2] + + options = { + "user": user, + } + request_form['options'] = json.dumps(options) + return request_form + + def process(self, request_form): + from nostr_sdk import Filter + from types import SimpleNamespace + ns = SimpleNamespace() + + 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()) + signer = ClientSigner.keys(keys) + cli = Client.with_opts(signer, opts) + for relay in self.dvm_config.RELAY_LIST: + cli.add_relay(relay) + cli.connect() + + options = DVMTaskInterface.set_options(request_form) + step = 20 + + followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(3) + followers = cli.get_events_of([followers_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)) + + if len(followers) > 0: + result_list = [] + newest = 0 + best_entry = followers[0] + for entry in followers: + if entry.created_at().as_secs() > newest: + newest = entry.created_at().as_secs() + best_entry = entry + + print(best_entry.as_json()) + followings = [] + ns.dic = {} + for tag in best_entry.tags(): + if tag.as_vec()[0] == "p": + following = tag.as_vec()[1] + followings.append(following) + ns.dic[following] = "False" + print("Followings: " + str(len(followings))) + + + def scanList(users, instance, i, st): + from nostr_sdk import Filter + + keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) + opts = Options().wait_for_send(True).send_timeout( + timedelta(seconds=5)).skip_disconnected_relays(True) + signer = ClientSigner.keys(keys) + cli = Client.with_opts(signer, opts) + for relay in self.dvm_config.RELAY_LIST: + cli.add_relay(relay) + cli.connect() + + + for i in range(i, i + st): + filters = [] + filter1 = Filter().author(PublicKey.from_hex(users[i])).kind(3) + filters.append(filter1) + followers = cli.get_events_of(filters, timedelta(seconds=3)) + + if len(followers) > 0: + result_list = [] + newest = 0 + best_entry = followers[0] + for entry in followers: + if entry.created_at().as_secs() > newest: + newest = entry.created_at().as_secs() + best_entry = entry + + #print(best_entry.as_json()) + for tag in best_entry.tags(): + if tag.as_vec()[0] == "p": + if len(tag.as_vec()) > 1: + if tag.as_vec()[1] == options["user"]: + print("FOUND FOLLOWING") + instance.dic[best_entry.author().to_hex()] = "True" + break + + print(str(i) + "/" + str(len(users))) + cli.disconnect() + + threads = [] + begin = 0 + # Spawn some threads to speed things up + while begin < len(followings) - step: + args = [followings, ns, begin, step] + t = Thread(target=scanList, args=args) + threads.append(t) + begin = begin + step -1 + + # last to step size + missing_scans = (len(followings) - begin) + args = [followings, ns, begin, missing_scans] + t = Thread(target=scanList, args=args) + threads.append(t) + + # Start all threads + for x in threads: + x.start() + + # Wait for all of them to finish + for x in threads: + x.join() + + result = {k for (k, v) in ns.dic.items() if v == "False"} + + print("Non backfollowing accounts found: " + str(len(result))) + for k in result: + p_tag = Tag.parse(["p", k]) + result_list.append(p_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_users(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 = build_default_config(identifier) + admin_config.LUD16 = dvm_config.LN_ADDRESS + # Add NIP89 + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I discover users you follow, but that don't follow you back.", + "encryptionSupported": True, + "cashuAccepted": True, + "nip90Params": { + "user": { + "required": False, + "values": [], + "description": "Do the task for another user" + }, + "since_days": { + "required": False, + "values": [], + "description": "The number of days a user has not been active to be considered inactive" + } + } + } + nip89config = NIP89Config() + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) + + return DiscoverNonFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config) + + +if __name__ == '__main__': + process_venv(DiscoverNonFollowers)