mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-08-02 11:02:16 +02:00
@@ -12,6 +12,7 @@ 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)
|
||||
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"
|
||||
|
||||
|
||||
|
12
.idea/dataSources.xml
generated
12
.idea/dataSources.xml
generated
@@ -44,5 +44,17 @@
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="Unstable Diffusion" uuid="ef14cba4-1991-4706-8760-77dba3d8e8e4">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/Unstable Diffusion.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
<libraries>
|
||||
<library>
|
||||
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar</url>
|
||||
</library>
|
||||
</libraries>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
216
bot.py
216
bot.py
@@ -4,17 +4,20 @@ from datetime import timedelta
|
||||
from threading import Thread
|
||||
|
||||
from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey,
|
||||
Options)
|
||||
Options, Tag, Event, nip04_encrypt)
|
||||
|
||||
from utils.admin_utils import admin_make_database_updates
|
||||
from utils.backend_utils import get_amount_per_task
|
||||
from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User
|
||||
from utils.definitions import EventDefinitions
|
||||
from utils.nostr_utils import send_event
|
||||
from utils.zap_utils import parse_zap_event_tags
|
||||
from utils.zap_utils import parse_zap_event_tags, pay_bolt11_ln_bits, zap
|
||||
|
||||
|
||||
class Bot:
|
||||
job_list: list
|
||||
|
||||
# This is a simple list just to keep track which events we created and manage, so we don't pay for other requests
|
||||
def __init__(self, dvm_config, admin_config=None):
|
||||
self.NAME = "Bot"
|
||||
dvm_config.DB = "db/" + self.NAME + ".db"
|
||||
@@ -29,6 +32,8 @@ class Bot:
|
||||
|
||||
pk = self.keys.public_key()
|
||||
|
||||
self.job_list = []
|
||||
|
||||
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")
|
||||
@@ -39,8 +44,13 @@ class Bot:
|
||||
|
||||
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now())
|
||||
dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since(Timestamp.now())
|
||||
kinds = [EventDefinitions.KIND_NIP90_GENERIC, EventDefinitions.KIND_FEEDBACK]
|
||||
for dvm in self.dvm_config.SUPPORTED_DVMS:
|
||||
if dvm.KIND not in kinds:
|
||||
kinds.append(dvm.KIND + 1000)
|
||||
dvm_filter = (Filter().kinds(kinds).since(Timestamp.now()))
|
||||
|
||||
self.client.subscribe([zap_filter, dm_filter])
|
||||
self.client.subscribe([zap_filter, dm_filter, dvm_filter])
|
||||
|
||||
create_sql_table(self.dvm_config.DB)
|
||||
admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client)
|
||||
@@ -51,7 +61,11 @@ class Bot:
|
||||
keys = self.keys
|
||||
|
||||
def handle(self, relay_url, nostr_event):
|
||||
if nostr_event.kind() == EventDefinitions.KIND_DM:
|
||||
if EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC + 1000:
|
||||
handle_nip90_response_event(nostr_event)
|
||||
elif nostr_event.kind() == EventDefinitions.KIND_FEEDBACK:
|
||||
handle_nip90_feedback(nostr_event)
|
||||
elif nostr_event.kind() == EventDefinitions.KIND_DM:
|
||||
handle_dm(nostr_event)
|
||||
elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
|
||||
handle_zap(nostr_event)
|
||||
@@ -64,6 +78,7 @@ class Bot:
|
||||
|
||||
try:
|
||||
decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content())
|
||||
print(decrypted_text)
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
|
||||
|
||||
# We do a selection of tasks now, maybe change this later, Idk.
|
||||
@@ -81,54 +96,69 @@ class Bot:
|
||||
# For some reason an admin might blacklist npubs, e.g. for abusing the service
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
|
||||
"Your are currently blocked from all "
|
||||
"services.",None).to_event(self.keys)
|
||||
"services.", None).to_event(self.keys)
|
||||
send_event(evt, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
elif user.iswhitelisted or user.balance >= required_amount or required_amount == 0:
|
||||
time.sleep(2.0)
|
||||
if user.iswhitelisted:
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
|
||||
"As you are "
|
||||
"whitelisted, your balance remains at"
|
||||
+ str(user.balance) + " Sats.\n",
|
||||
nostr_event.id()).to_event(self.keys)
|
||||
|
||||
if not user.iswhitelisted:
|
||||
|
||||
else:
|
||||
balance = max(user.balance - required_amount, 0)
|
||||
update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance,
|
||||
iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted,
|
||||
nip05=user.nip05, lud16=user.lud16, name=user.name,
|
||||
lastactive=Timestamp.now().as_secs())
|
||||
time.sleep(2.0)
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
|
||||
"Your Job is now scheduled. New balance is " +
|
||||
"New balance is " +
|
||||
str(balance)
|
||||
+ " Sats.\nI will DM you once I'm done "
|
||||
"processing.",
|
||||
nostr_event.id()).to_event(self.keys)
|
||||
else:
|
||||
time.sleep(2.0)
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
|
||||
"Your Job is now scheduled. As you are "
|
||||
"whitelisted, your balance remains at"
|
||||
+ str(user.balance) + " Sats.\n"
|
||||
"I will DM you once I'm "
|
||||
"done processing.",
|
||||
+ " Sats.\n",
|
||||
nostr_event.id()).to_event(self.keys)
|
||||
|
||||
input = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "")
|
||||
|
||||
dvm_keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK)
|
||||
i_tag = Tag.parse(["i", input, "text"])
|
||||
|
||||
# we use the y tag to keep information about the original sender, in order to forward the
|
||||
# results later
|
||||
|
||||
# TODO more advanced logic, more parsing, params etc, just very basic test functions for now
|
||||
# outTag = Tag.parse(["output", "image/png;format=url"])
|
||||
# paramTag1 = Tag.parse(["param", "size", "1024x1024"])
|
||||
|
||||
bid = str(self.dvm_config.SUPPORTED_DVMS[index].COST * 1000)
|
||||
bid_tag = Tag.parse(['bid', bid, bid])
|
||||
relays_tag = Tag.parse(["relays", json.dumps(self.dvm_config.RELAY_LIST)])
|
||||
alt_tag = Tag.parse(["alt", self.dvm_config.SUPPORTED_DVMS[index].TASK])
|
||||
|
||||
encrypted_params_string = json.dumps([i_tag.as_vec(), bid_tag.as_vec(),
|
||||
relays_tag.as_vec(), alt_tag.as_vec()])
|
||||
|
||||
print(encrypted_params_string)
|
||||
|
||||
encrypted_params = nip04_encrypt(self.keys.secret_key(), dvm_keys.public_key(),
|
||||
encrypted_params_string)
|
||||
p_tag = Tag.parse(['p', dvm_keys.public_key().to_hex()])
|
||||
encrypted_tag = Tag.parse(['encrypted'])
|
||||
nip90request = EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND, encrypted_params,
|
||||
[p_tag, encrypted_tag]).to_event(self.keys)
|
||||
|
||||
entry = {"npub": user.npub, "event_id": nip90request.id().to_hex(),
|
||||
"dvm_key": dvm_keys.public_key().to_hex(), "is_paid": False}
|
||||
self.job_list.append(entry)
|
||||
|
||||
send_event(nip90request, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation")
|
||||
send_event(evt, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "")
|
||||
# TODO more advanced logic, more parsing, params etc, just very basic test functions for now
|
||||
dvm_keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK)
|
||||
params = {
|
||||
"sender": nostr_event.pubkey().to_hex(),
|
||||
"input": i_tag,
|
||||
"task": self.dvm_config.SUPPORTED_DVMS[index].TASK
|
||||
}
|
||||
message = json.dumps(params)
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, dvm_keys.public_key(),
|
||||
message, None).to_event(self.keys)
|
||||
print("[" + self.NAME + "] Forwarding task " + self.dvm_config.SUPPORTED_DVMS[index].TASK +
|
||||
" for user " + user.name + " to " + self.dvm_config.SUPPORTED_DVMS[index].NAME)
|
||||
send_event(evt, client=self.client, dvm_config=dvm_config)
|
||||
else:
|
||||
print("payment-required")
|
||||
print("Bot payment-required")
|
||||
time.sleep(2.0)
|
||||
evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(),
|
||||
"Balance required, please zap me with at least " +
|
||||
@@ -138,21 +168,6 @@ class Bot:
|
||||
send_event(evt, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
|
||||
# TODO if we receive the result from one of the dvms, need some better management, maybe check for keys
|
||||
elif decrypted_text.startswith('{"result":'):
|
||||
|
||||
dvm_result = json.loads(decrypted_text)
|
||||
user_npub_hex = dvm_result["sender"]
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex,
|
||||
client=self.client, config=self.dvm_config)
|
||||
print("[" + self.NAME + "] Received results, message to orignal sender " + user.name)
|
||||
reply_event = EventBuilder.new_encrypted_direct_msg(self.keys,
|
||||
PublicKey.from_hex(user.npub),
|
||||
dvm_result["result"],
|
||||
None).to_event(self.keys)
|
||||
|
||||
send_event(reply_event, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
else:
|
||||
print("[" + self.NAME + "] Message from " + user.name + ": " + decrypted_text)
|
||||
message = "DVMs that I support:\n\n"
|
||||
@@ -173,10 +188,107 @@ class Bot:
|
||||
|
||||
print("Error in bot " + str(e))
|
||||
|
||||
def handle_nip90_feedback(nostr_event):
|
||||
try:
|
||||
status = ""
|
||||
etag = ""
|
||||
ptag = ""
|
||||
|
||||
for tag in nostr_event.tags():
|
||||
if tag.as_vec()[0] == "status":
|
||||
status = tag.as_vec()[1]
|
||||
elif tag.as_vec()[0] == "e":
|
||||
etag = tag.as_vec()[1]
|
||||
elif tag.as_vec()[0] == "p":
|
||||
ptag = tag.as_vec()[1]
|
||||
|
||||
if status == "success" or status == "error" or status == "processing" or status == "partial":
|
||||
entry = next((x for x in self.job_list if x['event_id'] == etag), None)
|
||||
if entry is not None:
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=entry['npub'],
|
||||
client=self.client, config=self.dvm_config)
|
||||
|
||||
reply_event = EventBuilder.new_encrypted_direct_msg(self.keys,
|
||||
PublicKey.from_hex(user.npub),
|
||||
nostr_event.content(),
|
||||
None).to_event(self.keys)
|
||||
print(status + ": " + nostr_event.content())
|
||||
print(
|
||||
"[" + self.NAME + "] Received reaction from " + nostr_event.pubkey().to_hex() + " message to orignal sender " + user.name)
|
||||
send_event(reply_event, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
|
||||
elif status == "payment-required" or status == "partial":
|
||||
amount = 0
|
||||
for tag in nostr_event.tags():
|
||||
if tag.as_vec()[0] == "amount":
|
||||
amount_msats = int(tag.as_vec()[1])
|
||||
amount = int(amount_msats / 1000)
|
||||
|
||||
entry = next((x for x in self.job_list if x['event_id'] == etag), None)
|
||||
if entry is not None and entry['is_paid'] is False and entry['dvm_key'] == ptag:
|
||||
|
||||
#if we get a bolt11, we pay and move on
|
||||
if len(tag.as_vec()) > 2:
|
||||
bolt11 = tag.as_vec()[2]
|
||||
|
||||
# else we create a zap
|
||||
else:
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=nostr_event.pubkey().to_hex(),
|
||||
client=self.client, config=self.dvm_config)
|
||||
bolt11 = zap(user.lud16, amount, "Zap", nostr_event, self.keys, self.dvm_config, "private")
|
||||
if bolt11 == None:
|
||||
print("Receiver has no Lightning address")
|
||||
return
|
||||
try:
|
||||
payment_hash = pay_bolt11_ln_bits(bolt11, self.dvm_config)
|
||||
self.job_list[self.job_list.index(entry)]['is_paid'] = True
|
||||
print("[" + self.NAME + "] payment_hash: " + payment_hash +
|
||||
" Forwarding payment of " + str(amount) + " Sats to DVM")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def handle_nip90_response_event(nostr_event: Event):
|
||||
try:
|
||||
is_encrypted = False
|
||||
for tag in nostr_event.tags():
|
||||
if tag.as_vec()[0] == "e":
|
||||
etag = tag.as_vec()[1]
|
||||
elif tag.as_vec()[0] == "p":
|
||||
ptag = tag.as_vec()[1]
|
||||
elif tag.as_vec()[0] == "encrypted":
|
||||
is_encrypted = True
|
||||
|
||||
entry = next((x for x in self.job_list if x['event_id'] == etag), None)
|
||||
if entry is not None:
|
||||
print(entry)
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=entry['npub'],
|
||||
client=self.client, config=self.dvm_config)
|
||||
|
||||
self.job_list.remove(entry)
|
||||
content = nostr_event.content()
|
||||
if is_encrypted:
|
||||
content = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), content)
|
||||
|
||||
print("[" + self.NAME + "] Received results, message to orignal sender " + user.name)
|
||||
time.sleep(1.0)
|
||||
reply_event = EventBuilder.new_encrypted_direct_msg(self.keys,
|
||||
PublicKey.from_hex(user.npub),
|
||||
content,
|
||||
None).to_event(self.keys)
|
||||
send_event(reply_event, client=self.client, dvm_config=dvm_config)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def handle_zap(zap_event):
|
||||
print("[" + self.NAME + "] Zap received")
|
||||
try:
|
||||
invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event,
|
||||
invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event,
|
||||
self.keys, self.NAME,
|
||||
self.client, self.dvm_config)
|
||||
|
||||
|
297
dvm.py
297
dvm.py
@@ -1,8 +1,9 @@
|
||||
import json
|
||||
import typing
|
||||
from datetime import timedelta
|
||||
|
||||
from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \
|
||||
init_logger, LogLevel, nip04_decrypt, Options
|
||||
init_logger, LogLevel, nip04_decrypt, Options, nip04_encrypt
|
||||
|
||||
import time
|
||||
|
||||
@@ -10,9 +11,8 @@ from utils.definitions import EventDefinitions, RequiredJobToWatch, JobToWatch
|
||||
from utils.dvmconfig import DVMConfig
|
||||
from utils.admin_utils import admin_make_database_updates, AdminConfig
|
||||
from utils.backend_utils import get_amount_per_task, check_task_is_supported, get_task
|
||||
from utils.database_utils import update_sql_table, \
|
||||
create_sql_table, get_or_add_user, update_user_balance
|
||||
from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event
|
||||
from utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table
|
||||
from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event, check_and_decrypt_tags
|
||||
from utils.output_utils import post_process_result, build_status_reaction
|
||||
from utils.zap_utils import check_bolt11_ln_bits_is_paid, create_bolt11_ln_bits, parse_zap_event_tags
|
||||
|
||||
@@ -53,15 +53,15 @@ class DVM:
|
||||
self.client.connect()
|
||||
|
||||
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now())
|
||||
bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(
|
||||
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]
|
||||
for dvm in self.dvm_config.SUPPORTED_DVMS:
|
||||
if dvm.KIND not in kinds:
|
||||
kinds.append(dvm.KIND)
|
||||
dvm_filter = (Filter().kinds(kinds).since(Timestamp.now()))
|
||||
self.client.subscribe([dvm_filter, zap_filter, bot_dm_filter])
|
||||
self.client.subscribe([dvm_filter, zap_filter])
|
||||
|
||||
create_sql_table(self.dvm_config.DB)
|
||||
admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client)
|
||||
@@ -73,21 +73,29 @@ class DVM:
|
||||
|
||||
def handle(self, relay_url, nostr_event):
|
||||
if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC:
|
||||
print(
|
||||
"[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}:"
|
||||
f" {nostr_event.as_json()}")
|
||||
handle_nip90_job_event(nostr_event)
|
||||
elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
|
||||
handle_zap(nostr_event)
|
||||
elif nostr_event.kind() == EventDefinitions.KIND_DM:
|
||||
handle_dm(nostr_event)
|
||||
# elif nostr_event.kind() == EventDefinitions.KIND_DM:
|
||||
# handle_dm(nostr_event)
|
||||
|
||||
def handle_msg(self, relay_url, msg):
|
||||
return
|
||||
|
||||
def handle_nip90_job_event(nip90_event):
|
||||
"""
|
||||
:type nip90_event: Event
|
||||
"""
|
||||
|
||||
tags: typing.List[Tag]
|
||||
tags, p_tag_str = check_and_decrypt_tags(nip90_event, self.dvm_config)
|
||||
if p_tag_str is None:
|
||||
return
|
||||
nip90_event.tags = tags
|
||||
|
||||
user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client,
|
||||
config=self.dvm_config)
|
||||
|
||||
task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client,
|
||||
get_duration=(not user.iswhitelisted),
|
||||
config=self.dvm_config)
|
||||
@@ -107,17 +115,39 @@ class DVM:
|
||||
if dvm.TASK == task and dvm.COST == 0:
|
||||
task_is_free = True
|
||||
|
||||
# if user is whitelisted or task is free, just do the job
|
||||
if user.iswhitelisted or task_is_free:
|
||||
print(
|
||||
"[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task +
|
||||
". Starting processing..")
|
||||
|
||||
send_job_status_reaction(nip90_event, "processing", True, 0,
|
||||
client=self.client, dvm_config=self.dvm_config)
|
||||
do_work(nip90_event, is_from_bot=False)
|
||||
|
||||
do_work(nip90_event)
|
||||
# if task is directed to us via p tag and user has balance, do the job and update balance
|
||||
elif p_tag_str == Keys.from_sk_str(
|
||||
self.dvm_config.PRIVATE_KEY).public_key().to_hex() and user.balance >= amount:
|
||||
balance = max(user.balance - amount, 0)
|
||||
update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance,
|
||||
iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted,
|
||||
nip05=user.nip05, lud16=user.lud16, name=user.name,
|
||||
lastactive=Timestamp.now().as_secs())
|
||||
|
||||
print(
|
||||
"[" + self.dvm_config.NIP89.name + "] Using user's balance for task: " + task +
|
||||
|
||||
". Starting processing.. New balance is: " + str(balance))
|
||||
|
||||
send_job_status_reaction(nip90_event, "processing", True, 0,
|
||||
client=self.client, dvm_config=self.dvm_config)
|
||||
|
||||
do_work(nip90_event)
|
||||
|
||||
# else send a payment required event to user
|
||||
else:
|
||||
bid = 0
|
||||
for tag in nip90_event.tags():
|
||||
for tag in nip90_event.tags:
|
||||
if tag.as_vec()[0] == 'bid':
|
||||
bid = int(tag.as_vec()[1])
|
||||
|
||||
@@ -137,40 +167,51 @@ class DVM:
|
||||
nip90_event.id().to_hex())
|
||||
send_job_status_reaction(nip90_event, "payment-required",
|
||||
False, amount, client=self.client, dvm_config=self.dvm_config)
|
||||
|
||||
else:
|
||||
print("Task not supported on this DVM, skipping..")
|
||||
|
||||
def handle_zap(zap_event):
|
||||
print("Zap received")
|
||||
|
||||
try:
|
||||
invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event,
|
||||
invoice_amount, zapped_event, sender, message, anon = parse_zap_event_tags(zap_event,
|
||||
self.keys, self.dvm_config.NIP89.name,
|
||||
self.client, self.dvm_config)
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
|
||||
|
||||
if zapped_event is not None:
|
||||
if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped
|
||||
print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str(
|
||||
user.name))
|
||||
if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK:
|
||||
|
||||
amount = 0
|
||||
job_event = None
|
||||
p_tag_str = ""
|
||||
for tag in zapped_event.tags():
|
||||
if tag.as_vec()[0] == 'amount':
|
||||
amount = int(float(tag.as_vec()[1]) / 1000)
|
||||
elif tag.as_vec()[0] == 'e':
|
||||
job_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config)
|
||||
tags: typing.List[Tag]
|
||||
tags, p_tag_str = check_and_decrypt_tags(job_event, self.dvm_config)
|
||||
job_event.tags = tags
|
||||
|
||||
task_supported, task, duration = check_task_is_supported(job_event, client=self.client,
|
||||
if p_tag_str is None:
|
||||
return
|
||||
|
||||
# if a reaction by us got zapped
|
||||
|
||||
task_supported, task, duration = check_task_is_supported(job_event,
|
||||
client=self.client,
|
||||
get_duration=False,
|
||||
config=self.dvm_config)
|
||||
if job_event is not None and task_supported:
|
||||
print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str(
|
||||
user.name))
|
||||
if amount <= invoice_amount:
|
||||
print("[" + self.dvm_config.NIP89.name + "] Payment-request fulfilled...")
|
||||
send_job_status_reaction(job_event, "processing", client=self.client,
|
||||
dvm_config=self.dvm_config)
|
||||
indices = [i for i, x in enumerate(self.job_list) if
|
||||
x.event_id == job_event.id().to_hex()]
|
||||
x.event == job_event]
|
||||
index = -1
|
||||
if len(indices) > 0:
|
||||
index = indices[0]
|
||||
@@ -183,10 +224,10 @@ class DVM:
|
||||
# If payment-required appears before processing
|
||||
self.job_list.pop(index)
|
||||
print("Starting work...")
|
||||
do_work(job_event, is_from_bot=False)
|
||||
do_work(job_event)
|
||||
else:
|
||||
print("Job not in List, but starting work...")
|
||||
do_work(job_event, is_from_bot=False)
|
||||
do_work(job_event)
|
||||
|
||||
else:
|
||||
send_job_status_reaction(job_event, "payment-rejected",
|
||||
@@ -198,46 +239,24 @@ class DVM:
|
||||
print("[" + self.dvm_config.NIP89.name + "] "
|
||||
"Someone zapped the result of an exisiting Task. Nice")
|
||||
elif not anon:
|
||||
print("[" + self.dvm_config.NIP89.name + "] Note Zap received for Bot balance: " +
|
||||
print("[" + self.dvm_config.NIP89.name + "] Note Zap received for DVM balance: " +
|
||||
str(invoice_amount) + " Sats from " + str(user.name))
|
||||
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
|
||||
config=self.dvm_config)
|
||||
|
||||
# a regular note
|
||||
elif not anon:
|
||||
print("[" + self.dvm_config.NIP89.name + "] Profile Zap received for Bot balance: " +
|
||||
print("[" + self.dvm_config.NIP89.name + "] Profile Zap received for DVM balance: " +
|
||||
str(invoice_amount) + " Sats from " + str(user.name))
|
||||
update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client,
|
||||
config=self.dvm_config)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during content decryption: {e}")
|
||||
print("[" + self.dvm_config.NIP89.name + "] Error during content decryption: " + str(e))
|
||||
|
||||
def handle_dm(dm_event):
|
||||
# Note that DVMs only listen and answer to Bots, if at all.
|
||||
decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content())
|
||||
ob = json.loads(decrypted_text)
|
||||
|
||||
# One key might host multiple DVMs, so we check current task
|
||||
if ob['task'] == self.dvm_config.SUPPORTED_DVMS[0].TASK:
|
||||
input_type = "text"
|
||||
print(decrypted_text)
|
||||
if str(ob['input']).startswith("http"):
|
||||
input_type = "url"
|
||||
|
||||
# TODO Handle additional inputs/params
|
||||
j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_DVMS[0].TASK])
|
||||
i_tag = Tag.parse(["i", ob['input'], input_type])
|
||||
|
||||
y_tag = Tag.parse(["y", dm_event.pubkey().to_hex()])
|
||||
z_tag = Tag.parse(["z", ob['sender']])
|
||||
tags = [j_tag, i_tag, y_tag, z_tag]
|
||||
job_event = EventBuilder(EventDefinitions.KIND_DM, "", tags).to_event(self.keys)
|
||||
|
||||
do_work(job_event, is_from_bot=True)
|
||||
|
||||
def check_event_has_not_unfinished_job_input(nevent, append, client, dvm_config):
|
||||
task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvm_config)
|
||||
def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig):
|
||||
task_supported, task, duration = check_task_is_supported(nevent, client, False,
|
||||
config=dvmconfig)
|
||||
if not task_supported:
|
||||
return False
|
||||
|
||||
@@ -252,34 +271,36 @@ class DVM:
|
||||
if input_type == "job":
|
||||
evt = get_referenced_event_by_id(event_id=input, client=client,
|
||||
kinds=EventDefinitions.ANY_RESULT,
|
||||
dvm_config=dvm_config)
|
||||
dvm_config=dvmconfig)
|
||||
if evt is None:
|
||||
if append:
|
||||
job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs())
|
||||
self.jobs_on_hold_list.append(job)
|
||||
job_ = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs())
|
||||
self.jobs_on_hold_list.append(job_)
|
||||
send_job_status_reaction(nevent, "chain-scheduled", True, 0,
|
||||
client=client, dvm_config=dvm_config)
|
||||
client=client, dvm_config=dvmconfig)
|
||||
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def check_and_return_event(data, original_event_str: str, is_from_bot: bool):
|
||||
def check_and_return_event(data, original_event_str: str):
|
||||
original_event = Event.from_json(original_event_str)
|
||||
|
||||
for x in self.job_list:
|
||||
if x.event_id == original_event.id().to_hex():
|
||||
if x.event == original_event:
|
||||
is_paid = x.is_paid
|
||||
amount = x.amount
|
||||
x.result = data
|
||||
x.is_processed = True
|
||||
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,
|
||||
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:
|
||||
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?
|
||||
|
||||
if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and is_paid:
|
||||
self.job_list.remove(x)
|
||||
@@ -290,103 +311,68 @@ class DVM:
|
||||
|
||||
try:
|
||||
post_processed_content = post_process_result(data, original_event)
|
||||
|
||||
if is_from_bot:
|
||||
# Reply to Bot
|
||||
original_sender = ""
|
||||
receiver_key = PublicKey
|
||||
for tag in original_event.tags():
|
||||
if tag.as_vec()[0] == "y": # TODO we temporally use internal tags in Kind4 to move information
|
||||
receiver_key = PublicKey.from_hex(tag.as_vec()[1])
|
||||
elif tag.as_vec()[0] == "z":
|
||||
original_sender = tag.as_vec()[1]
|
||||
|
||||
params = {
|
||||
"result": post_processed_content,
|
||||
"sender": original_sender
|
||||
}
|
||||
message = json.dumps(params)
|
||||
print("[" + self.dvm_config.NIP89.name + "] " + message)
|
||||
response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message,
|
||||
None).to_event(self.keys)
|
||||
send_event(response_event, client=self.client, dvm_config=self.dvm_config)
|
||||
else:
|
||||
# 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:
|
||||
respond_to_error(str(e), original_event_str, False)
|
||||
send_job_status_reaction(original_event, "error", content=str(e), dvm_config=self.dvm_config,
|
||||
)
|
||||
|
||||
def send_nostr_reply_event(content, original_event_as_str):
|
||||
original_event = Event.from_json(original_event_as_str)
|
||||
request_tag = Tag.parse(["request", original_event_as_str.replace("\\", "")])
|
||||
e_tag = Tag.parse(["e", original_event.id().to_hex()])
|
||||
p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
|
||||
# p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
|
||||
alt_tag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str(
|
||||
original_event.kind()) + ". The task was: " + original_event.content()])
|
||||
status_tag = Tag.parse(["status", "success"])
|
||||
reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag]
|
||||
reply_tags = [request_tag, e_tag, alt_tag, status_tag]
|
||||
encrypted = False
|
||||
for tag in original_event.tags():
|
||||
if tag.as_vec()[0] == "encrypted":
|
||||
encrypted = True
|
||||
encrypted_tag = Tag.parse(["encrypted"])
|
||||
reply_tags.append(encrypted_tag)
|
||||
|
||||
for tag in original_event.tags():
|
||||
if tag.as_vec()[0] == "i":
|
||||
i_content = tag.as_vec()[1]
|
||||
i_kind = tag.as_vec()[2]
|
||||
i_tag = Tag.parse(["i", i_content, i_kind])
|
||||
reply_tags.append(i_tag)
|
||||
i_tag = tag
|
||||
if not encrypted:
|
||||
reply_tags.append(i_tag)
|
||||
elif tag.as_vec()[0] == "p":
|
||||
p_tag = tag
|
||||
reply_tags.append(p_tag)
|
||||
|
||||
key = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY)
|
||||
if encrypted:
|
||||
content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.pubkey().to_hex()),
|
||||
content)
|
||||
|
||||
reply_event = EventBuilder(original_event.kind() + 1000, str(content), reply_tags).to_event(self.keys)
|
||||
|
||||
response_kind = original_event.kind() + 1000
|
||||
reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key)
|
||||
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())
|
||||
return reply_event.as_json()
|
||||
|
||||
def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False):
|
||||
print("ERROR: " + str(content))
|
||||
keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY)
|
||||
original_event = Event.from_json(original_event_as_str)
|
||||
sender = ""
|
||||
task = ""
|
||||
if not is_from_bot:
|
||||
send_job_status_reaction(original_event, "error", content=content, dvm_config=self.dvm_config)
|
||||
# TODO Send Zap back
|
||||
else:
|
||||
for tag in original_event.tags():
|
||||
if tag.as_vec()[0] == "p":
|
||||
sender = tag.as_vec()[1]
|
||||
elif tag.as_vec()[0] == "i":
|
||||
task = tag.as_vec()[1]
|
||||
|
||||
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
|
||||
if not user.iswhitelisted:
|
||||
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,
|
||||
user.nip05, user.lud16, user.name, Timestamp.now().as_secs())
|
||||
message = "There was the following error : " + content + ". Credits have been reimbursed"
|
||||
else:
|
||||
# User didn't pay, so no reimbursement
|
||||
message = "There was the following error : " + content
|
||||
|
||||
evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message,
|
||||
None).to_event(keys)
|
||||
send_event(evt, client=self.client, dvm_config=self.dvm_config)
|
||||
original_event.kind() + 1000) + " Job Response event sent: " + reply_event.as_json())
|
||||
|
||||
def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None,
|
||||
content=None,
|
||||
dvm_config=None):
|
||||
dvm_config = dvm_config
|
||||
|
||||
task = get_task(original_event, client=client, dvmconfig=dvm_config)
|
||||
alt_description, reaction = build_status_reaction(status, task, amount, content)
|
||||
|
||||
e_tag = Tag.parse(["e", original_event.id().to_hex()])
|
||||
p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
|
||||
# p_tag = Tag.parse(["p", original_event.pubkey().to_hex()])
|
||||
alt_tag = Tag.parse(["alt", alt_description])
|
||||
status_tag = Tag.parse(["status", status])
|
||||
tags = [e_tag, p_tag, alt_tag, status_tag]
|
||||
|
||||
tags = [e_tag, alt_tag, status_tag]
|
||||
for tag in original_event.tags:
|
||||
|
||||
if tag.as_vec()[0] == "p":
|
||||
p_tag = tag
|
||||
tags.append(p_tag)
|
||||
|
||||
if status == "success" or status == "error": #
|
||||
for x in self.job_list:
|
||||
if x.event_id == original_event.id():
|
||||
if x.event == original_event:
|
||||
is_paid = x.is_paid
|
||||
amount = x.amount
|
||||
break
|
||||
@@ -401,15 +387,15 @@ class DVM:
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
if not any(x.event_id == original_event.id().to_hex() for x in self.job_list):
|
||||
if not any(x.event == original_event for x in self.job_list):
|
||||
self.job_list.append(
|
||||
JobToWatch(event_id=original_event.id().to_hex(),
|
||||
JobToWatch(event=original_event,
|
||||
timestamp=original_event.created_at().as_secs(),
|
||||
amount=amount,
|
||||
is_paid=is_paid,
|
||||
status=status, result="", is_processed=False, bolt11=bolt11,
|
||||
payment_hash=payment_hash,
|
||||
expires=expires, from_bot=False))
|
||||
expires=expires))
|
||||
# print(str(self.job_list))
|
||||
if (status == "payment-required" or status == "payment-rejected" or (
|
||||
status == "processing" and not is_paid)
|
||||
@@ -422,14 +408,13 @@ class DVM:
|
||||
tags.append(amount_tag)
|
||||
|
||||
keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
|
||||
event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys)
|
||||
|
||||
send_event(event, client=self.client, dvm_config=self.dvm_config)
|
||||
reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys)
|
||||
send_event(reaction_event, client=self.client, dvm_config=self.dvm_config)
|
||||
print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str(
|
||||
EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json())
|
||||
return event.as_json()
|
||||
EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + reaction_event.as_json())
|
||||
return reaction_event.as_json()
|
||||
|
||||
def do_work(job_event, is_from_bot=False):
|
||||
def do_work(job_event):
|
||||
if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC)
|
||||
or job_event.kind() == EventDefinitions.KIND_DM):
|
||||
|
||||
@@ -440,42 +425,36 @@ class DVM:
|
||||
request_form = dvm.create_request_form_from_nostr_event(job_event, self.client,
|
||||
self.dvm_config)
|
||||
result = dvm.process(request_form)
|
||||
check_and_return_event(result, str(job_event.as_json()), is_from_bot=is_from_bot)
|
||||
check_and_return_event(result, str(job_event.as_json()))
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
respond_to_error(str(e), job_event.as_json(), is_from_bot=is_from_bot)
|
||||
send_job_status_reaction(job_event, "error", content=str(e), dvm_config=self.dvm_config)
|
||||
return
|
||||
|
||||
self.client.handle_notifications(NotificationHandler())
|
||||
while True:
|
||||
for job in self.job_list:
|
||||
if job.bolt11 != "" and job.payment_hash != "" and not job.is_paid:
|
||||
if str(check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)) == "True":
|
||||
job.is_paid = True
|
||||
event = get_event_by_id(job.event_id, client=self.client, config=self.dvm_config)
|
||||
if event is not None:
|
||||
send_job_status_reaction(event, "processing", True, 0,
|
||||
client=self.client,
|
||||
dvm_config=self.dvm_config)
|
||||
print("[" + self.dvm_config.NIP89.name + "] doing work from joblist")
|
||||
ispaid = check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)
|
||||
if ispaid and job.is_paid is False:
|
||||
print("is paid")
|
||||
|
||||
do_work(event, is_from_bot=False)
|
||||
elif check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config) is None: # invoice expired
|
||||
try:
|
||||
self.job_list.remove(job)
|
||||
except:
|
||||
print("[" + self.dvm_config.NIP89.name + "] Error removing Job from List after payment")
|
||||
job.is_paid = True
|
||||
send_job_status_reaction(job.event, "processing", True, 0,
|
||||
client=self.client,
|
||||
dvm_config=self.dvm_config)
|
||||
print("[" + self.dvm_config.NIP89.name + "] doing work from joblist")
|
||||
do_work(job.event)
|
||||
elif ispaid is None: # invoice expired
|
||||
self.job_list.remove(job)
|
||||
|
||||
if Timestamp.now().as_secs() > job.expires:
|
||||
try:
|
||||
self.job_list.remove(job)
|
||||
except:
|
||||
print("[" + self.dvm_config.NIP89.name + "] Error removing Job from List after expiry")
|
||||
self.job_list.remove(job)
|
||||
|
||||
for job in self.jobs_on_hold_list:
|
||||
if check_event_has_not_unfinished_job_input(job.event, False, client=self.client,
|
||||
dvm_config=self.dvm_config):
|
||||
dvmconfig=self.dvm_config):
|
||||
handle_nip90_job_event(nip90_event=job.event)
|
||||
try:
|
||||
self.jobs_on_hold_list.remove(job)
|
||||
|
1
main.py
1
main.py
@@ -22,6 +22,7 @@ def run_nostr_dvm_with_local_config():
|
||||
bot_config = DVMConfig()
|
||||
bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY")
|
||||
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")
|
||||
|
||||
# Spawn some DVMs in the playground and run them
|
||||
|
@@ -95,8 +95,8 @@ def build_translator(name, dm_allowed_keys):
|
||||
def build_unstable_diffusion(name, dm_allowed_keys):
|
||||
dvm_config = DVMConfig()
|
||||
dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY")
|
||||
dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY")
|
||||
dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST")
|
||||
dvm_config.LNBITS_INVOICE_KEY = "" #This one will not use Lnbits to create invoices, but rely on zaps
|
||||
dvm_config.LNBITS_URL = ""
|
||||
dvm_config.DM_ALLOWED = dm_allowed_keys
|
||||
|
||||
# A module might have options it can be initialized with, here we set a default model, and the nova-server
|
||||
|
@@ -46,7 +46,7 @@ class ImageGenerationDALLE(DVMTaskInterface):
|
||||
model = "dall-e-3"
|
||||
quality = "standard"
|
||||
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
input_type = tag.as_vec()[2]
|
||||
if input_type == "text":
|
||||
|
@@ -55,7 +55,7 @@ class ImageGenerationSDXL(DVMTaskInterface):
|
||||
lora_weight = ""
|
||||
strength = ""
|
||||
guidance_scale = ""
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
input_type = tag.as_vec()[2]
|
||||
if input_type == "text":
|
||||
|
@@ -43,7 +43,7 @@ class TextExtractionPDF(DVMTaskInterface):
|
||||
input_content = ""
|
||||
url = ""
|
||||
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
input_type = tag.as_vec()[2]
|
||||
input_content = tag.as_vec()[1]
|
||||
|
@@ -42,7 +42,7 @@ class Translation(DVMTaskInterface):
|
||||
text = ""
|
||||
translation_lang = "en"
|
||||
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
input_type = tag.as_vec()[2]
|
||||
|
||||
@@ -52,20 +52,20 @@ class Translation(DVMTaskInterface):
|
||||
translation_lang = str(tag.as_vec()[2]).split('-')[0]
|
||||
|
||||
if input_type == "event":
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvm_config)
|
||||
text = evt.content()
|
||||
break
|
||||
|
||||
elif input_type == "text":
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
text = tag.as_vec()[1]
|
||||
break
|
||||
|
||||
elif input_type == "job":
|
||||
for tag in event.tags():
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
evt = get_referenced_event_by_id(event_id=tag.as_vec()[1], client=client,
|
||||
kinds=[EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT,
|
||||
|
@@ -84,11 +84,11 @@ def nostr_client():
|
||||
EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events
|
||||
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("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("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20)
|
||||
|
||||
#nostr_client_test_image("a beautiful purple ostrich watching the sunset")
|
||||
nostr_client_test_image("a beautiful purple ostrich watching the sunset")
|
||||
class NotificationHandler(HandleNotification):
|
||||
def handle(self, relay_url, event):
|
||||
print(f"Received new event from {relay_url}: {event.as_json()}")
|
||||
|
@@ -51,47 +51,53 @@ def get_task(event, client, dvmconfig):
|
||||
|
||||
|
||||
def check_task_is_supported(event, client, get_duration=False, config=None):
|
||||
dvm_config = config
|
||||
input_value = ""
|
||||
input_type = ""
|
||||
duration = 1
|
||||
task = get_task(event, client=client, dvmconfig=dvm_config)
|
||||
for tag in event.tags():
|
||||
if tag.as_vec()[0] == 'i':
|
||||
if len(tag.as_vec()) < 3:
|
||||
print("Job Event missing/malformed i tag, skipping..")
|
||||
return False, "", 0
|
||||
else:
|
||||
input_value = tag.as_vec()[1]
|
||||
input_type = tag.as_vec()[2]
|
||||
if input_type == "event":
|
||||
evt = get_event_by_id(input_value, client=client, config=dvm_config)
|
||||
if evt is None:
|
||||
print("Event not found")
|
||||
return False, "", 0
|
||||
elif input_type == 'url' and check_url_is_readable(input_value) is None:
|
||||
print("Url not readable / supported")
|
||||
try:
|
||||
dvm_config = config
|
||||
input_value = ""
|
||||
input_type = ""
|
||||
duration = 1
|
||||
|
||||
task = get_task(event, client=client, dvmconfig=dvm_config)
|
||||
|
||||
for tag in event.tags:
|
||||
if tag.as_vec()[0] == 'i':
|
||||
if len(tag.as_vec()) < 3:
|
||||
print("Job Event missing/malformed i tag, skipping..")
|
||||
return False, "", 0
|
||||
else:
|
||||
input_value = tag.as_vec()[1]
|
||||
input_type = tag.as_vec()[2]
|
||||
if input_type == "event":
|
||||
evt = get_event_by_id(input_value, client=client, config=dvm_config)
|
||||
if evt is None:
|
||||
print("Event not found")
|
||||
return False, "", 0
|
||||
elif input_type == 'url' and check_url_is_readable(input_value) is None:
|
||||
print("Url not readable / supported")
|
||||
return False, task, duration
|
||||
|
||||
elif tag.as_vec()[0] == 'output':
|
||||
output = tag.as_vec()[1]
|
||||
if not (output == "text/plain"
|
||||
or output == "text/json" or output == "json"
|
||||
or output == "image/png" or "image/jpg"
|
||||
or output == "image/png;format=url" or output == "image/jpg;format=url"
|
||||
or output == ""):
|
||||
print("Output format not supported, skipping..")
|
||||
return False, "", 0
|
||||
|
||||
for dvm in dvm_config.SUPPORTED_DVMS:
|
||||
if dvm.TASK == task:
|
||||
if not dvm.is_input_supported(input_type, event.content()):
|
||||
return False, task, duration
|
||||
|
||||
elif tag.as_vec()[0] == 'output':
|
||||
output = tag.as_vec()[1]
|
||||
if not (output == "text/plain"
|
||||
or output == "text/json" or output == "json"
|
||||
or output == "image/png" or "image/jpg"
|
||||
or output == "image/png;format=url" or output == "image/jpg;format=url"
|
||||
or output == ""):
|
||||
print("Output format not supported, skipping..")
|
||||
return False, "", 0
|
||||
if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS):
|
||||
return False, task, duration
|
||||
|
||||
for dvm in dvm_config.SUPPORTED_DVMS:
|
||||
if dvm.TASK == task:
|
||||
if not dvm.is_input_supported(input_type, event.content()):
|
||||
return False, task, duration
|
||||
return True, task, duration
|
||||
|
||||
if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS):
|
||||
return False, task, duration
|
||||
|
||||
return True, task, duration
|
||||
except Exception as e:
|
||||
print("Check task: " + str(e))
|
||||
|
||||
|
||||
def check_url_is_readable(url):
|
||||
|
@@ -35,7 +35,7 @@ class EventDefinitions:
|
||||
|
||||
@dataclass
|
||||
class JobToWatch:
|
||||
event_id: str
|
||||
event: str
|
||||
timestamp: int
|
||||
is_paid: bool
|
||||
amount: int
|
||||
@@ -45,8 +45,6 @@ class JobToWatch:
|
||||
bolt11: str
|
||||
payment_hash: str
|
||||
expires: int
|
||||
from_bot: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequiredJobToWatch:
|
||||
|
@@ -14,6 +14,7 @@ class DVMConfig:
|
||||
|
||||
RELAY_TIMEOUT = 3
|
||||
LNBITS_INVOICE_KEY = ''
|
||||
LNBITS_ADMIN_KEY = '' # In order to pay invoices, e.g. from the bot to DVMs, or reimburse users.
|
||||
LNBITS_URL = 'https://lnbits.com'
|
||||
DB: str
|
||||
NEW_USER_BALANCE: int = 250 # Free credits for new users
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey
|
||||
from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt
|
||||
|
||||
|
||||
def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None:
|
||||
@@ -57,3 +58,41 @@ def send_event(event: Event, client: Client, dvm_config) -> EventId:
|
||||
client.remove_relay(relay)
|
||||
|
||||
return event_id
|
||||
|
||||
|
||||
def check_and_decrypt_tags(event, dvm_config):
|
||||
tags = []
|
||||
is_encrypted = False
|
||||
p = ""
|
||||
for tag in event.tags():
|
||||
if tag.as_vec()[0] == 'encrypted':
|
||||
is_encrypted = True
|
||||
elif tag.as_vec()[0] == 'p':
|
||||
p = tag.as_vec()[1]
|
||||
|
||||
if is_encrypted:
|
||||
if p != Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex():
|
||||
print("[" + dvm_config.NIP89.name + "] Task encrypted and not addressed to this DVM, "
|
||||
"skipping..")
|
||||
return None, None
|
||||
|
||||
elif p == Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex():
|
||||
encrypted_tag = Tag.parse(["encrypted"])
|
||||
p_tag = Tag.parse(["p", p])
|
||||
|
||||
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
|
||||
event.pubkey(), event.content())
|
||||
params = json.loads(tags_str)
|
||||
|
||||
for element in params:
|
||||
tags.append(Tag.parse(element))
|
||||
|
||||
# Keep the encrypted tag
|
||||
tags.append(p_tag)
|
||||
tags.append(encrypted_tag)
|
||||
|
||||
return tags, p
|
||||
|
||||
else:
|
||||
return event.tags, p
|
||||
|
||||
|
@@ -17,7 +17,7 @@ Post process results to either given output format or a Nostr readable plain tex
|
||||
def post_process_result(anno, original_event):
|
||||
print("Post-processing...")
|
||||
if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format
|
||||
for tag in original_event.tags():
|
||||
for tag in original_event.tags:
|
||||
print(tag.as_vec()[0])
|
||||
if tag.as_vec()[0] == "output":
|
||||
output_format = tag.as_vec()[1]
|
||||
|
@@ -1,12 +1,17 @@
|
||||
# LIGHTNING FUNCTIONS
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
from Crypto.Cipher import AES
|
||||
from bech32 import bech32_decode, convertbits
|
||||
from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event
|
||||
from Crypto.Util.Padding import pad
|
||||
from bech32 import bech32_decode, convertbits, bech32_encode
|
||||
from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys
|
||||
from utils.dvmconfig import DVMConfig
|
||||
from utils.nostr_utils import get_event_by_id
|
||||
import lnurl
|
||||
from hashlib import sha256
|
||||
|
||||
|
||||
def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int:
|
||||
@@ -40,6 +45,7 @@ def parse_zap_event_tags(zap_event, keys, name, client, config):
|
||||
zapped_event = None
|
||||
invoice_amount = 0
|
||||
anon = False
|
||||
message = ""
|
||||
sender = zap_event.pubkey()
|
||||
|
||||
for tag in zap_event.tags():
|
||||
@@ -48,28 +54,28 @@ def parse_zap_event_tags(zap_event, keys, name, client, config):
|
||||
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")
|
||||
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
|
||||
return invoice_amount, zapped_event, sender, message, anon
|
||||
|
||||
|
||||
def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str):
|
||||
@@ -91,11 +97,24 @@ def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig):
|
||||
try:
|
||||
res = requests.get(url, headers=headers)
|
||||
obj = json.loads(res.text)
|
||||
return obj["paid"] #TODO cast
|
||||
return obj["paid"]
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def pay_bolt11_ln_bits(bolt11: str, config: DVMConfig):
|
||||
url = config.LNBITS_URL + "/api/v1/payments"
|
||||
data = {'out': True, 'bolt11': bolt11}
|
||||
headers = {'X-API-Key': config.LNBITS_ADMIN_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'}
|
||||
try:
|
||||
res = requests.post(url, json=data, headers=headers)
|
||||
obj = json.loads(res.text)
|
||||
return obj["payment_hash"]
|
||||
except Exception as e:
|
||||
print("LNBITS: " + str(e))
|
||||
return None, None
|
||||
|
||||
|
||||
# DECRYPT ZAPS
|
||||
def check_for_zapplepay(pubkey_hex: str, content: str):
|
||||
try:
|
||||
@@ -111,6 +130,22 @@ def check_for_zapplepay(pubkey_hex: str, content: str):
|
||||
return pubkey_hex
|
||||
|
||||
|
||||
def enrypt_private_zap_message(message, privatekey, publickey):
|
||||
# Generate a random IV
|
||||
shared_secret = nostr_sdk.generate_shared_key(privatekey, publickey)
|
||||
iv = os.urandom(16)
|
||||
|
||||
# Encrypt the message
|
||||
cipher = AES.new(bytearray(shared_secret), AES.MODE_CBC, bytearray(iv))
|
||||
utf8message = message.encode('utf-8')
|
||||
padded_message = pad(utf8message, AES.block_size)
|
||||
encrypted_msg = cipher.encrypt(padded_message)
|
||||
|
||||
encrypted_msg_bech32 = bech32_encode("pzap", convertbits(encrypted_msg, 8, 5, True))
|
||||
iv_bech32 = bech32_encode("iv", convertbits(iv, 8, 5, True))
|
||||
return encrypted_msg_bech32 + "_" + iv_bech32
|
||||
|
||||
|
||||
def decrypt_private_zap_message(msg: str, privkey: SecretKey, pubkey: PublicKey):
|
||||
shared_secret = nostr_sdk.generate_shared_key(privkey, pubkey)
|
||||
if len(shared_secret) != 16 and len(shared_secret) != 32:
|
||||
@@ -133,3 +168,47 @@ def decrypt_private_zap_message(msg: str, privkey: SecretKey, pubkey: PublicKey)
|
||||
return decoded
|
||||
except Exception as ex:
|
||||
return str(ex)
|
||||
|
||||
|
||||
def zap(lud16: str, amount: int, content, zapped_event: Event, keys, dvm_config, zaptype="public"):
|
||||
if lud16.startswith("LNURL") or lud16.startswith("lnurl"):
|
||||
url = lnurl.decode(lud16)
|
||||
elif '@' in lud16: # LNaddress
|
||||
url = 'https://' + str(lud16).split('@')[1] + '/.well-known/lnurlp/' + str(lud16).split('@')[0]
|
||||
else: # No lud16 set or format invalid
|
||||
return None
|
||||
try:
|
||||
response = requests.get(url)
|
||||
ob = json.loads(response.content)
|
||||
callback = ob["callback"]
|
||||
encoded_lnurl = lnurl.encode(url)
|
||||
amount_tag = Tag.parse(['amount', str(amount * 1000)])
|
||||
relays_tag = Tag.parse(['relays', str(dvm_config.RELAY_LIST)])
|
||||
p_tag = Tag.parse(['p', zapped_event.pubkey().to_hex()])
|
||||
e_tag = Tag.parse(['e', zapped_event.id().to_hex()])
|
||||
lnurl_tag = Tag.parse(['lnurl', encoded_lnurl])
|
||||
tags = [amount_tag, relays_tag, p_tag, e_tag, lnurl_tag]
|
||||
|
||||
if zaptype == "private":
|
||||
key_str = keys.secret_key().to_hex() + zapped_event.id().to_hex() + str(zapped_event.created_at().as_secs())
|
||||
encryption_key = sha256(key_str.encode('utf-8')).hexdigest()
|
||||
|
||||
zap_request = EventBuilder(9733, content,
|
||||
[p_tag, e_tag]).to_event(keys).as_json()
|
||||
keys = Keys.from_sk_str(encryption_key)
|
||||
encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_event.pubkey())
|
||||
anon_tag = Tag.parse(['anon', encrypted_content])
|
||||
tags.append(anon_tag)
|
||||
content = ""
|
||||
|
||||
zap_request = EventBuilder(9734, content,
|
||||
tags).to_event(keys).as_json()
|
||||
|
||||
response = requests.get(callback + "?amount=" + str(int(amount) * 1000) + "&nostr=" + urllib.parse.quote_plus(
|
||||
zap_request) + "&lnurl=" + encoded_lnurl)
|
||||
ob = json.loads(response.content)
|
||||
return ob["pr"]
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
Reference in New Issue
Block a user