basic bot is working, spawn dvms dependent on config

This commit is contained in:
Believethehype
2023-11-24 21:29:24 +01:00
parent 215916c1ef
commit 6be3372d76
16 changed files with 263 additions and 279 deletions

View File

@@ -24,7 +24,5 @@ TASK_IMAGE_GENERATION_NIP89_DTAG3 = "asdasd"
#Backend Specific Options for tasks that require them #Backend Specific Options for tasks that require them
#nova-server is a local backend supporting some AI modules and needs to be installed separately, OPENAI_API_KEY = "" # Enter your OpenAI API Key to use DVMs with OpenAI services
#if dvms supporting it should be used NOVA_SERVER = "" # Enter the address of a nova-server instance, locally or on a machine in your network host:port
OPENAI_API_KEY = "your-openai-api-key"
NOVA_SERVER = "127.0.0.1:37318"

View File

@@ -1,19 +1,28 @@
# NostrAI Data Vending Machine # NostrAI: Nostr NIP90 Data Vending Machine Framework
This example DVM implementation in Python currently supports simple translations using Google translate, as well as extraction of text from links with pdf files. This framework provides a way to easily build and/or run `Nostr NIP90 DVMs in Python`.
At a later stage, additional example tasks will be added, as well as the integration into a larger Machine Learning backend This project is currently under development and additional tasks and features are added along the way.
This means the project is in alpha status, interfaces might still change/break.
Place .env file (based on .env_example) in main folder, install requirements.txt (python 3.10) run main.py. Optionally supports LNbits to create invoices instead of lnaddresses. ## To get started:
(Tested on Python 3.10)
Use vendata.io to create a nip89 announcement of your dvm and save the dtag in your .env config. Create a new venv by running `"python -m venv venv"`
- Place .env file (based on .env_example) in main folder.
- Set your own private hex keys, create NIP89 dtags on vendata.io,
- Install requirements.txt
- Run python main.py.
A tutorial on how to add additional tasks, as well as the larger server backend will be added soon. In `playground.py` some DVMs are already prepared. Feel free to play along with the existing ones.
You can also add new tasks by using the interface, just like the existing tasks in the `tasks` folder.
Known Issues: A `bot` is running by default that lists and communicates with the `DVMs` added to it,
- After refactoring DVMs work independent from each other for the most part. so your DVMs can be controled via any regular client as well.
- Some functions might work easier than they did before (need some refactoring)
- Bot currently not implemented The Framework optionally supports `LNbits` to create invoices instead of using a `lightning address`. If LNBits is not used,
- Some basic functionality is still missing, e.g. handling various mediasources make sure your nostr accounts have a valid lightning address.
- Interface might still change a lot and brick things.
A tutorial on how to add additional tasks, as well as the larger server backend will be added at a later stage.

95
bot.py
View File

