diff --git a/bot.py b/bot.py index 55c5b7e..8ab0323 100644 --- a/bot.py +++ b/bot.py @@ -3,16 +3,15 @@ import time from datetime import timedelta from threading import Thread -from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ - Event, Options +from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, + Options) from utils.admin_utils import admin_make_database_updates from utils.backend_utils import get_amount_per_task from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User from utils.definitions import EventDefinitions -from utils.nostr_utils import send_event, get_event_by_id -from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message, \ - parse_zap_event_tags +from utils.nostr_utils import send_event +from utils.zap_utils import parse_zap_event_tags class Bot: @@ -67,27 +66,26 @@ class Bot: decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) 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 print("[" + self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) + ", Balance: " - + str(user.balance)+ " Sats) Task: " + str(task)) + + str(user.balance) + " Sats) Task: " + str(task)) duration = 1 required_amount = get_amount_per_task(self.dvm_config.SUPPORTED_DVMS[index].TASK, self.dvm_config, duration) - if user.isblacklisted: # For some reason an admin might blacklist npubs, e.g. for abusing the service evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), - "Your are currently blocked from all services.", - None).to_event(self.keys) + "Your are currently blocked from all " + "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: + if not user.iswhitelisted: balance = max(user.balance - required_amount, 0) @@ -95,7 +93,7 @@ class Bot: 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, nostr_event.pubkey(), "Your Job is now scheduled. New balance is " + str(balance) @@ -103,6 +101,7 @@ class Bot: "processing.", nostr_event.id()).to_event(self.keys) else: + time.sleep(2.0) evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), "Your Job is now scheduled. As you are " "whitelisted, your balance remains at" @@ -112,7 +111,6 @@ class Bot: nostr_event.id()).to_event(self.keys) print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation") - time.sleep(2.0) send_event(evt, client=self.client, dvm_config=dvm_config) i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") @@ -137,14 +135,12 @@ class Bot: str(int(required_amount - user.balance)) + " Sats, then try again.", nostr_event.id()).to_event(self.keys) - time.sleep(2.0) send_event(evt, client=self.client, dvm_config=dvm_config) # TODO if we receive the result from one of the dvms, need some better management, maybe check for keys elif decrypted_text.startswith('{"result":'): - dvm_result = json.loads(decrypted_text) user_npub_hex = dvm_result["sender"] user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex, @@ -154,7 +150,7 @@ class Bot: PublicKey.from_hex(user.npub), dvm_result["result"], None).to_event(self.keys) - time.sleep(2.0) + send_event(reply_event, client=self.client, dvm_config=dvm_config) else: @@ -165,22 +161,20 @@ class Bot: message += str(index) + " " + p.NAME + " " + p.TASK + " " + str(p.COST) + " Sats" + "\n" index += 1 + time.sleep(1.0) evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), message + "\nSelect an Index and provide an input (" "e.g. 1 A purple ostrich)", - None).to_event(self.keys) - #nostr_event.id()).to_event(self.keys) - time.sleep(3) + nostr_event.id()).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) except Exception as e: - pass - # TODO we still receive (broken content) events after fetching the metadata, but we don't listen to them. - # probably in client.get_events_of in fetch_user_metadata + print("Error in bot " + str(e)) + def handle_zap(zap_event): print("[" + self.NAME + "] Zap received") - try: invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event, self.keys, self.NAME, @@ -190,14 +184,16 @@ class Bot: if zapped_event is not None: if not anon: - print("[" + self.NAME + "] Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + print("[" + self.NAME + "] Note Zap received for Bot balance: " + str( + invoice_amount) + " Sats from " + str( user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) # a regular note elif not anon: - print("[" + self.NAME + "] Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + print("[" + self.NAME + "] Profile Zap received for Bot balance: " + str( + invoice_amount) + " Sats from " + str( user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) @@ -213,4 +209,3 @@ class Bot: bot = Bot nostr_dvm_thread = Thread(target=bot, args=[self.dvm_config]) nostr_dvm_thread.start() - diff --git a/dvm.py b/dvm.py index d352346..65c5604 100644 --- a/dvm.py +++ b/dvm.py @@ -2,7 +2,7 @@ import json from datetime import timedelta from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel, nip04_decrypt, EventId, Options + init_logger, LogLevel, nip04_decrypt, Options import time @@ -10,12 +10,11 @@ from utils.definitions import EventDefinitions, RequiredJobToWatch, JobToWatch 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 update_sql_table, get_from_sql_table, \ +from utils.database_utils import update_sql_table, \ create_sql_table, get_or_add_user, update_user_balance from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event from utils.output_utils import post_process_result, build_status_reaction -from utils.zap_utils import check_bolt11_ln_bits_is_paid, parse_amount_from_bolt11_invoice, \ - check_for_zapplepay, decrypt_private_zap_message, 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 use_logger = False if use_logger: @@ -30,10 +29,10 @@ class DVM: job_list: list jobs_on_hold_list: list - def __init__(self, dvmconfig, adminconfig=None): - self.dvm_config = dvmconfig - self.admin_config = adminconfig - self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) + def __init__(self, dvm_config, admin_config=None): + self.dvm_config = dvm_config + self.admin_config = admin_config + self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) wait_for_send = True skip_disconnected_relays = True opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)) @@ -75,7 +74,8 @@ 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}: {nostr_event.as_json()}") + "[" + 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) @@ -109,9 +109,10 @@ class DVM: if user.iswhitelisted or task_is_free: print( - "[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") - send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, - dvm_config=self.dvm_config) + "[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + + ". Starting processing..") + send_job_status_reaction(nip90_event, "processing", True, 0, + client=self.client, dvm_config=self.dvm_config) do_work(nip90_event, is_from_bot=False) else: @@ -121,7 +122,8 @@ class DVM: bid = int(tag.as_vec()[1]) print( - "[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json()) + "[" + 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 >= amount: @@ -131,7 +133,8 @@ class DVM: 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()) + "[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + + nip90_event.id().to_hex()) send_job_status_reaction(nip90_event, "payment-required", False, amount, client=self.client, dvm_config=self.dvm_config) else: @@ -192,17 +195,18 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Invoice was not paid sufficiently") elif zapped_event.kind() in EventDefinitions.ANY_RESULT: - print("Someone zapped the result of an exisiting Task. Nice") + print("[" + self.dvm_config.NIP89.name + "] " + "Someone zapped the result of an exisiting Task. Nice") elif not anon: - print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( - user.name)) + print("[" + self.dvm_config.NIP89.name + "] Note Zap received for Bot balance: " + + str(invoice_amount) + " Sats from " + str(user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) # a regular note elif not anon: - print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( - user.name)) + print("[" + self.dvm_config.NIP89.name + "] Profile Zap received for Bot balance: " + + str(invoice_amount) + " Sats from " + str(user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) @@ -210,6 +214,7 @@ class DVM: print(f"Error during content decryption: {e}") def handle_dm(dm_event): + # Note that DVMs only listen and answer to Bots, if at all. decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content()) ob = json.loads(decrypted_text) @@ -220,17 +225,19 @@ class DVM: if str(ob['input']).startswith("http"): input_type = "url" + # TODO Handle additional inputs/params j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_DVMS[0].TASK]) i_tag = Tag.parse(["i", ob['input'], input_type]) - tags = [j_tag, i_tag] - tags.append(Tag.parse(["y", dm_event.pubkey().to_hex()])) - tags.append(Tag.parse(["z", ob['sender']])) + + y_tag = Tag.parse(["y", dm_event.pubkey().to_hex()]) + z_tag = Tag.parse(["z", ob['sender']]) + tags = [j_tag, i_tag, y_tag, z_tag] job_event = EventBuilder(EventDefinitions.KIND_DM, "", tags).to_event(self.keys) do_work(job_event, is_from_bot=True) - def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): - task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) + def check_event_has_not_unfinished_job_input(nevent, append, client, dvm_config): + task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvm_config) if not task_supported: return False @@ -245,13 +252,13 @@ class DVM: if input_type == "job": evt = get_referenced_event_by_id(event_id=input, client=client, kinds=EventDefinitions.ANY_RESULT, - dvm_config=dvmconfig) + dvm_config=dvm_config) if evt is None: if append: job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) self.jobs_on_hold_list.append(job) - send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, - dvm_config=dvmconfig) + send_job_status_reaction(nevent, "chain-scheduled", True, 0, + client=client, dvm_config=dvm_config) return False else: @@ -286,8 +293,10 @@ class DVM: if is_from_bot: # Reply to Bot + original_sender = "" + receiver_key = PublicKey for tag in original_event.tags(): - if tag.as_vec()[0] == "y": # TODO we temporally use internal tags to move information + if tag.as_vec()[0] == "y": # TODO we temporally use internal tags in Kind4 to move information receiver_key = PublicKey.from_hex(tag.as_vec()[1]) elif tag.as_vec()[0] == "z": original_sender = tag.as_vec()[1] @@ -297,7 +306,7 @@ class DVM: "sender": original_sender } message = json.dumps(params) - print(message) + print("[" + self.dvm_config.NIP89.name + "] " + message) response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) send_event(response_event, client=self.client, dvm_config=self.dvm_config) @@ -449,29 +458,29 @@ class DVM: send_job_status_reaction(event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) - print("do work from joblist") + print("[" + self.dvm_config.NIP89.name + "] doing work from joblist") do_work(event, is_from_bot=False) elif check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config) is None: # invoice expired try: self.job_list.remove(job) except: - print("Error removing Job from List after payment") + print("[" + self.dvm_config.NIP89.name + "] Error removing Job from List after payment") if Timestamp.now().as_secs() > job.expires: try: self.job_list.remove(job) except: - print("Error removing Job from List after expiry") + print("[" + self.dvm_config.NIP89.name + "] Error removing Job from List after expiry") for job in self.jobs_on_hold_list: if check_event_has_not_unfinished_job_input(job.event, False, client=self.client, - dvmconfig=self.dvm_config): + dvm_config=self.dvm_config): handle_nip90_job_event(nip90_event=job.event) try: self.jobs_on_hold_list.remove(job) except: - print("Error removing Job on Hold from List after expiry") + print("[" + self.dvm_config.NIP89.name + "] Error removing Job on Hold from List after expiry") if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. self.jobs_on_hold_list.remove(job) diff --git a/utils/database_utils.py b/utils/database_utils.py index a022645..426c7f8 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import timedelta from logging import Filter -from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter, Client, Options +from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter from utils.nostr_utils import send_event @@ -195,11 +195,6 @@ def get_or_add_user(db, npub, client, config): return user - -class DvmConfig: - pass - - def fetch_user_metadata(npub, client): name = "" nip05 = "" @@ -208,8 +203,6 @@ def fetch_user_metadata(npub, client): print(f"\nGetting profile metadata for {pk.to_bech32()}...") profile_filter = Filter().kind(0).author(pk).limit(1) events = client.get_events_of([profile_filter], timedelta(seconds=5)) - #TODO, it seems our client is still subscribed after that - if len(events) > 0: latest_entry = events[0] latest_time = 0 diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index be918dd..cefca0e 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -1,5 +1,4 @@ from nostr_sdk import Tag, Keys, EventBuilder - from utils.nostr_utils import send_event diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index b30ee6e..eb79ab4 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,5 +1,5 @@ from datetime import timedelta -from nostr_sdk import Keys, Filter, Client, Alphabet, EventId, Options, Event, PublicKey +from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: diff --git a/utils/output_utils.py b/utils/output_utils.py index 6d1b0cd..90aaa0d 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -30,7 +30,8 @@ def post_process_result(anno, original_event): for i in str(each_row).split('\n'): result = result + i + "\n" result = replace_broken_words( - str(result).replace("\"", "").replace('[', "").replace(']', "").lstrip(None)) + str(result).replace("\"", "").replace('[', "").replace(']', + "").lstrip(None)) return result elif output_format == "text/vtt": @@ -47,7 +48,8 @@ def post_process_result(anno, original_event): result = result + str(convertstart) + " --> " + str( convertend) + "\n" + cleared_name + "\n\n" result = replace_broken_words( - str(result).replace("\"", "").replace('[', "").replace(']', "").lstrip(None)) + str(result).replace("\"", "").replace('[', "").replace(']', + "").lstrip(None)) return result elif output_format == "text/json" or output_format == "json": diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 2e6c372..728328c 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -100,7 +100,8 @@ def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig): def check_for_zapplepay(pubkey_hex: str, content: str): try: # Special case Zapplepay - if pubkey_hex == PublicKey.from_bech32("npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan").to_hex(): + if (pubkey_hex == PublicKey.from_bech32("npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan") + .to_hex()): real_sender_bech32 = content.replace("From: nostr:", "") pubkey_hex = PublicKey.from_bech32(real_sender_bech32).to_hex() return pubkey_hex