diff --git a/nostr_dvm/bot.py b/nostr_dvm/bot.py index 6e3022f..4ce783d 100644 --- a/nostr_dvm/bot.py +++ b/nostr_dvm/bot.py @@ -5,7 +5,8 @@ import time from datetime import timedelta from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, - Options, Tag, Event, nip04_encrypt, NostrSigner, EventId, Nip19Event) + Options, Tag, Event, nip04_encrypt, NostrSigner, EventId, Nip19Event, Kind, KindEnum, + UnsignedEvent, nip59_extract_rumor) from nostr_dvm.utils.admin_utils import admin_make_database_updates from nostr_dvm.utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table @@ -55,10 +56,10 @@ class Bot: kinds = [EventDefinitions.KIND_NIP90_GENERIC, EventDefinitions.KIND_FEEDBACK] for dvm in self.dvm_config.SUPPORTED_DVMS: if dvm.KIND not in kinds: - kinds.append(dvm.KIND + 1000) + kinds.append(Kind(dvm.KIND.as_u64() + 1000)) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) - self.client.subscribe([zap_filter, dm_filter, dvm_filter]) + self.client.subscribe([zap_filter, dm_filter, dvm_filter], None) create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) @@ -68,17 +69,34 @@ class Bot: dvm_config = self.dvm_config keys = self.keys - def handle(self, relay_url, nostr_event): - if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind() - <= EventDefinitions.KIND_NIP90_GENERIC + 1000): + def handle(self, relay_url, subscription_id, nostr_event): + if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64() + 1000 <= nostr_event.kind().as_u64() + <= EventDefinitions.KIND_NIP90_GENERIC.as_u64() + 1000): handle_nip90_response_event(nostr_event) - elif nostr_event.kind() == EventDefinitions.KIND_FEEDBACK: + elif nostr_event.kind().as_u64() == EventDefinitions.KIND_FEEDBACK.as_u64(): handle_nip90_feedback(nostr_event) - elif nostr_event.kind() == EventDefinitions.KIND_DM: - handle_dm(nostr_event) - elif nostr_event.kind() == EventDefinitions.KIND_ZAP: + + elif nostr_event.kind().as_u64() == EventDefinitions.KIND_ZAP.as_u64(): handle_zap(nostr_event) + elif nostr_event.kind().match_enum(KindEnum.ENCRYPTED_DIRECT_MESSAGE()): + try: + handle_dm(nostr_event) + except Exception as e: + print(f"Error during content NIP04 decryption: {e}") + elif nostr_event.kind().match_enum(KindEnum.GIFT_WRAP()): + print("Decrypting NIP59 event") + try: + rumor: UnsignedEvent = nip59_extract_rumor(self.keys, nostr_event) + if rumor.kind().match_enum(KindEnum.SEALED_DIRECT()): + msg = rumor.content() + print(f"Received new msg [sealed]: {msg}") + self.client.send_sealed_msg(rumor.author(), "Nip44 is not supported yet, but coming soon", None) + else: + print(f"{rumor.as_json()}") + except Exception as e: + print(f"Error during content NIP59 decryption: {e}") + def handle_msg(self, relay_url, msg): return @@ -265,7 +283,7 @@ class Bot: update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, nip05=user.nip05, lud16=user.lud16, name=user.name, - lastactive=Timestamp.now().as_secs()) + lastactive=Timestamp.now().as_secs(), subscribed=user.subscribed) evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.from_hex(entry["npub"]), "Paid " + str( diff --git a/nostr_dvm/dvm.py b/nostr_dvm/dvm.py index 1481958..ff42a09 100644 --- a/nostr_dvm/dvm.py +++ b/nostr_dvm/dvm.py @@ -5,7 +5,7 @@ from datetime import timedelta from sys import platform from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel, Options, nip04_encrypt, NostrSigner + init_logger, LogLevel, Options, nip04_encrypt, NostrSigner, Kind, SubscribeAutoCloseOptions import time @@ -13,8 +13,10 @@ from nostr_dvm.utils.definitions import EventDefinitions, RequiredJobToWatch, Jo from nostr_dvm.utils.dvmconfig import DVMConfig from nostr_dvm.utils.admin_utils import admin_make_database_updates, AdminConfig from nostr_dvm.utils.backend_utils import get_amount_per_task, check_task_is_supported, get_task -from nostr_dvm.utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table +from nostr_dvm.utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table, \ + update_user_subscription from nostr_dvm.utils.mediasource_utils import input_data_file_duration +from nostr_dvm.utils.nip88_utils import nip88_has_active_subscription from nostr_dvm.utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event, check_and_decrypt_tags from nostr_dvm.utils.output_utils import build_status_reaction from nostr_dvm.utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags, \ @@ -46,8 +48,7 @@ class DVM: self.jobs_on_hold_list = [] pk = self.keys.public_key() - print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + - ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") + print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) @@ -59,7 +60,8 @@ class DVM: if dvm.KIND not in kinds: kinds.append(dvm.KIND) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) - self.client.subscribe([dvm_filter, zap_filter]) + + self.client.subscribe([dvm_filter, zap_filter], None) create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) @@ -69,10 +71,11 @@ class DVM: dvm_config = self.dvm_config keys = self.keys - def handle(self, relay_url, nostr_event): - if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: + def handle(self, relay_url, subscription_id, nostr_event: Event): + + if EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64() <= nostr_event.kind().as_u64() <= EventDefinitions.KIND_NIP90_GENERIC.as_u64(): handle_nip90_job_event(nostr_event) - elif nostr_event.kind() == EventDefinitions.KIND_ZAP: + elif nostr_event.kind().as_u64() == EventDefinitions.KIND_ZAP.as_u64(): handle_zap(nostr_event) def handle_msg(self, relay_url, msg): @@ -81,6 +84,7 @@ class DVM: def handle_nip90_job_event(nip90_event): nip90_event = check_and_decrypt_tags(nip90_event, self.dvm_config) + if nip90_event is None: return @@ -96,7 +100,7 @@ class DVM: task_supported, task = check_task_is_supported(nip90_event, client=self.client, config=self.dvm_config) - + print(task_supported) if user.isblacklisted: send_job_status_reaction(nip90_event, "error", client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.NAME + "] Request by blacklisted user, skipped") @@ -109,8 +113,33 @@ class DVM: return task_is_free = False + user_has_active_subscription = False + + if dvm_config.NIP88 is not None and p_tag_str == self.dvm_config.PUBLIC_KEY: + + # if we stored in the database that the user has an active subscription, we don't need to check it + if user.subscribed > Timestamp.now().as_secs(): + print("User subscribed until: " + str(Timestamp.from_secs(user.subscribed).to_human_datetime())) + user_has_active_subscription = True + else: + # otherwise we check for an active subscription + print("[" + self.dvm_config.NIP89.NAME + "] Checking Subscription status") + subscription_status = nip88_has_active_subscription(PublicKey.parse(user.npub), + self.dvm_config.NIP88.DTAG, self.client, + self.dvm_config) + + if subscription_status["isActive"]: + print("User subscribed until: " + str( + Timestamp.from_secs(int(subscription_status["validUntil"])).to_human_datetime())) + user_has_active_subscription = True + update_user_subscription(user.npub, + int(subscription_status["validUntil"]), + self.client, self.dvm_config) + else: + print("No active subscription found") + for dvm in self.dvm_config.SUPPORTED_DVMS: - if dvm.TASK == task and dvm.FIX_COST == 0 and dvm.PER_UNIT_COST == 0: + if dvm.TASK == task and dvm.FIX_COST == 0 and dvm.PER_UNIT_COST == 0 and dvm_config.NIP88 is None: task_is_free = True cashu_redeemed = False @@ -124,31 +153,39 @@ class DVM: self.dvm_config) return # if user is whitelisted or task is free, just do the job - if (user.iswhitelisted or task_is_free or cashu_redeemed) and (p_tag_str == "" or p_tag_str == - self.dvm_config.PUBLIC_KEY): + if (user.iswhitelisted or task_is_free or cashu_redeemed) and ( + p_tag_str == "" or p_tag_str == + self.dvm_config.PUBLIC_KEY): print( "[" + self.dvm_config.NIP89.NAME + "] Free task or Whitelisted for task " + task + ". Starting processing..") if dvm_config.SEND_FEEDBACK_EVENTS: send_job_status_reaction(nip90_event, "processing", True, 0, - client=self.client, dvm_config=self.dvm_config) + client=self.client, dvm_config=self.dvm_config, user=user) # when we reimburse users on error make sure to not send anything if it was free if user.iswhitelisted or task_is_free: amount = 0 do_work(nip90_event, amount) - # if task is directed to us via p tag and user has balance, do the job and update balance - elif p_tag_str == self.dvm_config.PUBLIC_KEY and user.balance >= int(amount): - balance = max(user.balance - int(amount), 0) - update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, - iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, - nip05=user.nip05, lud16=user.lud16, name=user.name, - lastactive=Timestamp.now().as_secs()) + # if task is directed to us via p tag and user has balance or is subscribed, do the job and update balance + elif (p_tag_str == self.dvm_config.PUBLIC_KEY and ( + user.balance >= int( + amount) and dvm_config.NIP88 is None) or (p_tag_str == self.dvm_config.PUBLIC_KEY and user_has_active_subscription)): - print( - "[" + self.dvm_config.NIP89.NAME + "] Using user's balance for task: " + task + - ". Starting processing.. New balance is: " + str(balance)) + if not user_has_active_subscription: + balance = max(user.balance - int(amount), 0) + update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, + iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, + nip05=user.nip05, lud16=user.lud16, name=user.name, + lastactive=Timestamp.now().as_secs(), subscribed=user.subscribed) + + print( + "[" + self.dvm_config.NIP89.NAME + "] Using user's balance for task: " + task + + ". Starting processing.. New balance is: " + str(balance)) + else: + print("[" + self.dvm_config.NIP89.NAME + "] User has active subscription for task: " + task + + ". Starting processing.. Balance remains at: " + str(user.balance)) send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) @@ -157,27 +194,40 @@ class DVM: # else send a payment required event to user elif p_tag_str == "" or p_tag_str == self.dvm_config.PUBLIC_KEY: - bid = 0 - for tag in nip90_event.tags(): - if tag.as_vec()[0] == 'bid': - bid = int(tag.as_vec()[1]) - print( - "[" + self.dvm_config.NIP89.NAME + "] Payment required: New Nostr " + task + " Job event: " - + nip90_event.as_json()) - if bid > 0: - bid_offer = int(bid / 1000) - if bid_offer >= int(amount): - send_job_status_reaction(nip90_event, "payment-required", False, - int(amount), # bid_offer - client=self.client, dvm_config=self.dvm_config) - - else: # If there is no bid, just request server rate from user + if dvm_config.NIP88 is not None: print( - "[" + self.dvm_config.NIP89.NAME + "] Requesting payment for Event: " + + "[" + self.dvm_config.NIP89.NAME + "] Hinting user for Subscription: " + nip90_event.id().to_hex()) - send_job_status_reaction(nip90_event, "payment-required", - False, int(amount), client=self.client, dvm_config=self.dvm_config) + send_job_status_reaction(nip90_event, "subscription-required", + False, 0, client=self.client, + dvm_config=self.dvm_config) + else: + bid = 0 + for tag in nip90_event.tags(): + if tag.as_vec()[0] == 'bid': + bid = int(tag.as_vec()[1]) + + print( + "[" + self.dvm_config.NIP89.NAME + "] Payment required: New Nostr " + task + " Job event: " + + nip90_event.as_json()) + if bid > 0: + bid_offer = int(bid / 1000) + if bid_offer >= int(amount): + send_job_status_reaction(nip90_event, "payment-required", False, + int(amount), # bid_offer + client=self.client, dvm_config=self.dvm_config) + + else: # If there is no bid, just request server rate from user + print( + "[" + self.dvm_config.NIP89.NAME + "] Requesting payment for Event: " + + nip90_event.id().to_hex()) + send_job_status_reaction(nip90_event, "payment-required", + False, int(amount), client=self.client, dvm_config=self.dvm_config) + + + + else: print("[" + self.dvm_config.NIP89.NAME + "] Job addressed to someone else, skipping..") # else: @@ -192,11 +242,12 @@ class DVM: user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config) if zapped_event is not None: - if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: + if zapped_event.kind().as_u64() == EventDefinitions.KIND_FEEDBACK.as_u64(): amount = 0 job_event = None p_tag_str = "" + status = "" for tag in zapped_event.tags(): if tag.as_vec()[0] == 'amount': amount = int(float(tag.as_vec()[1]) / 1000) @@ -208,41 +259,53 @@ class DVM: return else: return + elif tag.as_vec()[0] == 'status': + status = tag.as_vec()[1] + print(status) # if a reaction by us got zapped + print(status) + if job_event.kind().as_u64() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT.as_u64(): + send_job_status_reaction(job_event, "subscription-success", client=self.client, + dvm_config=self.dvm_config, user=user) - task_supported, task = check_task_is_supported(job_event, client=self.client, - config=self.dvm_config) - if job_event is not None and task_supported: - print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( - user.name)) - if amount <= invoice_amount: - print("[" + self.dvm_config.NIP89.NAME + "] Payment-request fulfilled...") - send_job_status_reaction(job_event, "processing", client=self.client, - dvm_config=self.dvm_config) - indices = [i for i, x in enumerate(self.job_list) if - x.event == job_event] - index = -1 - if len(indices) > 0: - index = indices[0] - if index > -1: - if self.job_list[index].is_processed: # If payment-required appears a processing - self.job_list[index].is_paid = True - check_and_return_event(self.job_list[index].result, job_event) - elif not (self.job_list[index]).is_processed: - # If payment-required appears before processing - self.job_list.pop(index) - print("Starting work...") + + + else: + task_supported, task = check_task_is_supported(job_event, client=self.client, + config=self.dvm_config) + if job_event is not None and task_supported: + print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( + user.name)) + if amount <= invoice_amount: + print("[" + self.dvm_config.NIP89.NAME + "] Payment-request fulfilled...") + send_job_status_reaction(job_event, "processing", client=self.client, + dvm_config=self.dvm_config, user=user) + indices = [i for i, x in enumerate(self.job_list) if + x.event == job_event] + index = -1 + if len(indices) > 0: + index = indices[0] + if index > -1: + if self.job_list[index].is_processed: + self.job_list[index].is_paid = True + check_and_return_event(self.job_list[index].result, job_event) + elif not (self.job_list[index]).is_processed: + # If payment-required appears before processing + self.job_list.pop(index) + print("Starting work...") + do_work(job_event, invoice_amount) + else: + print("Job not in List, but starting work...") do_work(job_event, invoice_amount) - else: - print("Job not in List, but starting work...") - do_work(job_event, invoice_amount) - else: - send_job_status_reaction(job_event, "payment-rejected", - False, invoice_amount, client=self.client, - dvm_config=self.dvm_config) - print("[" + self.dvm_config.NIP89.NAME + "] Invoice was not paid sufficiently") + else: + send_job_status_reaction(job_event, "payment-rejected", + False, invoice_amount, client=self.client, + dvm_config=self.dvm_config) + print("[" + self.dvm_config.NIP89.NAME + "] Invoice was not paid sufficiently") + elif zapped_event.kind().as_u64() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT.as_u64(): + print("new subscription, doing nothing") elif zapped_event.kind() in EventDefinitions.ANY_RESULT: print("[" + self.dvm_config.NIP89.NAME + "] " @@ -350,7 +413,7 @@ class DVM: e_tag = Tag.parse(["e", original_event.id().to_hex()]) p_tag = Tag.parse(["p", original_event.author().to_hex()]) alt_tag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str( - original_event.kind()) + ". The task was: " + original_event.content()]) + original_event.kind().as_u64()) + ". The task was: " + original_event.content()]) status_tag = Tag.parse(["status", "success"]) reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag] encrypted = False @@ -371,15 +434,15 @@ class DVM: content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.author().to_hex()), content) - reply_event = EventBuilder(original_event.kind() + 1000, str(content), reply_tags).to_event(self.keys) + reply_event = EventBuilder(Kind(original_event.kind().as_u64() + 1000), str(content), reply_tags).to_event(self.keys) send_event(reply_event, client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.NAME + "] " + str( - original_event.kind() + 1000) + " Job Response event sent: " + reply_event.as_json()) + original_event.kind().as_u64() + 1000) + " Job Response event sent: " + reply_event.as_json()) def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, content=None, - dvm_config=None): + dvm_config=None, user=None): task = get_task(original_event, client=client, dvm_config=dvm_config) alt_description, reaction = build_status_reaction(status, task, amount, content, dvm_config) @@ -413,7 +476,8 @@ class DVM: bolt11 = "" payment_hash = "" expires = original_event.created_at().as_secs() + (60 * 60 * 24) - if status == "payment-required" or (status == "processing" and not is_paid): + if status == "payment-required" or ( + status == "processing" and not is_paid): if dvm_config.LNBITS_INVOICE_KEY != "": try: bolt11, payment_hash = create_bolt11_ln_bits(amount, dvm_config) @@ -471,12 +535,12 @@ class DVM: reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, str(content), reply_tags).to_event(keys) send_event(reaction_event, client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.NAME + "]" + ": Sent Kind " + str( - EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + reaction_event.as_json()) + EventDefinitions.KIND_FEEDBACK.as_u64()) + " Reaction: " + status + " " + reaction_event.as_json()) return reaction_event.as_json() def do_work(job_event, amount): - if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) - or job_event.kind() == EventDefinitions.KIND_DM): + if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64() <= job_event.kind().as_u64() <= EventDefinitions.KIND_NIP90_GENERIC.as_u64()) + or job_event.kind().as_u64() == EventDefinitions.KIND_DM.as_u64()): task = get_task(job_event, client=self.client, dvm_config=self.dvm_config) @@ -545,19 +609,19 @@ class DVM: for dvm in self.dvm_config.SUPPORTED_DVMS: scheduled_result = dvm.schedule(self.dvm_config) - for job in self.job_list: if job.bolt11 != "" and job.payment_hash != "" and not job.payment_hash is None and not job.is_paid: ispaid = check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config) if ispaid and job.is_paid is False: print("is paid") + job.is_paid = True + amount = parse_amount_from_bolt11_invoice(job.bolt11) job.is_paid = True send_job_status_reaction(job.event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.NAME + "] doing work from joblist") - amount = parse_amount_from_bolt11_invoice(job.bolt11) do_work(job.event, amount) elif ispaid is None: # invoice expired self.job_list.remove(job) diff --git a/nostr_dvm/interfaces/dvmtaskinterface.py b/nostr_dvm/interfaces/dvmtaskinterface.py index d8d2a4c..5f0cb5c 100644 --- a/nostr_dvm/interfaces/dvmtaskinterface.py +++ b/nostr_dvm/interfaces/dvmtaskinterface.py @@ -7,17 +7,18 @@ import sys from sys import platform from threading import Thread from venv import create -from nostr_sdk import Keys +from nostr_sdk import Keys, Kind from nostr_dvm.dvm import DVM from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_result class DVMTaskInterface: NAME: str - KIND: int + KIND: Kind TASK: str = "" FIX_COST: float = 0 PER_UNIT_COST: float = 0 @@ -30,13 +31,14 @@ class DVMTaskInterface: admin_config: AdminConfig dependencies = [] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config, + admin_config: AdminConfig = None, options=None, task=None): - self.init(name, dvm_config, admin_config, nip89config, task) + self.init(name, dvm_config, admin_config, nip88config, nip89config, task) self.options = options self.install_dependencies(dvm_config) - def init(self, name, dvm_config, admin_config=None, nip89config=None, task=None): + def init(self, name, dvm_config, admin_config=None, nip88config=None, nip89config=None, task=None): self.NAME = name self.PRIVATE_KEY = dvm_config.PRIVATE_KEY if dvm_config.PUBLIC_KEY == "" or dvm_config.PUBLIC_KEY is None: @@ -55,6 +57,12 @@ class DVMTaskInterface: self.KIND = nip89config.KIND dvm_config.NIP89 = self.NIP89_announcement(nip89config) + + if nip88config is None: + dvm_config.NIP88 = NIP88Config() + else: + dvm_config.NIP88 = nip88config + self.dvm_config = dvm_config self.admin_config = admin_config @@ -150,4 +158,3 @@ def process_venv(identifier): DVMTaskInterface.write_output(result, args.output) except Exception as e: DVMTaskInterface.write_output("Error: " + str(e), args.output) - diff --git a/nostr_dvm/tasks/advanced_search.py b/nostr_dvm/tasks/advanced_search.py index 6c3872c..9aa3646 100644 --- a/nostr_dvm/tasks/advanced_search.py +++ b/nostr_dvm/tasks/advanced_search.py @@ -1,12 +1,13 @@ import json import os from datetime import timedelta -from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_list_to_events @@ -19,15 +20,16 @@ Params: None class AdvancedSearch(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_CONTENT_SEARCH + KIND: Kind = 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, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/advanced_search_wine.py b/nostr_dvm/tasks/advanced_search_wine.py index 3787b5b..4dabce6 100644 --- a/nostr_dvm/tasks/advanced_search_wine.py +++ b/nostr_dvm/tasks/advanced_search_wine.py @@ -3,12 +3,13 @@ import os from datetime import timedelta import requests -from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Event +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Event, Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_list_to_events @@ -21,15 +22,16 @@ Params: None class AdvancedSearchWine(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_CONTENT_SEARCH + KIND: Kind = 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, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/content_discovery_currently_popular.py b/nostr_dvm/tasks/content_discovery_currently_popular.py index e445775..11b934c 100644 --- a/nostr_dvm/tasks/content_discovery_currently_popular.py +++ b/nostr_dvm/tasks/content_discovery_currently_popular.py @@ -2,12 +2,14 @@ import json import os from datetime import timedelta from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, NostrDatabase, \ - ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Event, EventId + ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Event, EventId, Kind from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv +from nostr_dvm.utils import definitions 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.nip88_utils import NIP88Config, check_and_set_d_tag_nip88, check_and_set_tiereventid_nip88 from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_list_to_events, post_process_list_to_users @@ -20,23 +22,24 @@ Params: None class DicoverContentCurrentlyPopular(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY + KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY TASK: str = "discover-content" FIX_COST: float = 0 dvm_config: DVMConfig last_schedule: int - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) + self.last_schedule = Timestamp.now().as_secs() use_logger = False if use_logger: init_logger(LogLevel.DEBUG) - super().__init__(name, dvm_config, nip89config, admin_config, options) - self.sync_db() def is_input_supported(self, tags, client=None, dvm_config=None): @@ -94,20 +97,20 @@ class DicoverContentCurrentlyPopular(DVMTaskInterface): # Query events from database timestamp_hour_ago = Timestamp.now().as_secs() - 3600 lasthour = Timestamp.from_secs(timestamp_hour_ago) - filter1 = Filter().kind(1).since(lasthour) + filter1 = Filter().kind(definitions.EventDefinitions.KIND_NOTE).since(lasthour) events = cli.database().query([filter1]) ns.finallist = {} for event in events: if event.created_at().as_secs() > timestamp_hour_ago: ns.finallist[event.id().to_hex()] = 0 - filt = Filter().kinds([9735, 7, 1]).event(event.id()).since(lasthour) + filt = Filter().kinds([definitions.EventDefinitions.KIND_ZAP, definitions.EventDefinitions.KIND_REACTION, definitions.EventDefinitions.KIND_NOTE]).event(event.id()).since(lasthour) reactions = cli.database().query([filt]) ns.finallist[event.id().to_hex()] = len(reactions) result_list = [] finallist_sorted = sorted(ns.finallist.items(), key=lambda x: x[1], reverse=True)[:int(options["max_results"])] for entry in finallist_sorted: - print(EventId.parse(entry[0]).to_bech32() + "/" + EventId.parse(entry[0]).to_hex() + ": " + str(entry[1])) + #print(EventId.parse(entry[0]).to_bech32() + "/" + EventId.parse(entry[0]).to_hex() + ": " + str(entry[1])) e_tag = Tag.parse(["e", entry[0]]) result_list.append(e_tag.as_vec()) @@ -146,7 +149,8 @@ class DicoverContentCurrentlyPopular(DVMTaskInterface): timestamp_hour_ago = Timestamp.now().as_secs() - 3600 lasthour = Timestamp.from_secs(timestamp_hour_ago) - filter1 = Filter().kinds([1, 7, 9735]).since(lasthour) # Notes, reactions, zaps + + filter1 = Filter().kinds([definitions.EventDefinitions.KIND_NOTE, definitions.EventDefinitions.KIND_REACTION, definitions.EventDefinitions.KIND_ZAP]).since(lasthour) # Notes, reactions, zaps # filter = Filter().author(keys.public_key()) print("Syncing Notes of last hour.. this might take a while..") @@ -163,13 +167,20 @@ def build_example(name, identifier, admin_config): dvm_config.USE_OWN_VENV = False dvm_config.SHOWLOG = True dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 minutes + # Activate these to use a subscription based model instead + # dvm_config.SUBSCRIPTION_REQUIRED = True + # dvm_config.SUBSCRIPTION_DAILY_COST = 1 + dvm_config.FIX_COST = 0 + # Add NIP89 nip89info = { "name": name, - "image": "https://image.nostr.build/f720192abfbfbcc21ce78281aca4bbd1ccf89ee7c90b54ae16b71ae9c1ad88e0.png", - "about": "I show popular content", + "image": "https://image.nostr.build/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg", + "about": "I show notes that are currently popular", + "lud16": dvm_config.LN_ADDRESS, "encryptionSupported": True, "cashuAccepted": True, + "amount": "free", "nip90Params": { "max_results": { "required": False, @@ -183,8 +194,68 @@ def build_example(name, identifier, admin_config): nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) + admin_config.UPDATE_PROFILE = False + admin_config.REBROADCAST_NIP89 = True + return DicoverContentCurrentlyPopular(name=name, dvm_config=dvm_config, nip89config=nip89config, - admin_config=admin_config) + admin_config=admin_config) + + +def build_example_subscription(name, identifier, admin_config): + dvm_config = build_default_config(identifier) + dvm_config.USE_OWN_VENV = False + dvm_config.SHOWLOG = True + dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 minutes + # Activate these to use a subscription based model instead + # dvm_config.SUBSCRIPTION_DAILY_COST = 1 + dvm_config.FIX_COST = 0 + + # Add NIP89 + nip89info = { + "name": name, + "image": "https://image.nostr.build/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg", + "about": "I show notes that are currently popular, just like the free DVM, I'm also used for testing subscriptions. (beta, might break" + ")", + "lud16": dvm_config.LN_ADDRESS, + "encryptionSupported": True, + "cashuAccepted": True, + "amount": "subscription", + "nip90Params": { + "max_results": { + "required": False, + "values": [], + "description": "The number of maximum results to return (default currently 100)" + } + } + } + + nip89config = NIP89Config() + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) + + nip88config = NIP88Config() + nip88config.DTAG = check_and_set_d_tag_nip88(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) + nip88config.TIER_EVENT = check_and_set_tiereventid_nip88(identifier, "1") + nip89config.NAME = name + nip88config.IMAGE = nip89info["image"] + nip88config.TITLE = name + nip88config.AMOUNT_DAILY = 100 + nip88config.AMOUNT_MONTHLY = 2000 + nip88config.CONTENT = "Subscribe to the DVM for unlimited use during your subscription" + nip88config.PERK1DESC = "Unlimited requests" + nip88config.PAYMENT_VERIFIER_PUBKEY = "5b5c045ecdf66fb540bdf2049fe0ef7f1a566fa427a4fe50d400a011b65a3a7e" + + admin_config.UPDATE_PROFILE = False + admin_config.REBROADCAST_NIP89 = True + admin_config.REBROADCAST_NIP88 = False + + # admin_config.FETCH_NIP88 = True + # admin_config.EVENTID = "63a791cdc7bf78c14031616963105fce5793f532bb231687665b14fb6d805fdb" + # admin_config.PRIVKEY = dvm_config.PRIVATE_KEY + + return DicoverContentCurrentlyPopular(name=name, dvm_config=dvm_config, nip89config=nip89config, + nip88config=nip88config, + admin_config=admin_config) if __name__ == '__main__': diff --git a/nostr_dvm/tasks/convert_media.py b/nostr_dvm/tasks/convert_media.py index d1b7dad..9671756 100644 --- a/nostr_dvm/tasks/convert_media.py +++ b/nostr_dvm/tasks/convert_media.py @@ -1,9 +1,13 @@ import json import os + +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config from nostr_dvm.utils.mediasource_utils import organize_input_media_data from nostr_dvm.utils.output_utils import upload_media_to_hoster @@ -18,15 +22,16 @@ Params: -language The target language class MediaConverter(DVMTaskInterface): - KIND = EventDefinitions.KIND_NIP90_CONVERT_VIDEO + KIND: Kind = EventDefinitions.KIND_NIP90_CONVERT_VIDEO TASK = "convert" FIX_COST = 20 PER_UNIT_COST = 0.1 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/discovery_inactive_follows.py b/nostr_dvm/tasks/discovery_inactive_follows.py index bc3c86e..6849d27 100644 --- a/nostr_dvm/tasks/discovery_inactive_follows.py +++ b/nostr_dvm/tasks/discovery_inactive_follows.py @@ -3,12 +3,13 @@ import os from datetime import timedelta from threading import Thread -from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind 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.nip88_utils import NIP88Config 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 @@ -22,16 +23,17 @@ Params: None class DiscoverInactiveFollows(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY + KIND: Kind = 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, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): # no input required diff --git a/nostr_dvm/tasks/discovery_nonfollowers.py b/nostr_dvm/tasks/discovery_nonfollowers.py index b1b9f08..4239401 100644 --- a/nostr_dvm/tasks/discovery_nonfollowers.py +++ b/nostr_dvm/tasks/discovery_nonfollowers.py @@ -3,12 +3,13 @@ import os from datetime import timedelta from threading import Thread -from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind 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.nip88_utils import NIP88Config 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 @@ -22,16 +23,17 @@ Params: None class DiscoverNonFollowers(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY + KIND: Kind = 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, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): # no input required @@ -66,9 +68,9 @@ class DiscoverNonFollowers(DVMTaskInterface): keys = Keys.parse(sk.to_hex()) signer = NostrSigner.keys(keys) cli = Client.with_opts(signer, opts) - #cli.add_relay("wss://relay.nostr.band") + # cli.add_relay("wss://relay.nostr.band") for relay in self.dvm_config.RELAY_LIST: - cli.add_relay(relay) + cli.add_relay(relay) cli.connect() options = DVMTaskInterface.set_options(request_form) @@ -96,7 +98,6 @@ class DiscoverNonFollowers(DVMTaskInterface): ns.dic[following] = "True" print("Followings: " + str(len(followings))) - def scanList(users, instance, i, st): from nostr_sdk import Filter @@ -109,7 +110,6 @@ class DiscoverNonFollowers(DVMTaskInterface): cli.add_relay(relay) cli.connect() - for i in range(i, i + st): filters = [] filter1 = Filter().author(PublicKey.from_hex(users[i])).kind(3) @@ -130,12 +130,12 @@ class DiscoverNonFollowers(DVMTaskInterface): if tag.as_vec()[0] == "p": if len(tag.as_vec()) > 1: if tag.as_vec()[1] == options["user"]: - foundfollower = True - break + foundfollower = True + break if not foundfollower: instance.dic[best_entry.author().to_hex()] = "False" - print( "DIDNT FIND " + best_entry.author().to_nostr_uri()) + print("DIDNT FIND " + best_entry.author().to_nostr_uri()) print(str(i) + "/" + str(len(users))) cli.disconnect() @@ -147,7 +147,7 @@ class DiscoverNonFollowers(DVMTaskInterface): args = [followings, ns, begin, step] t = Thread(target=scanList, args=args) threads.append(t) - begin = begin + step -1 + begin = begin + step - 1 # last to step size missing_scans = (len(followings) - begin) @@ -216,7 +216,7 @@ def build_example(name, identifier, admin_config): nip89config.CONTENT = json.dumps(nip89info) return DiscoverNonFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config, - admin_config=admin_config) + admin_config=admin_config) if __name__ == '__main__': diff --git a/nostr_dvm/tasks/discovery_relevant_notes.py b/nostr_dvm/tasks/discovery_relevant_notes.py index 2b8b655..c864b96 100644 --- a/nostr_dvm/tasks/discovery_relevant_notes.py +++ b/nostr_dvm/tasks/discovery_relevant_notes.py @@ -1 +1,299 @@ -from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, EventId +import json +import os +import random +import re +from datetime import timedelta +from threading import Thread + +from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, EventId, Kind + +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.nip88_utils import NIP88Config +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 DiscoverInactiveFollows(DVMTaskInterface): + KIND: Kind = 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, nip88config: NIP88Config = None, + admin_config: AdminConfig = None, options=None): + dvm_config.SCRIPT = os.path.abspath(__file__) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=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() + 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() + + 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.parse(sk.to_hex()) + signer = NostrSigner.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) + + + + inactivefollowerslist = "" + relay_list = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", + "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", + "wss://relay.nostr.bg", "wss://relay.f7z.io"] + relaytimeout = 5 + step = 20 + keys = Keys.parse(os.getenv(env.NOSTR_PRIVATE_KEY)) + opts = Options().wait_for_send(False).send_timeout(timedelta(seconds=5)).skip_disconnected_relays( + True) + cl = Client.with_opts(keys, opts) + for relay in relay_list: + cl.add_relay(relay) + cl.connect() + + timeinseconds = 3 * 24 * 60 * 60 + dif = Timestamp.now().as_secs() - timeinseconds + considernotessince = Timestamp.from_secs(dif) + filt = Filter().author(user).kind(1).since(considernotessince) + reactions = cl.get_events_of([filt], timedelta(seconds=relaytimeout)) + list = [] + random.shuffle(reactions) + for reaction in reactions: + if reaction.kind() == 1: + list.append(reaction.content()) + all = json.dumps(list) + all = all.replace("\n", " ").replace("\n\n", " ") + cleared = "" + tokens = all.split() + for item in tokens: + item = item.replace("\n", " ").lstrip("\"").rstrip(",").rstrip((".")) + # print(item) + if item.__contains__("http") or item.__contains__("\nhttp") or item.__contains__( + "\n\nhttp") or item.lower().__contains__("nostr:") or item.lower().__contains__( + "nevent") or item.__contains__("\\u"): + cleareditem = "" + else: + cleareditem = item + cleared = cleared + " " + cleareditem + + cleared = cleared.replace("\n", " ") + # res = re.sub(r"[^ a-zA-Z0-9.!?/\\:,]+", '', all) + # print(cleared) + try: + answer = LLAMA2( + "Give me the 15 most important substantives as keywords of the following input: " + cleared, + "nostruser", + "Reply only with a comma-seperated keywords. return topics starting with a *", clear=True) + except: + answer = "" + + promptarr = answer.split(":") + if len(promptarr) > 1: + # print(promptarr[1]) + prompt = promptarr[1].lstrip("\n").replace("\n", ",").replace("*", ",").replace("•", ",") + else: + prompt = promptarr[0].replace("\n", ",").replace("*", "") + + pattern = r"[^a-zA-Z,#'\s]" + text = re.sub(pattern, "", prompt) + "," + + # text = (text.replace("Let's,", "").replace("Why,", "").replace("GM,", "") + # .replace("Remember,", "").replace("I,", "").replace("Think,", "") + # .replace("Already,", "")) + # print(text) + keywords = text.split(', ') + keywords = [x.lstrip().rstrip(',') for x in keywords if x] + + print(keywords) + + # answer = LLAMA2("Extent the given list with 5 synonyms per entry " + str(keywords), user, + # "Reply only with a comma-seperated keywords. return topics starting with a *") + # answer.replace(" - Alternatives:", ",") + # print(answer) + # promptarr = answer.split(":") + # if len(promptarr) > 1: + # # print(promptarr[1]) + # prompt = promptarr[1].lstrip("\n").replace("\n", ",").replace("*", "").replace("•", "") + # else: + # prompt = promptarr[0].replace("\n", ",").replace("*", "") + + # pattern = r"[^a-zA-Z,'\s]" + # text = re.sub(pattern, "", prompt) + "," + # keywords = text.split(', ') + + # print(keywords) + + timeinseconds = 30 * 60 # last 30 min? + dif = Timestamp.now().as_secs() - timeinseconds + looksince = Timestamp.from_secs(dif) + filt2 = Filter().kind(1).since(looksince) + notes = cl.get_events_of([filt2], timedelta(seconds=6)) + + # finallist = [] + ns.finallist = {} + + print("Notes found: " + str(len(notes))) + + def scanList(noteid: EventId, instance, i, length): + + relay_list = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", + "wss://nostr.wine", + "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", + "wss://relay.nostr.bg", "wss://relay.f7z.io"] + keys = Keys.parse(os.getenv(env.NOSTR_PRIVATE_KEY)) + opts = Options().wait_for_send(wait_for_send).send_timeout( + timedelta(seconds=5)).skip_disconnected_relays(True) + cli = Client.with_opts(keys, opts) + for relay in relay_list: + cli.add_relay(relay) + cli.connect() + + filters = [] + instance.finallist[noteid.to_hex()] = 0 + filt = Filter().kinds([9735, 7, 1]).event(noteid) + reactions = cl.get_events_of([filt], timedelta(seconds=5)) + print(str(len(reactions)) + " " + str(j) + "/" + str(len(notes))) + instance.finallist[noteid.to_hex()] = len(reactions) + + print(str(i) + "/" + str(length)) + cli.disconnect() + + j = 0 + + threads = [] + + for note in notes: + + j = j + 1 + res = [ele for ele in keywords if (ele.replace(',', "") in note.content())] + if bool(res): + if not note.id().to_hex() in ns.finallist and note.pubkey().to_hex() != user: + args = [note.id(), ns, j, len(notes)] + 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() + + finallist_sorted = sorted(ns.finallist.items(), key=lambda x: x[1], reverse=True) + converted_dict = dict(finallist_sorted) + print(json.dumps(converted_dict)) + + notelist = "" + resultlist = [] + i = 0 + notelist = "Based on topics: " + json.dumps(keywords).lstrip("[").rstrip(("]")) + "\n\n" + for k in converted_dict: + # print(k) + if is_bot: + i = i + 1 + notelist = notelist + "nostr:" + EventId.from_hex(k).to_bech32() + "\n\n" + if i == 25: + break + else: + p_tag = Tag.parse(["p", k]) + resultlist.append(p_tag.as_vec()) + + else: + return json.dumps(resultlist[:25]) + + 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/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg", + "about": "I discover users you follow, but that have been inactive on Nostr", + "encryptionSupported": True, + "cashuAccepted": True, + "amount": "Free", + "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 DiscoverInactiveFollows(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config) + + +if __name__ == '__main__': + process_venv(DiscoverInactiveFollows) diff --git a/nostr_dvm/tasks/discovery_trending_notes_nostrband.py b/nostr_dvm/tasks/discovery_trending_notes_nostrband.py index 6ada615..502bede 100644 --- a/nostr_dvm/tasks/discovery_trending_notes_nostrband.py +++ b/nostr_dvm/tasks/discovery_trending_notes_nostrband.py @@ -1,11 +1,12 @@ import json import os -from nostr_sdk import Tag +from nostr_sdk import Tag, Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_list_to_events @@ -18,15 +19,16 @@ Params: None class TrendingNotesNostrBand(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY + KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY TASK: str = "trending-content" FIX_COST: float = 0 dvm_config: DVMConfig - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/imagegeneration_openai_dalle.py b/nostr_dvm/tasks/imagegeneration_openai_dalle.py index e08c5b8..9ac9a95 100644 --- a/nostr_dvm/tasks/imagegeneration_openai_dalle.py +++ b/nostr_dvm/tasks/imagegeneration_openai_dalle.py @@ -5,11 +5,13 @@ from io import BytesIO import requests from PIL import Image +from nostr_sdk import Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import upload_media_to_hoster from nostr_dvm.utils.zap_utils import get_price_per_sat @@ -23,16 +25,17 @@ Outputs: An url to an Image class ImageGenerationDALLE(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" FIX_COST: float = 120 dependencies = [("nostr-dvm", "nostr-dvm"), ("openai", "openai==1.3.5")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py b/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py index f9b83c1..3077fd6 100644 --- a/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py +++ b/nostr_dvm/tasks/imagegeneration_replicate_sdxl.py @@ -3,11 +3,13 @@ import os from io import BytesIO import requests from PIL import Image +from nostr_sdk import Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import upload_media_to_hoster from nostr_dvm.utils.zap_utils import get_price_per_sat @@ -22,16 +24,17 @@ Params: class ImageGenerationReplicateSDXL(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" FIX_COST: float = 120 dependencies = [("nostr-dvm", "nostr-dvm"), ("replicate", "replicate")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/imagegeneration_sd21_mlx.py b/nostr_dvm/tasks/imagegeneration_sd21_mlx.py index 9330345..e49fe79 100644 --- a/nostr_dvm/tasks/imagegeneration_sd21_mlx.py +++ b/nostr_dvm/tasks/imagegeneration_sd21_mlx.py @@ -1,12 +1,14 @@ import json import os from PIL import Image +from nostr_sdk import Kind from tqdm import tqdm 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import upload_media_to_hoster from nostr_dvm.utils.zap_utils import get_price_per_sat @@ -21,7 +23,7 @@ Params: class ImageGenerationMLX(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" FIX_COST: float = 120 dependencies = [("nostr-dvm", "nostr-dvm"), @@ -32,10 +34,11 @@ class ImageGenerationMLX(DVMTaskInterface): ("tqdm", "tqdm"), ] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/imagegeneration_sdxl.py b/nostr_dvm/tasks/imagegeneration_sdxl.py index 057c24a..de64068 100644 --- a/nostr_dvm/tasks/imagegeneration_sdxl.py +++ b/nostr_dvm/tasks/imagegeneration_sdxl.py @@ -1,10 +1,13 @@ import json from multiprocessing.pool import ThreadPool +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -19,13 +22,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea class ImageGenerationSDXL(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" FIX_COST: float = 50 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/imagegeneration_sdxlimg2img.py b/nostr_dvm/tasks/imagegeneration_sdxlimg2img.py index 751b210..fc656bc 100644 --- a/nostr_dvm/tasks/imagegeneration_sdxlimg2img.py +++ b/nostr_dvm/tasks/imagegeneration_sdxlimg2img.py @@ -1,10 +1,13 @@ import json from multiprocessing.pool import ThreadPool +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -19,13 +22,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea class ImageGenerationSDXLIMG2IMG(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "image-to-image" FIX_COST: float = 70 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): hasurl = False diff --git a/nostr_dvm/tasks/imageinterrogator.py b/nostr_dvm/tasks/imageinterrogator.py index a1c3374..dd0c236 100644 --- a/nostr_dvm/tasks/imageinterrogator.py +++ b/nostr_dvm/tasks/imageinterrogator.py @@ -1,10 +1,13 @@ import json from multiprocessing.pool import ThreadPool +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -18,13 +21,14 @@ Outputs: An textual description of the image class ImageInterrogator(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "image-to-text" FIX_COST: float = 80 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): hasurl = False diff --git a/nostr_dvm/tasks/imageupscale.py b/nostr_dvm/tasks/imageupscale.py index 695bc2e..edb8eab 100644 --- a/nostr_dvm/tasks/imageupscale.py +++ b/nostr_dvm/tasks/imageupscale.py @@ -1,10 +1,13 @@ import json from multiprocessing.pool import ThreadPool +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -18,13 +21,14 @@ Params: -upscale 2,3,4 class ImageUpscale(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "image-to-image" FIX_COST: float = 20 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): hasurl = False diff --git a/nostr_dvm/tasks/search_users.py b/nostr_dvm/tasks/search_users.py index 2eb86fd..e9f8041 100644 --- a/nostr_dvm/tasks/search_users.py +++ b/nostr_dvm/tasks/search_users.py @@ -2,12 +2,13 @@ import json import os from datetime import timedelta from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, NostrDatabase, \ - ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel + ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import post_process_list_to_events, post_process_list_to_users @@ -20,23 +21,22 @@ Params: None class SearchUser(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_USER_SEARCH + KIND: Kind = EventDefinitions.KIND_NIP90_USER_SEARCH TASK: str = "search-user" FIX_COST: float = 0 dvm_config: DVMConfig last_schedule: int - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - self.last_schedule = Timestamp.now().as_secs() + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) use_logger = False if use_logger: init_logger(LogLevel.DEBUG) - super().__init__(name, dvm_config, nip89config, admin_config, options) - self.sync_db() def is_input_supported(self, tags, client=None, dvm_config=None): diff --git a/nostr_dvm/tasks/summarization_huggingchat.py b/nostr_dvm/tasks/summarization_huggingchat.py index e12f995..46ce7f6 100644 --- a/nostr_dvm/tasks/summarization_huggingchat.py +++ b/nostr_dvm/tasks/summarization_huggingchat.py @@ -5,9 +5,10 @@ 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id, get_events_by_ids -from nostr_sdk import Tag +from nostr_sdk import Tag, Kind """ This File contains a Module to summarize Text, based on a prompt using a the HuggingChat LLM on Huggingface @@ -18,16 +19,17 @@ Outputs: Generated text class TextSummarizationHuggingChat(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT TASK: str = "summarization" FIX_COST: float = 0 dependencies = [("nostr-dvm", "nostr-dvm"), ("hugchat", "hugchat")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/summarization_unleashed_chat.py b/nostr_dvm/tasks/summarization_unleashed_chat.py index c39dd21..375d815 100644 --- a/nostr_dvm/tasks/summarization_unleashed_chat.py +++ b/nostr_dvm/tasks/summarization_unleashed_chat.py @@ -1,11 +1,12 @@ import json import os import re -from nostr_sdk import Tag +from nostr_sdk import Tag, Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_events_by_ids, get_event_by_id @@ -18,16 +19,17 @@ Outputs: Generated text class SummarizationUnleashedChat(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT TASK: str = "text-to-text" FIX_COST: float = 10 dependencies = [("nostr-dvm", "nostr-dvm"), ("openai", "openai")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/textextraction_google.py b/nostr_dvm/tasks/textextraction_google.py index 3a7d649..e737288 100644 --- a/nostr_dvm/tasks/textextraction_google.py +++ b/nostr_dvm/tasks/textextraction_google.py @@ -2,10 +2,13 @@ import json import os import time +from nostr_sdk import Kind + from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config from nostr_dvm.utils.mediasource_utils import organize_input_media_data +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -19,17 +22,18 @@ Outputs: Transcribed text class SpeechToTextGoogle(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "speech-to-text" FIX_COST: float = 10 PER_UNIT_COST: float = 0.1 dependencies = [("nostr-dvm", "nostr-dvm"), ("speech_recognition", "SpeechRecognition==3.10.0")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) if options is None: self.options = {} diff --git a/nostr_dvm/tasks/textextraction_pdf.py b/nostr_dvm/tasks/textextraction_pdf.py index 2bc771b..5a830dd 100644 --- a/nostr_dvm/tasks/textextraction_pdf.py +++ b/nostr_dvm/tasks/textextraction_pdf.py @@ -2,10 +2,13 @@ import json import os import re +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.nostr_utils import get_event_by_id @@ -19,16 +22,17 @@ Params: None class TextExtractionPDF(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" FIX_COST: float = 0 dependencies = [("nostr-dvm", "nostr-dvm"), ("pypdf", "pypdf==3.17.1")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/textextraction_whisperx.py b/nostr_dvm/tasks/textextraction_whisperx.py index d33436c..282dbd9 100644 --- a/nostr_dvm/tasks/textextraction_whisperx.py +++ b/nostr_dvm/tasks/textextraction_whisperx.py @@ -2,11 +2,15 @@ import json import os import time from multiprocessing.pool import ThreadPool + +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server, send_file_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config from nostr_dvm.utils.mediasource_utils import organize_input_media_data +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -20,14 +24,16 @@ Outputs: Transcribed text class SpeechToTextWhisperX(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "speech-to-text" FIX_COST: float = 10 PER_UNIT_COST: float = 0.1 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + dvm_config.SCRIPT = os.path.abspath(__file__) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/textgeneration_huggingchat.py b/nostr_dvm/tasks/textgeneration_huggingchat.py index a32e944..0776f85 100644 --- a/nostr_dvm/tasks/textgeneration_huggingchat.py +++ b/nostr_dvm/tasks/textgeneration_huggingchat.py @@ -1,10 +1,13 @@ import json import os +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag """ @@ -16,16 +19,17 @@ Outputs: Generated text class TextGenerationHuggingChat(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT TASK: str = "text-to-text" FIX_COST: float = 0 dependencies = [("nostr-dvm", "nostr-dvm"), ("hugchat", "hugchat")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/textgeneration_llmlite.py b/nostr_dvm/tasks/textgeneration_llmlite.py index beee0cc..e6ff667 100644 --- a/nostr_dvm/tasks/textgeneration_llmlite.py +++ b/nostr_dvm/tasks/textgeneration_llmlite.py @@ -1,10 +1,13 @@ import json import os +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag """ @@ -16,16 +19,17 @@ Outputs: Generated text class TextGenerationLLMLite(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT TASK: str = "text-to-text" FIX_COST: float = 0 dependencies = [("nostr-dvm", "nostr-dvm"), ("litellm", "litellm==1.12.3")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/textgeneration_unleashed_chat.py b/nostr_dvm/tasks/textgeneration_unleashed_chat.py index 057140e..8cee96e 100644 --- a/nostr_dvm/tasks/textgeneration_unleashed_chat.py +++ b/nostr_dvm/tasks/textgeneration_unleashed_chat.py @@ -1,10 +1,13 @@ import json import os +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag """ @@ -16,16 +19,17 @@ Outputs: Generated text class TextGenerationUnleashedChat(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT TASK: str = "text-to-text" FIX_COST: float = 10 dependencies = [("nostr-dvm", "nostr-dvm"), ("openai", "openai")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/texttospeech.py b/nostr_dvm/tasks/texttospeech.py index 2a1a071..e5e22d2 100644 --- a/nostr_dvm/tasks/texttospeech.py +++ b/nostr_dvm/tasks/texttospeech.py @@ -1,6 +1,10 @@ import json import os +from nostr_sdk import Kind + +from nostr_dvm.utils.nip88_utils import NIP88Config + os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" from pathlib import Path import urllib.request @@ -22,17 +26,18 @@ Outputs: Generated Audiofile class TextToSpeech(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_TEXT_TO_SPEECH + KIND: Kind = EventDefinitions.KIND_NIP90_TEXT_TO_SPEECH TASK: str = "text-to-speech" FIX_COST: float = 50 PER_UNIT_COST = 0.5 dependencies = [("nostr-dvm", "nostr-dvm"), ("TTS", "TTS==0.22.0")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/translation_google.py b/nostr_dvm/tasks/translation_google.py index 896790f..e37764b 100644 --- a/nostr_dvm/tasks/translation_google.py +++ b/nostr_dvm/tasks/translation_google.py @@ -1,9 +1,13 @@ import json import os + +from nostr_sdk import Kind + 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id @@ -17,16 +21,17 @@ Params: -language The target language class TranslationGoogle(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" FIX_COST: float = 0 dependencies = [("nostr-dvm", "nostr-dvm"), ("translatepy", "translatepy==2.3")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/translation_libretranslate.py b/nostr_dvm/tasks/translation_libretranslate.py index 8f39c97..65d42d3 100644 --- a/nostr_dvm/tasks/translation_libretranslate.py +++ b/nostr_dvm/tasks/translation_libretranslate.py @@ -1,11 +1,13 @@ import json import os import requests +from nostr_sdk import Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id @@ -21,14 +23,15 @@ Requires API key or self-hosted instance class TranslationLibre(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT + KIND: Kind = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" FIX_COST: float = 0 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, - admin_config: AdminConfig = None, options=None, task=None): + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, + admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options, task) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/videogeneration_replicate_svd.py b/nostr_dvm/tasks/videogeneration_replicate_svd.py index 0dfd0ab..5624958 100644 --- a/nostr_dvm/tasks/videogeneration_replicate_svd.py +++ b/nostr_dvm/tasks/videogeneration_replicate_svd.py @@ -4,11 +4,13 @@ from io import BytesIO import requests import urllib.request from PIL import Image +from nostr_sdk import Kind 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.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.output_utils import upload_media_to_hoster from nostr_dvm.utils.zap_utils import get_price_per_sat @@ -23,16 +25,17 @@ Params: class VideoGenerationReplicateSVD(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_VIDEO + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_VIDEO TASK: str = "image-to-video" FIX_COST: float = 120 dependencies = [("nostr-dvm", "nostr-dvm"), ("replicate", "replicate")] - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): dvm_config.SCRIPT = os.path.abspath(__file__) - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/tasks/videogeneration_svd.py b/nostr_dvm/tasks/videogeneration_svd.py index cbaadfe..2e4794c 100644 --- a/nostr_dvm/tasks/videogeneration_svd.py +++ b/nostr_dvm/tasks/videogeneration_svd.py @@ -1,10 +1,14 @@ import json +import os from multiprocessing.pool import ThreadPool +from nostr_sdk import Kind + from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag from nostr_dvm.utils.definitions import EventDefinitions @@ -18,13 +22,14 @@ Outputs: An url to a video class VideoGenerationSVD(DVMTaskInterface): - KIND: int = EventDefinitions.KIND_NIP90_GENERATE_VIDEO + KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_VIDEO TASK: str = "image-to-video" FIX_COST: float = 120 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None, admin_config: AdminConfig = None, options=None): - super().__init__(name, dvm_config, nip89config, admin_config, options) + super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config, + admin_config=admin_config, options=options) def is_input_supported(self, tags, client=None, dvm_config=None): for tag in tags: diff --git a/nostr_dvm/utils/admin_utils.py b/nostr_dvm/utils/admin_utils.py index fefe7c0..ec2b3ac 100644 --- a/nostr_dvm/utils/admin_utils.py +++ b/nostr_dvm/utils/admin_utils.py @@ -5,23 +5,28 @@ from nostr_sdk import Keys, PublicKey, Client from nostr_dvm.utils.database_utils import get_from_sql_table, list_db, delete_from_sql_table, update_sql_table, \ get_or_add_user, clean_db from nostr_dvm.utils.dvmconfig import DVMConfig -from nostr_dvm.utils.nip89_utils import nip89_announce_tasks, fetch_nip89_paramters_for_deletion +from nostr_dvm.utils.nip88_utils import nip88_announce_tier, fetch_nip88_parameters_for_deletion, fetch_nip88_event, \ + check_and_set_tiereventid_nip88 +from nostr_dvm.utils.nip89_utils import nip89_announce_tasks, fetch_nip89_parameters_for_deletion from nostr_dvm.utils.nostr_utils import update_profile class AdminConfig: REBROADCAST_NIP89: bool = False + REBROADCAST_NIP88: bool = False UPDATE_PROFILE: bool = False DELETE_NIP89: bool = False + DELETE_NIP88: bool = False + FETCH_NIP88: bool = False WHITELISTUSER: bool = False UNWHITELISTUSER: bool = False BLACKLISTUSER: bool = False DELETEUSER: bool = False LISTDATABASE: bool = False ClEANDB: bool = False + INDEX: str = "1" USERNPUBS: list = [] - LUD16: str = "" EVENTID: str = "" PRIVKEY: str = "" @@ -56,17 +61,17 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC if adminconfig.WHITELISTUSER: user = get_or_add_user(db, publickey, client=client, config=dvmconfig) - update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) + update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed) user = get_from_sql_table(db, publickey) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) if adminconfig.UNWHITELISTUSER: user = get_from_sql_table(db, publickey) - update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive) + update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed) if adminconfig.BLACKLISTUSER: user = get_from_sql_table(db, publickey) - update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive) + update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed) if adminconfig.DELETEUSER: delete_from_sql_table(db, publickey) @@ -80,11 +85,27 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC if adminconfig.REBROADCAST_NIP89: nip89_announce_tasks(dvmconfig, client=client) + if adminconfig.REBROADCAST_NIP88: + annotier_id = nip88_announce_tier(dvmconfig, client=client) + check_and_set_tiereventid_nip88(dvmconfig.IDENTIFIER, adminconfig.INDEX, annotier_id.to_hex()) + if adminconfig.DELETE_NIP89: event_id = adminconfig.EVENTID keys = Keys.parse( adminconfig.PRIVKEY) # Private key from sender of Event (e.g. the key of an nip89 announcement you want to delete) - fetch_nip89_paramters_for_deletion(keys, event_id, client, dvmconfig) + fetch_nip89_parameters_for_deletion(keys, event_id, client, dvmconfig) + + if adminconfig.DELETE_NIP88: + event_id = adminconfig.EVENTID + keys = Keys.parse( + adminconfig.PRIVKEY) # Private key from sender of Event (e.g. the key of an nip89 announcement you want to delete) + fetch_nip88_parameters_for_deletion(keys, event_id, client, dvmconfig) + + if adminconfig.FETCH_NIP88: + event_id = adminconfig.EVENTID + keys = Keys.parse( + adminconfig.PRIVKEY) + fetch_nip88_event(keys, event_id, client, dvmconfig) if adminconfig.UPDATE_PROFILE: - update_profile(dvmconfig, client, lud16=adminconfig.LUD16) + update_profile(dvmconfig, client, lud16=dvmconfig.LN_ADDRESS) diff --git a/nostr_dvm/utils/backend_utils.py b/nostr_dvm/utils/backend_utils.py index ed972c1..2398453 100644 --- a/nostr_dvm/utils/backend_utils.py +++ b/nostr_dvm/utils/backend_utils.py @@ -12,7 +12,7 @@ from nostr_dvm.utils.nostr_utils import get_event_by_id, get_referenced_event_by def get_task(event, client, dvm_config): try: - if event.kind() == EventDefinitions.KIND_NIP90_GENERIC: # use this for events that have no id yet, inclufr j tag + if event.kind().as_u64() == EventDefinitions.KIND_NIP90_GENERIC.as_u64(): # use this for events that have no id yet, inclufr j tag for tag in event.tags(): if tag.as_vec()[0] == 'j': return tag.as_vec()[1] @@ -26,7 +26,7 @@ def get_task(event, client, dvm_config): return "unknown job: " + event.as_json() # This looks a bit more complicated, but we do several tasks for text-extraction in the future - elif event.kind() == EventDefinitions.KIND_NIP90_EXTRACT_TEXT: + elif event.kind().as_u64() == EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64(): for tag in event.tags(): if tag.as_vec()[0] == "i": if tag.as_vec()[2] == "url": @@ -57,7 +57,7 @@ def get_task(event, client, dvm_config): return "unknown type" else: return "unknown job" - elif event.kind() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE: + elif event.kind().as_u64() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE.as_u64(): has_image_tag = False has_text_tag = False for tag in event.tags(): @@ -92,7 +92,7 @@ def get_task(event, client, dvm_config): else: for dvm in dvm_config.SUPPORTED_DVMS: - if dvm.KIND == event.kind(): + if dvm.KIND.as_u64() == event.kind().as_u64(): return dvm.TASK except Exception as e: print("Get task: " + str(e)) diff --git a/nostr_dvm/utils/database_utils.py b/nostr_dvm/utils/database_utils.py index 163d2e9..882a5d0 100644 --- a/nostr_dvm/utils/database_utils.py +++ b/nostr_dvm/utils/database_utils.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import timedelta from logging import Filter -from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter +from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter, Kind from nostr_dvm.utils.nostr_utils import send_event @@ -21,6 +21,7 @@ class User: nip05: str lud16: str lastactive: int + subscribed: int def create_sql_table(db): @@ -40,7 +41,8 @@ def create_sql_table(db): nip05 text, lud16 text, name text, - lastactive integer + lastactive integer, + subscribed integer ); """) cur.execute("SELECT name FROM sqlite_master") con.close() @@ -53,29 +55,29 @@ def add_sql_table_column(db): try: con = sqlite3.connect(db) cur = con.cursor() - cur.execute(""" ALTER TABLE users ADD COLUMN lastactive 'integer' """) + cur.execute(""" ALTER TABLE users ADD COLUMN subscribed 'integer' """) con.close() except Error as e: print(e) -def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): +def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed): try: con = sqlite3.connect(db) cur = con.cursor() - data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive) - cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?)", data) + data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed) + cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", data) con.commit() con.close() except Error as e: print("Error when Adding to DB: " + str(e)) -def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): +def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed): try: con = sqlite3.connect(db) cur = con.cursor() - data = (balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, npub) + data = (balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed, npub) cur.execute(""" UPDATE users SET sats = ? , @@ -84,7 +86,8 @@ def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud nip05 = ? , lud16 = ? , name = ? , - lastactive = ? + lastactive = ?, + subscribed = ? WHERE npub = ?""", data) con.commit() con.close() @@ -111,6 +114,7 @@ def get_from_sql_table(db, npub): user.lud16 = row[5] user.name = row[6] user.lastactive = row[7] + user.subscribed = row[8] return user @@ -162,14 +166,14 @@ def update_user_balance(db, npub, additional_sats, client, config): if user is None: name, nip05, lud16 = fetch_user_metadata(npub, client) add_to_sql_table(db, npub, (int(additional_sats) + config.NEW_USER_BALANCE), False, False, - nip05, lud16, name, Timestamp.now().as_secs()) + nip05, lud16, name, Timestamp.now().as_secs(), 0) print("Adding User: " + npub + " (" + npub + ")") else: user = get_from_sql_table(db, npub) new_balance = int(user.balance) + int(additional_sats) update_sql_table(db, npub, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, - Timestamp.now().as_secs()) + Timestamp.now().as_secs(), user.subscribed) print("Updated user balance for: " + str(user.name) + " Zap amount: " + str(additional_sats) + " Sats. New balance: " + str(new_balance) +" Sats") @@ -184,6 +188,33 @@ def update_user_balance(db, npub, additional_sats, client, config): send_event(evt, client=client, dvm_config=config) +def update_user_subscription(npub, subscribed_until, client, dvm_config): + user = get_from_sql_table(dvm_config.DB, npub) + if user is None: + name, nip05, lud16 = fetch_user_metadata(npub, client) + add_to_sql_table(dvm_config.DB, npub, dvm_config.NEW_USER_BALANCE, False, False, + nip05, lud16, name, Timestamp.now().as_secs(), 0) + print("Adding User: " + npub + " (" + npub + ")") + else: + user = get_from_sql_table(dvm_config.DB, npub) + + update_sql_table(dvm_config.DB, npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user.name, + Timestamp.now().as_secs(), subscribed_until) + print("Updated user subscription for: " + str(user.name)) + + if dvm_config is not None: + keys = Keys.parse(dvm_config.PRIVATE_KEY) + #time.sleep(1.0) + + message = ("Subscribed to DVM " + dvm_config.NIP89.NAME + " until: " + str(Timestamp.from_secs(subscribed_until).to_human_datetime().replace("Z", " ").replace("T", " "))) + + + evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.from_hex(npub), message, + None).to_event(keys) + send_event(evt, client=client, dvm_config=dvm_config) + + def get_or_add_user(db, npub, client, config, update=False): user = get_from_sql_table(db, npub) if user is None: @@ -191,7 +222,7 @@ def get_or_add_user(db, npub, client, config, update=False): name, nip05, lud16 = fetch_user_metadata(npub, client) print("Adding User: " + npub + " (" + npub + ")") add_to_sql_table(db, npub, config.NEW_USER_BALANCE, False, False, nip05, - lud16, name, Timestamp.now().as_secs()) + lud16, name, Timestamp.now().as_secs(), 0) user = get_from_sql_table(db, npub) return user except Exception as e: @@ -201,7 +232,7 @@ def get_or_add_user(db, npub, client, config, update=False): name, nip05, lud16 = fetch_user_metadata(npub, client) print("Updating User: " + npub + " (" + npub + ")") update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, nip05, - lud16, name, Timestamp.now().as_secs()) + lud16, name, Timestamp.now().as_secs(), user.subscribed) user = get_from_sql_table(db, npub) return user except Exception as e: @@ -214,9 +245,9 @@ def fetch_user_metadata(npub, client): name = "" nip05 = "" lud16 = "" - pk = PublicKey.from_hex(npub) + pk = PublicKey.parse(npub) print(f"\nGetting profile metadata for {pk.to_bech32()}...") - profile_filter = Filter().kind(0).author(pk).limit(1) + profile_filter = Filter().kind(Kind(0)).author(pk).limit(1) events = client.get_events_of([profile_filter], timedelta(seconds=1)) if len(events) > 0: latest_entry = events[0] diff --git a/nostr_dvm/utils/definitions.py b/nostr_dvm/utils/definitions.py index 7e3d90f..a887d30 100644 --- a/nostr_dvm/utils/definitions.py +++ b/nostr_dvm/utils/definitions.py @@ -1,41 +1,49 @@ import os from dataclasses import dataclass -from nostr_sdk import Event +from nostr_sdk import Event, Kind class EventDefinitions: - KIND_DM = 4 - KIND_ZAP = 9735 - KIND_ANNOUNCEMENT = 31990 - KIND_NIP94_METADATA = 1063 - KIND_FEEDBACK = 7000 - KIND_NIP90_EXTRACT_TEXT = 5000 - KIND_NIP90_RESULT_EXTRACT_TEXT = KIND_NIP90_EXTRACT_TEXT + 1000 - KIND_NIP90_SUMMARIZE_TEXT = 5001 - KIND_NIP90_RESULT_SUMMARIZE_TEXT = KIND_NIP90_SUMMARIZE_TEXT + 1000 - KIND_NIP90_TRANSLATE_TEXT = 5002 - KIND_NIP90_RESULT_TRANSLATE_TEXT = KIND_NIP90_TRANSLATE_TEXT + 1000 - KIND_NIP90_GENERATE_TEXT = 5050 - KIND_NIP90_RESULT_GENERATE_TEXT = KIND_NIP90_GENERATE_TEXT + 1000 - KIND_NIP90_GENERATE_IMAGE = 5100 - KIND_NIP90_RESULT_GENERATE_IMAGE = KIND_NIP90_GENERATE_IMAGE + 1000 - KIND_NIP90_CONVERT_VIDEO = 5200 - KIND_NIP90_RESULT_CONVERT_VIDEO = KIND_NIP90_CONVERT_VIDEO + 1000 - KIND_NIP90_GENERATE_VIDEO = 5202 - KIND_NIP90_RESULT_GENERATE_VIDEO = KIND_NIP90_GENERATE_VIDEO + 1000 - KIND_NIP90_TEXT_TO_SPEECH = 5250 - KIND_NIP90_RESULT_TEXT_TO_SPEECH = KIND_NIP90_TEXT_TO_SPEECH + 1000 - KIND_NIP90_CONTENT_DISCOVERY = 5300 - KIND_NIP90_RESULT_CONTENT_DISCOVERY = KIND_NIP90_CONTENT_DISCOVERY + 1000 - KIND_NIP90_PEOPLE_DISCOVERY = 5301 - KIND_NIP90_RESULT_PEOPLE_DISCOVERY = KIND_NIP90_PEOPLE_DISCOVERY + 1000 - KIND_NIP90_CONTENT_SEARCH = 5302 - KIND_NIP90_RESULTS_CONTENT_SEARCH = KIND_NIP90_CONTENT_SEARCH + 1000 - KIND_NIP90_USER_SEARCH = 5303 - KIND_NIP90_RESULTS_USER_SEARCH = KIND_NIP90_USER_SEARCH + 1000 - KIND_NIP90_GENERIC = 5999 - KIND_NIP90_RESULT_GENERIC = KIND_NIP90_GENERIC + 1000 + KIND_NOTE = Kind(1) + KIND_DM = Kind(4) + KIND_REACTION = Kind(7) + KIND_ZAP = Kind(9735) + KIND_ANNOUNCEMENT = Kind(31990) + KIND_NIP94_METADATA = Kind(1063) + KIND_FEEDBACK = Kind(7000) + KIND_NIP90_EXTRACT_TEXT = Kind(5000) + KIND_NIP90_RESULT_EXTRACT_TEXT = Kind(6000) + KIND_NIP90_SUMMARIZE_TEXT = Kind(5001) + KIND_NIP90_RESULT_SUMMARIZE_TEXT = Kind(6001) + KIND_NIP90_TRANSLATE_TEXT = Kind(5002) + KIND_NIP90_RESULT_TRANSLATE_TEXT = Kind(6002) + KIND_NIP90_GENERATE_TEXT = Kind(5050) + KIND_NIP90_RESULT_GENERATE_TEXT = Kind(6050) + KIND_NIP90_GENERATE_IMAGE = Kind(5100) + KIND_NIP90_RESULT_GENERATE_IMAGE = Kind(6100) + KIND_NIP90_CONVERT_VIDEO = Kind(5200) + KIND_NIP90_RESULT_CONVERT_VIDEO = Kind(6200) + KIND_NIP90_GENERATE_VIDEO = Kind(5202) + KIND_NIP90_RESULT_GENERATE_VIDEO =Kind(6202) + KIND_NIP90_TEXT_TO_SPEECH = Kind(5250) + KIND_NIP90_RESULT_TEXT_TO_SPEECH = Kind(5650) + KIND_NIP90_CONTENT_DISCOVERY = Kind(5300) + KIND_NIP90_RESULT_CONTENT_DISCOVERY = Kind(6300) + KIND_NIP90_PEOPLE_DISCOVERY = Kind(5301) + KIND_NIP90_RESULT_PEOPLE_DISCOVERY = Kind(6301) + KIND_NIP90_CONTENT_SEARCH = Kind(5302) + KIND_NIP90_RESULTS_CONTENT_SEARCH = Kind(6302) + KIND_NIP90_USER_SEARCH = Kind(5303) + KIND_NIP90_RESULTS_USER_SEARCH = Kind(6303) + KIND_NIP90_GENERIC = Kind(5999) + KIND_NIP90_RESULT_GENERIC = Kind(6999) + + KIND_NIP88_SUBSCRIBE_EVENT = Kind(7001) + KIND_NIP88_STOP_SUBSCRIPTION_EVENT = Kind(7002) + KIND_NIP88_PAYMENT_RECIPE = Kind(7003) + KIND_NIP88_TIER_EVENT = Kind(37001) + ANY_RESULT = [KIND_NIP90_RESULT_EXTRACT_TEXT, KIND_NIP90_RESULT_SUMMARIZE_TEXT, KIND_NIP90_RESULT_TRANSLATE_TEXT, diff --git a/nostr_dvm/utils/dvmconfig.py b/nostr_dvm/utils/dvmconfig.py index 35d242e..4215c75 100644 --- a/nostr_dvm/utils/dvmconfig.py +++ b/nostr_dvm/utils/dvmconfig.py @@ -2,6 +2,7 @@ import os from nostr_sdk import Keys +from nostr_dvm.utils.nip88_utils import NIP88Config from nostr_dvm.utils.nip89_utils import NIP89Config from nostr_dvm.utils.nostr_utils import check_and_set_private_key from nostr_dvm.utils.output_utils import PostProcessFunctionType @@ -32,12 +33,12 @@ class DVMConfig: USE_OWN_VENV = True # Make an own venv for each dvm's process function.Disable if you want to install packages into main venv. Only recommended if you dont want to run dvms with different dependency versions DB: str NEW_USER_BALANCE: int = 0 # Free credits for new users + NIP88: NIP88Config NIP89: NIP89Config SEND_FEEDBACK_EVENTS = True SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess SCHEDULE_UPDATES_SECONDS = 0 - def build_default_config(identifier): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) diff --git a/nostr_dvm/utils/nip88_utils.py b/nostr_dvm/utils/nip88_utils.py new file mode 100644 index 0000000..ff65e0e --- /dev/null +++ b/nostr_dvm/utils/nip88_utils.py @@ -0,0 +1,201 @@ +import os +from datetime import timedelta +from hashlib import sha256 +from pathlib import Path + +import dotenv +from nostr_sdk import Filter, Tag, Keys, EventBuilder, Client, EventId, PublicKey, Event, Timestamp, SingleLetterTag, \ + Alphabet +from nostr_sdk.nostr_sdk import Duration + +from nostr_dvm.utils import definitions +from nostr_dvm.utils.definitions import EventDefinitions +from nostr_dvm.utils.nostr_utils import send_event + + +class NIP88Config: + DTAG: str = "" + TITLE: str = "" + CONTENT: str = "" + IMAGE: str = "" + TIER_EVENT: str = "" + PERK1DESC: str = "" + PERK2DESC: str = "" + PERK3DESC: str = "" + PERK4DESC: str = "" + PAYMENT_VERIFIER_PUBKEY: str = "" + + AMOUNT_DAILY: int = None + AMOUNT_MONTHLY: int = None + AMOUNT_YEARLY: int = None + +def nip88_create_d_tag(name, pubkey, image): + key_str = str(name + image + pubkey) + d_tag = sha256(key_str.encode('utf-8')).hexdigest()[:16] + return d_tag + + +def fetch_nip88_parameters_for_deletion(keys, eventid, client, dvmconfig): + idfilter = Filter().id(EventId.from_hex(eventid)).limit(1) + nip88events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT)) + d_tag = "" + if len(nip88events) == 0: + print("Event not found. Potentially gone.") + + for event in nip88events: + print(event.as_json()) + for tag in event.tags(): + if tag.as_vec()[0] == "d": + d_tag = tag.as_vec()[1] + if d_tag == "": + print("No dtag found") + return + + if event.author().to_hex() == keys.public_key().to_hex(): + nip88_delete_announcement(event.id().to_hex(), keys, d_tag, client, dvmconfig) + print("NIP88 announcement deleted from known relays!") + else: + print("Privatekey does not belong to event") + + +def fetch_nip88_event(keys, eventid, client, dvmconfig): + idfilter = Filter().id(EventId.parse(eventid)).limit(1) + nip88events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT)) + d_tag = "" + if len(nip88events) == 0: + print("Event not found. Potentially gone.") + + for event in nip88events: + + for tag in event.tags(): + if tag.as_vec()[0] == "d": + d_tag = tag.as_vec()[1] + if d_tag == "": + print("No dtag found") + return + + if event.author().to_hex() == keys.public_key().to_hex(): + print(event.as_json()) + else: + print("Privatekey does not belong to event") + + +def nip88_delete_announcement(eid: str, keys: Keys, dtag: str, client: Client, config): + e_tag = Tag.parse(["e", eid]) + a_tag = Tag.parse( + ["a", str(EventDefinitions.KIND_NIP88_TIER_EVENT) + ":" + keys.public_key().to_hex() + ":" + dtag]) + event = EventBuilder(5, "", [e_tag, a_tag]).to_event(keys) + send_event(event, client, config) + + +def nip88_has_active_subscription(user: PublicKey, tiereventdtag, client: Client, dvm_config): + subscription_status = { + "isActive": False, + "validUntil": 0, + "subscriptionId": "", + } + + subscriptionfilter = Filter().kind(definitions.EventDefinitions.KIND_NIP88_PAYMENT_RECIPE).pubkey( + PublicKey.parse(dvm_config.PUBLIC_KEY)).custom_tag(SingleLetterTag.uppercase(Alphabet.P), + [user.to_hex()]).limit(1) + evts = client.get_events_of([subscriptionfilter], timedelta(seconds=5)) + if len(evts) > 0: + print(evts[0].as_json()) + matchesdtag = False + for tag in evts[0].tags(): + if tag.as_vec()[0] == "valid": + subscription_status["validUntil"] = int(tag.as_vec()[2]) + elif tag.as_vec()[0] == "e": + subscription_status["subscriptionId"] = tag.as_vec()[1] + elif tag.as_vec()[0] == "tier": + if tag.as_vec()[1] == tiereventdtag: + matchesdtag = True + + if subscription_status["validUntil"] > Timestamp.now().as_secs() & matchesdtag: + subscription_status["isActive"] = True + + return subscription_status + + +def nip88_announce_tier(dvm_config, client): + title_tag = Tag.parse(["title", str(dvm_config.NIP88.TITLE)]) + image_tag = Tag.parse(["image", str(dvm_config.NIP88.IMAGE)]) + d_tag = Tag.parse(["d", dvm_config.NIP88.DTAG]) + + # may todo + zaptag1 = Tag.parse(["zap", dvm_config.PUBLIC_KEY, "wss://damus.io", "19"]) + zaptag2 = Tag.parse(["zap", "", "wss://damus.io", "1"]) + p_tag = Tag.parse(["p", dvm_config.NIP88.PAYMENT_VERIFIER_PUBKEY]) + + tags = [title_tag, image_tag, zaptag1, zaptag2, d_tag, p_tag] + + if dvm_config.NIP88.AMOUNT_DAILY is not None: + amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_DAILY * 1000), "msats", "daily"]) + tags.append(amount_tag) + + if dvm_config.NIP88.AMOUNT_MONTHLY is not None: + amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_MONTHLY * 1000), "msats", "monthly"]) + tags.append(amount_tag) + + if dvm_config.NIP88.AMOUNT_YEARLY is not None: + amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_YEARLY * 1000), "msats", "yearly"]) + tags.append(amount_tag) + + if dvm_config.NIP88.PERK1DESC != "": + perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK1DESC)]) + tags.append(perk_tag) + if dvm_config.NIP88.PERK2DESC != "": + perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK2DESC)]) + tags.append(perk_tag) + if dvm_config.NIP88.PERK3DESC != "": + perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK3DESC)]) + tags.append(perk_tag) + if dvm_config.NIP88.PERK4DESC != "": + perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK4DESC)]) + tags.append(perk_tag) + + keys = Keys.parse(dvm_config.NIP89.PK) + content = dvm_config.NIP88.CONTENT + event = EventBuilder(EventDefinitions.KIND_NIP88_TIER_EVENT, content, tags).to_event(keys) + annotier_id = send_event(event, client=client, dvm_config=dvm_config) + + print("Announced NIP 88 Tier for " + dvm_config.NIP89.NAME) + return annotier_id + + # Relay and payment-verification + + +# ["r", "wss://my-subscribers-only-relay.com"], +# ["p", ""], + +def check_and_set_d_tag_nip88(identifier, name, pk, imageurl): + if not os.getenv("NIP88_DTAG_" + identifier.upper()): + new_dtag = nip88_create_d_tag(name, Keys.parse(pk).public_key().to_hex(), + imageurl) + nip88_add_dtag_to_env_file("NIP88_DTAG_" + identifier.upper(), new_dtag) + print("Some new dtag:" + new_dtag) + return new_dtag + else: + return os.getenv("NIP88_DTAG_" + identifier.upper()) + + +def check_and_set_tiereventid_nip88(identifier, index="1", eventid=None): + if eventid is None: + if not os.getenv("NIP88_TIEREVENT_" + index + identifier.upper()): + print("No Tier Event ID set") + return None + else: + return os.getenv("NIP88_TIEREVENT_" + index + identifier.upper()) + else: + nip88_add_dtag_to_env_file("NIP88_TIEREVENT_" + index + identifier.upper(), eventid) + return eventid + + +def nip88_add_dtag_to_env_file(dtag, oskey): + 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) + dotenv.set_key(env_path, dtag, oskey) + + diff --git a/nostr_dvm/utils/nip89_utils.py b/nostr_dvm/utils/nip89_utils.py index 724f5f8..25483d4 100644 --- a/nostr_dvm/utils/nip89_utils.py +++ b/nostr_dvm/utils/nip89_utils.py @@ -25,7 +25,7 @@ def nip89_create_d_tag(name, pubkey, image): def nip89_announce_tasks(dvm_config, client): - k_tag = Tag.parse(["k", str(dvm_config.NIP89.KIND)]) + k_tag = Tag.parse(["k", str(dvm_config.NIP89.KIND.as_u64())]) d_tag = Tag.parse(["d", dvm_config.NIP89.DTAG]) keys = Keys.parse(dvm_config.NIP89.PK) content = dvm_config.NIP89.CONTENT @@ -34,7 +34,7 @@ def nip89_announce_tasks(dvm_config, client): print("Announced NIP 89 for " + dvm_config.NIP89.NAME) -def fetch_nip89_paramters_for_deletion(keys, eventid, client, dvmconfig): +def fetch_nip89_parameters_for_deletion(keys, eventid, client, dvmconfig): idfilter = Filter().id(EventId.from_hex(eventid)).limit(1) nip89events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT)) d_tag = "" diff --git a/nostr_dvm/utils/nwc_tools.py b/nostr_dvm/utils/nwc_tools.py index ef33280..d4e869a 100644 --- a/nostr_dvm/utils/nwc_tools.py +++ b/nostr_dvm/utils/nwc_tools.py @@ -1,44 +1,84 @@ import json import os +from datetime import timedelta import requests -from nostr_sdk import Keys, PublicKey, Client, nip04_encrypt, EventBuilder, Tag, NostrSigner +from nostr_sdk import Keys, PublicKey, Client, nip04_encrypt, EventBuilder, Tag, NostrSigner, Filter, Timestamp, \ + NostrWalletConnectUri, Nwc from nostr_dvm.utils.dvmconfig import DVMConfig from nostr_dvm.utils.nostr_utils import check_and_set_private_key from nostr_dvm.utils.zap_utils import zaprequest -def nwc_zap(connectionstr, bolt11, keys): - target_pubkey, relay, secret = parse_connection_str(connectionstr) - SecretSK = Keys.parse(secret) +def nwc_zap(connectionstr, bolt11, keys, externalrelay=None): + uri = NostrWalletConnectUri.parse(connectionstr) - content = { - "method": "pay_invoice", - "params": { - "invoice": bolt11 - } - } + # Initialize NWC client + nwc = Nwc(uri) - signer = NostrSigner.keys(keys) - client = Client(signer) - client.add_relay(relay) - client.connect() + info = nwc.get_info() + print(info) - client_public_key = PublicKey.from_hex(target_pubkey) - encrypted_content = nip04_encrypt(SecretSK.secret_key(), client_public_key, json.dumps(content)) + balance = nwc.get_balance() + print(f"Balance: {balance} SAT") - pTag = Tag.parse(["p", client_public_key.to_hex()]) - event = EventBuilder(23194, encrypted_content, - [pTag]).to_event(keys) + event_id = nwc.pay_invoice(bolt11) + print("NWC event: " + event_id) - event_id = client.send_event(event) - print(event_id.to_hex()) + + #target_pubkey, relay, secret = parse_connection_str(connectionstr) + #print(target_pubkey) + #print(relay) + #print(secret) + #SecretSK = Keys.parse(secret) + + #content = { + # "method": "pay_invoice", + # "params": { + # "invoice": bolt11 + # } + #} + + #signer = NostrSigner.keys(keys) + #client = Client(signer) + #client.add_relay(relay) + #if externalrelay is not None: + # client.add_relay(externalrelay) + + #client.connect() + + #client_public_key = PublicKey.from_hex(target_pubkey) + #encrypted_content = nip04_encrypt(SecretSK.secret_key(), client_public_key, json.dumps(content)) + + #pTag = Tag.parse(["p", client_public_key.to_hex()]) + + #event = EventBuilder(23194, encrypted_content, + # [pTag]).to_event(keys) + + #ts = Timestamp.now() + #event_id = client.send_event(event) + + + + + + + #nwc_response_filter = Filter().kind(23195).since(ts) + #events = client.get_events_of([nwc_response_filter], timedelta(seconds=5)) + + #if len(events) > 0: + # for evt in events: + # print(evt.as_json()) + #else: + # print("No response found") + + return event_id def parse_connection_str(connectionstring): split = connectionstring.split("?") - targetpubkey = split[0].split(":")[1] + targetpubkey = split[0].split(":")[1].replace("//", "") split2 = split[1].split("&") relay = split2[0].split("=")[1] relay = relay.replace("%3A%2F%2F", "://") diff --git a/nostr_dvm/utils/output_utils.py b/nostr_dvm/utils/output_utils.py index ca50ab7..5a9ecd6 100644 --- a/nostr_dvm/utils/output_utils.py +++ b/nostr_dvm/utils/output_utils.py @@ -206,6 +206,12 @@ def build_status_reaction(status, task, amount, content, dvm_config): amount) + " Sats. " reaction = alt_description + emoji.emojize(":orange_heart:") + elif status == "subscription-required": + alt_description = "NIP90 DVM AI task " + task + " requires payment for subscription" + reaction = alt_description + emoji.emojize(":orange_heart:") + + + elif status == "payment-rejected": alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( amount) + " Sats. " diff --git a/nostr_dvm/utils/subscription_utils.py b/nostr_dvm/utils/subscription_utils.py new file mode 100644 index 0000000..5c34688 --- /dev/null +++ b/nostr_dvm/utils/subscription_utils.py @@ -0,0 +1,130 @@ +import sqlite3 +from dataclasses import dataclass +from sqlite3 import Error + + +@dataclass +class Subscription: + id: str + recipent: str + subscriber: str + nwc: str + cadence: str + amount: int + begin: int + end: int + tier_dtag: str + zaps: str + recipe: str + active: bool + + +def create_subscription_sql_table(db): + try: + import os + if not os.path.exists(r'db'): + os.makedirs(r'db') + if not os.path.exists(r'outputs'): + os.makedirs(r'outputs') + con = sqlite3.connect(db) + cur = con.cursor() + cur.execute(""" CREATE TABLE IF NOT EXISTS subscriptions ( + id text PRIMARY KEY, + recipient text, + subscriber text, + nwc text NOT NULL, + cadence text, + amount int, + begin int, + end int, + tier_dtag text, + zaps text, + recipe text, + active boolean + + + ); """) + cur.execute("SELECT name FROM sqlite_master") + con.close() + + except Error as e: + print(e) + + +def add_to_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, + recipe, active): + try: + con = sqlite3.connect(db) + cur = con.cursor() + data = (id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active) + print(id) + print(recipient) + print(subscriber) + print(nwc) + cur.execute("INSERT or IGNORE INTO subscriptions VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", data) + con.commit() + con.close() + except Error as e: + print("Error when Adding to DB: " + str(e)) + + +def get_from_subscription__sql_table(db, id): + try: + con = sqlite3.connect(db) + cur = con.cursor() + cur.execute("SELECT * FROM subscriptions WHERE id=?", (id,)) + row = cur.fetchone() + con.close() + if row is None: + return None + else: + subscription = Subscription + subscription.id = row[0] + subscription.recipent = row[1] + subscription.subscriber = row[2] + subscription.nwc = row[3] + subscription.cadence = row[4] + subscription.amount = row[5] + subscription.begin = row[6] + subscription.end = row[7] + subscription.tier_dtag = row[8] + subscription.zaps = row[9] + subscription.recipe = row[10] + subscription.active = row[11] + + return subscription + + except Error as e: + print("Error Getting from DB: " + str(e)) + return None + + +def update_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, + recipe, active): + try: + con = sqlite3.connect(db) + cur = con.cursor() + data = (recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, id) + + cur.execute(""" UPDATE subscriptions + SET recipient = ? , + subscriber = ? , + nwc = ? , + cadence = ? , + amount = ? , + begin = ? , + end = ?, + tier_dtag = ?, + zaps = ?, + recipe = ?, + active = ? + + WHERE id = ?""", data) + con.commit() + con.close() + except Error as e: + print("Error Updating DB: " + str(e)) + + + + diff --git a/nostr_dvm/utils/zap_utils.py b/nostr_dvm/utils/zap_utils.py index 3cc999c..de24b26 100644 --- a/nostr_dvm/utils/zap_utils.py +++ b/nostr_dvm/utils/zap_utils.py @@ -8,7 +8,7 @@ import requests from Crypto.Cipher import AES from Crypto.Util.Padding import pad from bech32 import bech32_decode, convertbits, bech32_encode -from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys, generate_shared_key +from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys, generate_shared_key, Kind from nostr_dvm.utils.nostr_utils import get_event_by_id, check_and_decrypt_own_tags import lnurl @@ -132,7 +132,7 @@ def create_bolt11_lud16(lud16, amount): def create_lnbits_account(name): if os.getenv("LNBITS_ADMIN_ID") is None or os.getenv("LNBITS_ADMIN_ID") == "": print("No admin id set, no wallet created.") - return "","","","", "failed" + return "", "", "", "", "failed" data = { 'admin_id': os.getenv("LNBITS_ADMIN_ID"), 'wallet_name': name, @@ -240,6 +240,10 @@ def decrypt_private_zap_message(msg: str, privkey: SecretKey, pubkey: PublicKey) def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys, relay_list, zaptype="public"): + print(lud16) + print(str(amount)) + print(content) + print(zapped_user.to_hex()) if lud16.startswith("LNURL") or lud16.startswith("lnurl"): url = lnurl.decode(lud16) elif '@' in lud16: # LNaddress @@ -250,6 +254,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys response = requests.get(url) ob = json.loads(response.content) callback = ob["callback"] + print(ob["callback"]) encoded_lnurl = lnurl.encode(url) amount_tag = Tag.parse(['amount', str(amount * 1000)]) relays_tag = Tag.parse(['relays', str(relay_list)]) @@ -262,12 +267,11 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys p_tag = Tag.parse(['p', zapped_user.to_hex()]) tags = [amount_tag, relays_tag, p_tag, lnurl_tag] - if zaptype == "private": key_str = keys.secret_key().to_hex() + zapped_event.id().to_hex() + str(zapped_event.created_at().as_secs()) encryption_key = sha256(key_str.encode('utf-8')).hexdigest() - zap_request = EventBuilder(9733, content, + zap_request = EventBuilder(Kind(9733), content, [p_tag, e_tag]).to_event(keys).as_json() keys = Keys.parse(encryption_key) encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_event.author()) @@ -275,7 +279,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys tags.append(anon_tag) content = "" - zap_request = EventBuilder(9734, content, + zap_request = EventBuilder(Kind(9734), content, tags).to_event(keys).as_json() response = requests.get(callback + "?amount=" + str(int(amount) * 1000) + "&nostr=" + urllib.parse.quote_plus( @@ -287,6 +291,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys print("ZAP REQUEST: " + e) return None + def get_price_per_sat(currency): import requests @@ -334,8 +339,6 @@ def make_ln_address_nostdress(identifier, npub, pin, nostdressdomain): return "", "" - - def check_and_set_ln_bits_keys(identifier, npub): if not os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper()): invoicekey, adminkey, walletid, userid, success = create_lnbits_account(identifier) @@ -365,4 +368,4 @@ def add_key_to_env_file(value, oskey): env_path = Path('.env') if env_path.is_file(): dotenv.load_dotenv(env_path, verbose=True, override=True) - dotenv.set_key(env_path, value, oskey) \ No newline at end of file + dotenv.set_key(env_path, value, oskey) diff --git a/setup.py b/setup.py index 21729a8..5e55684 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( long_description=LONG_DESCRIPTION, packages=find_packages(include=['nostr_dvm', 'nostr_dvm.*']), - install_requires=["nostr-sdk==0.9.1", + install_requires=["nostr-sdk==0.10.0", "bech32", "pycryptodome==3.20.0", "python-dotenv==1.0.0", diff --git a/tests/discovery.py b/tests/discovery.py index 538c6aa..450eeee 100644 --- a/tests/discovery.py +++ b/tests/discovery.py @@ -4,9 +4,13 @@ from pathlib import Path import dotenv from nostr_sdk import Keys +from nostr_dvm.subscription import Subscription from nostr_dvm.tasks import content_discovery_currently_popular from nostr_dvm.utils.admin_utils import AdminConfig from nostr_dvm.utils.backend_utils import keep_alive +from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nostr_utils import check_and_set_private_key +from nostr_dvm.utils.zap_utils import check_and_set_ln_bits_keys def playground(): @@ -19,8 +23,21 @@ def playground(): admin_config.REBROADCAST_NIP89 = False admin_config.UPDATE_PROFILE = False - discovery_test = content_discovery_currently_popular.build_example("Currently Popular Notes DVM", "discovery_content_test", admin_config) - discovery_test.run() + discovery_test_sub = content_discovery_currently_popular.build_example_subscription("Currently Popular Notes DVM (with Subscriptions)", "discovery_content_test", admin_config) + discovery_test_sub.run() + + #discovery_test = content_discovery_currently_popular.build_example("Currently Popular Notes DVM", + # "discovery_content_test", admin_config) + #discovery_test.run() + + subscription_config = DVMConfig() + subscription_config.PRIVATE_KEY = check_and_set_private_key("dvm_subscription") + npub = Keys.parse(subscription_config.PRIVATE_KEY).public_key().to_bech32() + invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys("dvm_subscription", npub) + subscription_config.LNBITS_INVOICE_KEY = invoice_key + subscription_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back + subscription_config.LNBITS_URL = os.getenv("LNBITS_HOST") + Subscription(subscription_config) keep_alive()