@@ -1,6 +1,7 @@
import json import json
import time import time
from datetime import timedelta from datetime import timedelta
from threading import Thread
from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \
Event, Options Event, Options
@@ -10,7 +11,8 @@ 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.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User
from utils.definitions import EventDefinitions from utils.definitions import EventDefinitions
from utils.nostr_utils import send_event, get_event_by_id 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 from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message, \
parse_zap_event_tags
class Bot: class Bot:
@@ -28,7 +30,8 @@ class Bot:
pk = self.keys.public_key() pk = self.keys.public_key()
print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Name: " + self.NAME + " Supported DVM tasks: " + print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Name: " + self.NAME +
" Supported DVM tasks: " +
', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n")
for relay in self.dvm_config.RELAY_LIST: for relay in self.dvm_config.RELAY_LIST:
@@ -36,8 +39,7 @@ class Bot:
self.client.connect() self.client.connect()
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now())
dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since( dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since(Timestamp.now())
Timestamp.now())
self.client.subscribe([zap_filter, dm_filter]) self.client.subscribe([zap_filter, dm_filter])
@@ -50,7 +52,7 @@ class Bot:
keys = self.keys keys = self.keys
def handle(self, relay_url, nostr_event): def handle(self, relay_url, nostr_event):
if EventDefinitions.KIND_DM: if nostr_event.kind() == EventDefinitions.KIND_DM:
handle_dm(nostr_event) handle_dm(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_ZAP: elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
handle_zap(nostr_event) handle_zap(nostr_event)
@@ -63,24 +65,20 @@ class Bot:
try: try:
decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) 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) user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
# user = User
# user.npub = sender
# user.balance = 250
# user.iswhitelisted = False
# user.isblacklisted = False
# user.name = "Test"
# user.nip05 = "Test@test"
# user.lud16 = "Test@test"
# We do a selection of tasks now, maybe change this later, Idk. # We do a selection of tasks now, maybe change this later, Idk.
if decrypted_text[0].isdigit(): if decrypted_text[0].isdigit():
index = int(decrypted_text.split(' ')[0]) - 1 index = int(decrypted_text.split(' ')[0]) - 1
task = self.dvm_config.SUPPORTED_DVMS[index].TASK 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)) print("[" + self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) + ", Balance: "
+ 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)
required_amount = self.dvm_config.SUPPORTED_DVMS[index].COST
if user.isblacklisted: if user.isblacklisted:
# For some reason an admin might blacklist npubs, e.g. for abusing the service # For some reason an admin might blacklist npubs, e.g. for abusing the service
@@ -135,8 +133,8 @@ class Bot:
print("payment-required") print("payment-required")
time.sleep(2.0) time.sleep(2.0)
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
"Balance required, please zap me with at least " + str( "Balance required, please zap me with at least " +
int(required_amount - user.balance)) str(int(required_amount - user.balance))
+ " Sats, then try again.", + " Sats, then try again.",
nostr_event.id()).to_event(self.keys) nostr_event.id()).to_event(self.keys)
time.sleep(2.0) time.sleep(2.0)
@@ -149,7 +147,8 @@ class Bot:
dvm_result = json.loads(decrypted_text) dvm_result = json.loads(decrypted_text)
user_npub_hex = dvm_result["sender"] user_npub_hex = dvm_result["sender"]
user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex, client=self.client) user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex,
client=self.client, config=self.dvm_config)
print("[" + self.NAME + "] Received results, message to orignal sender " + user.name) print("[" + self.NAME + "] Received results, message to orignal sender " + user.name)
reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, reply_event = EventBuilder.new_encrypted_direct_msg(self.keys,
PublicKey.from_hex(user.npub), PublicKey.from_hex(user.npub),
@@ -159,7 +158,7 @@ class Bot:
send_event(reply_event, client=self.client, dvm_config=dvm_config) send_event(reply_event, client=self.client, dvm_config=dvm_config)
else: else:
print("Message from " + user.name + ": " + decrypted_text) print("[" + self.NAME + "] Message from " + user.name + ": " + decrypted_text)
message = "DVMs that I support:\n\n" message = "DVMs that I support:\n\n"
index = 1 index = 1
for p in self.dvm_config.SUPPORTED_DVMS: for p in self.dvm_config.SUPPORTED_DVMS:
@@ -173,61 +172,45 @@ class Bot:
#nostr_event.id()).to_event(self.keys) #nostr_event.id()).to_event(self.keys)
time.sleep(3) time.sleep(3)
send_event(evt, client=self.client, dvm_config=dvm_config) send_event(evt, client=self.client, dvm_config=dvm_config)
except Exception as e:
print(e)
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): def handle_zap(zap_event):
zapped_event = None print("[" + self.NAME + "] Zap received")
invoice_amount = 0
anon = False
sender = zap_event.pubkey()
print("Zap received")
try: try:
for tag in zap_event.tags(): invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event,
if tag.as_vec()[0] == 'bolt11': self.keys, self.NAME,
invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) self.client, self.dvm_config)
elif tag.as_vec()[0] == 'e':
zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) user = get_or_add_user(self.dvm_config.DB, sender, client=self.client, config=self.dvm_config)
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(),
zap_request_event.content())
for z_tag in zap_request_event.tags():
if z_tag.as_vec()[0] == 'anon':
if len(z_tag.as_vec()) > 1:
print("Private Zap received.")
decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1],
self.keys.secret_key(),
zap_request_event.pubkey())
decrypted_private_event = Event.from_json(decrypted_content)
if decrypted_private_event.kind() == 9733:
sender = decrypted_private_event.pubkey().to_hex()
message = decrypted_private_event.content()
if message != "":
print("Zap Message: " + message)
else:
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)
if zapped_event is not None: if zapped_event is not None:
if not anon: if not anon:
print("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)) user.name))
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
config=self.dvm_config) config=self.dvm_config)
# a regular note # a regular note
elif not anon: elif not anon:
print("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)) user.name))
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
config=self.dvm_config) config=self.dvm_config)
except Exception as e: except Exception as e:
print(f"Error during content decryption: {e}") print("[" + self.NAME + "] Error during content decryption:" + str(e))
self.client.handle_notifications(NotificationHandler()) self.client.handle_notifications(NotificationHandler())
while True: while True:
time.sleep(1.0) time.sleep(1.0)
def run(self):
bot = Bot
nostr_dvm_thread = Thread(target=bot, args=[self.dvm_config])
nostr_dvm_thread.start()

