diff --git a/bot.py b/bot.py index 4d1e465..5960692 100644 --- a/bot.py +++ b/bot.py @@ -16,7 +16,6 @@ from utils.zap_utils import parse_zap_event_tags, pay_bolt11_ln_bits, zap class Bot: job_list: list - # This is a simple list just to keep track which events we created and manage, so we don't pay for other requests def __init__(self, dvm_config, admin_config=None): self.NAME = "Bot" @@ -61,7 +60,8 @@ class Bot: 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: + if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind() + <= EventDefinitions.KIND_NIP90_GENERIC + 1000): handle_nip90_response_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_FEEDBACK: handle_nip90_feedback(nostr_event) @@ -81,7 +81,6 @@ class Bot: print(decrypted_text) user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config) - # We do a selection of tasks now, maybe change this later, Idk. if decrypted_text[0].isdigit(): index = int(decrypted_text.split(' ')[0]) - 1 task = self.dvm_config.SUPPORTED_DVMS[index].TASK @@ -99,65 +98,58 @@ class Bot: "services.", None).to_event(self.keys) send_event(evt, client=self.client, dvm_config=dvm_config) - elif user.iswhitelisted or user.balance >= required_amount or required_amount == 0: - time.sleep(2.0) - if user.iswhitelisted: - evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), - "As you are " - "whitelisted, your balance remains at" - + str(user.balance) + " Sats.\n", - nostr_event.id()).to_event(self.keys) + elif user.balance >= required_amount or required_amount == 0: + command = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") + input = command.split("-")[0].rstrip() - else: - balance = max(user.balance - required_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()) - evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), - "New balance is " + - str(balance) - + " Sats.\n", - nostr_event.id()).to_event(self.keys) - - input = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") - - dvm_keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK) i_tag = Tag.parse(["i", input, "text"]) - - # we use the y tag to keep information about the original sender, in order to forward the - # results later - - # TODO more advanced logic, more parsing, params etc, just very basic test functions for now - # outTag = Tag.parse(["output", "image/png;format=url"]) - # paramTag1 = Tag.parse(["param", "size", "1024x1024"]) - bid = str(self.dvm_config.SUPPORTED_DVMS[index].COST * 1000) 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(), p_tag.as_vec()]) + tags = [i_tag.as_vec(), bid_tag.as_vec(), relays_tag.as_vec(), alt_tag.as_vec()] + + remaining_text = command.replace(input, "") + params = remaining_text.rstrip().split("-") + + for i in params: + if i != " ": + try: + split = i.split(" ") + param = str(split[0]) + print(param) + value = str(split[1]) + print(value) + tag = Tag.parse(["param", param, value]) + tags.append(tag.as_vec()) + print("Added params: " + tag.as_vec()) + except Exception as e: + print(e) + print("Couldn't add " + i) + + encrypted_params_string = json.dumps(tags) print(encrypted_params_string) - encrypted_params = nip04_encrypt(self.keys.secret_key(), dvm_keys.public_key(), + encrypted_params = nip04_encrypt(self.keys.secret_key(), + PublicKey.from_hex( + self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY), encrypted_params_string) encrypted_tag = Tag.parse(['encrypted']) - nip90request = EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND, encrypted_params, - [p_tag, encrypted_tag]).to_event(self.keys) + p_tag = Tag.parse(['p', self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY]) + encrypted_nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND, + encrypted_params, [p_tag, encrypted_tag]). + to_event(self.keys)) - entry = {"npub": user.npub, "event_id": nip90request.id().to_hex(), - "dvm_key": dvm_keys.public_key().to_hex(), "is_paid": False} + entry = {"npub": user.npub, "event_id": encrypted_nip90request.id().to_hex(), + "dvm_key": self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY, "is_paid": False} self.job_list.append(entry) - send_event(nip90request, client=self.client, dvm_config=dvm_config) + send_event(encrypted_nip90request, client=self.client, dvm_config=dvm_config) + - print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation") - send_event(evt, client=self.client, dvm_config=dvm_config) else: print("Bot payment-required") time.sleep(2.0) @@ -190,7 +182,9 @@ class Bot: print("Error in bot " + str(e)) def handle_nip90_feedback(nostr_event): + try: + is_encrypted = False status = "" etag = "" ptag = "" @@ -202,6 +196,33 @@ class Bot: etag = tag.as_vec()[1] elif tag.as_vec()[0] == "p": ptag = tag.as_vec()[1] + elif tag.as_vec()[0] == "encrypted": + is_encrypted = True + + content = nostr_event.content() + if is_encrypted: + if ptag == self.dvm_config.PUBLIC_KEY: + tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(), + nostr_event.pubkey(), nostr_event.content()) + params = json.loads(tags_str) + params.append(Tag.parse(["p", ptag]).as_vec()) + params.append(Tag.parse(["encrypted"]).as_vec()) + print(params) + event_as_json = json.loads(nostr_event.as_json()) + event_as_json['tags'] = params + event_as_json['content'] = "" + nostr_event = Event.from_json(json.dumps(event_as_json)) + + for tag in nostr_event.tags(): + if tag.as_vec()[0] == "status": + status = tag.as_vec()[1] + elif tag.as_vec()[0] == "e": + etag = tag.as_vec()[1] + elif tag.as_vec()[0] == "content": + content = tag.as_vec()[1] + + else: + return if status == "success" or status == "error" or status == "processing" or status == "partial": entry = next((x for x in self.job_list if x['event_id'] == etag), None) @@ -211,34 +232,49 @@ class Bot: reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, PublicKey.from_hex(user.npub), - nostr_event.content(), + content, None).to_event(self.keys) - print(status + ": " + nostr_event.content()) + print(status + ": " + content) print( "[" + self.NAME + "] Received reaction from " + nostr_event.pubkey().to_hex() + " message to orignal sender " + user.name) send_event(reply_event, client=self.client, dvm_config=dvm_config) - 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 entry is not None and entry['is_paid'] is False and entry['dvm_key'] == nostr_event.pubkey().to_hex(): + # if we get a bolt11, we pay and move on + user = get_or_add_user(db=self.dvm_config.DB, npub=entry["npub"], + client=self.client, config=self.dvm_config) + balance = max(user.balance - 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()) + time.sleep(2.0) + evt = EventBuilder.new_encrypted_direct_msg(self.keys, + PublicKey.from_hex(entry["npub"]), + "Paid " + str( + amount) + " Sats from balance to DVM. New balance is " + + str(balance) + + " Sats.\n", + None).to_event(self.keys) + + print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation") + send_event(evt, client=self.client, dvm_config=dvm_config) + if len(tag.as_vec()) > 2: bolt11 = tag.as_vec()[2] - # else we create a zap else: user = get_or_add_user(db=self.dvm_config.DB, npub=nostr_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) - print("PAYING: " + user.name) - 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, + "private") if bolt11 == None: print("Receiver has no Lightning address") return @@ -256,6 +292,7 @@ class Bot: def handle_nip90_response_event(nostr_event: Event): try: + ptag = "" is_encrypted = False for tag in nostr_event.tags(): if tag.as_vec()[0] == "e": @@ -274,7 +311,10 @@ class Bot: self.job_list.remove(entry) content = nostr_event.content() if is_encrypted: - content = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), content) + if ptag == self.dvm_config.PUBLIC_KEY: + content = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), content) + else: + return print("[" + self.NAME + "] Received results, message to orignal sender " + user.name) time.sleep(1.0) @@ -291,11 +331,11 @@ class Bot: print("[" + self.NAME + "] Zap received") try: invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event, - self.keys, self.NAME, - self.client, self.dvm_config) + self.keys, self.NAME, + self.client, self.dvm_config) user = get_or_add_user(self.dvm_config.DB, sender, client=self.client, config=self.dvm_config) - + print("ZAPED EVENT: " + zapped_event.as_json()) if zapped_event is not None: if not anon: print("[" + self.NAME + "] Note Zap received for Bot balance: " + str( diff --git a/dvm.py b/dvm.py index e7f9fff..8146746 100644 --- a/dvm.py +++ b/dvm.py @@ -90,8 +90,6 @@ class DVM: user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) - - cashu = "" p_tag_str = "" for tag in nip90_event.tags(): @@ -120,8 +118,13 @@ class DVM: task_is_free = True cashu_redeemed = False + cashu_message = "" if cashu != "": - cashu_redeemed = redeem_cashu(cashu, self.dvm_config, self.client) + cashu_redeemed, cashu_message = redeem_cashu(cashu, amount, self.dvm_config, self.client) + if cashu_message != "": + send_job_status_reaction(nip90_event, "error", False, amount, self.client, cashu_message, + 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: print( @@ -135,7 +138,7 @@ class DVM: # 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: + self.dvm_config.PUBLIC_KEY) and user.balance >= amount: balance = max(user.balance - amount, 0) update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, @@ -176,11 +179,10 @@ class DVM: send_job_status_reaction(nip90_event, "payment-required", False, amount, client=self.client, dvm_config=self.dvm_config) - else: - print("[" + self.dvm_config.NIP89.name + "] Task " + task + " not supported on this DVM, skipping..") + #else: + #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, @@ -188,6 +190,7 @@ class DVM: 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: if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: @@ -201,12 +204,11 @@ class DVM: job_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) if job_event is not None: job_event = check_and_decrypt_tags(job_event, self.dvm_config) + if job_event is None: + return else: return - if p_tag_str is None: - return - # if a reaction by us got zapped task_supported, task, duration = check_task_is_supported(job_event, @@ -330,11 +332,11 @@ class DVM: original_event = Event.from_json(original_event_as_str) request_tag = Tag.parse(["request", original_event_as_str.replace("\\", "")]) e_tag = Tag.parse(["e", original_event.id().to_hex()]) - # p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().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()]) status_tag = Tag.parse(["status", "success"]) - reply_tags = [request_tag, e_tag, alt_tag, status_tag] + reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag] encrypted = False for tag in original_event.tags(): if tag.as_vec()[0] == "encrypted": @@ -347,9 +349,6 @@ class DVM: i_tag = tag if not encrypted: reply_tags.append(i_tag) - elif tag.as_vec()[0] == "p": - p_tag = tag - reply_tags.append(p_tag) if encrypted: content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.pubkey().to_hex()), @@ -369,16 +368,23 @@ class DVM: alt_description, reaction = build_status_reaction(status, task, amount, content) e_tag = Tag.parse(["e", original_event.id().to_hex()]) - # p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) alt_tag = Tag.parse(["alt", alt_description]) status_tag = Tag.parse(["status", status]) + reply_tags = [e_tag, alt_tag, status_tag] + encryption_tags = [] - tags = [e_tag, alt_tag, status_tag] + encrypted = False for tag in original_event.tags(): + if tag.as_vec()[0] == "encrypted": + encrypted = True + encrypted_tag = Tag.parse(["encrypted"]) + encryption_tags.append(encrypted_tag) - if tag.as_vec()[0] == "p": - p_tag = tag - tags.append(p_tag) + if encrypted: + encryption_tags.append(p_tag) + else: + reply_tags.append(p_tag) if status == "success" or status == "error": # for x in self.job_list: @@ -415,10 +421,25 @@ class DVM: amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) else: amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats - tags.append(amount_tag) + reply_tags.append(amount_tag) + + if encrypted: + content_tag = Tag.parse(["content", reaction]) + reply_tags.append(content_tag) + str_tags = [] + for element in reply_tags: + str_tags.append(element.as_vec()) + + content = json.dumps(str_tags) + content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.pubkey().to_hex()), + content) + reply_tags = encryption_tags + + else: + content = reaction keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) + 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()) diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 10fa5a4..af874c9 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -12,23 +12,21 @@ class DVMTaskInterface: KIND: int TASK: str COST: int - PK: str + PRIVATE_KEY: str + PUBLIC_KEY: str DVM = DVM dvm_config: DVMConfig admin_config: AdminConfig - def NIP89_announcement(self, nip89config: NIP89Config): - nip89 = NIP89Announcement() - nip89.name = self.NAME - nip89.kind = self.KIND - nip89.pk = self.PK - nip89.dtag = nip89config.DTAG - nip89.content = nip89config.CONTENT - return nip89 + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, + options=None): + self.init(name, dvm_config, admin_config, nip89config) + self.options = options - def init(self, name, dvm_config, admin_config, nip89config): + def init(self, name, dvm_config, admin_config=None, nip89config=None): self.NAME = name - self.PK = dvm_config.PRIVATE_KEY + self.PRIVATE_KEY = dvm_config.PRIVATE_KEY + self.PUBLIC_KEY = dvm_config.PUBLIC_KEY if dvm_config.COST is not None: self.COST = dvm_config.COST @@ -43,6 +41,14 @@ class DVMTaskInterface: nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) nostr_dvm_thread.start() + def NIP89_announcement(self, nip89config: NIP89Config): + nip89 = NIP89Announcement() + nip89.name = self.NAME + nip89.kind = self.KIND + nip89.pk = self.PRIVATE_KEY + nip89.dtag = nip89config.DTAG + nip89.content = nip89config.CONTENT + return nip89 def is_input_supported(self, input_type, input_content) -> bool: """Check if input is supported for current Task.""" diff --git a/main.py b/main.py index d1846ad..d7ed993 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ def run_nostr_dvm_with_local_config(): # Note this is very basic for now and still under development bot_config = DVMConfig() bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") + bot_config.PUBLIC_KEY = Keys.from_sk_str(bot_config.PRIVATE_KEY).public_key().to_hex() bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_ADMIN_KEY = os.getenv("LNBITS_ADMIN_KEY") # The bot will forward zaps for us, use responsibly bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") diff --git a/playground.py b/playground.py index a6c047f..3ddcfd0 100644 --- a/playground.py +++ b/playground.py @@ -1,6 +1,9 @@ import json import os +from nostr_sdk import PublicKey, Keys + +from interfaces.dvmtaskinterface import DVMTaskInterface from tasks.imagegeneration_openai_dalle import ImageGenerationDALLE from tasks.imagegeneration_sdxl import ImageGenerationSDXL from tasks.textextractionpdf import TextExtractionPDF @@ -40,6 +43,7 @@ admin_config.REBROADCAST_NIP89 = False def build_pdf_extractor(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex() dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Add NIP89 @@ -61,6 +65,7 @@ def build_pdf_extractor(name): def build_translator(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex() dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -93,6 +98,7 @@ def build_translator(name): def build_unstable_diffusion(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex() dvm_config.LNBITS_INVOICE_KEY = "" #This one will not use Lnbits to create invoices, but rely on zaps dvm_config.LNBITS_URL = "" @@ -126,6 +132,7 @@ def build_unstable_diffusion(name): def build_sketcher(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2") + dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex() dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -161,6 +168,7 @@ def build_sketcher(name): def build_dalle(name): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") + dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex() dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") profit_in_sats = 10 @@ -190,6 +198,17 @@ def build_dalle(name): admin_config=admin_config) +def external_dvm(name, pubkey): + dvm_config = DVMConfig() + dvm_config.PUBLIC_KEY = Keys.from_public_key(pubkey).public_key().to_hex() + nip89info = { + "name": name, + } + nip89config = NIP89Config() + nip89config.CONTENT = json.dumps(nip89info) + + return DVMTaskInterface(name=name, dvm_config=dvm_config, nip89config=nip89config) + # Little Gimmick: # For Dalle where we have to pay 4cent per image, we fetch current sat price in fiat # and update cost at each start diff --git a/tasks/imagegeneration_openai_dalle.py b/tasks/imagegeneration_openai_dalle.py index 1b1784a..a94f97a 100644 --- a/tasks/imagegeneration_openai_dalle.py +++ b/tasks/imagegeneration_openai_dalle.py @@ -27,11 +27,9 @@ class ImageGenerationDALLE(DVMTaskInterface): TASK: str = "text-to-image" COST: int = 120 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, - options=None): - - self.init(name, dvm_config, admin_config, nip89config) - self.options = options + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + super().__init__(name, dvm_config, nip89config, admin_config, options) def is_input_supported(self, input_type, input_content): if input_type != "text": diff --git a/tasks/imagegeneration_sdxl.py b/tasks/imagegeneration_sdxl.py index df07576..e32950c 100644 --- a/tasks/imagegeneration_sdxl.py +++ b/tasks/imagegeneration_sdxl.py @@ -24,9 +24,9 @@ class ImageGenerationSDXL(DVMTaskInterface): TASK: str = "text-to-image" COST: int = 50 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): - self.init(name, dvm_config, admin_config, nip89config) - self.options = options + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + super().__init__(name, dvm_config, nip89config, admin_config, options) def is_input_supported(self, input_type, input_content): if input_type != "text": diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index ba85899..f1ca68c 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -25,9 +25,9 @@ class TextExtractionPDF(DVMTaskInterface): TASK: str = "pdf-to-text" COST: int = 0 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): - self.init(name, dvm_config, admin_config, nip89config) - self.options = options + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + super().__init__(name, dvm_config, nip89config, admin_config, options) def is_input_supported(self, input_type, input_content): diff --git a/tasks/translation.py b/tasks/translation.py index fd97c1a..dda618a 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -23,10 +23,9 @@ class Translation(DVMTaskInterface): TASK: str = "translation" COST: int = 0 - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, - options=None): - self.init(name, dvm_config, admin_config, nip89config) - self.options = options + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, + admin_config: AdminConfig = None, options=None): + super().__init__(name, dvm_config, nip89config, admin_config, options) def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": diff --git a/test_dvm_client.py b/test_dvm_client.py index 7ff37c0..a654778 100644 --- a/test_dvm_client.py +++ b/test_dvm_client.py @@ -132,7 +132,7 @@ def nostr_client(): #nostr_client_test_image("a beautiful purple ostrich watching the sunset") - cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MSwiQyI6IjAzMjBjMjBkNWZkNTYwODlmYjZjYTllNDFkYjVlM2MzYTAwMTdjNTUzYmY5MzNkZTgwNTg3NDg1YTk5Yjk2Y2E3OSIsInNlY3JldCI6IktrcnVtakdSeDlHTExxZHBQU3J4WUxaZnJjWmFHekdmZ3Q4T2pZN0c4NHM9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MiwiQyI6IjAyNjYyMjQzNWUxMzBmM2E0ZWE2NGUyMmI4NGQyYWRhNzM2MjE4MTE3YzZjOWIyMmFkYjAwZTFjMzhmZDBiOTNjNCIsInNlY3JldCI6Ikw4dU1BbnBsQm1pdDA4cDZjQk0vcXhpVDFmejlpbnA3V3RzZEJTV284aEk9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6NCwiQyI6IjAzMTAxNWM0ZmZhN2U1NzhkNjA0MjFhY2Q2OWEzMTY5NGI4YmRlYTI2YjIwZjgxOWYxOWZhOTNjN2QwZTBiMTdlOCIsInNlY3JldCI6ImRVZ2E2VFo2emRhclozN015NXg2MFdHMzMraitDZnEyOWkzWExjVStDMFE9In0seyJpZCI6IlhXQzAvRXRhcVM4QyIsImFtb3VudCI6MTYsIkMiOiIwMzU0YmYxODdjOTgxZjdmNDk5MGExMDVlMmI2MjIxZDNmYTQ2ZWNlMmNjNWE0ZmI2Mzc3NTdjZDJjM2VhZTkzMGMiLCJzZWNyZXQiOiIyeUJJeEo4dkNGVnUvK1VWSzdwSXFjekkrbkZISngyNXF2ZGhWNDByNzZnPSJ9LHsiaWQiOiJYV0MwL0V0YXFTOEMiLCJhbW91bnQiOjMyLCJDIjoiMDJlYTNmMmFhZGI5MTA4MzljZDA5YTlmMTQ1YTZkY2Q4OGZmZDFmM2M5MjZhMzM5MGFmZjczYjM4ZjY0YjQ5NTU2Iiwic2VjcmV0IjoiQU1mU2FxUWFTN0l5WVdEbUpUaVM4NW9ReFNva0p6SzVJL1R6OUJ5UlFLdz0ifV0sIm1pbnQiOiJodHRwczovL2xuYml0cy5iaXRjb2luZml4ZXN0aGlzLm9yZy9jYXNodS9hcGkvdjEvOXVDcDIyUllWVXE4WjI0bzVCMlZ2VyJ9XX0=" + cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MSwiQyI6IjAyNWU3ODZhOGFkMmExYTg0N2YxMzNiNGRhM2VhMGIyYWRhZGFkOTRiYzA4M2E2NWJjYjFlOTgwYTE1NGIyMDA2NCIsInNlY3JldCI6InQ1WnphMTZKMGY4UElQZ2FKTEg4V3pPck5rUjhESWhGa291LzVzZFd4S0U9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6NCwiQyI6IjAyOTQxNmZmMTY2MzU5ZWY5ZDc3MDc2MGNjZmY0YzliNTMzMzVmZTA2ZGI5YjBiZDg2Njg5Y2ZiZTIzMjVhYWUwYiIsInNlY3JldCI6IlRPNHB5WE43WlZqaFRQbnBkQ1BldWhncm44UHdUdE5WRUNYWk9MTzZtQXM9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MTYsIkMiOiIwMmRiZTA3ZjgwYmMzNzE0N2YyMDJkNTZiMGI3ZTIzZTdiNWNkYTBhNmI3Yjg3NDExZWYyOGRiZDg2NjAzNzBlMWIiLCJzZWNyZXQiOiJHYUNIdHhzeG9HM3J2WWNCc0N3V0YxbU1NVXczK0dDN1RKRnVwOHg1cURzPSJ9XSwibWludCI6Imh0dHBzOi8vbG5iaXRzLmJpdGNvaW5maXhlc3RoaXMub3JnL2Nhc2h1L2FwaS92MS9ScDlXZGdKZjlxck51a3M1eVQ2SG5rIn1dfQ==" nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) class NotificationHandler(HandleNotification): def handle(self, relay_url, event): diff --git a/utils/backend_utils.py b/utils/backend_utils.py index d5b7450..cf3473e 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -62,39 +62,36 @@ def check_task_is_supported(event: Event, client, get_duration=False, config=Non duration = 1 task = get_task(event, client=client, dvmconfig=dvm_config) - try: - 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 # + 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 # + + 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 - 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)) 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 diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index ec07a6d..fda96c0 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -1,11 +1,14 @@ import os +from nostr_sdk import Keys + from utils.nip89_utils import NIP89Announcement class DVMConfig: SUPPORTED_DVMS= [] - PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") + PRIVATE_KEY: str = "" + PUBLIC_KEY: str = "" COST: int = None RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index 86a2b02..758a66f 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -74,23 +74,54 @@ def check_and_decrypt_tags(event, dvm_config): p = tag.as_vec()[1] if is_encrypted: - if p != Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(): + if p != dvm_config.PUBLIC_KEY: 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(): - print("encrypted") + elif p == dvm_config.PUBLIC_KEY: tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(), event.pubkey(), event.content()) params = json.loads(tags_str) params.append(Tag.parse(["p", p]).as_vec()) - print(params) - eventasjson = json.loads(event.as_json()) - eventasjson['tags'] = params - eventasjson['content'] = "" - event = Event.from_json(json.dumps(eventasjson)) - print(event.as_json()) + params.append(Tag.parse(["encrypted"]).as_vec()) + event_as_json = json.loads(event.as_json()) + event_as_json['tags'] = params + event_as_json['content'] = "" + event = Event.from_json(json.dumps(event_as_json)) + except Exception as e: + print(e) + + return event + +def check_and_decrypt_own_tags(event, dvm_config): + 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 dvm_config.PUBLIC_KEY != event.pubkey().to_hex(): + print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, " + "skipping..") + return None + + elif event.pubkey().to_hex() == dvm_config.PUBLIC_KEY: + tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(), + PublicKey.from_hex(p), event.content()) + params = json.loads(tags_str) + params.append(Tag.parse(["p", p]).as_vec()) + params.append(Tag.parse(["encrypted"]).as_vec()) + event_as_json = json.loads(event.as_json()) + event_as_json['tags'] = params + event_as_json['content'] = "" + event = Event.from_json(json.dumps(event_as_json)) except Exception as e: print(e) diff --git a/utils/output_utils.py b/utils/output_utils.py index 5ca215c..676aaf9 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -18,7 +18,6 @@ def post_process_result(anno, original_event): print("Post-processing...") if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format for tag in original_event.tags: - print(tag.as_vec()[0]) if tag.as_vec()[0] == "output": output_format = tag.as_vec()[1] print("requested output is " + str(tag.as_vec()[1]) + "...") @@ -166,7 +165,7 @@ def build_status_reaction(status, task, amount, content): if content is None: reaction = alt_description + emoji.emojize(":thumbs_down:") else: - reaction = alt_description + emoji.emojize(":thumbs_down:") + content + reaction = alt_description + emoji.emojize(":thumbs_down:") + " " + content elif status == "payment-required": alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 59678a5..cd4f2d6 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -11,7 +11,7 @@ from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, from utils.database_utils import get_or_add_user from utils.dvmconfig import DVMConfig -from utils.nostr_utils import get_event_by_id +from utils.nostr_utils import get_event_by_id, check_and_decrypt_tags, check_and_decrypt_own_tags import lnurl from hashlib import sha256 @@ -22,12 +22,14 @@ def parse_zap_event_tags(zap_event, keys, name, client, config): anon = False message = "" sender = zap_event.pubkey() - for tag in zap_event.tags(): if tag.as_vec()[0] == 'bolt11': invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) elif tag.as_vec()[0] == 'e': zapped_event = get_event_by_id(tag.as_vec()[1], client=client, config=config) + zapped_event = check_and_decrypt_own_tags(zapped_event, config) + elif tag.as_vec()[0] == 'p': + p_tag = tag.as_vec()[1] 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(), @@ -249,32 +251,39 @@ def parse_cashu(cashu_token): print(e) token = cashu["token"][0] - print(token) proofs = token["proofs"] mint = token["mint"] total_amount = 0 for proof in proofs: total_amount += proof["amount"] - fees = max(int(total_amount * 0.02), 2) + fees = max(int(total_amount * 0.02), 3) redeem_invoice_amount = total_amount - fees - return proofs, mint, redeem_invoice_amount + return proofs, mint, redeem_invoice_amount, total_amount except Exception as e: print("Could not parse this cashu token") - return None, None, None + return None, None, None, None -def redeem_cashu(cashu, config, client): - proofs, mint, redeem_invoice_amount = parse_cashu(cashu) +def redeem_cashu(cashu, required_amount, config, client) -> (bool, str): + proofs, mint, redeem_invoice_amount, total_amount = parse_cashu(cashu) + fees = total_amount - redeem_invoice_amount + if redeem_invoice_amount <= required_amount: + err = ("Token value (Payment: " + str(total_amount) + " Sats. Fees: " + + str(fees) + " Sats) below required amount of " + str(required_amount) + + " Sats. Cashu token has not been claimed.") + print("[" + config.NIP89.name + "] " + err) + return False, err + if config.LNBITS_INVOICE_KEY != "": invoice = create_bolt11_ln_bits(redeem_invoice_amount, config) else: - user = get_or_add_user(db=config.DB, npub=Keys.from_sk_str(config.PRIVATE_KEY).public_key().to_hex(), + user = get_or_add_user(db=config.DB, npub=config.PUBLIC_KEY, client=client, config=config) invoice = create_bolt11_lud16(user.lud16, redeem_invoice_amount) print(invoice) if invoice is None: - return False + return False, "couldn't create invoice" try: url = mint + "/melt" # Melt cashu tokens at Mint json_object = {"proofs": proofs, "pr": invoice} @@ -282,15 +291,17 @@ def redeem_cashu(cashu, config, client): request_body = json.dumps(json_object).encode('utf-8') request = requests.post(url, data=request_body, headers=headers) tree = json.loads(request.text) - is_paid = (tree.get("paid") == "true") if tree.get("detail") else False - if is_paid: + print(request.text) + is_paid = tree["paid"] if tree.get("paid") else "false" + print(is_paid) + if is_paid == "true": print("token redeemed") - return True + return True, "success" else: msg = tree.get("detail").split('.')[0].strip() if tree.get("detail") else None print(msg) - return False + return False, msg except Exception as e: print(e) - return False + return False, ""