diff --git a/bot.py b/bot.py index cfe34a8..21cc454 100644 --- a/bot.py +++ b/bot.py @@ -236,7 +236,7 @@ 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) + bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config, "private") if bolt11 == None: print("Receiver has no Lightning address") return @@ -288,7 +288,7 @@ class Bot: def handle_zap(zap_event): print("[" + self.NAME + "] Zap received") try: - invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event, + invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event, self.keys, self.NAME, self.client, self.dvm_config) diff --git a/dvm.py b/dvm.py index 395db6d..d65be56 100644 --- a/dvm.py +++ b/dvm.py @@ -12,7 +12,7 @@ from utils.dvmconfig import DVMConfig from utils.admin_utils import admin_make_database_updates, AdminConfig from utils.backend_utils import get_amount_per_task, check_task_is_supported, get_task 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 +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 @@ -53,7 +53,7 @@ class DVM: self.client.connect() zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) - #bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since( + # bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since( # Timestamp.now()) kinds = [EventDefinitions.KIND_NIP90_GENERIC] @@ -73,9 +73,6 @@ class DVM: def handle(self, relay_url, nostr_event): if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: - print( - "[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}:" - f" {nostr_event.as_json()}") handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) @@ -89,40 +86,13 @@ class DVM: """ :type nip90_event: Event """ - p = "" - is_encrypted = False + tags: typing.List[Tag] - tags = nip90_event.tags() - - for tag in 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(self.dvm_config.PRIVATE_KEY).public_key().to_hex(): - print("[" + self.dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, " - "skipping..") - return - - elif p == Keys.from_sk_str(self.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(self.dvm_config.PRIVATE_KEY).secret_key(), - nip90_event.pubkey(), nip90_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) - + tags, p_tag_str = check_and_decrypt_tags(nip90_event, self.dvm_config) + if p_tag_str is None: + return nip90_event.tags = tags + user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client, config=self.dvm_config) @@ -145,8 +115,7 @@ class DVM: if dvm.TASK == task and dvm.COST == 0: task_is_free = True - - #if user is whitelisted or task is free, just do the job + # if user is whitelisted or task is free, just do the job if user.iswhitelisted or task_is_free: print( "[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + @@ -157,7 +126,8 @@ 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 == Keys.from_sk_str(self.dvm_config.PRIVATE_KEY).public_key().to_hex() and user.balance >= amount: + 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) update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, @@ -174,7 +144,7 @@ class DVM: do_work(nip90_event) - #else send a payment required event to user + # else send a payment required event to user else: bid = 0 for tag in nip90_event.tags: @@ -202,31 +172,40 @@ class DVM: print("Task not supported on this DVM, skipping..") def handle_zap(zap_event): - print("Zap received") try: - invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event, + 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) 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 a reaction by us got zapped - print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( - user.name)) + if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: + amount = 0 job_event = None + p_tag_str = "" for tag in zapped_event.tags(): if tag.as_vec()[0] == 'amount': 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 p_tag_str is None: + return + + # if a reaction by us got zapped task_supported, task, duration = check_task_is_supported(job_event, client=self.client, get_duration=False, 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, @@ -273,7 +252,7 @@ class DVM: config=self.dvm_config) except Exception as e: - print(f"Error during content decryption: {e}") + 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, diff --git a/utils/backend_utils.py b/utils/backend_utils.py index afa2019..4162ba2 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -51,47 +51,53 @@ def get_task(event, client, dvmconfig): def check_task_is_supported(event, client, get_duration=False, config=None): - dvm_config = config - input_value = "" - input_type = "" - duration = 1 - 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") + try: + dvm_config = config + input_value = "" + input_type = "" + duration = 1 + + 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 + + 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: + if not dvm.is_input_supported(input_type, event.content()): return False, task, duration - 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 + if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS): + return False, task, duration - for dvm in dvm_config.SUPPORTED_DVMS: - if dvm.TASK == task: - if not dvm.is_input_supported(input_type, event.content()): - return False, task, duration + return True, task, duration - if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS): - return False, task, duration - - return True, task, duration + except Exception as e: + print("Check task: " + str(e)) def check_url_is_readable(url): diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index eb79ab4..4dfb394 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,5 +1,6 @@ +import json from datetime import timedelta -from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey +from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: @@ -57,3 +58,41 @@ def send_event(event: Event, client: Client, dvm_config) -> EventId: client.remove_relay(relay) return event_id + + +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] + + 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 + + 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]) + + 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 + diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 6389104..55f8c50 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -45,6 +45,7 @@ def parse_zap_event_tags(zap_event, keys, name, client, config): zapped_event = None invoice_amount = 0 anon = False + message = "" sender = zap_event.pubkey() for tag in zap_event.tags(): @@ -59,7 +60,7 @@ def parse_zap_event_tags(zap_event, keys, name, client, config): 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.") + #print("[" + name + "] Private Zap received.") decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], keys.secret_key(), zap_request_event.pubkey()) @@ -67,14 +68,14 @@ def parse_zap_event_tags(zap_event, keys, name, client, config): if decrypted_private_event.kind() == 9733: sender = decrypted_private_event.pubkey().to_hex() message = decrypted_private_event.content() - if message != "": - print("Zap Message: " + message) + #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, anon + return invoice_amount, zapped_event, sender, message, anon def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): @@ -142,10 +143,6 @@ def enrypt_private_zap_message(message, privatekey, publickey): encrypted_msg_bech32 = bech32_encode("pzap", convertbits(encrypted_msg, 8, 5, True)) iv_bech32 = bech32_encode("iv", convertbits(iv, 8, 5, True)) - - print("Encrypted Message:", encrypted_msg_bech32) - print("IV:", iv_bech32) - return encrypted_msg_bech32 + "_" + iv_bech32