85
dvm.py
View File

@@ -15,15 +15,13 @@ from utils.database_utils import update_sql_table, get_from_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
from utils.output_utils import post_process_result, build_status_reaction 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, \ 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 check_for_zapplepay, decrypt_private_zap_message, create_bolt11_ln_bits, parse_zap_event_tags
use_logger = False use_logger = False
if use_logger: if use_logger:
init_logger(LogLevel.DEBUG) init_logger(LogLevel.DEBUG)
class DVM: class DVM:
dvm_config: DVMConfig dvm_config: DVMConfig
admin_config: AdminConfig admin_config: AdminConfig
@@ -32,7 +30,7 @@ class DVM:
job_list: list job_list: list
jobs_on_hold_list: list jobs_on_hold_list: list
def __init__(self, dvmconfig, adminconfig = None): def __init__(self, dvmconfig, adminconfig=None):
self.dvm_config = dvmconfig self.dvm_config = dvmconfig
self.admin_config = adminconfig self.admin_config = adminconfig
self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY)
@@ -56,7 +54,8 @@ class DVM:
self.client.connect() self.client.connect()
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) 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()) bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(
Timestamp.now())
kinds = [EventDefinitions.KIND_NIP90_GENERIC] kinds = [EventDefinitions.KIND_NIP90_GENERIC]
for dvm in self.dvm_config.SUPPORTED_DVMS: for dvm in self.dvm_config.SUPPORTED_DVMS:
@@ -75,7 +74,8 @@ class DVM:
def handle(self, relay_url, nostr_event): def handle(self, relay_url, nostr_event):
if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: 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()}") print(
"[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}")
handle_nip90_job_event(nostr_event) handle_nip90_job_event(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_ZAP: elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
handle_zap(nostr_event) handle_zap(nostr_event)
@@ -86,7 +86,8 @@ class DVM:
return return
def handle_nip90_job_event(nip90_event): def handle_nip90_job_event(nip90_event):
user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client) user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client,
config=self.dvm_config)
task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client,
get_duration=(not user.iswhitelisted), get_duration=(not user.iswhitelisted),
config=self.dvm_config) config=self.dvm_config)
@@ -107,7 +108,8 @@ class DVM:
task_is_free = True task_is_free = True
if user.iswhitelisted or task_is_free: if user.iswhitelisted or task_is_free:
print("[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") 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, send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client,
dvm_config=self.dvm_config) dvm_config=self.dvm_config)
do_work(nip90_event, is_from_bot=False) do_work(nip90_event, is_from_bot=False)
@@ -118,7 +120,8 @@ class DVM:
if tag.as_vec()[0] == 'bid': if tag.as_vec()[0] == 'bid':
bid = int(tag.as_vec()[1]) bid = int(tag.as_vec()[1])
print("[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json()) print(
"[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json())
if bid > 0: if bid > 0:
bid_offer = int(bid / 1000) bid_offer = int(bid / 1000)
if bid_offer >= amount: if bid_offer >= amount:
@@ -127,47 +130,21 @@ class DVM:
client=self.client, dvm_config=self.dvm_config) client=self.client, dvm_config=self.dvm_config)
else: # If there is no bid, just request server rate from user 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()) print(
"[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + nip90_event.id().to_hex())
send_job_status_reaction(nip90_event, "payment-required", send_job_status_reaction(nip90_event, "payment-required",
False, amount, client=self.client, dvm_config=self.dvm_config) False, amount, client=self.client, dvm_config=self.dvm_config)
else: else:
print("Task not supported on this DVM, skipping..") print("Task not supported on this DVM, skipping..")
def handle_zap(zap_event): def handle_zap(zap_event):
zapped_event = None
invoice_amount = 0
anon = False
sender = zap_event.pubkey()
print("Zap received") print("Zap received")
try: try:
for tag in zap_event.tags(): invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event,
if tag.as_vec()[0] == 'bolt11': self.keys, self.dvm_config.NIP89.name,
invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) self.client, self.dvm_config)
elif tag.as_vec()[0] == 'e': user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config)
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(),
zap_request_event.content())
for ztag in zap_request_event.tags():
if ztag.as_vec()[0] == 'anon':
if len(ztag.as_vec()) > 1:
print("Private Zap received.")
decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1],
self.keys.secret_key(),
zap_request_event.pubkey())
decrypted_private_event = Event.from_json(decrypted_content)
if decrypted_private_event.kind() == 9733:
sender = decrypted_private_event.pubkey().to_hex()
message = decrypted_private_event.content()
if message != "":
print("Zap Message: " + message)
else:
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)
if zapped_event is not None: if zapped_event is not None:
if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped
@@ -219,13 +196,15 @@ class DVM:
elif not anon: elif not anon:
print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str(
user.name)) user.name))
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
config=self.dvm_config)
# a regular note # a regular note
elif not anon: elif not anon:
print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str(
user.name)) user.name))
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
config=self.dvm_config)
except Exception as e: except Exception as e:
print(f"Error during content decryption: {e}") print(f"Error during content decryption: {e}")
@@ -264,7 +243,8 @@ class DVM:
input = tag.as_vec()[1] input = tag.as_vec()[1]
input_type = tag.as_vec()[2] input_type = tag.as_vec()[2]
if input_type == "job": if input_type == "job":
evt = get_referenced_event_by_id(event_id=input, client=client, kinds=EventDefinitions.ANY_RESULT, evt = get_referenced_event_by_id(event_id=input, client=client,
kinds=EventDefinitions.ANY_RESULT,
dvm_config=dvmconfig) dvm_config=dvmconfig)
if evt is None: if evt is None:
if append: if append:
@@ -287,7 +267,7 @@ class DVM:
x.result = data x.result = data
x.is_processed = True x.is_processed = True
if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid:
send_nostr_reply_event(data, original_event_str,) send_nostr_reply_event(data, original_event_str, )
send_job_status_reaction(original_event, "success", amount, send_job_status_reaction(original_event, "success", amount,
dvm_config=self.dvm_config) # or payment-required, or both? dvm_config=self.dvm_config) # or payment-required, or both?
elif not self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: elif not self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid:
@@ -318,10 +298,11 @@ class DVM:
} }
message = json.dumps(params) message = json.dumps(params)
print(message) print(message)
response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) 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) send_event(response_event, client=self.client, dvm_config=self.dvm_config)
else: else:
#Regular DVM reply # Regular DVM reply
send_nostr_reply_event(post_processed_content, original_event_str) send_nostr_reply_event(post_processed_content, original_event_str)
except Exception as e: except Exception as e:
respond_to_error(str(e), original_event_str, False) respond_to_error(str(e), original_event_str, False)
@@ -347,7 +328,8 @@ class DVM:
response_kind = original_event.kind() + 1000 response_kind = original_event.kind() + 1000
reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key) reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key)
send_event(reply_event, client=self.client, dvm_config=self.dvm_config) send_event(reply_event, client=self.client, dvm_config=self.dvm_config)
print("[" + self.dvm_config.NIP89.name + "] " + str(response_kind) + " Job Response event sent: " + reply_event.as_json()) print("[" + self.dvm_config.NIP89.name + "] " + str(
response_kind) + " Job Response event sent: " + reply_event.as_json())
return reply_event.as_json() return reply_event.as_json()
def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False): def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False):
@@ -366,7 +348,7 @@ class DVM:
elif tag.as_vec()[0] == "i": elif tag.as_vec()[0] == "i":
task = tag.as_vec()[1] task = tag.as_vec()[1]
user = get_or_add_user(self.dvm_config.DB, sender, self.client) user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
if not user.iswhitelisted: if not user.iswhitelisted:
amount = int(user.balance) + get_amount_per_task(task, self.dvm_config) amount = int(user.balance) + get_amount_per_task(task, self.dvm_config)
update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted, update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted,
@@ -419,7 +401,7 @@ class DVM:
status=status, result="", is_processed=False, bolt11=bolt11, status=status, result="", is_processed=False, bolt11=bolt11,
payment_hash=payment_hash, payment_hash=payment_hash,
expires=expires, from_bot=False)) expires=expires, from_bot=False))
#print(str(self.job_list)) # print(str(self.job_list))
if (status == "payment-required" or status == "payment-rejected" or ( if (status == "payment-required" or status == "payment-rejected" or (
status == "processing" and not is_paid) status == "processing" and not is_paid)
or (status == "success" and not is_paid)): or (status == "success" and not is_paid)):
@@ -435,7 +417,7 @@ class DVM:
send_event(event, client=self.client, dvm_config=self.dvm_config) send_event(event, client=self.client, dvm_config=self.dvm_config)
print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str( print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str(
EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json())
return event.as_json() return event.as_json()
def do_work(job_event, is_from_bot=False): def do_work(job_event, is_from_bot=False):
@@ -495,4 +477,3 @@ class DVM:
self.jobs_on_hold_list.remove(job) self.jobs_on_hold_list.remove(job)
time.sleep(1.0) time.sleep(1.0)

View File

@@ -26,6 +26,19 @@ class DVMTaskInterface:
nip89.content = nip89config.CONTENT nip89.content = nip89config.CONTENT
return nip89 return nip89
def init(self, name, dvm_config, admin_config, nip89config):
self.NAME = name
self.PK = dvm_config.PRIVATE_KEY
if dvm_config.COST is not None:
self.COST = dvm_config.COST
dvm_config.SUPPORTED_DVMS = [self]
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
self.dvm_config = dvm_config
self.admin_config = admin_config
def run(self): def run(self):
nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config])
nostr_dvm_thread.start() nostr_dvm_thread.start()

