diff --git a/.env_example b/.env_example index a2cbb86..b88e846 100644 --- a/.env_example +++ b/.env_example @@ -1,3 +1,9 @@ +#This is needed for the test_client +NOSTR_TEST_CLIENT_PRIVATE_KEY = "a secret hex key for the test dvm client" +#This is needed for the (optional) bot +BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" + +#These are all for the playground and can be replaced and adjusted however needed NOSTR_PRIVATE_KEY = "a secret hexkey for some demo dvms" NOSTR_PRIVATE_KEY2 = "another secret hexkey for demo dvm with another key" BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" @@ -16,7 +22,6 @@ TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" #Backend Specific Options for tasks that require them - #nova-server is a local backend supporting some AI modules and needs to be installed separately, #if dvms supporting it should be used NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/bot.py b/bot.py index d23ead4..60d49b9 100644 --- a/bot.py +++ b/bot.py @@ -1,11 +1,13 @@ import json import time +from datetime import timedelta from threading import Thread -from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, Event +from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ + Event, Options from utils.admin_utils import admin_make_database_updates -from utils.database_utils import get_or_add_user, update_user_balance +from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table 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 @@ -18,14 +20,19 @@ class Bot: self.dvm_config = dvm_config self.admin_config = admin_config self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - self.client = Client(self.keys) + 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)) + .skip_disconnected_relays(skip_disconnected_relays)) + + self.client = Client.with_opts(self.keys, opts) self.job_list = [] pk = self.keys.public_key() self.dvm_config.DB = "db/bot.db" print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + - ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) @@ -35,6 +42,7 @@ class Bot: Timestamp.now()) self.client.subscribe([dm_zap_filter]) + create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) class NotificationHandler(HandleNotification): @@ -55,7 +63,6 @@ class Bot: return def handle_dm(nostr_event): - sender = nostr_event.pubkey().to_hex() try: decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) # TODO more advanced logic, more parsing, just very basic test functions for now @@ -63,11 +70,11 @@ class Bot: index = int(decrypted_text.split(' ')[0]) - 1 i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") - keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_TASKS[index].PK) + keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK) params = { "sender": nostr_event.pubkey().to_hex(), "input": i_tag, - "task": self.dvm_config.SUPPORTED_TASKS[index].TASK + "task": self.dvm_config.SUPPORTED_DVMS[index].TASK } message = json.dumps(params) evt = EventBuilder.new_encrypted_direct_msg(self.keys, keys.public_key(), @@ -77,20 +84,20 @@ class Bot: elif decrypted_text.startswith('{"result":'): dvm_result = json.loads(decrypted_text) + # user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) + # print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) + reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, + PublicKey.from_hex(dvm_result["sender"]), + dvm_result["result"], + None).to_event(self.keys) + send_event(reply_event, client=self.client, dvm_config=dvm_config) - job_event = EventBuilder.new_encrypted_direct_msg(self.keys, - PublicKey.from_hex(dvm_result["sender"]), - dvm_result["result"], - None).to_event(self.keys) - send_event(job_event, client=self.client, dvm_config=dvm_config) - user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) - print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) else: message = "DVMs that I support:\n\n" index = 1 - for p in self.dvm_config.SUPPORTED_TASKS: + for p in self.dvm_config.SUPPORTED_DVMS: message += str(index) + " " + p.NAME + " " + p.TASK + "\n" index += 1 @@ -136,7 +143,6 @@ class Bot: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) - print(str(user.name)) if zapped_event is not None: if not anon: diff --git a/dvm.py b/dvm.py index 6201646..b392181 100644 --- a/dvm.py +++ b/dvm.py @@ -1,7 +1,8 @@ import json +from datetime import timedelta from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel, nip04_decrypt + init_logger, LogLevel, nip04_decrypt, EventId, Options import time @@ -35,14 +36,20 @@ class DVM: self.dvm_config = dvmconfig self.admin_config = adminconfig self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) - self.client = Client(self.keys) + 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)) + .skip_disconnected_relays(skip_disconnected_relays)) + + self.client = Client.with_opts(self.keys, opts) + self.job_list = [] self.jobs_on_hold_list = [] pk = self.keys.public_key() print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + - ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) @@ -50,10 +57,9 @@ class DVM: 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(Timestamp.now()) - #TODO only from allowed account kinds = [EventDefinitions.KIND_NIP90_GENERIC] - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: if dvm.KIND not in kinds: kinds.append(dvm.KIND) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) @@ -96,7 +102,7 @@ class DVM: return task_is_free = False - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: if dvm.TASK == task and dvm.COST == 0: task_is_free = True @@ -161,7 +167,7 @@ class DVM: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) - print(str(user)) + if zapped_event is not None: if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped @@ -228,12 +234,19 @@ class DVM: decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content()) ob = json.loads(decrypted_text) - #TODO SOME PARSING, OPTIONS, ZAP HANDLING + # One key might host multiple DVMs, so we check current task + if ob['task'] == self.dvm_config.SUPPORTED_DVMS[0].TASK: + input_type = "text" + print(decrypted_text) + if str(ob['input']).startswith("http"): + input_type = "url" + #elif str(ob['input']).startswith("nostr:nevent"): + # ob['input'] = str(ob['input']).replace("nostr:", "") + # ob['input'] = EventId.from_bech32(ob['input']).to_hex() + # input_type = "event" - # One key might host multiple dvms, so we check current task - if ob['task'] == self.dvm_config.SUPPORTED_TASKS[0].TASK: - j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_TASKS[0].TASK]) - i_tag = Tag.parse(["i", ob['input'], "text"]) + 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']])) @@ -294,7 +307,9 @@ class DVM: try: post_processed_content = post_process_result(data, original_event) + if is_from_bot: + # Reply to Bot for tag in original_event.tags(): if tag.as_vec()[0] == "y": # TODO we temporally use internal tags to move information receiver_key = PublicKey.from_hex(tag.as_vec()[1]) @@ -306,9 +321,11 @@ class DVM: "sender": original_sender } message = json.dumps(params) + print(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) else: + #Regular DVM reply send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: respond_to_error(str(e), original_event_str, False) @@ -427,13 +444,11 @@ class DVM: return event.as_json() def do_work(job_event, is_from_bot=False): - if (( - EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) + if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) or job_event.kind() == EventDefinitions.KIND_DM): task = get_task(job_event, client=self.client, dvmconfig=self.dvm_config) - result = "" - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: try: if task == dvm.TASK: request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, diff --git a/main.py b/main.py index dc999a3..d968c40 100644 --- a/main.py +++ b/main.py @@ -41,7 +41,7 @@ def run_nostr_dvm_with_local_config(): bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Finally we add some of the DVMs we created before to the Bot and start it. - bot_config.SUPPORTED_TASKS = [sketcher, unstable_artist, translator] + bot_config.SUPPORTED_DVMS = [sketcher, unstable_artist, translator] bot = Bot nostr_dvm_thread = Thread(target=bot, args=[bot_config]) diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 0af0be0..24e6027 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -32,7 +32,7 @@ class ImageGenerationSDXL(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 80121d0..3e4453a 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -31,7 +31,7 @@ class TextExtractionPDF(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config diff --git a/tasks/translation.py b/tasks/translation.py index 9596927..331d984 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -29,13 +29,12 @@ class Translation(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config self.admin_config = admin_config - def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": return False diff --git a/utils/backend_utils.py b/utils/backend_utils.py index bd56e96..22617d0 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -83,12 +83,12 @@ def check_task_is_supported(event, client, get_duration=False, config=None): print("Output format not supported, skipping..") return False, "", 0 - for dvm in dvm_config.SUPPORTED_TASKS: + 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 - if task not in (x.TASK for x in dvm_config.SUPPORTED_TASKS): + if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS): return False, task, duration return True, task, duration @@ -118,7 +118,7 @@ def check_url_is_readable(url): def get_amount_per_task(task, dvm_config, duration=1): - for dvm in dvm_config.SUPPORTED_TASKS: #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 diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 5e16096..679597e 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -4,14 +4,14 @@ from utils.nip89_utils import NIP89Announcement class DVMConfig: - SUPPORTED_TASKS = [] + SUPPORTED_DVMS= [] PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", "wss://relay.f7z.io"] - RELAY_TIMEOUT = 5 + RELAY_TIMEOUT = 3 LNBITS_INVOICE_KEY = '' LNBITS_URL = 'https://lnbits.com' DB: str