2023-11-23 11:53:57 +01:00
import json
2023-12-16 00:42:06 +01:00
import os
import subprocess
2023-11-23 13:30:09 +01:00
from datetime import timedelta
2023-12-20 18:21:40 +01:00
from sys import platform
2023-11-23 11:53:57 +01:00
2023-11-18 20:20:18 +01:00
from nostr_sdk import PublicKey , Keys , Client , Tag , Event , EventBuilder , Filter , HandleNotification , Timestamp , \
2024-04-15 17:31:51 +02:00
init_logger , LogLevel , Options , nip04_encrypt , NostrSigner , Kind
2023-11-21 23:51:48 +01:00
2023-11-18 20:20:18 +01:00
import time
2023-11-19 18:47:05 +01:00
2023-12-13 20:00:03 +01:00
from nostr_dvm . utils . definitions import EventDefinitions , RequiredJobToWatch , JobToWatch
from nostr_dvm . utils . dvmconfig import DVMConfig
from nostr_dvm . utils . admin_utils import admin_make_database_updates , AdminConfig
from nostr_dvm . utils . backend_utils import get_amount_per_task , check_task_is_supported , get_task
2024-03-18 19:44:44 +01:00
from nostr_dvm . utils . database_utils import create_sql_table , get_or_add_user , update_user_balance , update_sql_table , \
update_user_subscription
2023-12-13 20:00:03 +01:00
from nostr_dvm . utils . mediasource_utils import input_data_file_duration
2024-03-18 19:44:44 +01:00
from nostr_dvm . utils . nip88_utils import nip88_has_active_subscription
2023-12-13 20:00:03 +01:00
from nostr_dvm . utils . nostr_utils import get_event_by_id , get_referenced_event_by_id , send_event , check_and_decrypt_tags
from nostr_dvm . utils . output_utils import build_status_reaction
from nostr_dvm . utils . zap_utils import check_bolt11_ln_bits_is_paid , create_bolt11_ln_bits , parse_zap_event_tags , \
2024-01-06 17:47:22 +01:00
parse_amount_from_bolt11_invoice , zaprequest , pay_bolt11_ln_bits , create_bolt11_lud16
2023-12-13 20:00:03 +01:00
from nostr_dvm . utils . cashu_utils import redeem_cashu
2023-11-18 20:20:18 +01:00
2023-11-20 22:09:38 +01:00
class DVM :
dvm_config : DVMConfig
2023-11-21 11:11:12 +01:00
admin_config : AdminConfig
2023-11-20 22:09:38 +01:00
keys : Keys
client : Client
2023-11-21 10:03:04 +01:00
job_list : list
jobs_on_hold_list : list
2023-11-18 20:20:18 +01:00
2023-11-24 22:07:00 +01:00
def __init__ ( self , dvm_config , admin_config = None ) :
self . dvm_config = dvm_config
self . admin_config = admin_config
2024-02-17 20:40:56 +01:00
self . keys = Keys . parse ( dvm_config . PRIVATE_KEY )
2024-03-21 13:41:11 +01:00
wait_for_send = False
2023-11-23 13:30:09 +01:00
skip_disconnected_relays = True
opts = ( Options ( ) . wait_for_send ( wait_for_send ) . send_timeout ( timedelta ( seconds = self . dvm_config . RELAY_TIMEOUT ) )
. skip_disconnected_relays ( skip_disconnected_relays ) )
2024-02-17 20:40:56 +01:00
signer = NostrSigner . keys ( self . keys )
2024-03-04 15:42:37 +01:00
self . client = Client . with_opts ( signer , opts )
2024-01-05 14:54:21 +01:00
2023-11-21 10:03:04 +01:00
self . job_list = [ ]
self . jobs_on_hold_list = [ ]
2023-11-20 22:09:38 +01:00
pk = self . keys . public_key ( )
2023-11-18 20:20:18 +01:00
2024-03-28 10:14:30 +01:00
print ( " Nostr DVM public key: " + str ( pk . to_bech32 ( ) ) + " Hex: " + str ( pk . to_hex ( ) ) + " Supported DVM tasks: " +
' , ' . join ( p . NAME + " : " + p . TASK for p in self . dvm_config . SUPPORTED_DVMS ) + " \n " )
2023-11-18 20:20:18 +01:00
2023-11-20 22:09:38 +01:00
for relay in self . dvm_config . RELAY_LIST :
self . client . add_relay ( relay )
self . client . connect ( )
2023-11-19 20:35:40 +01:00
2023-11-23 11:53:57 +01:00
zap_filter = Filter ( ) . pubkey ( pk ) . kinds ( [ EventDefinitions . KIND_ZAP ] ) . since ( Timestamp . now ( ) )
2023-11-20 22:09:38 +01:00
kinds = [ EventDefinitions . KIND_NIP90_GENERIC ]
2023-11-23 13:30:09 +01:00
for dvm in self . dvm_config . SUPPORTED_DVMS :
2023-11-20 22:09:38 +01:00
if dvm . KIND not in kinds :
kinds . append ( dvm . KIND )
dvm_filter = ( Filter ( ) . kinds ( kinds ) . since ( Timestamp . now ( ) ) )
2024-03-18 19:44:44 +01:00
self . client . subscribe ( [ dvm_filter , zap_filter ] , None )
2023-11-18 20:20:18 +01:00
2023-11-21 10:03:04 +01:00
create_sql_table ( self . dvm_config . DB )
2023-11-21 11:11:12 +01:00
admin_make_database_updates ( adminconfig = self . admin_config , dvmconfig = self . dvm_config , client = self . client )
2023-11-18 20:20:18 +01:00
2023-11-20 22:09:38 +01:00
class NotificationHandler ( HandleNotification ) :
client = self . client
dvm_config = self . dvm_config
keys = self . keys
2023-11-19 20:35:40 +01:00
2024-03-18 19:44:44 +01:00
def handle ( self , relay_url , subscription_id , nostr_event : Event ) :
if EventDefinitions . KIND_NIP90_EXTRACT_TEXT . as_u64 ( ) < = nostr_event . kind ( ) . as_u64 ( ) < = EventDefinitions . KIND_NIP90_GENERIC . as_u64 ( ) :
2023-11-20 23:18:05 +01:00
handle_nip90_job_event ( nostr_event )
2024-03-18 19:44:44 +01:00
elif nostr_event . kind ( ) . as_u64 ( ) == EventDefinitions . KIND_ZAP . as_u64 ( ) :
2023-11-20 23:18:05 +01:00
handle_zap ( nostr_event )
2023-11-19 20:35:40 +01:00
2023-11-20 22:09:38 +01:00
def handle_msg ( self , relay_url , msg ) :
return
2023-11-20 19:17:10 +01:00
2023-11-20 23:18:05 +01:00
def handle_nip90_job_event ( nip90_event ) :
2024-03-19 12:33:26 +01:00
# decrypted encrypted events
2023-11-27 10:35:58 +01:00
nip90_event = check_and_decrypt_tags ( nip90_event , self . dvm_config )
2024-03-19 12:33:26 +01:00
# if event is encrypted, but we can't decrypt it (e.g. because its directed to someone else), return
2023-11-27 10:35:58 +01:00
if nip90_event is None :
2023-11-26 21:47:33 +01:00
return
2024-03-19 12:59:16 +01:00
task_is_free = False
user_has_active_subscription = False
cashu = " "
p_tag_str = " "
for tag in nip90_event . tags ( ) :
if tag . as_vec ( ) [ 0 ] == " cashu " :
cashu = tag . as_vec ( ) [ 1 ]
elif tag . as_vec ( ) [ 0 ] == " p " :
p_tag_str = tag . as_vec ( ) [ 1 ]
if p_tag_str != " " and p_tag_str != self . dvm_config . PUBLIC_KEY :
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] No public request, also not addressed to me. " )
return
2024-03-19 12:33:26 +01:00
# check if task is supported by the current DVM
2023-11-28 16:20:56 +01:00
task_supported , task = check_task_is_supported ( nip90_event , client = self . client ,
config = self . dvm_config )
2024-03-19 12:33:26 +01:00
# if task is supported, continue, else do nothing.
if task_supported :
# fetch or add user contacting the DVM from/to local database
user = get_or_add_user ( self . dvm_config . DB , nip90_event . author ( ) . to_hex ( ) , client = self . client ,
2024-03-21 13:41:11 +01:00
config = self . dvm_config , skip_meta = False )
2024-03-19 12:33:26 +01:00
# if user is blacklisted for some reason, send an error reaction and return
if user . isblacklisted :
send_job_status_reaction ( nip90_event , " error " , client = self . client , dvm_config = self . dvm_config )
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Request by blacklisted user, skipped " )
return
2023-11-20 23:18:05 +01:00
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Received new Request: " + task + " from " + user . name )
2023-11-28 16:20:56 +01:00
duration = input_data_file_duration ( nip90_event , dvm_config = self . dvm_config , client = self . client )
2023-11-20 23:18:05 +01:00
amount = get_amount_per_task ( task , self . dvm_config , duration )
if amount is None :
return
2024-03-19 12:59:16 +01:00
2024-03-19 12:33:26 +01:00
# If this is a subscription DVM and the Task is directed to us, check for active subscription
2024-03-18 19:44:44 +01:00
if dvm_config . NIP88 is not None and p_tag_str == self . dvm_config . PUBLIC_KEY :
2024-03-21 13:41:11 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " , True , amount , self . client ,
" Checking Subscription Status, please wait.. " , self . dvm_config )
2024-03-18 19:44:44 +01:00
# if we stored in the database that the user has an active subscription, we don't need to check it
2024-03-19 11:13:20 +01:00
print ( " User Subscription: " + str ( user . subscribed ) + " Current time: " + str (
Timestamp . now ( ) . as_secs ( ) ) )
2024-03-19 12:33:26 +01:00
# if we have an entry in the db that user is subscribed, continue
2024-03-19 12:59:16 +01:00
if int ( user . subscribed ) > int ( Timestamp . now ( ) . as_secs ( ) ) :
2024-03-18 19:44:44 +01:00
print ( " User subscribed until: " + str ( Timestamp . from_secs ( user . subscribed ) . to_human_datetime ( ) ) )
user_has_active_subscription = True
2024-03-21 13:41:11 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " , True , amount ,
self . client , " User subscripton active until " +
Timestamp . from_secs ( int ( user . subscribed ) ) . to_human_datetime ( ) . replace ( " Z " , " " ) . replace ( " T " , " " ) + " GMT " , self . dvm_config )
2024-03-19 12:33:26 +01:00
# otherwise we check for an active subscription by checking recipie events
2024-03-18 19:44:44 +01:00
else :
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Checking Subscription status " )
2024-03-21 13:41:11 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " , True , amount , self . client ,
" I Don ' t have information about subscription status, checking on the Nostr. This might take a few seconds " ,
self . dvm_config )
2024-03-18 19:44:44 +01:00
subscription_status = nip88_has_active_subscription ( PublicKey . parse ( user . npub ) ,
self . dvm_config . NIP88 . DTAG , self . client ,
2024-03-18 22:50:48 +01:00
self . dvm_config . PUBLIC_KEY )
2024-03-18 19:44:44 +01:00
if subscription_status [ " isActive " ] :
2024-03-21 13:41:11 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " , True , amount , self . client ,
" User subscripton active until " + Timestamp . from_secs ( int ( subscription_status [ " validUntil " ] ) ) . to_human_datetime ( ) . replace ( " Z " , " " ) . replace ( " T " , " " ) + " GMT " ,
self . dvm_config )
2024-03-19 10:28:46 +01:00
print ( " Checked Recipe: User subscribed until: " + str (
2024-03-18 19:44:44 +01:00
Timestamp . from_secs ( int ( subscription_status [ " validUntil " ] ) ) . to_human_datetime ( ) ) )
user_has_active_subscription = True
update_user_subscription ( user . npub ,
int ( subscription_status [ " validUntil " ] ) ,
self . client , self . dvm_config )
else :
print ( " No active subscription found " )
2024-03-21 13:41:11 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " , True , amount , self . client ,
2024-04-15 09:48:37 +02:00
" No active subscription found. Manage your subscription at: " + self . dvm_config . SUBSCRIPTION_MANAGEMENT ,
2024-03-21 13:41:11 +01:00
self . dvm_config )
2024-03-18 19:44:44 +01:00
2023-11-23 13:30:09 +01:00
for dvm in self . dvm_config . SUPPORTED_DVMS :
2024-03-18 19:44:44 +01:00
if dvm . TASK == task and dvm . FIX_COST == 0 and dvm . PER_UNIT_COST == 0 and dvm_config . NIP88 is None :
2023-11-20 23:18:05 +01:00
task_is_free = True
2023-11-27 00:02:56 +01:00
cashu_redeemed = False
if cashu != " " :
2023-11-28 18:25:59 +01:00
print ( cashu )
2023-12-09 23:58:15 +01:00
cashu_redeemed , cashu_message , redeem_amount , fees = redeem_cashu ( cashu , self . dvm_config ,
self . client , int ( amount ) )
2023-11-28 18:25:59 +01:00
print ( cashu_message )
2023-11-28 08:16:34 +01:00
if cashu_message != " success " :
2023-11-27 23:37:44 +01:00
send_job_status_reaction ( nip90_event , " error " , False , amount , self . client , cashu_message ,
self . dvm_config )
return
2023-11-26 21:47:33 +01:00
# if user is whitelisted or task is free, just do the job
2024-03-18 19:44:44 +01:00
if ( user . iswhitelisted or task_is_free or cashu_redeemed ) and (
p_tag_str == " " or p_tag_str ==
self . dvm_config . PUBLIC_KEY ) :
2023-11-24 21:29:24 +01:00
print (
2023-11-29 15:09:35 +01:00
" [ " + self . dvm_config . NIP89 . NAME + " ] Free task or Whitelisted for task " + task +
2023-11-24 22:07:00 +01:00
" . Starting processing.. " )
2023-11-26 10:31:38 +01:00
2024-01-22 19:01:39 +01:00
if dvm_config . SEND_FEEDBACK_EVENTS :
send_job_status_reaction ( nip90_event , " processing " , True , 0 ,
2024-03-18 19:44:44 +01:00
client = self . client , dvm_config = self . dvm_config , user = user )
2023-11-22 19:20:34 +01:00
2023-12-09 23:58:15 +01:00
# when we reimburse users on error make sure to not send anything if it was free
if user . iswhitelisted or task_is_free :
amount = 0
do_work ( nip90_event , amount )
2024-03-18 19:44:44 +01:00
# if task is directed to us via p tag and user has balance or is subscribed, do the job and update balance
elif ( p_tag_str == self . dvm_config . PUBLIC_KEY and (
user . balance > = int (
2024-03-19 12:33:26 +01:00
amount ) and dvm_config . NIP88 is None ) or (
p_tag_str == self . dvm_config . PUBLIC_KEY and user_has_active_subscription ) ) :
2024-03-18 19:44:44 +01:00
if not user_has_active_subscription :
balance = max ( user . balance - int ( 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 ( ) , subscribed = user . subscribed )
2023-11-26 10:31:38 +01:00
2024-03-18 19:44:44 +01:00
print (
" [ " + self . dvm_config . NIP89 . NAME + " ] Using user ' s balance for task: " + task +
" . Starting processing.. New balance is: " + str ( balance ) )
else :
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] User has active subscription for task: " + task +
" . Starting processing.. Balance remains at: " + str ( user . balance ) )
2023-11-26 10:31:38 +01:00
2023-11-26 13:10:19 +01:00
send_job_status_reaction ( nip90_event , " processing " , True , 0 ,
client = self . client , dvm_config = self . dvm_config )
2023-11-26 10:31:38 +01:00
2023-12-09 23:58:15 +01:00
do_work ( nip90_event , amount )
2023-11-26 10:31:38 +01:00
2023-11-26 21:47:33 +01:00
# else send a payment required event to user
2023-11-30 08:07:30 +01:00
elif p_tag_str == " " or p_tag_str == self . dvm_config . PUBLIC_KEY :
2023-11-20 23:18:05 +01:00
2024-03-18 19:44:44 +01:00
if dvm_config . NIP88 is not None :
2023-11-24 21:29:24 +01:00
print (
2024-03-18 19:44:44 +01:00
" [ " + self . dvm_config . NIP89 . NAME + " ] Hinting user for Subscription: " +
2023-11-24 22:07:00 +01:00
nip90_event . id ( ) . to_hex ( ) )
2024-03-18 19:44:44 +01:00
send_job_status_reaction ( nip90_event , " subscription-required " ,
False , 0 , client = self . client ,
dvm_config = self . dvm_config )
else :
bid = 0
for tag in nip90_event . tags ( ) :
if tag . as_vec ( ) [ 0 ] == ' bid ' :
bid = int ( tag . as_vec ( ) [ 1 ] )
print (
" [ " + self . dvm_config . NIP89 . NAME + " ] Payment required: New Nostr " + task + " Job event: "
+ nip90_event . as_json ( ) )
if bid > 0 :
bid_offer = int ( bid / 1000 )
if bid_offer > = int ( amount ) :
send_job_status_reaction ( nip90_event , " payment-required " , False ,
int ( amount ) , # bid_offer
client = self . client , dvm_config = self . dvm_config )
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 ( ) )
send_job_status_reaction ( nip90_event , " payment-required " ,
False , int ( amount ) , client = self . client , dvm_config = self . dvm_config )
2023-11-30 08:07:30 +01:00
else :
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Job addressed to someone else, skipping.. " )
2023-11-28 16:20:56 +01:00
# else:
2023-11-29 15:09:35 +01:00
# print("[" + self.dvm_config.NIP89.NAME + "] Task " + task + " not supported on this DVM, skipping..")
2023-11-20 23:18:05 +01:00
2023-11-21 23:51:48 +01:00
def handle_zap ( zap_event ) :
2023-11-20 23:18:05 +01:00
try :
2023-11-26 21:47:33 +01:00
invoice_amount , zapped_event , sender , message , anon = parse_zap_event_tags ( zap_event ,
2023-11-27 10:35:58 +01:00
self . keys ,
2023-11-29 15:09:35 +01:00
self . dvm_config . NIP89 . NAME ,
2023-11-27 10:35:58 +01:00
self . client , self . dvm_config )
2023-11-24 21:29:24 +01:00
user = get_or_add_user ( db = self . dvm_config . DB , npub = sender , client = self . client , config = self . dvm_config )
2023-11-20 23:18:05 +01:00
if zapped_event is not None :
2024-04-15 11:06:46 +02:00
if zapped_event . kind ( ) == EventDefinitions . KIND_FEEDBACK :
2023-11-26 21:47:33 +01:00
2023-11-20 23:18:05 +01:00
amount = 0
job_event = None
2023-11-26 21:47:33 +01:00
p_tag_str = " "
2024-03-18 19:44:44 +01:00
status = " "
2023-11-20 23:18:05 +01:00
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 ' :
2023-11-21 23:51:48 +01:00
job_event = get_event_by_id ( tag . as_vec ( ) [ 1 ] , client = self . client , config = self . dvm_config )
2023-11-27 10:35:58 +01:00
if job_event is not None :
job_event = check_and_decrypt_tags ( job_event , self . dvm_config )
2023-11-27 23:37:44 +01:00
if job_event is None :
return
2023-11-27 10:35:58 +01:00
else :
return
2024-03-18 19:44:44 +01:00
elif tag . as_vec ( ) [ 0 ] == ' status ' :
status = tag . as_vec ( ) [ 1 ]
print ( status )
2023-11-26 21:47:33 +01:00
2023-11-27 10:35:58 +01:00
# if a reaction by us got zapped
2024-03-18 19:44:44 +01:00
print ( status )
2024-04-15 11:06:46 +02:00
if job_event . kind ( ) == EventDefinitions . KIND_NIP88_SUBSCRIBE_EVENT :
2024-03-18 19:44:44 +01:00
send_job_status_reaction ( job_event , " subscription-success " , client = self . client ,
dvm_config = self . dvm_config , user = user )
else :
task_supported , task = check_task_is_supported ( job_event , client = self . client ,
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 , user = user )
indices = [ i for i , x in enumerate ( self . job_list ) if
x . event == job_event ]
index = - 1
if len ( indices ) > 0 :
index = indices [ 0 ]
if index > - 1 :
if self . job_list [ index ] . is_processed :
self . job_list [ index ] . is_paid = True
check_and_return_event ( self . job_list [ index ] . result , job_event )
elif not ( self . job_list [ index ] ) . is_processed :
# If payment-required appears before processing
self . job_list . pop ( index )
print ( " Starting work... " )
do_work ( job_event , invoice_amount )
else :
print ( " Job not in List, but starting work... " )
2023-12-09 23:58:15 +01:00
do_work ( job_event , invoice_amount )
2023-11-20 23:18:05 +01:00
2024-03-18 19:44:44 +01:00
else :
send_job_status_reaction ( job_event , " payment-rejected " ,
False , invoice_amount , client = self . client ,
dvm_config = self . dvm_config )
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Invoice was not paid sufficiently " )
2024-04-15 11:06:46 +02:00
elif zapped_event . kind ( ) == EventDefinitions . KIND_NIP88_SUBSCRIBE_EVENT :
2024-03-18 19:44:44 +01:00
print ( " new subscription, doing nothing " )
2023-11-20 23:18:05 +01:00
elif zapped_event . kind ( ) in EventDefinitions . ANY_RESULT :
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] "
2023-11-24 22:07:00 +01:00
" Someone zapped the result of an exisiting Task. Nice " )
2023-11-20 23:18:05 +01:00
elif not anon :
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Note Zap received for DVM balance: " +
2023-11-24 22:07:00 +01:00
str ( invoice_amount ) + " Sats from " + str ( user . name ) )
2023-11-24 21:29:24 +01:00
update_user_balance ( self . dvm_config . DB , sender , invoice_amount , client = self . client ,
config = self . dvm_config )
2023-11-19 18:47:05 +01:00
2023-11-20 23:18:05 +01:00
# a regular note
2024-03-18 22:50:48 +01:00
elif not anon and dvm_config . NIP88 is None :
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Profile Zap received for DVM balance: " +
2023-11-24 22:07:00 +01:00
str ( invoice_amount ) + " Sats from " + str ( user . name ) )
2023-11-24 21:29:24 +01:00
update_user_balance ( self . dvm_config . DB , sender , invoice_amount , client = self . client ,
config = self . dvm_config )
2023-11-20 22:09:38 +01:00
2023-11-20 23:18:05 +01:00
except Exception as e :
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Error during content decryption: " + str ( e ) )
2023-11-20 22:09:38 +01:00
2023-11-26 10:31:38 +01:00
def check_event_has_not_unfinished_job_input ( nevent , append , client , dvmconfig ) :
2023-11-28 16:20:56 +01:00
task_supported , task = check_task_is_supported ( nevent , client , config = dvmconfig )
2023-11-20 22:09:38 +01:00
if not task_supported :
return False
for tag in nevent . tags ( ) :
if tag . as_vec ( ) [ 0 ] == ' i ' :
if len ( tag . as_vec ( ) ) < 3 :
print ( " Job Event missing/malformed i tag, skipping.. " )
return False
else :
input = tag . as_vec ( ) [ 1 ]
input_type = tag . as_vec ( ) [ 2 ]
if input_type == " job " :
2023-11-24 21:29:24 +01:00
evt = get_referenced_event_by_id ( event_id = input , client = client ,
kinds = EventDefinitions . ANY_RESULT ,
2023-11-26 10:31:38 +01:00
dvm_config = dvmconfig )
2023-11-20 22:09:38 +01:00
if evt is None :
if append :
2023-11-26 10:31:38 +01:00
job_ = RequiredJobToWatch ( event = nevent , timestamp = Timestamp . now ( ) . as_secs ( ) )
self . jobs_on_hold_list . append ( job_ )
2023-11-24 22:07:00 +01:00
send_job_status_reaction ( nevent , " chain-scheduled " , True , 0 ,
2023-11-26 10:31:38 +01:00
client = client , dvm_config = dvmconfig )
2023-11-20 22:09:38 +01:00
return False
2023-11-19 18:47:05 +01:00
else :
2023-11-20 22:09:38 +01:00
return True
2023-11-19 18:47:05 +01:00
2023-12-10 14:17:06 +01:00
def check_and_return_event ( data , original_event : Event ) :
amount = 0
2023-11-21 10:03:04 +01:00
for x in self . job_list :
2023-11-26 10:31:38 +01:00
if x . event == original_event :
2023-11-19 18:47:05 +01:00
is_paid = x . is_paid
amount = x . amount
2023-11-20 22:09:38 +01:00
x . result = data
x . is_processed = True
2023-11-22 19:20:34 +01:00
if self . dvm_config . SHOW_RESULT_BEFORE_PAYMENT and not is_paid :
2023-12-10 14:17:06 +01:00
send_nostr_reply_event ( data , original_event . as_json ( ) )
2023-11-20 22:09:38 +01:00
send_job_status_reaction ( original_event , " success " , amount ,
2023-11-26 10:31:38 +01:00
dvm_config = self . dvm_config ,
) # or payment-required, or both?
2023-11-22 19:20:34 +01:00
elif not self . dvm_config . SHOW_RESULT_BEFORE_PAYMENT and not is_paid :
2023-11-20 22:09:38 +01:00
send_job_status_reaction ( original_event , " success " , amount ,
2023-11-26 10:31:38 +01:00
dvm_config = self . dvm_config ,
) # or payment-required, or both?
2023-11-20 22:09:38 +01:00
2023-11-22 19:20:34 +01:00
if self . dvm_config . SHOW_RESULT_BEFORE_PAYMENT and is_paid :
2023-11-21 10:03:04 +01:00
self . job_list . remove ( x )
2023-11-22 19:20:34 +01:00
elif not self . dvm_config . SHOW_RESULT_BEFORE_PAYMENT and is_paid :
2023-11-21 10:03:04 +01:00
self . job_list . remove ( x )
2023-12-10 14:17:06 +01:00
send_nostr_reply_event ( data , original_event . as_json ( ) )
2023-11-19 18:47:05 +01:00
break
2023-11-30 08:07:30 +01:00
task = get_task ( original_event , self . client , self . dvm_config )
for dvm in self . dvm_config . SUPPORTED_DVMS :
2023-12-09 23:58:15 +01:00
if task == dvm . TASK :
try :
post_processed = dvm . post_process ( data , original_event )
send_nostr_reply_event ( post_processed , original_event . as_json ( ) )
except Exception as e :
2024-03-19 14:28:08 +01:00
print ( e )
2023-12-10 14:17:06 +01:00
# Zapping back by error in post-processing is a risk for the DVM because work has been done,
# but maybe something with parsing/uploading failed. Try to avoid errors here as good as possible
2023-12-16 00:42:06 +01:00
send_job_status_reaction ( original_event , " error " ,
content = " Error in Post-processing: " + str ( e ) ,
2023-11-30 08:07:30 +01:00
dvm_config = self . dvm_config ,
2023-12-09 23:58:15 +01:00
)
2023-12-12 10:37:59 +01:00
if amount > 0 and self . dvm_config . LNBITS_ADMIN_KEY != " " :
2024-01-19 18:19:28 +01:00
user = get_or_add_user ( self . dvm_config . DB , original_event . author ( ) . to_hex ( ) ,
2023-12-10 14:17:06 +01:00
client = self . client , config = self . dvm_config )
print ( user . lud16 + " " + str ( amount ) )
2023-12-29 17:58:07 +01:00
bolt11 = zaprequest ( user . lud16 , amount , " Couldn ' t finish job, returning sats " ,
2024-01-19 18:19:28 +01:00
original_event , " " ,
self . keys , self . dvm_config . RELAY_LIST , zaptype = " private " )
2023-12-10 14:17:06 +01:00
if bolt11 is None :
print ( " Receiver has no Lightning address, can ' t zap back. " )
return
try :
payment_hash = pay_bolt11_ln_bits ( bolt11 , self . dvm_config )
except Exception as e :
print ( e )
2023-11-20 22:09:38 +01:00
2023-11-21 23:51:48 +01:00
def send_nostr_reply_event ( content , original_event_as_str ) :
original_event = Event . from_json ( original_event_as_str )
2024-01-03 20:17:25 +01:00
request_tag = Tag . parse ( [ " request " , original_event_as_str ] )
2023-11-21 23:51:48 +01:00
e_tag = Tag . parse ( [ " e " , original_event . id ( ) . to_hex ( ) ] )
2024-01-19 18:19:28 +01:00
p_tag = Tag . parse ( [ " p " , original_event . author ( ) . to_hex ( ) ] )
2023-11-21 23:51:48 +01:00
alt_tag = Tag . parse ( [ " alt " , " This is the result of a NIP90 DVM AI task with kind " + str (
2024-03-18 19:44:44 +01:00
original_event . kind ( ) . as_u64 ( ) ) + " . The task was: " + original_event . content ( ) ] )
2023-11-21 23:51:48 +01:00
status_tag = Tag . parse ( [ " status " , " success " ] )
2023-11-27 23:37:44 +01:00
reply_tags = [ request_tag , e_tag , p_tag , alt_tag , status_tag ]
2023-11-26 10:31:38 +01:00
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 )
2023-11-21 23:51:48 +01:00
for tag in original_event . tags ( ) :
if tag . as_vec ( ) [ 0 ] == " i " :
2023-11-26 10:31:38 +01:00
i_tag = tag
if not encrypted :
reply_tags . append ( i_tag )
if encrypted :
2024-01-03 20:17:25 +01:00
print ( content )
2024-01-19 18:19:28 +01:00
content = nip04_encrypt ( self . keys . secret_key ( ) , PublicKey . from_hex ( original_event . author ( ) . to_hex ( ) ) ,
2023-11-26 10:31:38 +01:00
content )
2023-11-21 23:51:48 +01:00
2024-03-19 12:33:26 +01:00
reply_event = EventBuilder ( Kind ( original_event . kind ( ) . as_u64 ( ) + 1000 ) , str ( content ) , reply_tags ) . to_event (
self . keys )
2023-11-21 23:51:48 +01:00
send_event ( reply_event , client = self . client , dvm_config = self . dvm_config )
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] " + str (
2024-03-18 19:44:44 +01:00
original_event . kind ( ) . as_u64 ( ) + 1000 ) + " Job Response event sent: " + reply_event . as_json ( ) )
2023-11-20 22:09:38 +01:00
def send_job_status_reaction ( original_event , status , is_paid = True , amount = 0 , client = None ,
2023-11-20 23:18:05 +01:00
content = None ,
2024-03-18 19:44:44 +01:00
dvm_config = None , user = None ) :
2023-11-26 10:31:38 +01:00
2023-11-28 10:08:43 +01:00
task = get_task ( original_event , client = client , dvm_config = dvm_config )
2024-02-05 18:20:22 +01:00
alt_description , reaction = build_status_reaction ( status , task , amount , content , dvm_config )
2023-11-20 23:18:05 +01:00
e_tag = Tag . parse ( [ " e " , original_event . id ( ) . to_hex ( ) ] )
2024-01-19 18:19:28 +01:00
p_tag = Tag . parse ( [ " p " , original_event . author ( ) . to_hex ( ) ] )
2023-11-20 23:18:05 +01:00
alt_tag = Tag . parse ( [ " alt " , alt_description ] )
status_tag = Tag . parse ( [ " status " , status ] )
2023-11-27 23:37:44 +01:00
reply_tags = [ e_tag , alt_tag , status_tag ]
encryption_tags = [ ]
2023-11-26 10:31:38 +01:00
2023-11-27 23:37:44 +01:00
encrypted = False
2023-11-27 10:35:58 +01:00
for tag in original_event . tags ( ) :
2023-11-27 23:37:44 +01:00
if tag . as_vec ( ) [ 0 ] == " encrypted " :
encrypted = True
encrypted_tag = Tag . parse ( [ " encrypted " ] )
encryption_tags . append ( encrypted_tag )
2023-11-26 10:31:38 +01:00
2023-11-27 23:37:44 +01:00
if encrypted :
encryption_tags . append ( p_tag )
2024-03-22 13:36:57 +01:00
encryption_tags . append ( e_tag )
2023-11-27 23:37:44 +01:00
else :
reply_tags . append ( p_tag )
2023-11-20 23:18:05 +01:00
2024-03-22 13:36:57 +01:00
2023-11-20 23:18:05 +01:00
if status == " success " or status == " error " : #
2023-11-21 10:03:04 +01:00
for x in self . job_list :
2023-11-26 10:31:38 +01:00
if x . event == original_event :
2023-11-20 23:18:05 +01:00
is_paid = x . is_paid
amount = x . amount
break
bolt11 = " "
payment_hash = " "
expires = original_event . created_at ( ) . as_secs ( ) + ( 60 * 60 * 24 )
2024-03-18 19:44:44 +01:00
if status == " payment-required " or (
status == " processing " and not is_paid ) :
2023-11-21 23:51:48 +01:00
if dvm_config . LNBITS_INVOICE_KEY != " " :
2023-11-20 23:18:05 +01:00
try :
2024-03-04 15:42:37 +01:00
bolt11 , payment_hash = create_bolt11_ln_bits ( amount , dvm_config )
2023-11-20 23:18:05 +01:00
except Exception as e :
print ( e )
2024-01-06 17:47:22 +01:00
try :
bolt11 , payment_hash = create_bolt11_lud16 ( dvm_config . LN_ADDRESS ,
2024-03-04 15:42:37 +01:00
amount )
2024-01-06 17:47:22 +01:00
except Exception as e :
2024-03-04 15:42:37 +01:00
print ( e )
bolt11 = None
2024-01-06 17:47:22 +01:00
elif dvm_config . LN_ADDRESS != " " :
try :
bolt11 , payment_hash = create_bolt11_lud16 ( dvm_config . LN_ADDRESS , amount )
except Exception as e :
print ( e )
bolt11 = None
2023-11-20 23:18:05 +01:00
2023-11-26 10:31:38 +01:00
if not any ( x . event == original_event for x in self . job_list ) :
2023-11-21 10:03:04 +01:00
self . job_list . append (
2023-11-26 10:31:38 +01:00
JobToWatch ( event = original_event ,
2023-11-20 23:18:05 +01:00
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 ,
2023-11-26 10:31:38 +01:00
expires = expires ) )
2023-11-24 21:29:24 +01:00
# print(str(self.job_list))
2023-11-21 23:51:48 +01:00
if ( status == " payment-required " or status == " payment-rejected " or (
status == " processing " and not is_paid )
or ( status == " success " and not is_paid ) ) :
2023-11-20 23:18:05 +01:00
2024-02-20 10:00:52 +01:00
if dvm_config . LNBITS_INVOICE_KEY != " " and bolt11 is not None :
2023-11-20 23:18:05 +01:00
amount_tag = Tag . parse ( [ " amount " , str ( amount * 1000 ) , bolt11 ] )
2023-11-20 22:09:38 +01:00
else :
2023-11-20 23:18:05 +01:00
amount_tag = Tag . parse ( [ " amount " , str ( amount * 1000 ) ] ) # to millisats
2023-11-27 23:37:44 +01:00
reply_tags . append ( amount_tag )
if encrypted :
content_tag = Tag . parse ( [ " content " , reaction ] )
reply_tags . append ( content_tag )
str_tags = [ ]
for element in reply_tags :
str_tags . append ( element . as_vec ( ) )
content = json . dumps ( str_tags )
2024-01-19 18:19:28 +01:00
content = nip04_encrypt ( self . keys . secret_key ( ) , PublicKey . from_hex ( original_event . author ( ) . to_hex ( ) ) ,
2023-11-27 23:37:44 +01:00
content )
reply_tags = encryption_tags
else :
content = reaction
2023-11-21 23:51:48 +01:00
2024-02-17 20:40:56 +01:00
keys = Keys . parse ( dvm_config . PRIVATE_KEY )
2023-11-27 23:37:44 +01:00
reaction_event = EventBuilder ( EventDefinitions . KIND_FEEDBACK , str ( content ) , reply_tags ) . to_event ( keys )
2023-11-26 10:31:38 +01:00
send_event ( reaction_event , client = self . client , dvm_config = self . dvm_config )
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] " + " : Sent Kind " + str (
2024-03-18 19:44:44 +01:00
EventDefinitions . KIND_FEEDBACK . as_u64 ( ) ) + " Reaction: " + status + " " + reaction_event . as_json ( ) )
2023-11-26 10:31:38 +01:00
return reaction_event . as_json ( )
2023-11-20 23:18:05 +01:00
2023-12-09 23:58:15 +01:00
def do_work ( job_event , amount ) :
2024-03-19 12:33:26 +01:00
if ( (
EventDefinitions . KIND_NIP90_EXTRACT_TEXT . as_u64 ( ) < = job_event . kind ( ) . as_u64 ( ) < = EventDefinitions . KIND_NIP90_GENERIC . as_u64 ( ) )
2024-03-18 19:44:44 +01:00
or job_event . kind ( ) . as_u64 ( ) == EventDefinitions . KIND_DM . as_u64 ( ) ) :
2023-11-20 22:09:38 +01:00
2023-11-28 10:08:43 +01:00
task = get_task ( job_event , client = self . client , dvm_config = self . dvm_config )
2023-11-28 07:32:57 +01:00
2023-11-23 13:30:09 +01:00
for dvm in self . dvm_config . SUPPORTED_DVMS :
2023-12-30 20:36:38 +01:00
result = " "
2023-11-20 22:09:38 +01:00
try :
if task == dvm . TASK :
2023-12-16 00:42:06 +01:00
request_form = dvm . create_request_from_nostr_event ( job_event , self . client , self . dvm_config )
2023-12-17 14:38:58 +01:00
if dvm_config . USE_OWN_VENV :
2023-12-20 18:21:40 +01:00
python_location = " /bin/python "
if platform == " win32 " :
python_location = " /Scripts/python "
2023-12-29 17:58:07 +01:00
python_bin = ( r ' cache/venvs/ ' + os . path . basename ( dvm_config . SCRIPT ) . split ( " .py " ) [ 0 ]
+ python_location )
2023-12-17 14:38:58 +01:00
retcode = subprocess . call ( [ python_bin , dvm_config . SCRIPT ,
2023-12-29 17:58:07 +01:00
' --request ' , json . dumps ( request_form ) ,
' --identifier ' , dvm_config . IDENTIFIER ,
' --output ' , ' output.txt ' ] )
2023-12-17 14:38:58 +01:00
print ( " Finished processing, loading data.. " )
with open ( os . path . abspath ( ' output.txt ' ) ) as f :
2023-12-19 15:46:30 +01:00
resultall = f . readlines ( )
for line in resultall :
if line != ' \n ' :
result + = line
2023-12-17 14:38:58 +01:00
os . remove ( os . path . abspath ( ' output.txt ' ) )
2023-12-30 20:36:38 +01:00
assert not result . startswith ( " Error: " )
print ( result )
2023-12-29 17:58:07 +01:00
else : # Some components might have issues with running code in otuside venv.
# We install locally in these cases for now
2023-12-17 14:38:58 +01:00
result = dvm . process ( request_form )
2023-11-30 08:07:30 +01:00
try :
2024-01-12 15:55:23 +01:00
post_processed = dvm . post_process ( result , job_event )
2023-11-30 08:07:30 +01:00
send_nostr_reply_event ( post_processed , job_event . as_json ( ) )
except Exception as e :
2024-03-19 14:28:08 +01:00
print ( e )
2023-11-30 08:07:30 +01:00
send_job_status_reaction ( job_event , " error " , content = str ( e ) ,
2023-12-09 23:58:15 +01:00
dvm_config = self . dvm_config )
2023-11-20 22:09:38 +01:00
except Exception as e :
2024-03-19 14:28:08 +01:00
print ( e )
2023-11-30 08:07:30 +01:00
# we could send the exception here to the user, but maybe that's not a good idea after all.
2023-12-30 20:36:38 +01:00
send_job_status_reaction ( job_event , " error " , content = result ,
2023-11-30 08:07:30 +01:00
dvm_config = self . dvm_config )
2023-12-09 23:58:15 +01:00
# Zapping back the user on error
2023-12-12 10:37:59 +01:00
if amount > 0 and self . dvm_config . LNBITS_ADMIN_KEY != " " :
2024-01-19 18:19:28 +01:00
user = get_or_add_user ( self . dvm_config . DB , job_event . author ( ) . to_hex ( ) ,
2023-12-09 23:58:15 +01:00
client = self . client , config = self . dvm_config )
print ( user . lud16 + " " + str ( amount ) )
2024-03-04 15:42:37 +01:00
bolt11 = zaprequest ( user . lud16 , amount , " Couldn ' t finish job, returning sats " , job_event ,
2024-03-19 00:24:49 +01:00
PublicKey . parse ( user . npub ) ,
2023-12-29 23:13:06 +01:00
self . keys , self . dvm_config . RELAY_LIST , zaptype = " private " )
2023-12-09 23:58:15 +01:00
if bolt11 is None :
print ( " Receiver has no Lightning address, can ' t zap back. " )
return
try :
payment_hash = pay_bolt11_ln_bits ( bolt11 , self . dvm_config )
except Exception as e :
print ( e )
2023-11-20 22:09:38 +01:00
return
self . client . handle_notifications ( NotificationHandler ( ) )
while True :
2024-03-04 15:42:37 +01:00
for dvm in self . dvm_config . SUPPORTED_DVMS :
scheduled_result = dvm . schedule ( self . dvm_config )
2023-11-21 10:03:04 +01:00
for job in self . job_list :
2024-02-20 10:29:29 +01:00
if job . bolt11 != " " and job . payment_hash != " " and not job . payment_hash is None and not job . is_paid :
2023-11-26 10:31:38 +01:00
ispaid = check_bolt11_ln_bits_is_paid ( job . payment_hash , self . dvm_config )
if ispaid and job . is_paid is False :
print ( " is paid " )
2024-03-18 19:44:44 +01:00
job . is_paid = True
amount = parse_amount_from_bolt11_invoice ( job . bolt11 )
2023-11-26 10:31:38 +01:00
2023-11-20 22:09:38 +01:00
job . is_paid = True
2023-11-26 10:31:38 +01:00
send_job_status_reaction ( job . event , " processing " , True , 0 ,
client = self . client ,
dvm_config = self . dvm_config )
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] doing work from joblist " )
2023-12-09 23:58:15 +01:00
do_work ( job . event , amount )
2023-11-26 10:31:38 +01:00
elif ispaid is None : # invoice expired
self . job_list . remove ( job )
2023-11-20 22:09:38 +01:00
if Timestamp . now ( ) . as_secs ( ) > job . expires :
2023-11-26 10:31:38 +01:00
self . job_list . remove ( job )
2023-11-20 22:09:38 +01:00
2023-11-21 10:03:04 +01:00
for job in self . jobs_on_hold_list :
2023-11-20 22:09:38 +01:00
if check_event_has_not_unfinished_job_input ( job . event , False , client = self . client ,
2023-11-26 10:31:38 +01:00
dvmconfig = self . dvm_config ) :
2023-11-20 23:18:05 +01:00
handle_nip90_job_event ( nip90_event = job . event )
2023-11-20 22:09:38 +01:00
try :
2023-11-21 10:03:04 +01:00
self . jobs_on_hold_list . remove ( job )
2023-11-20 22:09:38 +01:00
except :
2023-11-29 15:09:35 +01:00
print ( " [ " + self . dvm_config . NIP89 . NAME + " ] Error removing Job on Hold from List after expiry " )
2023-11-20 22:09:38 +01:00
if Timestamp . now ( ) . as_secs ( ) > job . timestamp + 60 * 20 : # remove jobs to look for after 20 minutes..
2023-11-21 10:03:04 +01:00
self . jobs_on_hold_list . remove ( job )
2023-11-20 22:09:38 +01:00
2023-11-21 23:51:48 +01:00
time . sleep ( 1.0 )