64
main.py
View File

@@ -17,41 +17,49 @@ def run_nostr_dvm_with_local_config():
# We extract the Publickey from our bot, so the DVMs know who they should listen and react to. # We extract the Publickey from our bot, so the DVMs know who they should listen and react to.
bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key() bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key()
# Spawn some DVMs in the playground and run them
# You can add arbitrary DVMs there and instantiate them here
# Spawn DVM1 Kind 5000 Text Extractor from PDFs
pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey])
pdfextractor.run()
# Spawn DVM2 Kind 5002 Text Translation
translator = build_translator("Translator", [bot_publickey])
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
unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey])
unstable_artist.run()
# Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time.
sketcher = build_sketcher("Sketcher", [bot_publickey])
sketcher.run()
dalle = build_dalle("Dall-E 3", [bot_publickey])
dalle.run()
# We will run an optional bot that can communicate with the DVMs # We will run an optional bot that can communicate with the DVMs
# Note this is very basic for now and still under development # Note this is very basic for now and still under development
bot_config = DVMConfig() bot_config = DVMConfig()
bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY")
bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") 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_DVMS = [sketcher, unstable_artist, dalle, translator]
bot = Bot # Spawn some DVMs in the playground and run them
nostr_dvm_thread = Thread(target=bot, args=[bot_config]) # You can add arbitrary DVMs there and instantiate them here
nostr_dvm_thread.start()
# Spawn DVM1 Kind 5000: A local Text Extractor from PDFs
pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey])
# 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 Translation, calling the free Google API.
translator = build_translator("Translator", [bot_publickey])
bot_config.SUPPORTED_DVMS.append(translator) # We add translator to the bot
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", [bot_publickey])
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", [bot_publickey])
bot_config.SUPPORTED_DVMS.append(sketcher) # We also add Sketcher to the bot
sketcher.run()
# Spawn DVM5, this one requires an OPENAI API Key and balance with OpenAI, you will move the task to them and pay
# 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", [bot_publickey])
bot_config.SUPPORTED_DVMS.append(dalle)
dalle.run()
bot = Bot(bot_config)
bot.run()
# Keep the main function alive for libraries like openai # Keep the main function alive for libraries like openai
try: try:

