From 08137790d4cf3e018685145041988eda8a670910 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 27 Nov 2023 00:02:56 +0100 Subject: [PATCH 1/3] cleanup, preparing for cashu as input --- dvm.py | 18 ++++++--- main.py | 13 +++--- playground.py | 15 +++---- test_dvm_client.py | 71 +++++++++++++++++++++++++++++++- utils/backend_utils.py | 60 ++++++++++++++------------- utils/database_utils.py | 25 +++++++----- utils/dvmconfig.py | 2 - utils/zap_utils.py | 90 ++++++++++++++++++++++++++++++++--------- 8 files changed, 210 insertions(+), 84 deletions(-) diff --git a/dvm.py b/dvm.py index d65be56..005026e 100644 --- a/dvm.py +++ b/dvm.py @@ -14,7 +14,7 @@ from utils.backend_utils import get_amount_per_task, check_task_is_supported, ge from utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event, check_and_decrypt_tags from utils.output_utils import post_process_result, build_status_reaction -from utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags +from utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags, redeem_cashu use_logger = False if use_logger: @@ -92,11 +92,15 @@ class DVM: if p_tag_str is None: return nip90_event.tags = tags + cashu = "" + for tag in tags: + if tag.as_vec()[0] == "cashu": + cashu = tag.as_vec()[0] user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) - task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, + task_supported, task, duration = check_task_is_supported(nip90_event, tags, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) @@ -115,8 +119,12 @@ class DVM: if dvm.TASK == task and dvm.COST == 0: task_is_free = True + cashu_redeemed = False + if cashu != "": + cashu_redeemed = redeem_cashu(cashu, self.dvm_config) + # if user is whitelisted or task is free, just do the job - if user.iswhitelisted or task_is_free: + if user.iswhitelisted or task_is_free or cashu_redeemed: print( "[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") @@ -199,7 +207,7 @@ class DVM: # if a reaction by us got zapped - task_supported, task, duration = check_task_is_supported(job_event, + task_supported, task, duration = check_task_is_supported(job_event, tags, client=self.client, get_duration=False, config=self.dvm_config) @@ -255,7 +263,7 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Error during content decryption: " + str(e)) def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): - task_supported, task, duration = check_task_is_supported(nevent, client, False, + task_supported, task, duration = check_task_is_supported(nevent, nevent.tags, client, False, config=dvmconfig) if not task_supported: return False diff --git a/main.py b/main.py index c523c3c..d1846ad 100644 --- a/main.py +++ b/main.py @@ -14,9 +14,6 @@ from utils.dvmconfig import DVMConfig def run_nostr_dvm_with_local_config(): - # We extract the Publickey from our bot, so the DVMs know who they should listen and react to. - bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key() - # We will run an optional bot that can communicate with the DVMs # Note this is very basic for now and still under development bot_config = DVMConfig() @@ -29,25 +26,25 @@ def run_nostr_dvm_with_local_config(): # You can add arbitrary DVMs there and instantiate them here # Spawn DVM1 Kind 5000: A local Text Extractor from PDFs - pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey]) + pdfextractor = build_pdf_extractor("PDF Extractor") # If we don't add it to the bot, the bot will not provide access to the DVM pdfextractor.run() # Spawn DVM2 Kind 5002 Local Text Translation, calling the free Google API. - translator = build_translator("Translator", [bot_publickey]) + translator = build_translator("Translator") bot_config.SUPPORTED_DVMS.append(translator) # We add translator to the bot translator.run() # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. # If you want to use it, see the instructions in backends/nova_server if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": - unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey]) + unstable_artist = build_unstable_diffusion("Unstable Diffusion") bot_config.SUPPORTED_DVMS.append(unstable_artist) # We add unstable Diffusion to the bot unstable_artist.run() # Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time. if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": - sketcher = build_sketcher("Sketcher", [bot_publickey]) + sketcher = build_sketcher("Sketcher") bot_config.SUPPORTED_DVMS.append(sketcher) # We also add Sketcher to the bot sketcher.run() @@ -55,7 +52,7 @@ def run_nostr_dvm_with_local_config(): # per call. Make sure you have enough balance and the DVM's cost is set higher than what you pay yourself, except, you know, # you're being generous. if os.getenv("OPENAI_API_KEY") is not None and os.getenv("OPENAI_API_KEY") != "": - dalle = build_dalle("Dall-E 3", [bot_publickey]) + dalle = build_dalle("Dall-E 3") bot_config.SUPPORTED_DVMS.append(dalle) dalle.run() diff --git a/playground.py b/playground.py index b2fb743..a6c047f 100644 --- a/playground.py +++ b/playground.py @@ -37,12 +37,11 @@ admin_config.REBROADCAST_NIP89 = False # want to update your NIP89 descriptions -def build_pdf_extractor(name, dm_allowed_keys): +def build_pdf_extractor(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") - dvm_config.DM_ALLOWED = dm_allowed_keys # Add NIP89 nip90params = {} nip89info = { @@ -59,12 +58,11 @@ def build_pdf_extractor(name, dm_allowed_keys): admin_config=admin_config) -def build_translator(name, dm_allowed_keys): +def build_translator(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") - dvm_config.DM_ALLOWED = dm_allowed_keys nip90params = { "language": { @@ -92,12 +90,11 @@ def build_translator(name, dm_allowed_keys): admin_config=admin_config) -def build_unstable_diffusion(name, dm_allowed_keys): +def build_unstable_diffusion(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") dvm_config.LNBITS_INVOICE_KEY = "" #This one will not use Lnbits to create invoices, but rely on zaps dvm_config.LNBITS_URL = "" - dvm_config.DM_ALLOWED = dm_allowed_keys # A module might have options it can be initialized with, here we set a default model, and the nova-server # address it should use. These parameters can be freely defined in the task component @@ -126,12 +123,11 @@ def build_unstable_diffusion(name, dm_allowed_keys): admin_config=admin_config, options=options) -def build_sketcher(name, dm_allowed_keys): +def build_sketcher(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") - dvm_config.DM_ALLOWED = dm_allowed_keys nip90params = { "negative_prompt": { @@ -162,12 +158,11 @@ def build_sketcher(name, dm_allowed_keys): admin_config=admin_config, options=options) -def build_dalle(name, dm_allowed_keys): +def build_dalle(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") - dvm_config.DM_ALLOWED = dm_allowed_keys profit_in_sats = 10 dvm_config.COST = int(((4.0 / (get_price_per_sat("USD") * 100)) + profit_in_sats)) diff --git a/test_dvm_client.py b/test_dvm_client.py index 8455a28..60742e9 100644 --- a/test_dvm_client.py +++ b/test_dvm_client.py @@ -1,3 +1,4 @@ +import json import os import time import datetime as datetime @@ -5,7 +6,8 @@ from pathlib import Path from threading import Thread import dotenv -from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt +from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \ + nip04_encrypt from utils.dvmconfig import DVMConfig from utils.nostr_utils import send_event @@ -43,6 +45,70 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax): def nostr_client_test_image(prompt): keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) + iTag = Tag.parse(["i", prompt, "text"]) + outTag = Tag.parse(["output", "image/png;format=url"]) + paramTag1 = Tag.parse(["param", "size", "1024x1024"]) + tTag = Tag.parse(["t", "bitcoin"]) + + bidTag = Tag.parse(['bid', str(1000 * 1000), str(1000 * 1000)]) + relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"]) + alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to translate a given Input"]) + event = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, str("Generate an Image."), + [iTag, outTag, tTag, paramTag1, bidTag, relaysTag, alttag]).to_event(keys) + + relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"] + + client = Client(keys) + for relay in relay_list: + client.add_relay(relay) + client.connect() + config = DVMConfig + send_event(event, client=client, dvm_config=config) + return event.as_json() + + +def nostr_client_test_image_private(prompt, cashutoken): + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) + + + # TODO more advanced logic, more parsing, params etc, just very basic test functions for now + + relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"] + i_tag = Tag.parse(["i", prompt, "text"]) + outTag = Tag.parse(["output", "image/png;format=url"]) + paramTag1 = Tag.parse(["param", "size", "1024x1024"]) + tTag = Tag.parse(["t", "bitcoin"]) + + bid = str(50 * 1000) + bid_tag = Tag.parse(['bid', bid, bid]) + relays_tag = Tag.parse(["relays", json.dumps(relay_list)]) + alt_tag = Tag.parse(["alt", "Super secret test"]) + cashu_tag = Tag.parse(["cashu", cashutoken]) + + encrypted_params_string = json.dumps([i_tag.as_vec(), outTag.as_vec(), paramTag1.as_vec(), tTag, bid_tag.as_vec(), + relays_tag.as_vec(), alt_tag.as_vec(), cashu_tag.as_vec()]) + + + encrypted_params = nip04_encrypt(keys.secret_key(), keys.public_key(), + encrypted_params_string) + + p_tag = Tag.parse(['p', keys.public_key().to_hex()]) + encrypted_tag = Tag.parse(['encrypted']) + nip90request = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, encrypted_params, + [p_tag, encrypted_tag]).to_event(keys) + client = Client(keys) + for relay in relay_list: + client.add_relay(relay) + client.connect() + config = DVMConfig + send_event(nip90request, client=client, dvm_config=config) + return nip90request.as_json() + + + iTag = Tag.parse(["i", prompt, "text"]) outTag = Tag.parse(["output", "image/png;format=url"]) paramTag1 = Tag.parse(["param", "size", "1024x1024"]) @@ -89,6 +155,9 @@ def nostr_client(): #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20) nostr_client_test_image("a beautiful purple ostrich watching the sunset") + + cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzNzZhYTQ4YTJiMDU1NTlmYzQ4MTU2NjJjZThhMjZmZGM5OTQzYzY2Yzg0OWEzNTg3NDgwYWRmYzE0ZTEwNTRiZCIsInNlY3JldCI6IlIzTGhSZDI5UktJTzRkMHdNZ0Z0K2ZKWlVoYi90K0RmZXMxdFVrZVBVV0E9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMmYyNTdhYzYzOTU4NGY1YTE5NTNkMGI0ODI3OWJkN2EyMjdmZTBkYzI0OWY0MjQwNjgzMDZlOTI0ZGE3ZGVhZDciLCJzZWNyZXQiOiJ4Tmhwdm50SkNwcXFiYmFjWDA0NzluVld4SGo5U05jaVBvdTNYQ3JWcmRjPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDIyYjhiY2JkYTQ1OTNlMGZlNTY4ZWYyOTM2OWNmZjNmMzY2NzdlZDAzYTQ4ODMxNzYwNDQxN2JkNGE3MTYzZDYyIiwic2VjcmV0IjoiTEprUVlheWNyUE9yZ3hZcHhlcDZVV3U0RjZ3QUVydnZJNHZiRmN0R3h6MD0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0" = + nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) class NotificationHandler(HandleNotification): def handle(self, relay_url, event): print(f"Received new event from {relay_url}: {event.as_json()}") diff --git a/utils/backend_utils.py b/utils/backend_utils.py index 4162ba2..bedff8e 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -19,7 +19,7 @@ def get_task(event, client, dvmconfig): # 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: - for tag in event.tags(): + for tag in event.tags: if tag.as_vec()[0] == "i": if tag.as_vec()[2] == "url": file_type = check_url_is_readable(tag.as_vec()[1]) @@ -50,7 +50,7 @@ def get_task(event, client, dvmconfig): return "unknown type" -def check_task_is_supported(event, client, get_duration=False, config=None): +def check_task_is_supported(event, tags, client, get_duration=False, config=None): try: dvm_config = config input_value = "" @@ -59,32 +59,37 @@ def check_task_is_supported(event, client, get_duration=False, config=None): task = get_task(event, client=client, dvmconfig=dvm_config) - for tag in event.tags: - if tag.as_vec()[0] == 'i': - if len(tag.as_vec()) < 3: - print("Job Event missing/malformed i tag, skipping..") - return False, "", 0 - else: - input_value = tag.as_vec()[1] - input_type = tag.as_vec()[2] - if input_type == "event": - evt = get_event_by_id(input_value, client=client, config=dvm_config) - if evt is None: - print("Event not found") - return False, "", 0 - elif input_type == 'url' and check_url_is_readable(input_value) is None: - print("Url not readable / supported") - return False, task, duration + try: + for tag in tags: + if tag.as_vec()[0] == 'i': + if len(tag.as_vec()) < 3: + print("Job Event missing/malformed i tag, skipping..") + return False, "", 0 + else: + input_value = tag.as_vec()[1] + input_type = tag.as_vec()[2] + if input_type == "event": + evt = get_event_by_id(input_value, client=client, config=dvm_config) + if evt is None: + print("Event not found") + return False, "", 0 + elif input_type == 'url' and check_url_is_readable(input_value) is None: + print("Url not readable / supported") + return False, task, duration# + + elif tag.as_vec()[0] == 'output': + # TODO move this to individual modules + output = tag.as_vec()[1] + if not (output == "text/plain" + or output == "text/json" or output == "json" + or output == "image/png" or "image/jpg" + or output == "image/png;format=url" or output == "image/jpg;format=url" + or output == ""): + print("Output format not supported, skipping..") + return False, "", 0 + except Exception as e: + print("Check task 2: " + str(e)) - elif tag.as_vec()[0] == 'output': - output = tag.as_vec()[1] - if not (output == "text/plain" - or output == "text/json" or output == "json" - or output == "image/png" or "image/jpg" - or output == "image/png;format=url" or output == "image/jpg;format=url" - or output == ""): - print("Output format not supported, skipping..") - return False, "", 0 for dvm in dvm_config.SUPPORTED_DVMS: if dvm.TASK == task: @@ -96,6 +101,7 @@ def check_task_is_supported(event, client, get_duration=False, config=None): return True, task, duration + except Exception as e: print("Check task: " + str(e)) diff --git a/utils/database_utils.py b/utils/database_utils.py index 426c7f8..77e9990 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -206,15 +206,18 @@ def fetch_user_metadata(npub, client): if len(events) > 0: latest_entry = events[0] latest_time = 0 - for entry in events: - if entry.created_at().as_secs() > latest_time: - latest_time = entry.created_at().as_secs() - latest_entry = entry - profile = json.loads(latest_entry.content()) - if profile.get("name"): - name = profile['name'] - if profile.get("nip05"): - nip05 = profile['nip05'] - if profile.get("lud16"): - lud16 = profile['lud16'] + try: + for entry in events: + if entry.created_at().as_secs() > latest_time: + latest_time = entry.created_at().as_secs() + latest_entry = entry + profile = json.loads(latest_entry.content()) + if profile.get("name"): + name = profile['name'] + if profile.get("nip05"): + nip05 = profile['nip05'] + if profile.get("lud16"): + lud16 = profile['lud16'] + except Exception as e: + print(e) return name, nip05, lud16 diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index e6776ba..ec07a6d 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -19,8 +19,6 @@ class DVMConfig: DB: str NEW_USER_BALANCE: int = 250 # Free credits for new users NIP89: NIP89Announcement - DM_ALLOWED = [] - SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 55f8c50..74b359b 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -1,4 +1,5 @@ # LIGHTNING FUNCTIONS +import base64 import json import os import urllib.parse @@ -54,26 +55,26 @@ def parse_zap_event_tags(zap_event, keys, name, client, config): elif tag.as_vec()[0] == 'e': zapped_event = get_event_by_id(tag.as_vec()[1], client=client, config=config) elif tag.as_vec()[0] == 'description': - zap_request_event = Event.from_json(tag.as_vec()[1]) - sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), - zap_request_event.content()) - for z_tag in zap_request_event.tags(): - if z_tag.as_vec()[0] == 'anon': - if len(z_tag.as_vec()) > 1: - #print("[" + name + "] Private Zap received.") - decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], - keys.secret_key(), - zap_request_event.pubkey()) - decrypted_private_event = Event.from_json(decrypted_content) - if decrypted_private_event.kind() == 9733: - sender = decrypted_private_event.pubkey().to_hex() - message = decrypted_private_event.content() - #if message != "": - # print("Zap Message: " + message) - else: - anon = True - print( - "[" + name + "] Anonymous Zap received. Unlucky, I don't know from whom, and never will") + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for z_tag in zap_request_event.tags(): + if z_tag.as_vec()[0] == 'anon': + if len(z_tag.as_vec()) > 1: + # print("[" + name + "] Private Zap received.") + decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], + keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + # if message != "": + # print("Zap Message: " + message) + else: + anon = True + print( + "[" + name + "] Anonymous Zap received. Unlucky, I don't know from whom, and never will") return invoice_amount, zapped_event, sender, message, anon @@ -212,3 +213,52 @@ def zap(lud16: str, amount: int, content, zapped_event: Event, keys, dvm_config, except Exception as e: print(e) return None + + +def parse_cashu(cashuToken): + try: + base64token = cashuToken.replace("cashuA", "") + cashu = json.loads(base64.b64decode(base64token).decode('utf-8')) + token = cashu["token"][0] + proofs = token["proofs"] + mint = token["mint"] + + totalAmount = 0 + for proof in proofs: + totalAmount += proof["amount"] + + fees = max(int(totalAmount * 0.02), 2) + redeemInvoiceAmount = totalAmount - fees + + return cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs + except Exception as e: + print("Could not parse this cashu token") + return None, None, None, None, None, None + + +def redeem_cashu(cashu, config): + # TODO untested + is_redeemed = False + cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs = parse_cashu(cashu) + invoice = create_bolt11_ln_bits(totalAmount, config) + try: + url = mint + "/melt" # Melt cashu tokens at Mint + json_object = {"proofs": proofs, "pr": invoice} + + headers = {"Content-Type": "application/json; charset=utf-8"} + request = requests.post(url, json=json_object, headers=headers) + + if request.status_code == 200: + tree = json.loads(request.text) + successful = tree.get("paid") == "true" + if successful: + is_redeemed = True + else: + msg = tree.get("detail", "").split('.')[0].strip() if tree.get("detail") else None + is_redeemed = False + print(msg) + + except Exception as e: + print(e) + + return is_redeemed From 248963ce90424c9cbec4bff155117fcbd45a74e6 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 27 Nov 2023 10:35:58 +0100 Subject: [PATCH 2/3] fixing the decrypted tags thing --- bot.py | 12 ++--- dvm.py | 49 ++++++++++---------- tasks/imagegeneration_openai_dalle.py | 2 +- tasks/imagegeneration_sdxl.py | 2 +- tasks/textextractionpdf.py | 2 +- tasks/translation.py | 2 +- test_dvm_client.py | 32 ++----------- utils/backend_utils.py | 22 +++++---- utils/nostr_utils.py | 65 ++++++++++++++------------- 9 files changed, 87 insertions(+), 101 deletions(-) diff --git a/bot.py b/bot.py index 21cc454..a9b2e52 100644 --- a/bot.py +++ b/bot.py @@ -136,15 +136,16 @@ class Bot: bid_tag = Tag.parse(['bid', bid, bid]) relays_tag = Tag.parse(["relays", json.dumps(self.dvm_config.RELAY_LIST)]) alt_tag = Tag.parse(["alt", self.dvm_config.SUPPORTED_DVMS[index].TASK]) + p_tag = Tag.parse(['p', dvm_keys.public_key().to_hex()]) encrypted_params_string = json.dumps([i_tag.as_vec(), bid_tag.as_vec(), - relays_tag.as_vec(), alt_tag.as_vec()]) + relays_tag.as_vec(), alt_tag.as_vec(), p_tag.as_vec()]) print(encrypted_params_string) encrypted_params = nip04_encrypt(self.keys.secret_key(), dvm_keys.public_key(), encrypted_params_string) - p_tag = Tag.parse(['p', dvm_keys.public_key().to_hex()]) + encrypted_tag = Tag.parse(['encrypted']) nip90request = EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND, encrypted_params, [p_tag, encrypted_tag]).to_event(self.keys) @@ -220,14 +221,14 @@ class Bot: elif status == "payment-required" or status == "partial": amount = 0 + for tag in nostr_event.tags(): if tag.as_vec()[0] == "amount": amount_msats = int(tag.as_vec()[1]) amount = int(amount_msats / 1000) - entry = next((x for x in self.job_list if x['event_id'] == etag), None) if entry is not None and entry['is_paid'] is False and entry['dvm_key'] == ptag: - + print("PAYMENT: " + nostr_event.as_json()) #if we get a bolt11, we pay and move on if len(tag.as_vec()) > 2: bolt11 = tag.as_vec()[2] @@ -236,7 +237,8 @@ class Bot: else: user = get_or_add_user(db=self.dvm_config.DB, npub=nostr_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) - bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config, "private") + print("PAYING: " + user.name) + bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config, "public") if bolt11 == None: print("Receiver has no Lightning address") return diff --git a/dvm.py b/dvm.py index 005026e..3d3af7f 100644 --- a/dvm.py +++ b/dvm.py @@ -83,24 +83,24 @@ class DVM: return def handle_nip90_job_event(nip90_event): - """ - :type nip90_event: Event - """ - tags: typing.List[Tag] - tags, p_tag_str = check_and_decrypt_tags(nip90_event, self.dvm_config) - if p_tag_str is None: + nip90_event = check_and_decrypt_tags(nip90_event, self.dvm_config) + if nip90_event is None: return - nip90_event.tags = tags - cashu = "" - for tag in tags: - if tag.as_vec()[0] == "cashu": - cashu = tag.as_vec()[0] user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) - task_supported, task, duration = check_task_is_supported(nip90_event, tags, client=self.client, + + cashu = "" + p_tag_str = "" + for tag in nip90_event.tags(): + if tag.as_vec()[0] == "cashu": + cashu = tag.as_vec()[0] + elif tag.as_vec()[0] == "p": + p_tag_str = tag.as_vec()[0] + + task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) @@ -134,6 +134,7 @@ class DVM: do_work(nip90_event) # if task is directed to us via p tag and user has balance, do the job and update balance + elif p_tag_str == Keys.from_sk_str( self.dvm_config.PRIVATE_KEY).public_key().to_hex() and user.balance >= amount: balance = max(user.balance - amount, 0) @@ -155,7 +156,7 @@ class DVM: # else send a payment required event to user else: bid = 0 - for tag in nip90_event.tags: + for tag in nip90_event.tags(): if tag.as_vec()[0] == 'bid': bid = int(tag.as_vec()[1]) @@ -177,14 +178,15 @@ class DVM: False, amount, client=self.client, dvm_config=self.dvm_config) else: - print("Task not supported on this DVM, skipping..") + print("[" + self.dvm_config.NIP89.name + "] Task " + task + " not supported on this DVM, skipping..") def handle_zap(zap_event): try: invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event, - self.keys, self.dvm_config.NIP89.name, - self.client, self.dvm_config) + self.keys, + self.dvm_config.NIP89.name, + self.client, self.dvm_config) 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: @@ -198,16 +200,17 @@ class DVM: amount = int(float(tag.as_vec()[1]) / 1000) elif tag.as_vec()[0] == 'e': job_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) - tags: typing.List[Tag] - tags, p_tag_str = check_and_decrypt_tags(job_event, self.dvm_config) - job_event.tags = tags + if job_event is not None: + job_event = check_and_decrypt_tags(job_event, self.dvm_config) + else: + return if p_tag_str is None: return - # if a reaction by us got zapped + # if a reaction by us got zapped - task_supported, task, duration = check_task_is_supported(job_event, tags, + task_supported, task, duration = check_task_is_supported(job_event, client=self.client, get_duration=False, config=self.dvm_config) @@ -263,7 +266,7 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Error during content decryption: " + str(e)) def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): - task_supported, task, duration = check_task_is_supported(nevent, nevent.tags, client, False, + task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) if not task_supported: return False @@ -372,7 +375,7 @@ class DVM: status_tag = Tag.parse(["status", status]) tags = [e_tag, alt_tag, status_tag] - for tag in original_event.tags: + for tag in original_event.tags(): if tag.as_vec()[0] == "p": p_tag = tag diff --git a/tasks/imagegeneration_openai_dalle.py b/tasks/imagegeneration_openai_dalle.py index f2801a5..1b1784a 100644 --- a/tasks/imagegeneration_openai_dalle.py +++ b/tasks/imagegeneration_openai_dalle.py @@ -46,7 +46,7 @@ class ImageGenerationDALLE(DVMTaskInterface): model = "dall-e-3" quality = "standard" - for tag in event.tags: + for tag in event.tags(): if tag.as_vec()[0] == 'i': input_type = tag.as_vec()[2] if input_type == "text": diff --git a/tasks/imagegeneration_sdxl.py b/tasks/imagegeneration_sdxl.py index b9e5de2..df07576 100644 --- a/tasks/imagegeneration_sdxl.py +++ b/tasks/imagegeneration_sdxl.py @@ -55,7 +55,7 @@ class ImageGenerationSDXL(DVMTaskInterface): lora_weight = "" strength = "" guidance_scale = "" - for tag in event.tags: + for tag in event.tags(): if tag.as_vec()[0] == 'i': input_type = tag.as_vec()[2] if input_type == "text": diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 1bf9639..ba85899 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -43,7 +43,7 @@ class TextExtractionPDF(DVMTaskInterface): input_content = "" url = "" - for tag in event.tags: + for tag in event.tags(): if tag.as_vec()[0] == 'i': input_type = tag.as_vec()[2] input_content = tag.as_vec()[1] diff --git a/tasks/translation.py b/tasks/translation.py index 8ed23d0..fd97c1a 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -42,7 +42,7 @@ class Translation(DVMTaskInterface): text = "" translation_lang = "en" - for tag in event.tags: + for tag in event.tags(): if tag.as_vec()[0] == 'i': input_type = tag.as_vec()[2] diff --git a/test_dvm_client.py b/test_dvm_client.py index 60742e9..e8875a3 100644 --- a/test_dvm_client.py +++ b/test_dvm_client.py @@ -71,6 +71,7 @@ def nostr_client_test_image(prompt): def nostr_client_test_image_private(prompt, cashutoken): keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) + receiver_keys = Keys.from_sk_str(os.getenv("NOSTR_PRIVATE_KEY")) # TODO more advanced logic, more parsing, params etc, just very basic test functions for now @@ -92,7 +93,7 @@ def nostr_client_test_image_private(prompt, cashutoken): relays_tag.as_vec(), alt_tag.as_vec(), cashu_tag.as_vec()]) - encrypted_params = nip04_encrypt(keys.secret_key(), keys.public_key(), + encrypted_params = nip04_encrypt(keys.secret_key(), receiver_keys.public_key(), encrypted_params_string) p_tag = Tag.parse(['p', keys.public_key().to_hex()]) @@ -107,31 +108,6 @@ def nostr_client_test_image_private(prompt, cashutoken): send_event(nip90request, client=client, dvm_config=config) return nip90request.as_json() - - - iTag = Tag.parse(["i", prompt, "text"]) - outTag = Tag.parse(["output", "image/png;format=url"]) - paramTag1 = Tag.parse(["param", "size", "1024x1024"]) - tTag = Tag.parse(["t", "bitcoin"]) - - bidTag = Tag.parse(['bid', str(1000 * 1000), str(1000 * 1000)]) - relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", - "wss://nostr-pub.wellorder.net"]) - alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to translate a given Input"]) - event = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, str("Generate an Image."), - [iTag, outTag, tTag, paramTag1, bidTag, relaysTag, alttag]).to_event(keys) - - relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", - "wss://nostr-pub.wellorder.net"] - - client = Client(keys) - for relay in relay_list: - client.add_relay(relay) - client.connect() - config = DVMConfig - send_event(event, client=client, dvm_config=config) - return event.as_json() - def nostr_client(): keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) sk = keys.secret_key() @@ -156,8 +132,8 @@ def nostr_client(): nostr_client_test_image("a beautiful purple ostrich watching the sunset") - cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzNzZhYTQ4YTJiMDU1NTlmYzQ4MTU2NjJjZThhMjZmZGM5OTQzYzY2Yzg0OWEzNTg3NDgwYWRmYzE0ZTEwNTRiZCIsInNlY3JldCI6IlIzTGhSZDI5UktJTzRkMHdNZ0Z0K2ZKWlVoYi90K0RmZXMxdFVrZVBVV0E9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMmYyNTdhYzYzOTU4NGY1YTE5NTNkMGI0ODI3OWJkN2EyMjdmZTBkYzI0OWY0MjQwNjgzMDZlOTI0ZGE3ZGVhZDciLCJzZWNyZXQiOiJ4Tmhwdm50SkNwcXFiYmFjWDA0NzluVld4SGo5U05jaVBvdTNYQ3JWcmRjPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDIyYjhiY2JkYTQ1OTNlMGZlNTY4ZWYyOTM2OWNmZjNmMzY2NzdlZDAzYTQ4ODMxNzYwNDQxN2JkNGE3MTYzZDYyIiwic2VjcmV0IjoiTEprUVlheWNyUE9yZ3hZcHhlcDZVV3U0RjZ3QUVydnZJNHZiRmN0R3h6MD0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0" = - nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) + # cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzNzZhYTQ4YTJiMDU1NTlmYzQ4MTU2NjJjZThhMjZmZGM5OTQzYzY2Yzg0OWEzNTg3NDgwYWRmYzE0ZTEwNTRiZCIsInNlY3JldCI6IlIzTGhSZDI5UktJTzRkMHdNZ0Z0K2ZKWlVoYi90K0RmZXMxdFVrZVBVV0E9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMmYyNTdhYzYzOTU4NGY1YTE5NTNkMGI0ODI3OWJkN2EyMjdmZTBkYzI0OWY0MjQwNjgzMDZlOTI0ZGE3ZGVhZDciLCJzZWNyZXQiOiJ4Tmhwdm50SkNwcXFiYmFjWDA0NzluVld4SGo5U05jaVBvdTNYQ3JWcmRjPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDIyYjhiY2JkYTQ1OTNlMGZlNTY4ZWYyOTM2OWNmZjNmMzY2NzdlZDAzYTQ4ODMxNzYwNDQxN2JkNGE3MTYzZDYyIiwic2VjcmV0IjoiTEprUVlheWNyUE9yZ3hZcHhlcDZVV3U0RjZ3QUVydnZJNHZiRmN0R3h6MD0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0" + #nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) class NotificationHandler(HandleNotification): def handle(self, relay_url, event): print(f"Received new event from {relay_url}: {event.as_json()}") diff --git a/utils/backend_utils.py b/utils/backend_utils.py index bedff8e..d5b7450 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -1,4 +1,8 @@ +import typing + import requests +from nostr_sdk import Event, Tag + from utils.definitions import EventDefinitions from utils.nostr_utils import get_event_by_id @@ -50,17 +54,16 @@ def get_task(event, client, dvmconfig): return "unknown type" -def check_task_is_supported(event, tags, client, get_duration=False, config=None): +def check_task_is_supported(event: Event, client, get_duration=False, config=None): try: dvm_config = config input_value = "" input_type = "" duration = 1 - task = get_task(event, client=client, dvmconfig=dvm_config) try: - for tag in tags: + for tag in event.tags(): if tag.as_vec()[0] == 'i': if len(tag.as_vec()) < 3: print("Job Event missing/malformed i tag, skipping..") @@ -75,7 +78,7 @@ def check_task_is_supported(event, tags, client, get_duration=False, config=None return False, "", 0 elif input_type == 'url' and check_url_is_readable(input_value) is None: print("Url not readable / supported") - return False, task, duration# + return False, task, duration # elif tag.as_vec()[0] == 'output': # TODO move this to individual modules @@ -85,13 +88,13 @@ def check_task_is_supported(event, tags, client, get_duration=False, config=None or output == "image/png" or "image/jpg" or output == "image/png;format=url" or output == "image/jpg;format=url" or output == ""): - print("Output format not supported, skipping..") - return False, "", 0 + print("Output format not supported, skipping..") + return False, "", 0 except Exception as e: print("Check task 2: " + str(e)) - for dvm in dvm_config.SUPPORTED_DVMS: + print(dvm.TASK) if dvm.TASK == task: if not dvm.is_input_supported(input_type, event.content()): return False, task, duration @@ -130,10 +133,11 @@ def check_url_is_readable(url): def get_amount_per_task(task, dvm_config, duration=1): - for dvm in dvm_config.SUPPORTED_DVMS: #this is currently just one + for dvm in dvm_config.SUPPORTED_DVMS: # this is currently just one if dvm.TASK == task: amount = dvm.COST * duration return amount else: - print("["+dvm_config.SUPPORTED_DVMS[0].NAME +"] Task " + task + " is currently not supported by this instance, skipping") + print("[" + dvm_config.SUPPORTED_DVMS[ + 0].NAME + "] Task " + task + " is currently not supported by this instance, skipping") return None diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index 4dfb394..9986630 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,6 +1,7 @@ import json +import typing from datetime import timedelta -from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt +from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt, EventBuilder def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: @@ -61,38 +62,38 @@ def send_event(event: Event, client: Client, dvm_config) -> EventId: def check_and_decrypt_tags(event, dvm_config): - tags = [] - is_encrypted = False - p = "" - for tag in event.tags(): - if tag.as_vec()[0] == 'encrypted': - is_encrypted = True - elif tag.as_vec()[0] == 'p': - p = tag.as_vec()[1] + try: + tags = [] + is_encrypted = False + p = "" + sender = event.pubkey() + for tag in event.tags(): + if tag.as_vec()[0] == 'encrypted': + is_encrypted = True + elif tag.as_vec()[0] == 'p': + p = tag.as_vec()[1] - if is_encrypted: - if p != Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(): - print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, " - "skipping..") - return None, None + if is_encrypted: + if p != Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(): + print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, " + "skipping..") + return None - elif p == Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(): - encrypted_tag = Tag.parse(["encrypted"]) - p_tag = Tag.parse(["p", p]) + elif p == Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(): + print("encrypted") + #encrypted_tag = Tag.parse(["encrypted"]) + #p_tag = Tag.parse(["p", p]) - tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(), - event.pubkey(), event.content()) - params = json.loads(tags_str) - - for element in params: - tags.append(Tag.parse(element)) - - # Keep the encrypted tag - tags.append(p_tag) - tags.append(encrypted_tag) - - return tags, p - - else: - return event.tags, p + tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(), + event.pubkey(), event.content()) + #TODO add outer p tag so it doesnt have to be sent twice + params = json.loads(tags_str) + eventasjson = json.loads(event.as_json()) + eventasjson['tags'] = params + eventasjson['content'] = "" + event = Event.from_json(json.dumps(eventasjson)) + print(event.as_json()) + except Exception as e: + print(e) + return event From 85ba0c4246b5a3948d9018b6a39d31d693a98750 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 27 Nov 2023 10:36:22 +0100 Subject: [PATCH 3/3] Update test_dvm_client.py --- test_dvm_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_dvm_client.py b/test_dvm_client.py index e8875a3..2ed0fbf 100644 --- a/test_dvm_client.py +++ b/test_dvm_client.py @@ -132,7 +132,6 @@ def nostr_client(): nostr_client_test_image("a beautiful purple ostrich watching the sunset") - # cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzNzZhYTQ4YTJiMDU1NTlmYzQ4MTU2NjJjZThhMjZmZGM5OTQzYzY2Yzg0OWEzNTg3NDgwYWRmYzE0ZTEwNTRiZCIsInNlY3JldCI6IlIzTGhSZDI5UktJTzRkMHdNZ0Z0K2ZKWlVoYi90K0RmZXMxdFVrZVBVV0E9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMmYyNTdhYzYzOTU4NGY1YTE5NTNkMGI0ODI3OWJkN2EyMjdmZTBkYzI0OWY0MjQwNjgzMDZlOTI0ZGE3ZGVhZDciLCJzZWNyZXQiOiJ4Tmhwdm50SkNwcXFiYmFjWDA0NzluVld4SGo5U05jaVBvdTNYQ3JWcmRjPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDIyYjhiY2JkYTQ1OTNlMGZlNTY4ZWYyOTM2OWNmZjNmMzY2NzdlZDAzYTQ4ODMxNzYwNDQxN2JkNGE3MTYzZDYyIiwic2VjcmV0IjoiTEprUVlheWNyUE9yZ3hZcHhlcDZVV3U0RjZ3QUVydnZJNHZiRmN0R3h6MD0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0" #nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) class NotificationHandler(HandleNotification): def handle(self, relay_url, event):