From 028c32f42d8fb4638d1125ee60368365b9292f0e Mon Sep 17 00:00:00 2001 From: Believethehype Date: Thu, 30 Nov 2023 15:30:49 +0100 Subject: [PATCH] auto generate keys/dtagsl auto update user profiles, sdimg2img --- .env_example | 38 ++---- .gitignore | 1 + main.py | 47 ++++--- playground.py | 157 ++++++++++++++-------- tasks/README.md | 19 +-- tasks/imagegeneration_sdxl.py | 4 +- tasks/imagegeneration_sdxlimg2img.py | 186 +++++++++++++++++++++++++++ tests/test_dvm_client.py | 20 +-- utils/admin_utils.py | 48 +++---- utils/backend_utils.py | 32 ++++- utils/nip89_utils.py | 35 +++-- utils/nostr_utils.py | 61 ++++++++- 12 files changed, 493 insertions(+), 155 deletions(-) create mode 100644 tasks/imagegeneration_sdxlimg2img.py diff --git a/.env_example b/.env_example index 9673bea..6f01954 100644 --- a/.env_example +++ b/.env_example @@ -1,38 +1,16 @@ -#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" -NOSTR_PRIVATE_KEY3 = "another secret hexkey for demo dvm with another key" -NOSTR_PRIVATE_KEY4 = "another secret hexkey for demo dvm with another key" -NOSTR_PRIVATE_KEY5 = "another secret hexkey for demo dvm with another key" - -BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" -NOSTR_TEST_CLIENT_PRIVATE_KEY = "a secret hex key for the test dvm client" - - -# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile, make sure to set one) +# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile) +# Admin Key is (only) required for bot or if any payments should be made LNBITS_INVOICE_KEY = "" LNBITS_ADMIN_KEY = "" # In order to pay invoices, e.g. from the bot to DVMs, or reimburse users. Keep this secret and use responsibly. LNBITS_HOST = "https://lnbits.com" +#Backend Specific Options for tasks that require them. A DVM needing these should only be started if these are set. -# Some d tags we use in the testfile to announce or dvms. Create one at vendata.io) -TASK_TEXT_EXTRACTION_NIP89_DTAG = "asdd" -TASK_TRANSLATION_NIP89_DTAG = "abcded" -TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" -TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" -TASK_IMAGE_GENERATION_NIP89_DTAG3 = "asdasd" -TASK_SPEECH_TO_TEXT_NIP89 = "asdasdas" -TASK_MEDIA_CONVERTER_NIP89_DTAG = "asdasdasd" -TASK_DISCOVER_INACTIVE_NIP89_DTAG = "sdfdfsdf12312" - - -#Backend Specific Options for tasks that require inputs, such as Endpoints or API Keys OPENAI_API_KEY = "" # Enter your OpenAI API Key to use DVMs with OpenAI services LIBRE_TRANSLATE_ENDPOINT = "" # Url to LibreTranslate Endpoint e.g. https://libretranslate.com LIBRE_TRANSLATE_API_KEY = "" # API Key, if required (You can host your own instance where you don't need it) -NOVA_SERVER = "" # Enter the address of a nova-server instance, locally or on a machine in your network host:port \ No newline at end of file +NOVA_SERVER = "" # Enter the address of a nova-server instance, locally or on a machine in your network host:port + +# We will automatically create dtags and private keys based on the identifier variable in main. +# If your DVM already has a dtag and private key you can replace it here before publishing the DTAG to not create a new one. +# The name and NIP90 info of the DVM can be changed but the identifier must stay the same in order to not create a different dtag. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ebdb3a..5b1c025 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,4 @@ cython_debug/ *.db outputs +.env2 diff --git a/main.py b/main.py index b135334..64fcb1e 100644 --- a/main.py +++ b/main.py @@ -1,26 +1,24 @@ import os import signal -import sys import time from pathlib import Path -from threading import Thread - import dotenv -from nostr_sdk import Keys from bot.bot import Bot from playground import build_pdf_extractor, build_googletranslator, build_unstable_diffusion, build_sketcher, \ build_dalle, \ - build_whisperx, build_libretranslator, build_external_dvm, build_media_converter, build_inactive_follows_finder + build_whisperx, build_libretranslator, build_external_dvm, build_media_converter, build_inactive_follows_finder, \ + build_image_converter from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig +from utils.nostr_utils import check_and_set_private_key def run_nostr_dvm_with_local_config(): # 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() - bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") + bot_config.PRIVATE_KEY = check_and_set_private_key("bot") 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") @@ -29,38 +27,46 @@ 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") + pdfextractor = build_pdf_extractor("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 TranslationGoogle, calling the free Google API. - translator = build_googletranslator("Google Translator") + translator = build_googletranslator("Google Translator", "google_translator") bot_config.SUPPORTED_DVMS.append(translator) # We add translator to the bot translator.run() # Spawn DVM3 Kind 5002 Local Text TranslationLibre, calling the free LibreTranslateApi, as an alternative. # This will only run and appear on the bot if an endpoint is set in the .env if os.getenv("LIBRE_TRANSLATE_ENDPOINT") is not None and os.getenv("LIBRE_TRANSLATE_ENDPOINT") != "": - libre_translator = build_libretranslator("Libre Translator") + libre_translator = build_libretranslator("Libre Translator", "google_translator") bot_config.SUPPORTED_DVMS.append(libre_translator) # We add translator to the bot libre_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") + unstable_artist = build_unstable_diffusion("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") + sketcher = build_sketcher("Sketcher", "sketcher") bot_config.SUPPORTED_DVMS.append(sketcher) # We also add Sketcher to the bot sketcher.run() + + # Spawn DVM5, image-to-image, . + if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": + imageconverter = build_image_converter("Image Converter Inkpunk", "image_converter_inkpunk") + bot_config.SUPPORTED_DVMS.append(imageconverter) # We also add Sketcher to the bot + imageconverter.run() + + # Spawn DVM5, Another script on nova-server calling WhisperX to transcribe media files if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": - whisperer = build_whisperx("Whisperer") + whisperer = build_whisperx("Whisperer", "whisperx") bot_config.SUPPORTED_DVMS.append(whisperer) # We also add Sketcher to the bot whisperer.run() @@ -68,7 +74,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") + dalle = build_dalle("Dall-E 3", "dalle3") bot_config.SUPPORTED_DVMS.append(dalle) dalle.run() @@ -82,7 +88,8 @@ def run_nostr_dvm_with_local_config(): tasktiger_external.SUPPORTS_ENCRYPTION = False # if the dvm does not support encrypted events, just send a regular event and mark it with p tag. Other dvms might initial answer bot_config.SUPPORTED_DVMS.append(tasktiger_external) - # Don't run it, it's on someone else's machine and we simply make the bot aware of it. + # Don't run it, it's on someone else's machine, and we simply make the bot aware of it. + # DVM: 8 Another external dvm for recommendations: ymhm_external = build_external_dvm(name="External DVM: You might have missed", @@ -95,16 +102,18 @@ def run_nostr_dvm_with_local_config(): bot_config.SUPPORTED_DVMS.append(ymhm_external) # Spawn DVM9.. A Media Grabber/Converter - media_bringer = build_media_converter("Media Bringer") - bot_config.SUPPORTED_DVMS.append(media_bringer) # We also add Sketcher to the bot + media_bringer = build_media_converter("Media Bringer", "media_converter") + bot_config.SUPPORTED_DVMS.append(media_bringer) media_bringer.run() - #Spawn DVM10 Discover inactive followers - discover_inactive = build_inactive_follows_finder("Bygones") - bot_config.SUPPORTED_DVMS.append(discover_inactive) # We also add Sketcher to the bot + # Spawn DVM10 Discover inactive followers + discover_inactive = build_inactive_follows_finder("Bygones", "discovery_inactive_follows") + bot_config.SUPPORTED_DVMS.append(discover_inactive) discover_inactive.run() + + Bot(bot_config) # Keep the main function alive for libraries that require it, like openai diff --git a/playground.py b/playground.py index 8b277ec..791b236 100644 --- a/playground.py +++ b/playground.py @@ -1,37 +1,36 @@ import json import os -from nostr_sdk import PublicKey, Keys +from nostr_sdk import PublicKey from interfaces.dvmtaskinterface import DVMTaskInterface from tasks.convert_media import MediaConverter from tasks.discovery_inactive_follows import DiscoverInactiveFollows from tasks.imagegeneration_openai_dalle import ImageGenerationDALLE from tasks.imagegeneration_sdxl import ImageGenerationSDXL +from tasks.imagegeneration_sdxlimg2img import ImageGenerationSDXLIMG2IMG from tasks.textextraction_whisperx import SpeechToTextWhisperX from tasks.textextraction_pdf import TextExtractionPDF from tasks.translation_google import TranslationGoogle from tasks.translation_libretranslate import TranslationLibre from utils.admin_utils import AdminConfig -from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig -from utils.nip89_utils import NIP89Config, nip89_create_d_tag +from utils.nip89_utils import NIP89Config, check_and_set_d_tag +from utils.nostr_utils import check_and_set_private_key """ This File is a playground to create DVMs. It shows some examples of DVMs that make use of the modules in the tasks folder -These DVMs should be considered examples and will be extended in the future. env variables are used to not commit keys, -but if used privatley, these can also be directly filled in this file. The main.py function calls some of the functions -defined here and starts the DVMs. +These DVMs should be considered examples and will be extended in the future. +Keys and dtags will be automatically generated and stored in the .env file. +If you already have a pk and dtag you can replace them there before publishing the nip89 + Note that the admin_config is optional, and if given commands as defined in admin_utils will be called at start of the -DVM. For example the NIP89 event can be rebroadcasted (store the d_tag somewhere). +DVM. For example the NIP89 event can be rebroadcasted. -DM_ALLOWED is used to tell the DVM to which npubs it should listen to. We use this here to listen to our bot, -as defined in main.py to perform jobs on it's behalf and reply. +If LNBITS_INVOICE_KEY is not set (=""), the DVM is still zappable but a lud16 address in required in the profile. -if LNBITS_INVOICE_KEY is not set (=""), the DVM is still zappable but a lud16 address in required in the profile. - -additional options can be set, for example to preinitalize vaiables or give parameters that are required to perform a +Additional options can be set, for example to preinitalize vaiables or give parameters that are required to perform a task, for example an address or an API key. @@ -39,17 +38,28 @@ task, for example an address or an API key. # Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast # their NIP89 announcement +# You can create individual admins configs and hand them over when initializing the dvm, +# for example to whilelist users or add to their balance. +# If you use this global config, options will be set for all dvms that use it. admin_config = AdminConfig() admin_config.REBROADCAST_NIP89 = False - - # Set rebroadcast to true once you have set your NIP89 descriptions and d tags. You only need to rebroadcast once you # want to update your NIP89 descriptions +admin_config.UPDATE_PROFILE = False +admin_config.LUD16 = "" -def build_pdf_extractor(name): + +# Auto update the profiles of your privkeys based on the nip89 information. +# Ideally set a lightning address in the LUD16 field above so our DVMs can get zapped from everywhere + + +# We build a couple of example dvms, create privatekeys and dtags and set their NIP89 descriptions +# We currently call these from the main function and start the dvms there. + +def build_pdf_extractor(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Add NIP89 @@ -62,15 +72,15 @@ def build_pdf_extractor(name): } nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_TEXT_EXTRACTION_NIP89_DTAG") + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return TextExtractionPDF(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) -def build_googletranslator(name): +def build_googletranslator(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -94,14 +104,15 @@ def build_googletranslator(name): "nip90Params": nip90params } nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_TRANSLATION_NIP89_DTAG") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return TranslationGoogle(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) -def build_libretranslator(name): +def build_libretranslator(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY5") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -127,15 +138,16 @@ def build_libretranslator(name): "nip90Params": nip90params } nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_TRANSLATION_NIP89_DTAG6") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return TranslationLibre(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) -def build_unstable_diffusion(name): +def build_unstable_diffusion(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = "" # This one will not use Lnbits to create invoices, but rely on zaps dvm_config.LNBITS_URL = "" @@ -160,15 +172,16 @@ def build_unstable_diffusion(name): "nip90Params": nip90params } nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) -def build_whisperx(name): +def build_whisperx(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY4") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -194,15 +207,16 @@ def build_whisperx(name): "nip90Params": nip90params } nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_SPEECH_TO_TEXT_NIP89") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return SpeechToTextWhisperX(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) -def build_sketcher(name): +def build_sketcher(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") @@ -228,16 +242,58 @@ def build_sketcher(name): options = {'default_model': "mohawk", 'default_lora': "timburton", 'nova_server': os.getenv("NOVA_SERVER")} nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) -def build_dalle(name): +def build_image_converter(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + + nip90params = { + "negative_prompt": { + "required": False, + "values": [] + }, + "lora": { + "required": False, + "values": ["inkpunk", "timburton", "voxel"] + }, + + "strength": { + "required": False, + "values": [] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg", + "about": "I convert an image to another image, kinda random for now. ", + "nip90Params": nip90params + } + + # A module might have options it can be initialized with, here we set a default model, lora and the nova-server + options = {'default_lora': "inkpunk", 'strength': 0.5, 'nova_server': os.getenv("NOVA_SERVER")} + + nip89config = NIP89Config() + + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) + # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + return ImageGenerationSDXLIMG2IMG(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config, options=options) + + +def build_dalle(name, identifier): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") profit_in_sats = 10 @@ -260,15 +316,16 @@ def build_dalle(name): # address it should use. These parameters can be freely defined in the task component nip89config = NIP89Config() - nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG3") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) -def build_media_converter(name): +def build_media_converter(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY6") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Add NIP89 @@ -286,19 +343,15 @@ def build_media_converter(name): } nip89config = NIP89Config() - new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(), - nip89info["image"]) - print("Some new dtag:" + new_dtag) - nip89config.DTAG = os.getenv("TASK_MEDIA_CONVERTER_NIP89_DTAG") + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) return MediaConverter(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) - -def build_inactive_follows_finder(name): +def build_inactive_follows_finder(name, identifier): dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY7") + dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier) dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Add NIP89 @@ -323,14 +376,16 @@ def build_inactive_follows_finder(name): } nip89config = NIP89Config() - new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex(), - nip89info["image"]) - print("Some new dtag:" + new_dtag) - nip89config.DTAG = os.getenv("TASK_DISCOVER_INACTIVE_NIP89_DTAG") + nip89config.DTAG = nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, + nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) return DiscoverInactiveFollows(name=name, dvm_config=dvm_config, nip89config=nip89config, - admin_config=admin_config) + admin_config=admin_config) + +# This function can be used to build a DVM object for a DVM we don't control, but we want the bot to be aware of. +# See main.py for examples. def build_external_dvm(name, pubkey, task, kind, fix_cost, per_unit_cost): dvm_config = DVMConfig() dvm_config.PUBLIC_KEY = PublicKey.from_hex(pubkey).to_hex() @@ -346,8 +401,8 @@ def build_external_dvm(name, pubkey, task, kind, fix_cost, per_unit_cost): return DVMTaskInterface(name=name, dvm_config=dvm_config, nip89config=nip89config, task=task) -# Little Gimmick: -# For Dalle where we have to pay 4cent per image, we fetch current sat price in fiat +# Little optional Gimmick: +# For Dalle where we have to pay 4cent per image to openai, we fetch current sat price in fiat from coinstats api # and update cost at each start def get_price_per_sat(currency): import requests diff --git a/tasks/README.md b/tasks/README.md index 39b2502..54c1dc6 100644 --- a/tasks/README.md +++ b/tasks/README.md @@ -6,12 +6,13 @@ Reusable backend functions can be defined in backends (e.g. API calls) Current List of Tasks: -| Module | Kind | Description | Backend | -|----------------------|------|------------------------------------------------|-------------| -| TextExtractionPDF | 5000 | Extracts Text from a PDF file | local | -| SpeechToTextWhisperX | 5000 | Extracts Speech from Media files | nova-server | -| TranslationGoogle | 5002 | Translates Inputs to another language | google API | -| TranslationLibre | 5002 | Translates Inputs to another language | libre API | -| ImageGenerationSDXL | 5100 | Generates an Image with StableDiffusionXL | nova-server | -| ImageGenerationDALLE | 5100 | Generates an Image with Dall-E | openAI | -| MediaConverter | 5300 | Converts a link of a media file and uploads it | openAI | \ No newline at end of file +| Module | Kind | Description | Backend | +|--------------------------|------|------------------------------------------------|-------------| +| TextExtractionPDF | 5000 | Extracts Text from a PDF file | local | +| SpeechToTextWhisperX | 5000 | Extracts Speech from Media files | nova-server | +| TranslationGoogle | 5002 | Translates Inputs to another language | googleAPI | +| TranslationLibre | 5002 | Translates Inputs to another language | libreAPI | +| ImageGenerationSDXL | 5100 | Generates an Image with StableDiffusionXL | nova-server | +| ImageGenerationDALLE | 5100 | Generates an Image with Dall-E | openAI | +| MediaConverter | 5200 | Converts a link of a media file and uploads it | openAI | +| DiscoverInactiveFollows | 5301 | Find inactive Nostr users | local | \ No newline at end of file diff --git a/tasks/imagegeneration_sdxl.py b/tasks/imagegeneration_sdxl.py index a2fc6ec..c7657ea 100644 --- a/tasks/imagegeneration_sdxl.py +++ b/tasks/imagegeneration_sdxl.py @@ -82,9 +82,9 @@ class ImageGenerationSDXL(DVMTaskInterface): elif tag.as_vec()[1] == "lora_weight": lora_weight = tag.as_vec()[2] elif tag.as_vec()[1] == "strength": - strength = tag.as_vec()[2] + strength = float(tag.as_vec()[2]) elif tag.as_vec()[1] == "guidance_scale": - guidance_scale = tag.as_vec()[2] + guidance_scale = float(tag.as_vec()[2]) elif tag.as_vec()[1] == "ratio": if len(tag.as_vec()) > 3: ratio_width = (tag.as_vec()[2]) diff --git a/tasks/imagegeneration_sdxlimg2img.py b/tasks/imagegeneration_sdxlimg2img.py new file mode 100644 index 0000000..f764585 --- /dev/null +++ b/tasks/imagegeneration_sdxlimg2img.py @@ -0,0 +1,186 @@ +import json +from multiprocessing.pool import ThreadPool + +from backends.nova_server import check_nova_server_status, send_request_to_nova_server +from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config +from utils.definitions import EventDefinitions + +""" +This File contains a Module to transform Text input on NOVA-Server and receive results back. + +Accepted Inputs: Prompt (text) +Outputs: An url to an Image +Params: -model # models: juggernaut, dynavision, colossusProject, newreality, unstable + -lora # loras (weights on top of models) voxel, +""" + + +class ImageGenerationSDXLIMG2IMG(DVMTaskInterface): + KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + TASK: str = "image-to-image" + FIX_COST: float = 50 + + 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, tags): + hasurl = False + hasprompt = False + for tag in tags: + if tag.as_vec()[0] == 'i': + input_value = tag.as_vec()[1] + input_type = tag.as_vec()[2] + if input_type == "url": + hasurl = True + elif input_type == "text": + hasprompt = True #Little optional when lora is set + + elif tag.as_vec()[0] == 'output': + output = tag.as_vec()[1] + if (output == "" or + not (output == "image/png" or "image/jpg" + or output == "image/png;format=url" or output == "image/jpg;format=url")): + print("Output format not supported, skipping..") + return False + + if not hasurl: + return False + + return True + + def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): + request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")} + request_form["trainerFilePath"] = r'modules\stablediffusionxl\stablediffusionxl-img2img.trainer' + + prompt = "" + negative_prompt = "" + url = "" + if self.options.get("default_model"): + model = self.options['default_model'] + else: + model = "stabilityai/stable-diffusion-xl-refiner-1.0" + + ratio_width = "1" + ratio_height = "1" + width = "" + height = "" + + if self.options.get("default_lora"): + lora = self.options['default_lora'] + else: + lora = "" + + lora_weight = "" + if self.options.get("strength"): + strength = float(self.options['strength']) + else: + strength = 0.8 + if self.options.get("guidance_scale"): + guidance_scale = float(self.options['guidance_scale']) + else: + guidance_scale = 11.0 + for tag in event.tags(): + if tag.as_vec()[0] == 'i': + input_type = tag.as_vec()[2] + if input_type == "text": + prompt = tag.as_vec()[1] + elif input_type == "url": + url = tag.as_vec()[1] + + elif tag.as_vec()[0] == 'param': + print("Param: " + tag.as_vec()[1] + ": " + tag.as_vec()[2]) + if tag.as_vec()[1] == "negative_prompt": + negative_prompt = tag.as_vec()[2] + elif tag.as_vec()[1] == "lora": + lora = tag.as_vec()[2] + elif tag.as_vec()[1] == "lora_weight": + lora_weight = tag.as_vec()[2] + elif tag.as_vec()[1] == "strength": + strength = float(tag.as_vec()[2]) + elif tag.as_vec()[1] == "guidance_scale": + guidance_scale = float(tag.as_vec()[2]) + elif tag.as_vec()[1] == "ratio": + if len(tag.as_vec()) > 3: + ratio_width = (tag.as_vec()[2]) + ratio_height = (tag.as_vec()[3]) + elif len(tag.as_vec()) == 3: + split = tag.as_vec()[2].split(":") + ratio_width = split[0] + ratio_height = split[1] + # if size is set it will overwrite ratio. + elif tag.as_vec()[1] == "size": + if len(tag.as_vec()) > 3: + width = (tag.as_vec()[2]) + height = (tag.as_vec()[3]) + elif len(tag.as_vec()) == 3: + split = tag.as_vec()[2].split("x") + if len(split) > 1: + width = split[0] + height = split[1] + elif tag.as_vec()[1] == "model": + model = tag.as_vec()[2] + + + + + + io_input_image = { + "id": "input_image", + "type": "input", + "src": "url:Image", + "uri": url + } + io_input = { + "id": "input_prompt", + "type": "input", + "src": "request:text", + "data": prompt + } + io_negative = { + "id": "negative_prompt", + "type": "input", + "src": "request:text", + "data": negative_prompt + } + io_output = { + "id": "output_image", + "type": "output", + "src": "request:image" + } + + request_form['data'] = json.dumps([io_input_image, io_input, io_negative, io_output]) + + options = { + "model": model, + "ratio": ratio_width + '-' + ratio_height, + "width": width, + "height": height, + "strength": strength, + "guidance_scale": guidance_scale, + "lora": lora, + "lora_weight": lora_weight, + "n_steps": 30 + } + request_form['options'] = json.dumps(options) + + return request_form + + def process(self, request_form): + try: + # Call the process route of NOVA-Server with our request form. + response = send_request_to_nova_server(request_form, self.options['nova_server']) + if bool(json.loads(response)['success']): + print("Job " + request_form['jobID'] + " sent to NOVA-server") + + pool = ThreadPool(processes=1) + thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], self.options['nova_server'])) + print("Wait for results of NOVA-Server...") + result = thread.get() + return result + + except Exception as e: + raise Exception(e) diff --git a/tests/test_dvm_client.py b/tests/test_dvm_client.py index 4a78fa9..f03a5db 100644 --- a/tests/test_dvm_client.py +++ b/tests/test_dvm_client.py @@ -10,13 +10,13 @@ from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotificatio nip04_encrypt from utils.dvmconfig import DVMConfig -from utils.nostr_utils import send_event +from utils.nostr_utils import send_event, check_and_set_private_key from utils.definitions import EventDefinitions # TODO HINT: Best use this path with a previously whitelisted privkey, as zapping events is not implemented in the lib/code def nostr_client_test_translation(input, kind, lang, sats, satsmax): - keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) if kind == "text": iTag = Tag.parse(["i", input, "text"]) elif kind == "event": @@ -43,7 +43,7 @@ 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")) + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) iTag = Tag.parse(["i", prompt, "text"]) outTag = Tag.parse(["output", "image/png;format=url"]) @@ -70,8 +70,8 @@ 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")) + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) + receiver_keys = Keys.from_sk_str(check_and_set_private_key("sketcher")) # TODO more advanced logic, more parsing, params etc, just very basic test functions for now @@ -109,7 +109,7 @@ def nostr_client_test_image_private(prompt, cashutoken): return nip90request.as_json() def nostr_client(): - keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) sk = keys.secret_key() pk = keys.public_key() print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") @@ -127,13 +127,13 @@ def nostr_client(): client.subscribe([dm_zap_filter, dvm_filter]) #nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) - #nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20) + nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20) #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20) #nostr_client_test_image("a beautiful purple ostrich watching the sunset") - cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MSwiQyI6IjAyNWU3ODZhOGFkMmExYTg0N2YxMzNiNGRhM2VhMGIyYWRhZGFkOTRiYzA4M2E2NWJjYjFlOTgwYTE1NGIyMDA2NCIsInNlY3JldCI6InQ1WnphMTZKMGY4UElQZ2FKTEg4V3pPck5rUjhESWhGa291LzVzZFd4S0U9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6NCwiQyI6IjAyOTQxNmZmMTY2MzU5ZWY5ZDc3MDc2MGNjZmY0YzliNTMzMzVmZTA2ZGI5YjBiZDg2Njg5Y2ZiZTIzMjVhYWUwYiIsInNlY3JldCI6IlRPNHB5WE43WlZqaFRQbnBkQ1BldWhncm44UHdUdE5WRUNYWk9MTzZtQXM9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MTYsIkMiOiIwMmRiZTA3ZjgwYmMzNzE0N2YyMDJkNTZiMGI3ZTIzZTdiNWNkYTBhNmI3Yjg3NDExZWYyOGRiZDg2NjAzNzBlMWIiLCJzZWNyZXQiOiJHYUNIdHhzeG9HM3J2WWNCc0N3V0YxbU1NVXczK0dDN1RKRnVwOHg1cURzPSJ9XSwibWludCI6Imh0dHBzOi8vbG5iaXRzLmJpdGNvaW5maXhlc3RoaXMub3JnL2Nhc2h1L2FwaS92MS9ScDlXZGdKZjlxck51a3M1eVQ2SG5rIn1dfQ==" - nostr_client_test_image_private("a beautiful ostrich watching the sunset", cashutoken ) + #cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MSwiQyI6IjAyNWU3ODZhOGFkMmExYTg0N2YxMzNiNGRhM2VhMGIyYWRhZGFkOTRiYzA4M2E2NWJjYjFlOTgwYTE1NGIyMDA2NCIsInNlY3JldCI6InQ1WnphMTZKMGY4UElQZ2FKTEg4V3pPck5rUjhESWhGa291LzVzZFd4S0U9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6NCwiQyI6IjAyOTQxNmZmMTY2MzU5ZWY5ZDc3MDc2MGNjZmY0YzliNTMzMzVmZTA2ZGI5YjBiZDg2Njg5Y2ZiZTIzMjVhYWUwYiIsInNlY3JldCI6IlRPNHB5WE43WlZqaFRQbnBkQ1BldWhncm44UHdUdE5WRUNYWk9MTzZtQXM9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MTYsIkMiOiIwMmRiZTA3ZjgwYmMzNzE0N2YyMDJkNTZiMGI3ZTIzZTdiNWNkYTBhNmI3Yjg3NDExZWYyOGRiZDg2NjAzNzBlMWIiLCJzZWNyZXQiOiJHYUNIdHhzeG9HM3J2WWNCc0N3V0YxbU1NVXczK0dDN1RKRnVwOHg1cURzPSJ9XSwibWludCI6Imh0dHBzOi8vbG5iaXRzLmJpdGNvaW5maXhlc3RoaXMub3JnL2Nhc2h1L2FwaS92MS9ScDlXZGdKZjlxck51a3M1eVQ2SG5rIn1dfQ==" + #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()}") @@ -161,7 +161,7 @@ def nostr_client(): if __name__ == '__main__': - env_path = Path('../.env') + env_path = Path('.env') if env_path.is_file(): print(f'loading environment from {env_path.resolve()}') dotenv.load_dotenv(env_path, verbose=True, override=True) diff --git a/utils/admin_utils.py b/utils/admin_utils.py index 96a1f54..69be854 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -6,18 +6,23 @@ from nostr_sdk import Keys, EventBuilder, PublicKey from utils.database_utils import get_from_sql_table, list_db, delete_from_sql_table, update_sql_table, \ get_or_add_user, clean_db from utils.dvmconfig import DVMConfig -from utils.nip89_utils import nip89_announce_tasks -from utils.nostr_utils import send_event +from utils.nip89_utils import nip89_announce_tasks, NIP89Config +from utils.nostr_utils import send_event, update_profile + class AdminConfig: REBROADCAST_NIP89: bool = False + UPDATE_PROFILE: bool = False WHITELISTUSER: bool = False UNWHITELISTUSER: bool = False BLACKLISTUSER: bool = False DELETEUSER: bool = False LISTDATABASE: bool = False ClEANDB: bool = False + USERNPUB: str = "" + LUD16: str = "" + def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMConfig = None, client=None): # This is called on start of Server, Admin function to manually whitelist/blacklist/add balance/delete users @@ -31,46 +36,43 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC and adminconfig.USERNPUB == ""): return + if adminconfig.UPDATE_PROFILE and (dvmconfig.NIP89 is None): + return db = dvmconfig.DB - rebroadcast_nip89 = adminconfig.REBROADCAST_NIP89 - cleandb = adminconfig.ClEANDB - listdatabase = adminconfig.LISTDATABASE - deleteuser = adminconfig.DELETEUSER - whitelistuser = adminconfig.WHITELISTUSER - unwhitelistuser = adminconfig.UNWHITELISTUSER - blacklistuser = adminconfig.BLACKLISTUSER + if str(adminconfig.USERNPUB).startswith("npub"): + publickey = PublicKey.from_bech32(adminconfig.USERNPUB).to_hex() + else: + publickey = adminconfig.USERNPUB - if adminconfig.USERNPUB != "": - if str(adminconfig.USERNPUB).startswith("npub"): - publickey = PublicKey.from_bech32(adminconfig.USERNPUB).to_hex() - else: - publickey = adminconfig.USERNPUB - - - if whitelistuser: + if adminconfig.WHITELISTUSER: user = get_or_add_user(db, publickey, client=client, config=dvmconfig) update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) user = get_from_sql_table(db, publickey) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) - if unwhitelistuser: + + if adminconfig.UNWHITELISTUSER: user = get_from_sql_table(db, publickey) update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive) - if blacklistuser: + if adminconfig.BLACKLISTUSER: user = get_from_sql_table(db, publickey) update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive) - if deleteuser: + if adminconfig.DELETEUSER: delete_from_sql_table(db, publickey) - if cleandb: + if adminconfig.ClEANDB: clean_db(db) - if listdatabase: + if adminconfig.LISTDATABASE: list_db(db) - if rebroadcast_nip89: + if adminconfig.REBROADCAST_NIP89: nip89_announce_tasks(dvmconfig, client=client) + + if adminconfig.UPDATE_PROFILE: + update_profile(dvmconfig, lud16=adminconfig.LUD16) + diff --git a/utils/backend_utils.py b/utils/backend_utils.py index fcba890..9808b19 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -5,7 +5,7 @@ from nostr_sdk import Event, Tag from utils.definitions import EventDefinitions from utils.mediasource_utils import check_source_type, media_source -from utils.nostr_utils import get_event_by_id +from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id def get_task(event, client, dvm_config): @@ -53,8 +53,38 @@ def get_task(event, client, dvm_config): return "unknown type" else: return "unknown job" + elif event.kind() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE: + has_image_tag = False + has_text_tag = False + 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]) + if file_type == "image": + has_image_tag = True + print("found image tag") + elif tag.as_vec()[2] == "job": + evt = get_referenced_event_by_id(event_id=tag.as_vec()[1], kinds= + [EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT, + EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT, + EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT], + client=client, + dvm_config=dvm_config) + if evt is not None: + file_type = check_url_is_readable(evt.content()) + if file_type == "image": + has_image_tag = True + elif tag.as_vec()[2] == "text": + has_text_tag = True + if has_image_tag: + return "image-to-image" + elif has_text_tag and not has_image_tag: + return "text-to-image" # TODO if a task can consist of multiple inputs add them here + # This is not ideal. Maybe such events should have their own kind + + # else if kind is supported, simply return task else: diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index ca1bf0d..40a47ee 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -1,6 +1,9 @@ +import os from datetime import timedelta from hashlib import sha256 +from pathlib import Path +import dotenv from nostr_sdk import Tag, Keys, EventBuilder, Filter, Alphabet, PublicKey, Event from utils.definitions import EventDefinitions @@ -16,21 +19,17 @@ class NIP89Config: def nip89_create_d_tag(name, pubkey, image): - #import hashlib - #m = hashlib.md5() - #m.update(str(name + image + pubkey).encode("utf-8")) - #d_tag = m.hexdigest()[0:16] - key_str = str(name + image + pubkey) d_tag = sha256(key_str.encode('utf-8')).hexdigest()[:16] return d_tag + def nip89_announce_tasks(dvm_config, client): - k_tag = Tag.parse(["k", str(dvm_config.NIP89.kind)]) - d_tag = Tag.parse(["d", dvm_config.NIP89.dtag]) - keys = Keys.from_sk_str(dvm_config.NIP89.pk) - content = dvm_config.NIP89.content + k_tag = Tag.parse(["k", str(dvm_config.NIP89.KIND)]) + d_tag = Tag.parse(["d", dvm_config.NIP89.DTAG]) + keys = Keys.from_sk_str(dvm_config.NIP89.PK) + content = dvm_config.NIP89.CONTENT event = EventBuilder(EventDefinitions.KIND_ANNOUNCEMENT, content, [k_tag, d_tag]).to_event(keys) send_event(event, client=client, dvm_config=dvm_config) print("Announced NIP 89 for " + dvm_config.NIP89.NAME) @@ -66,3 +65,21 @@ def nip89_fetch_events_pubkey(client, pubkey, kind): # should be one element of the kind now for dvm in dvms: return dvms[dvm].content() + + +def check_and_set_d_tag(identifier, name, pk, imageurl): + if not os.getenv("NIP89_DTAG_" + identifier.upper()): + new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(pk).public_key().to_hex(), + imageurl) + nip89_add_dtag_to_env_file("NIP89_DTAG_" + identifier.upper(), new_dtag) + print("Some new dtag:" + new_dtag) + return new_dtag + else: + return os.getenv("NIP89_DTAG_" + identifier.upper()) + +def nip89_add_dtag_to_env_file(dtag, oskey): + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + dotenv.set_key(env_path, dtag, oskey) \ No newline at end of file diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index 3969790..f64f47b 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,6 +1,10 @@ import json +import os from datetime import timedelta -from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt +from pathlib import Path + +import dotenv +from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt, Metadata, Options def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: @@ -99,6 +103,7 @@ def check_and_decrypt_tags(event, dvm_config): return event + def check_and_decrypt_own_tags(event, dvm_config): try: tags = [] @@ -131,3 +136,57 @@ def check_and_decrypt_own_tags(event, dvm_config): print(e) return event + + +def update_profile(dvm_config, lud16=""): + keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) + opts = (Options().wait_for_send(True).send_timeout(timedelta(seconds=dvm_config.RELAY_TIMEOUT)) + .skip_disconnected_relays(True)) + + client = Client.with_opts(keys, opts) + for relay in dvm_config.RELAY_LIST: + client.add_relay(relay) + client.connect() + + nip89content = json.loads(dvm_config.NIP89.CONTENT) + if nip89content.get("name"): + name = nip89content.get("name") + about = nip89content.get("about") + image = nip89content.get("image") + + + # Set metadata + metadata = Metadata() \ + .set_name(name) \ + .set_display_name(name) \ + .set_about(about) \ + .set_picture(image) \ + .set_lud16(lud16) + # .set_banner("https://example.com/banner.png") \ + # .set_nip05("username@example.com") \ + + print(f"Setting profile metadata for {keys.public_key().to_bech32()}...") + print(metadata.as_json()) + client.set_metadata(metadata) + client.disconnect() + + +def check_and_set_private_key(identifier): + if not os.getenv("DVM_PRIVATE_KEY_" + identifier.upper()): + pk = Keys.generate().secret_key().to_hex() + add_pk_to_env_file("DVM_PRIVATE_KEY_" + identifier.upper(), pk) + return pk + else: + return os.getenv("DVM_PRIVATE_KEY_" + identifier.upper()) + + +def add_pk_to_env_file(dtag, oskey): + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + dotenv.set_key(env_path, dtag, oskey) + + +def generate_private_key(): + return Keys.generate()