diff --git a/.env_example b/.env_example index dec41a2..9673bea 100644 --- a/.env_example +++ b/.env_example @@ -28,6 +28,7 @@ TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" TASK_IMAGE_GENERATION_NIP89_DTAG3 = "asdasd" TASK_SPEECH_TO_TEXT_NIP89 = "asdasdas" TASK_MEDIA_CONVERTER_NIP89_DTAG = "asdasdasd" +TASK_DISCOVER_INACTIVE_NIP89_DTAG = "sdfdfsdf12312" #Backend Specific Options for tasks that require inputs, such as Endpoints or API Keys diff --git a/bot/bot.py b/bot/bot.py index d690bf7..317091e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -138,7 +138,8 @@ class Bot: # send the event to the DVM send_event(nip90request, client=self.client, dvm_config=self.dvm_config) - print(nip90request.as_json()) + #print(nip90request.as_json()) + elif decrypted_text.lower().startswith("balance"): diff --git a/main.py b/main.py index c73c25a..b135334 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,7 @@ from nostr_sdk import Keys from bot.bot import Bot from playground import build_pdf_extractor, build_googletranslator, build_unstable_diffusion, build_sketcher, \ build_dalle, \ - build_whisperx, build_libretranslator, build_external_dvm, build_media_converter + build_whisperx, build_libretranslator, build_external_dvm, build_media_converter, build_inactive_follows_finder from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig @@ -99,6 +99,11 @@ def run_nostr_dvm_with_local_config(): bot_config.SUPPORTED_DVMS.append(media_bringer) # We also add Sketcher to the bot media_bringer.run() + #Spawn DVM10 Discover inactive followers + discover_inactive = build_inactive_follows_finder("Bygones") + bot_config.SUPPORTED_DVMS.append(discover_inactive) # We also add Sketcher to the bot + discover_inactive.run() + Bot(bot_config) diff --git a/playground.py b/playground.py index ed80c4e..8b277ec 100644 --- a/playground.py +++ b/playground.py @@ -5,10 +5,11 @@ from nostr_sdk import PublicKey, Keys from interfaces.dvmtaskinterface import DVMTaskInterface from tasks.convert_media import MediaConverter +from tasks.discovery_inactive_follows import DiscoverInactiveFollows from tasks.imagegeneration_openai_dalle import ImageGenerationDALLE from tasks.imagegeneration_sdxl import ImageGenerationSDXL from tasks.textextraction_whisperx import SpeechToTextWhisperX -from tasks.textextractionpdf import TextExtractionPDF +from tasks.textextraction_pdf import TextExtractionPDF from tasks.translation_google import TranslationGoogle from tasks.translation_libretranslate import TranslationLibre from utils.admin_utils import AdminConfig @@ -161,7 +162,7 @@ def build_unstable_diffusion(name): nip89config = NIP89Config() nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") nip89config.CONTENT = json.dumps(nip89info) - return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) @@ -195,7 +196,7 @@ def build_whisperx(name): nip89config = NIP89Config() nip89config.DTAG = os.getenv("TASK_SPEECH_TO_TEXT_NIP89") nip89config.CONTENT = json.dumps(nip89info) - return SpeechToTextWhisperX(name=name, dvm_config=dvm_config, nip89config=nip89config, + return SpeechToTextWhisperX(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) @@ -230,7 +231,7 @@ def build_sketcher(name): nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 - return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) @@ -262,7 +263,8 @@ def build_dalle(name): nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG3") nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 - return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) + return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) + def build_media_converter(name): dvm_config = DVMConfig() @@ -284,14 +286,51 @@ def build_media_converter(name): } nip89config = NIP89Config() - new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(), nip89info["image"]) + new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(), + nip89info["image"]) print("Some new dtag:" + new_dtag) nip89config.DTAG = os.getenv("TASK_MEDIA_CONVERTER_NIP89_DTAG") nip89config.CONTENT = json.dumps(nip89info) return MediaConverter(name=name, dvm_config=dvm_config, nip89config=nip89config, - admin_config=admin_config) + admin_config=admin_config) + +def build_inactive_follows_finder(name): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY7") + 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_days": { + "required": False, + "values": [], + "description": "The number of days a user has not been active to be considered inactive" + + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I discover users you follow, but that have been inactive on Nostr", + "nip90Params": nip90params + } + + nip89config = NIP89Config() + new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(), + nip89info["image"]) + print("Some new dtag:" + new_dtag) + nip89config.DTAG = os.getenv("TASK_DISCOVER_INACTIVE_NIP89_DTAG") + nip89config.CONTENT = json.dumps(nip89info) + return DiscoverInactiveFollows(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config) + def build_external_dvm(name, pubkey, task, kind, fix_cost, per_unit_cost): dvm_config = DVMConfig() dvm_config.PUBLIC_KEY = PublicKey.from_hex(pubkey).to_hex() diff --git a/tasks/discovery_inactive_follows.py b/tasks/discovery_inactive_follows.py new file mode 100644 index 0000000..3563ecb --- /dev/null +++ b/tasks/discovery_inactive_follows.py @@ -0,0 +1,170 @@ +import json +import os +import re +from datetime import timedelta +from threading import Thread + +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, Alphabet + +from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig +from utils.definitions import EventDefinitions +from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config +from utils.nostr_utils import get_event_by_id + +""" +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 DiscoverInactiveFollows(DVMTaskInterface): + KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY + TASK: str = "inactive-follows" + FIX_COST: float = 50 + client: Client + 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.client = client + self.dvm_config = dvm_config + + request_form = {"jobID": event.id().to_hex()} + + # default values + user = event.pubkey().to_hex() + since_days = 90 + + 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] + elif param == "since_days": # check for param type + since_days = int(tag.as_vec()[2]) + + options = { + "user": user, + "since_days": since_days + } + 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() + + options = DVMTaskInterface.set_options(request_form) + step = 20 + + followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(3).limit(1) + followers = self.client.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))) + + not_active_since_seconds = int(options["since_days"]) * 24 * 60 * 60 + dif = Timestamp.now().as_secs() - not_active_since_seconds + not_active_since = Timestamp.from_secs(dif) + + def scanList(users, instance, i, st, notactivesince): + 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=10)).skip_disconnected_relays(False) + cli = Client.with_opts(keys, opts) + for relay in self.dvm_config.RELAY_LIST: + cli.add_relay(relay) + cli.connect() + + filters = [] + for i in range(i + st): + filter1 = Filter().author(PublicKey.from_hex(users[i])).since(notactivesince).limit(1) + 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" + 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, not_active_since] + t = Thread(target=scanList, args=args) + threads.append(t) + begin = begin + step + + # last to step size + missing_scans = (len(followings) - begin) + args = [followings, ns, begin, missing_scans, not_active_since] + 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"} + + if len(result) == 0: + print("Not found") + return "No inactive followers found on relays." + print("Inactive 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_list = json.loads(result) + inactive_follows_list = "" + for tag in result_list: + p_tag = Tag.parse(tag) + inactive_follows_list = inactive_follows_list + "nostr:" + PublicKey.from_hex( + p_tag.as_vec()[1]).to_bech32() + "\n" + return inactive_follows_list + + # if not text/plain, don't post-process + return result diff --git a/tasks/textextractionpdf.py b/tasks/textextraction_pdf.py similarity index 100% rename from tasks/textextractionpdf.py rename to tasks/textextraction_pdf.py diff --git a/utils/mediasource_utils.py b/utils/mediasource_utils.py index 0cc63f3..9fd022c 100644 --- a/utils/mediasource_utils.py +++ b/utils/mediasource_utils.py @@ -11,7 +11,7 @@ from utils.nostr_utils import get_event_by_id def input_data_file_duration(event, dvm_config, client, start=0, end=0): #print("[" + dvm_config.NIP89.NAME + "] Getting Duration of the Media file..") input_value = "" - input_type = "url" + input_type = "" for tag in event.tags(): if tag.as_vec()[0] == 'i': input_value = tag.as_vec()[1] diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index cc4d7f6..ca1bf0d 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -1,4 +1,5 @@ from datetime import timedelta +from hashlib import sha256 from nostr_sdk import Tag, Keys, EventBuilder, Filter, Alphabet, PublicKey, Event @@ -15,10 +16,13 @@ class NIP89Config: def nip89_create_d_tag(name, pubkey, image): - import hashlib - m = hashlib.md5() - m.update(str(name + image + pubkey).encode("utf-8")) - d_tag = m.hexdigest()[0:16] + #import hashlib + #m = hashlib.md5() + #m.update(str(name + image + pubkey).encode("utf-8")) + #d_tag = m.hexdigest()[0:16] + + key_str = str(name + image + pubkey) + d_tag = sha256(key_str.encode('utf-8')).hexdigest()[:16] return d_tag