View File

@@ -8,6 +8,7 @@ from tasks.translation import Translation
from utils.admin_utils import AdminConfig from utils.admin_utils import AdminConfig
from utils.dvmconfig import DVMConfig from utils.dvmconfig import DVMConfig
from utils.nip89_utils import NIP89Config from utils.nip89_utils import NIP89Config
""" """
This File is a playground to create DVMs. It shows some examples of DVMs that make use of the modules in the tasks folder 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, These DVMs should be considered examples and will be extended in the future. env variables are used to not commit keys,
@@ -32,6 +33,8 @@ task, for example an address or an API key.
# their NIP89 announcement # their NIP89 announcement
admin_config = AdminConfig() admin_config = AdminConfig()
admin_config.REBROADCAST_NIP89 = False 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
def build_pdf_extractor(name, dm_allowed_keys): def build_pdf_extractor(name, dm_allowed_keys):
@@ -67,11 +70,11 @@ def build_translator(name, dm_allowed_keys):
"language": { "language": {
"required": False, "required": False,
"values": ["en", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co", "cs", "cy", "da", "de", "el", "eo", "es", "values": ["en", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co", "cs", "cy", "da", "de", "el", "eo", "es",
"et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl","gu", "ha", "haw", "hi", "hmn", "hr", "ht", "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl", "gu", "ha", "haw", "hi", "hmn", "hr", "ht",
"hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk","km", "kn", "ko", "ku", "ky", "hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk", "km", "kn", "ko", "ku", "ky",
"la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl", "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl",
"no", "ny", "or", "pa", "pl", "ps", "pt", "ro", "ru", "sd", "si", "sk", "sl", "sm", "sn", "so", "no", "ny", "or", "pa", "pl", "ps", "pt", "ro", "ru", "sd", "si", "sk", "sl", "sm", "sn", "so",
"sq", "sr", "st", "su", "sv", "sw", "ta", "te","tg", "th", "tl", "tr", "ug", "uk", "ur", "uz", "sq", "sr", "st", "su", "sv", "sw", "ta", "te", "tg", "th", "tl", "tr", "ug", "uk", "ur", "uz",
"vi", "xh", "yi", "yo", "zh", "zu"] "vi", "xh", "yi", "yo", "zh", "zu"]
} }
} }
@@ -158,12 +161,15 @@ def build_sketcher(name, dm_allowed_keys):
return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config, options=options) admin_config=admin_config, options=options)
def build_dalle(name, dm_allowed_keys): def build_dalle(name, dm_allowed_keys):
dvm_config = DVMConfig() dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3")
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
dvm_config.DM_ALLOWED = dm_allowed_keys dvm_config.DM_ALLOWED = dm_allowed_keys
profit_in_sats = 10
dvm_config.COST = int(((4.0 / (get_price_per_sat("USD") * 100)) + profit_in_sats))
nip90params = { nip90params = {
"size": { "size": {
@@ -186,4 +192,24 @@ def build_dalle(name, dm_allowed_keys):
nip89config.CONTENT = json.dumps(nip89info) nip89config.CONTENT = json.dumps(nip89info)
# We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 # 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, return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config) admin_config=admin_config)
# 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
def get_price_per_sat(currency):
import requests
url = "https://api.coinstats.app/public/v1/coins"
params = {"skip": 0, "limit": 1, "currency": currency}
try:
response = requests.get(url, params=params)
response_json = response.json()
bitcoin_price = response_json["coins"][0]["price"]
price_currency_per_sat = bitcoin_price / 100000000.0
except:
price_currency_per_sat = 0.0004
return price_currency_per_sat

View File

@@ -23,23 +23,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea
class ImageGenerationDALLE(DVMTaskInterface): class ImageGenerationDALLE(DVMTaskInterface):
NAME: str = ""
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image" TASK: str = "text-to-image"
COST: int = 120 COST: int = 120
PK: str
DVM = DVM
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
options=None): options=None):
self.NAME = name
self.PK = dvm_config.PRIVATE_KEY
dvm_config.SUPPORTED_DVMS = [self] self.init(name, dvm_config, admin_config, nip89config)
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
self.dvm_config = dvm_config
self.admin_config = admin_config
self.options = options self.options = options
def is_input_supported(self, input_type, input_content): def is_input_supported(self, input_type, input_content):

View File

@@ -20,22 +20,12 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea
class ImageGenerationSDXL(DVMTaskInterface): class ImageGenerationSDXL(DVMTaskInterface):
NAME: str = ""
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image" TASK: str = "text-to-image"
COST: int = 50 COST: int = 50
PK: str
DVM = DVM
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None):
self.NAME = name self.init(name, dvm_config, admin_config, nip89config)
self.PK = dvm_config.PRIVATE_KEY
dvm_config.SUPPORTED_DVMS = [self]
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
self.dvm_config = dvm_config
self.admin_config = admin_config
self.options = options self.options = options
def is_input_supported(self, input_type, input_content): def is_input_supported(self, input_type, input_content):

View File

@@ -21,22 +21,13 @@ Params: None
class TextExtractionPDF(DVMTaskInterface): class TextExtractionPDF(DVMTaskInterface):
NAME: str = ""
KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
TASK: str = "pdf-to-text" TASK: str = "pdf-to-text"
COST: int = 0 COST: int = 0
PK: str
DVM = DVM
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None):
self.NAME = name self.init(name, dvm_config, admin_config, nip89config)
self.PK = dvm_config.PRIVATE_KEY self.options = options
dvm_config.SUPPORTED_DVMS = [self]
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
self.dvm_config = dvm_config
self.admin_config = admin_config
def is_input_supported(self, input_type, input_content): def is_input_supported(self, input_type, input_content):

View File

@@ -19,22 +19,14 @@ Params: -language The target language
class Translation(DVMTaskInterface): class Translation(DVMTaskInterface):
NAME: str = ""
KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT
TASK: str = "translation" TASK: str = "translation"
COST: int = 0 COST: int = 0
PK: str
DVM = DVM
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
self.NAME = name options=None):
self.PK = dvm_config.PRIVATE_KEY self.init(name, dvm_config, admin_config, nip89config)
self.options = options
dvm_config.SUPPORTED_DVMS = [self]
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
self.dvm_config = dvm_config
self.admin_config = admin_config
def is_input_supported(self, input_type, input_content): def is_input_supported(self, input_type, input_content):
if input_type != "event" and input_type != "job" and input_type != "text": if input_type != "event" and input_type != "job" and input_type != "text":

View File

@@ -50,7 +50,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC
if whitelistuser: if whitelistuser:
user = get_or_add_user(db, publickey, client=client) 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) 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) user = get_from_sql_table(db, publickey)
print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted))

View File

@@ -8,9 +8,7 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from logging import Filter from logging import Filter
from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Metadata, Filter, Options, Client from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter, Client, Options
from utils.definitions import NEW_USER_BALANCE
from utils.nostr_utils import send_event from utils.nostr_utils import send_event
@@ -66,7 +64,7 @@ def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16,
con.commit() con.commit()
con.close() con.close()
except Error as e: except Error as e:
print(e) print("Error when Adding to DB: " + str(e))
def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive):
@@ -87,7 +85,7 @@ def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud
con.commit() con.commit()
con.close() con.close()
except Error as e: except Error as e:
print(e) print("Error Updating DB: " + str(e))
def get_from_sql_table(db, npub): def get_from_sql_table(db, npub):
@@ -113,7 +111,7 @@ def get_from_sql_table(db, npub):
return user return user
except Error as e: except Error as e:
print(e) print("Error Getting from DB: " + str(e))
def delete_from_sql_table(db, npub): def delete_from_sql_table(db, npub):
@@ -155,72 +153,70 @@ def list_db(db):
print(e) print(e)
def update_user_balance(db, sender, sats, client, config): def update_user_balance(db, npub, additional_sats, client, config):
user = get_from_sql_table(db, sender) user = get_from_sql_table(db, npub)
if user is None: if user is None:
add_to_sql_table(db, sender, (int(sats) + NEW_USER_BALANCE), False, False, name, nip05, lud16 = fetch_user_metadata(npub, client)
"", "", "", Timestamp.now().as_secs()) add_to_sql_table(db, npub, (int(additional_sats) + config.NEW_USER_BALANCE), False, False,
print("NEW USER: " + sender + " Zap amount: " + str(sats) + " Sats.") nip05, lud16, name, Timestamp.now().as_secs())
print("Adding User: " + npub + " (" + npub + ")")
else: else:
user = get_from_sql_table(db, sender) user = get_from_sql_table(db, npub)
print(str(sats)) new_balance = int(user.balance) + int(additional_sats)
update_sql_table(db, npub, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16,
if user.nip05 is None:
user.nip05 = ""
if user.lud16 is None:
user.lud16 = ""
if user.name is None:
user.name = ""
new_balance = int(user.balance) + int(sats)
update_sql_table(db, sender, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16,
user.name, user.name,
Timestamp.now().as_secs()) Timestamp.now().as_secs())
print("UPDATE USER BALANCE: " + str(user.name) + " Zap amount: " + str(sats) + " Sats.") print("Updated user balance for: " + str(user.name) +
" Zap amount: " + str(additional_sats) + " Sats. New balance: " + str(new_balance) +" Sats")
if config is not None: if config is not None:
keys = Keys.from_sk_str(config.PRIVATE_KEY) keys = Keys.from_sk_str(config.PRIVATE_KEY)
time.sleep(1.0) time.sleep(1.0)
message = ("Added " + str(sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats. ") message = ("Added " + str(additional_sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats.")
evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(npub), message,
None).to_event(keys) None).to_event(keys)
send_event(evt, client=client, dvm_config=config) send_event(evt, client=client, dvm_config=config)
def get_or_add_user(db, npub, client): def get_or_add_user(db, npub, client, config):
user = get_from_sql_table(db, npub) user = get_from_sql_table(db, npub)
if user is None: if user is None:
name, nip05, lud16 = fetch_user_metadata(npub, client) try:
print("Adding User: " + npub + " (" + npub + ")") name, nip05, lud16 = fetch_user_metadata(npub, client)
add_to_sql_table(db, npub, NEW_USER_BALANCE, False, False, nip05, print("Adding User: " + npub + " (" + npub + ")")
lud16, name, Timestamp.now().as_secs()) add_to_sql_table(db, npub, config.NEW_USER_BALANCE, False, False, nip05,
user = get_from_sql_table(db, npub) lud16, name, Timestamp.now().as_secs())
return user user = get_from_sql_table(db, npub)
return user
except Exception as e:
print("Error Adding User to DB: " + str(e))
return user return user
class DvmConfig:
pass
def fetch_user_metadata(npub, client): def fetch_user_metadata(npub, client):
name = "" name = ""
nip05 = "" nip05 = ""
lud16 = "" lud16 = ""
# Get metadata
pk = PublicKey.from_hex(npub) pk = PublicKey.from_hex(npub)
print(f"\nGetting profile metadata for {pk.to_bech32()}...") print(f"\nGetting profile metadata for {pk.to_bech32()}...")
filter = Filter().kind(0).author(pk).limit(1) profile_filter = Filter().kind(0).author(pk).limit(1)
events = client.get_events_of([filter], timedelta(seconds=3)) events = client.get_events_of([profile_filter], timedelta(seconds=5))
#TODO, it seems our client is still subscribed after that
if len(events) > 0: if len(events) > 0:
latest_entry = events[0] latest_entry = events[0]
newest = 0 latest_time = 0
for entry in events: for entry in events:
if entry.created_at().as_secs() > newest: if entry.created_at().as_secs() > latest_time:
newest = entry.created_at().as_secs() latest_time = entry.created_at().as_secs()
latest_entry = entry latest_entry = entry
print(latest_entry.content())
profile = json.loads(latest_entry.content()) profile = json.loads(latest_entry.content())
if profile.get("name"): if profile.get("name"):
name = profile['name'] name = profile['name']
@@ -228,32 +224,4 @@ def fetch_user_metadata(npub, client):
nip05 = profile['nip05'] nip05 = profile['nip05']
if profile.get("lud16"): if profile.get("lud16"):
lud16 = profile['lud16'] lud16 = profile['lud16']
return name, nip05, lud16
def fetch_user_metadata2(sender, client) -> (str, str, str):
name = ""
nip05 = ""
lud16 = ""
try:
pk = PublicKey.from_hex(sender)
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=1))
if len(events) > 0:
ev = events[0]
metadata = Metadata.from_json(ev.content())
name = metadata.get_display_name()
if str(name) == "" or name is None:
name = metadata.get_name()
nip05 = metadata.get_nip05()
lud16 = metadata.get_lud16()
else:
print("Profile not found")
return name, nip05, lud16
except:
print("Couldn't get meta information")
return name, nip05, lud16 return name, nip05, lud16

View File

@@ -2,10 +2,6 @@ import os
from dataclasses import dataclass from dataclasses import dataclass
from nostr_sdk import Event from nostr_sdk import Event
NEW_USER_BALANCE: int = 250 # Free credits for new users
class EventDefinitions: class EventDefinitions:
KIND_DM: int = 4 KIND_DM: int = 4
KIND_ZAP: int = 9735 KIND_ZAP: int = 9735

View File

@@ -6,6 +6,7 @@ from utils.nip89_utils import NIP89Announcement
class DVMConfig: class DVMConfig:
SUPPORTED_DVMS= [] SUPPORTED_DVMS= []
PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY")
COST: int = None
RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", 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.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg",
@@ -15,11 +16,11 @@ class DVMConfig:
LNBITS_INVOICE_KEY = '' LNBITS_INVOICE_KEY = ''
LNBITS_URL = 'https://lnbits.com' LNBITS_URL = 'https://lnbits.com'
DB: str DB: str
NEW_USER_BALANCE: int = 250 # Free credits for new users
NIP89: NIP89Announcement NIP89: NIP89Announcement
DM_ALLOWED = [] DM_ALLOWED = []
REQUIRES_NIP05: bool = False SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess
SHOW_RESULT_BEFORE_PAYMENT: bool = True # if this is true show results even when not paid right after autoprocess

View File

@@ -4,8 +4,9 @@ import json
import requests import requests
from Crypto.Cipher import AES from Crypto.Cipher import AES
from bech32 import bech32_decode, convertbits from bech32 import bech32_decode, convertbits
from nostr_sdk import nostr_sdk, PublicKey, SecretKey from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event
from utils.dvmconfig import DVMConfig from utils.dvmconfig import DVMConfig
from utils.nostr_utils import get_event_by_id
def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int: def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int:
@@ -35,6 +36,42 @@ def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int:
return int(number) return int(number)
def parse_zap_event_tags(zap_event, keys, name, client, config):
zapped_event = None
invoice_amount = 0
anon = False
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)
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(),
zap_request_event.content())
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.")
decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1],
keys.secret_key(),
zap_request_event.pubkey())
decrypted_private_event = Event.from_json(decrypted_content)
if decrypted_private_event.kind() == 9733:
sender = decrypted_private_event.pubkey().to_hex()
message = decrypted_private_event.content()
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
def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
url = config.LNBITS_URL + "/api/v1/payments" url = config.LNBITS_URL + "/api/v1/payments"
data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name} data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name}