Merge pull request #23 from believethehype/subscriptions-sdkupdate

Subscriptions sdkupdate
This commit is contained in:
believethehype 2024-03-20 08:57:39 +01:00 committed by GitHub
commit 8f9833b0f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1987 additions and 1525 deletions

View File

@ -5,7 +5,8 @@ import time
from datetime import timedelta
from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey,
Options, Tag, Event, nip04_encrypt, NostrSigner, EventId, Nip19Event)
Options, Tag, Event, nip04_encrypt, NostrSigner, EventId, Nip19Event, Kind, KindEnum,
UnsignedEvent, nip59_extract_rumor)
from nostr_dvm.utils.admin_utils import admin_make_database_updates
from nostr_dvm.utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table
@ -55,30 +56,48 @@ class Bot:
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)
kinds.append(Kind(dvm.KIND.as_u64() + 1000))
dvm_filter = (Filter().kinds(kinds).since(Timestamp.now()))
self.client.subscribe([zap_filter, dm_filter, dvm_filter])
self.client.subscribe([zap_filter, dm_filter, dvm_filter], None)
create_sql_table(self.dvm_config.DB)
admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client)
# add_sql_table_column(dvm_config.DB)
class NotificationHandler(HandleNotification):
client = self.client
dvm_config = self.dvm_config
keys = self.keys
def handle(self, relay_url, nostr_event):
if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT + 1000 <= nostr_event.kind()
<= EventDefinitions.KIND_NIP90_GENERIC + 1000):
def handle(self, relay_url, subscription_id, nostr_event):
if (EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64() + 1000 <= nostr_event.kind().as_u64()
<= EventDefinitions.KIND_NIP90_GENERIC.as_u64() + 1000):
handle_nip90_response_event(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_FEEDBACK:
elif nostr_event.kind().as_u64() == EventDefinitions.KIND_FEEDBACK.as_u64():
handle_nip90_feedback(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_DM:
handle_dm(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
elif nostr_event.kind().as_u64() == EventDefinitions.KIND_ZAP.as_u64():
handle_zap(nostr_event)
elif nostr_event.kind().match_enum(KindEnum.ENCRYPTED_DIRECT_MESSAGE()):
try:
handle_dm(nostr_event)
except Exception as e:
print(f"Error during content NIP04 decryption: {e}")
elif nostr_event.kind().match_enum(KindEnum.GIFT_WRAP()):
print("Decrypting NIP59 event")
try:
rumor: UnsignedEvent = nip59_extract_rumor(self.keys, nostr_event)
if rumor.kind().match_enum(KindEnum.SEALED_DIRECT()):
msg = rumor.content()
print(f"Received new msg [sealed]: {msg}")
self.client.send_sealed_msg(rumor.author(), "Nip44 is not supported yet, but coming soon", None)
else:
print(f"{rumor.as_json()}")
except Exception as e:
print(f"Error during content NIP59 decryption: {e}")
def handle_msg(self, relay_url, msg):
return
@ -190,7 +209,7 @@ class Bot:
print("Error in bot " + str(e))
def handle_nip90_feedback(nostr_event):
print(nostr_event.as_json())
#print(nostr_event.as_json())
try:
is_encrypted = False
status = ""
@ -265,7 +284,7 @@ class Bot:
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())
lastactive=Timestamp.now().as_secs(), subscribed=user.subscribed)
evt = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.from_hex(entry["npub"]),
"Paid " + str(
@ -346,7 +365,7 @@ class Bot:
return
dvms = [x for x in self.dvm_config.SUPPORTED_DVMS if
x.PUBLIC_KEY == nostr_event.author().to_hex() and x.KIND == nostr_event.kind() - 1000]
x.PUBLIC_KEY == nostr_event.author().to_hex() and x.KIND.as_u64() == nostr_event.kind().as_u64() - 1000]
if len(dvms) > 0:
dvm = dvms[0]
if dvm.dvm_config.EXTERNAL_POST_PROCESS_TYPE != PostProcessFunctionType.NONE:

View File

@ -5,7 +5,7 @@ from datetime import timedelta
from sys import platform
from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \
init_logger, LogLevel, Options, nip04_encrypt, NostrSigner
init_logger, LogLevel, Options, nip04_encrypt, NostrSigner, Kind, SubscribeAutoCloseOptions
import time
@ -13,8 +13,10 @@ from nostr_dvm.utils.definitions import EventDefinitions, RequiredJobToWatch, Jo
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
from nostr_dvm.utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table
from nostr_dvm.utils.database_utils import create_sql_table, get_or_add_user, update_user_balance, update_sql_table, \
update_user_subscription
from nostr_dvm.utils.mediasource_utils import input_data_file_duration
from nostr_dvm.utils.nip88_utils import nip88_has_active_subscription
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, \
@ -46,8 +48,7 @@ class DVM:
self.jobs_on_hold_list = []
pk = self.keys.public_key()
print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " +
', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n")
print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + "\n")
for relay in self.dvm_config.RELAY_LIST:
self.client.add_relay(relay)
@ -59,7 +60,8 @@ class DVM:
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])
self.client.subscribe([dvm_filter, zap_filter], None)
create_sql_table(self.dvm_config.DB)
admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client)
@ -69,48 +71,89 @@ class DVM:
dvm_config = self.dvm_config
keys = self.keys
def handle(self, relay_url, nostr_event):
if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC:
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():
handle_nip90_job_event(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
elif nostr_event.kind().as_u64() == EventDefinitions.KIND_ZAP.as_u64():
handle_zap(nostr_event)
def handle_msg(self, relay_url, msg):
return
def handle_nip90_job_event(nip90_event):
# decrypted encrypted events
nip90_event = check_and_decrypt_tags(nip90_event, self.dvm_config)
# if event is encrypted, but we can't decrypt it (e.g. because its directed to someone else), return
if nip90_event is None:
return
user = get_or_add_user(self.dvm_config.DB, nip90_event.author().to_hex(), client=self.client,
config=self.dvm_config)
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
# check if task is supported by the current DVM
task_supported, task = check_task_is_supported(nip90_event, client=self.client,
config=self.dvm_config)
# 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,
config=self.dvm_config)
# 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
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")
elif task_supported:
print("[" + self.dvm_config.NIP89.NAME + "] Received new Request: " + task + " from " + user.name)
duration = input_data_file_duration(nip90_event, dvm_config=self.dvm_config, client=self.client)
amount = get_amount_per_task(task, self.dvm_config, duration)
if amount is None:
return
task_is_free = False
# If this is a subscription DVM and the Task is directed to us, check for active subscription
if dvm_config.NIP88 is not None and p_tag_str == self.dvm_config.PUBLIC_KEY:
# if we stored in the database that the user has an active subscription, we don't need to check it
print("User Subscription: " + str(user.subscribed) + " Current time: " + str(
Timestamp.now().as_secs()))
# if we have an entry in the db that user is subscribed, continue
if int(user.subscribed) > int(Timestamp.now().as_secs()):
print("User subscribed until: " + str(Timestamp.from_secs(user.subscribed).to_human_datetime()))
user_has_active_subscription = True
# otherwise we check for an active subscription by checking recipie events
else:
print("[" + self.dvm_config.NIP89.NAME + "] Checking Subscription status")
subscription_status = nip88_has_active_subscription(PublicKey.parse(user.npub),
self.dvm_config.NIP88.DTAG, self.client,
self.dvm_config.PUBLIC_KEY)
if subscription_status["isActive"]:
print("Checked Recipe: User subscribed until: " + str(
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")
for dvm in self.dvm_config.SUPPORTED_DVMS:
if dvm.TASK == task and dvm.FIX_COST == 0 and dvm.PER_UNIT_COST == 0:
if dvm.TASK == task and dvm.FIX_COST == 0 and dvm.PER_UNIT_COST == 0 and dvm_config.NIP88 is None:
task_is_free = True
cashu_redeemed = False
@ -124,31 +167,40 @@ class DVM:
self.dvm_config)
return
# if user is whitelisted or task is free, just do the job
if (user.iswhitelisted or task_is_free or cashu_redeemed) and (p_tag_str == "" or p_tag_str ==
self.dvm_config.PUBLIC_KEY):
if (user.iswhitelisted or task_is_free or cashu_redeemed) and (
p_tag_str == "" or p_tag_str ==
self.dvm_config.PUBLIC_KEY):
print(
"[" + self.dvm_config.NIP89.NAME + "] Free task or Whitelisted for task " + task +
". Starting processing..")
if dvm_config.SEND_FEEDBACK_EVENTS:
send_job_status_reaction(nip90_event, "processing", True, 0,
client=self.client, dvm_config=self.dvm_config)
client=self.client, dvm_config=self.dvm_config, user=user)
# 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)
# if task is directed to us via p tag and user has balance, do the job and update balance
elif p_tag_str == self.dvm_config.PUBLIC_KEY and user.balance >= int(amount):
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())
# 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(
amount) and dvm_config.NIP88 is None) or (
p_tag_str == self.dvm_config.PUBLIC_KEY and user_has_active_subscription)):
print(
"[" + self.dvm_config.NIP89.NAME + "] Using user's balance for task: " + task +
". Starting processing.. New balance is: " + str(balance))
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)
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))
send_job_status_reaction(nip90_event, "processing", True, 0,
client=self.client, dvm_config=self.dvm_config)
@ -157,27 +209,40 @@ class DVM:
# else send a payment required event to user
elif p_tag_str == "" or p_tag_str == self.dvm_config.PUBLIC_KEY:
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
if dvm_config.NIP88 is not None:
print(
"[" + self.dvm_config.NIP89.NAME + "] Requesting payment for Event: " +
"[" + self.dvm_config.NIP89.NAME + "] Hinting user for Subscription: " +
nip90_event.id().to_hex())
send_job_status_reaction(nip90_event, "payment-required",
False, int(amount), client=self.client, dvm_config=self.dvm_config)
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)
else:
print("[" + self.dvm_config.NIP89.NAME + "] Job addressed to someone else, skipping..")
# else:
@ -192,11 +257,12 @@ class DVM:
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 zapped_event.kind().as_u64() == EventDefinitions.KIND_FEEDBACK.as_u64():
amount = 0
job_event = None
p_tag_str = ""
status = ""
for tag in zapped_event.tags():
if tag.as_vec()[0] == 'amount':
amount = int(float(tag.as_vec()[1]) / 1000)
@ -208,41 +274,53 @@ class DVM:
return
else:
return
elif tag.as_vec()[0] == 'status':
status = tag.as_vec()[1]
print(status)
# if a reaction by us got zapped
print(status)
if job_event.kind().as_u64() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT.as_u64():
send_job_status_reaction(job_event, "subscription-success", client=self.client,
dvm_config=self.dvm_config, user=user)
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)
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: # If payment-required appears a processing
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...")
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...")
do_work(job_event, invoice_amount)
else:
print("Job not in List, but starting work...")
do_work(job_event, invoice_amount)
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")
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")
elif zapped_event.kind().as_u64() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT.as_u64():
print("new subscription, doing nothing")
elif zapped_event.kind() in EventDefinitions.ANY_RESULT:
print("[" + self.dvm_config.NIP89.NAME + "] "
@ -254,7 +332,7 @@ class DVM:
config=self.dvm_config)
# a regular note
elif not anon:
elif not anon and dvm_config.NIP88 is None:
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,
@ -323,6 +401,7 @@ class DVM:
post_processed = dvm.post_process(data, original_event)
send_nostr_reply_event(post_processed, original_event.as_json())
except Exception as e:
print(e)
# 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
send_job_status_reaction(original_event, "error",
@ -350,7 +429,7 @@ class DVM:
e_tag = Tag.parse(["e", original_event.id().to_hex()])
p_tag = Tag.parse(["p", original_event.author().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()])
original_event.kind().as_u64()) + ". The task was: " + original_event.content()])
status_tag = Tag.parse(["status", "success"])
reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag]
encrypted = False
@ -371,15 +450,16 @@ class DVM:
content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.author().to_hex()),
content)
reply_event = EventBuilder(original_event.kind() + 1000, str(content), reply_tags).to_event(self.keys)
reply_event = EventBuilder(Kind(original_event.kind().as_u64() + 1000), str(content), reply_tags).to_event(
self.keys)
send_event(reply_event, client=self.client, dvm_config=self.dvm_config)
print("[" + self.dvm_config.NIP89.NAME + "] " + str(
original_event.kind() + 1000) + " Job Response event sent: " + reply_event.as_json())
original_event.kind().as_u64() + 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=None, user=None):
task = get_task(original_event, client=client, dvm_config=dvm_config)
alt_description, reaction = build_status_reaction(status, task, amount, content, dvm_config)
@ -413,7 +493,8 @@ class DVM:
bolt11 = ""
payment_hash = ""
expires = original_event.created_at().as_secs() + (60 * 60 * 24)
if status == "payment-required" or (status == "processing" and not is_paid):
if status == "payment-required" or (
status == "processing" and not is_paid):
if dvm_config.LNBITS_INVOICE_KEY != "":
try:
bolt11, payment_hash = create_bolt11_ln_bits(amount, dvm_config)
@ -471,12 +552,13 @@ class DVM:
reaction_event = EventBuilder(EventDefinitions.KIND_FEEDBACK, str(content), reply_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 + " " + reaction_event.as_json())
EventDefinitions.KIND_FEEDBACK.as_u64()) + " Reaction: " + status + " " + reaction_event.as_json())
return reaction_event.as_json()
def do_work(job_event, amount):
if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC)
or job_event.kind() == EventDefinitions.KIND_DM):
if ((
EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64() <= job_event.kind().as_u64() <= EventDefinitions.KIND_NIP90_GENERIC.as_u64())
or job_event.kind().as_u64() == EventDefinitions.KIND_DM.as_u64()):
task = get_task(job_event, client=self.client, dvm_config=self.dvm_config)
@ -515,9 +597,11 @@ class DVM:
post_processed = dvm.post_process(result, job_event)
send_nostr_reply_event(post_processed, job_event.as_json())
except Exception as e:
print(e)
send_job_status_reaction(job_event, "error", content=str(e),
dvm_config=self.dvm_config)
except Exception as e:
print(e)
# we could send the exception here to the user, but maybe that's not a good idea after all.
send_job_status_reaction(job_event, "error", content=result,
dvm_config=self.dvm_config)
@ -527,7 +611,7 @@ class DVM:
client=self.client, config=self.dvm_config)
print(user.lud16 + " " + str(amount))
bolt11 = zaprequest(user.lud16, amount, "Couldn't finish job, returning sats", job_event,
user.npub,
PublicKey.parse(user.npub),
self.keys, self.dvm_config.RELAY_LIST, zaptype="private")
if bolt11 is None:
print("Receiver has no Lightning address, can't zap back.")
@ -545,19 +629,19 @@ class DVM:
for dvm in self.dvm_config.SUPPORTED_DVMS:
scheduled_result = dvm.schedule(self.dvm_config)
for job in self.job_list:
if job.bolt11 != "" and job.payment_hash != "" and not job.payment_hash is None and not job.is_paid:
ispaid = check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)
if ispaid and job.is_paid is False:
print("is paid")
job.is_paid = True
amount = parse_amount_from_bolt11_invoice(job.bolt11)
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")
amount = parse_amount_from_bolt11_invoice(job.bolt11)
do_work(job.event, amount)
elif ispaid is None: # invoice expired
self.job_list.remove(job)

View File

@ -7,17 +7,18 @@ import sys
from sys import platform
from threading import Thread
from venv import create
from nostr_sdk import Keys
from nostr_sdk import Keys, Kind
from nostr_dvm.dvm import DVM
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_result
class DVMTaskInterface:
NAME: str
KIND: int
KIND: Kind
TASK: str = ""
FIX_COST: float = 0
PER_UNIT_COST: float = 0
@ -30,13 +31,14 @@ class DVMTaskInterface:
admin_config: AdminConfig
dependencies = []
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None,
options=None, task=None):
self.init(name, dvm_config, admin_config, nip89config, task)
self.init(name, dvm_config, admin_config, nip88config, nip89config, task)
self.options = options
self.install_dependencies(dvm_config)
def init(self, name, dvm_config, admin_config=None, nip89config=None, task=None):
def init(self, name, dvm_config, admin_config=None, nip88config=None, nip89config=None, task=None):
self.NAME = name
self.PRIVATE_KEY = dvm_config.PRIVATE_KEY
if dvm_config.PUBLIC_KEY == "" or dvm_config.PUBLIC_KEY is None:
@ -55,6 +57,12 @@ class DVMTaskInterface:
self.KIND = nip89config.KIND
dvm_config.NIP89 = self.NIP89_announcement(nip89config)
if nip88config is None:
dvm_config.NIP88 = None
else:
dvm_config.NIP88 = nip88config
self.dvm_config = dvm_config
self.admin_config = admin_config
@ -150,4 +158,3 @@ def process_venv(identifier):
DVMTaskInterface.write_output(result, args.output)
except Exception as e:
DVMTaskInterface.write_output("Error: " + str(e), args.output)

View File

@ -11,10 +11,12 @@ from nostr_sdk import (Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNot
from nostr_dvm.utils.database_utils import fetch_user_metadata
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nip88_utils import nip88_has_active_subscription
from nostr_dvm.utils.nip89_utils import NIP89Config
from nostr_dvm.utils.nwc_tools import nwc_zap
from nostr_dvm.utils.subscription_utils import create_subscription_sql_table, add_to_subscription_sql_table, \
get_from_subscription__sql_table, update_subscription_sql_table
get_from_subscription_sql_table, update_subscription_sql_table, get_all_subscriptions_from_sql_table, \
delete_from_subscription_sql_table
from nostr_dvm.utils.zap_utils import create_bolt11_lud16, zaprequest
@ -24,7 +26,7 @@ class Subscription:
# 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 = "Subscription Handler"
dvm_config.DB = "db/" + self.NAME + ".db"
dvm_config.DB = "db/" + "subscriptions" + ".db"
self.dvm_config = dvm_config
nip89config = NIP89Config()
nip89config.NAME = self.NAME
@ -43,9 +45,7 @@ class Subscription:
self.job_list = []
print("Nostr Subscription Handler 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")
pk.to_hex()) + "\n")
for relay in self.dvm_config.RELAY_LIST:
self.client.add_relay(relay)
@ -61,7 +61,7 @@ class Subscription:
self.client.subscribe([zap_filter, dm_filter, cancel_subscription_filter], None)
create_subscription_sql_table("db/subscriptions")
create_subscription_sql_table(dvm_config.DB)
# admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client)
@ -95,13 +95,85 @@ class Subscription:
kind7001eventid = tag.as_vec()[1]
if kind7001eventid != "":
subscription = get_from_subscription__sql_table("db/subscriptions", kind7001eventid)
subscription = get_from_subscription_sql_table(dvm_config.DB, kind7001eventid)
if subscription is not None:
update_subscription_sql_table("db/subscriptions", kind7001eventid, recipient,
update_subscription_sql_table(dvm_config.DB, kind7001eventid, recipient,
subscription.subscriber, subscription.nwc, subscription.cadence,
subscription.amount, subscription.begin, subscription.end,
subscription.tier_dtag, subscription.zaps, subscription.recipe, False)
subscription.tier_dtag, subscription.zaps, subscription.recipe,
False, Timestamp.now().as_secs())
def infer_subscription_end_time(start, cadence):
end = start
if cadence == "daily":
end = start + 60 * 60 * 24
elif cadence == "weekly":
end = start + 60 * 60 * 24 * 7
elif cadence == "monthly":
# TODO check days of month -.-
end = start + 60 * 60 * 24 * 31
elif cadence == "yearly":
# TODO check extra day every 4 years
end = start + 60 * 60 * 24 * 356
return end
def pay_zap_split(nwc, overall_amount, zaps):
overallsplit = 0
for zap in zaps:
overallsplit += int(zap['split'])
zapped_amount = 0
for zap in zaps:
name, nip05, lud16 = fetch_user_metadata(zap['key'], self.client)
splitted_amount = math.floor(
(int(zap['split']) / overallsplit) * int(overall_amount) / 1000)
# invoice = create_bolt11_lud16(lud16, splitted_amount)
# TODO add details about DVM in message
invoice = zaprequest(lud16, splitted_amount, "DVM subscription", None,
PublicKey.parse(zap['key']), self.keys, DVMConfig.RELAY_LIST)
print(invoice)
if invoice is not None:
nwc_event_id = nwc_zap(nwc, invoice, self.keys, zap['relay'])
if nwc_event_id is None:
print("error zapping " + lud16)
else:
zapped_amount = zapped_amount + (splitted_amount * 1000)
print(str(zapped_amount) + "/" + str(overall_amount))
if zapped_amount < overall_amount * 0.8: # TODO how do we handle failed zaps for some addresses? we are ok with 80% for now
success = False
else:
print("Zapped successfully")
success = True
# if no active subscription exists OR the subscription ended, pay
return success
def make_subscription_zap_recipe(event7001, recipient, subscriber, start, end, tier_dtag):
message = "payed by subscription service"
pTag = Tag.parse(["p", recipient])
PTag = Tag.parse(["P", subscriber])
eTag = Tag.parse(["e", event7001])
validTag = Tag.parse(["valid", str(start), str(end)])
tierTag = Tag.parse(["tier", tier_dtag])
alttag = Tag.parse(["alt", "This is a NIP90 DVM Subscription Payment Recipe"])
tags = [pTag, PTag, eTag, validTag, tierTag, alttag]
event = EventBuilder(EventDefinitions.KIND_NIP88_PAYMENT_RECIPE,
message, tags).to_event(self.keys)
dvmconfig = DVMConfig()
signer = NostrSigner.keys(self.keys)
client = Client(signer)
for relay in dvmconfig.RELAY_LIST:
client.add_relay(relay)
client.connect()
recipeid = client.send_event(event)
recipe = recipeid.to_hex()
return recipe
def handle_dm(nostr_event):
@ -123,105 +195,59 @@ class Subscription:
tier_dtag = jsonevent['tier_dtag']
start = Timestamp.now().as_secs()
end = Timestamp.now().as_secs()
isactivesubscription = False
recipe = ""
subscription = get_from_subscription__sql_table("db/subscriptions", event7001)
if subscription is not None and subscription.end > start:
start = subscription.end
isactivesubscription = True
subscription = get_from_subscription_sql_table(dvm_config.DB, event7001)
#if subscription is not None and subscription.end > start:
# start = subscription.end
# isactivesubscription = True
if cadence == "daily":
end = start + 60 * 60 * 24
elif cadence == "weekly":
end = start + 60 * 60 * 24 * 7
elif cadence == "monthly":
# TODO check days of month -.-
end = start + 60 * 60 * 24 * 31
elif cadence == "yearly":
# TODO check extra day every 4 years
end = start + 60 * 60 * 24 * 356
zapsstr = json.dumps(jsonevent['zaps'])
print(zapsstr)
success = True
if subscription is None or subscription.end <= Timestamp.now().as_secs():
#rather check nostr if our db is right
subscription_status = nip88_has_active_subscription(
PublicKey.parse(subscriber),
tier_dtag, self.client, recipient)
overallsplit = 0
if not subscription_status["isActive"]:
success = pay_zap_split(nwc, overall_amount, jsonevent['zaps'])
start = Timestamp.now().as_secs()
end = infer_subscription_end_time(start, cadence)
else:
start = Timestamp.now().as_secs()
end = subscription_status["validUntil"]
else:
start = subscription.begin
end = subscription.end
for zap in jsonevent['zaps']:
overallsplit += int(zap['split'])
zapped_amount = 0
for zap in jsonevent['zaps']:
name, nip05, lud16 = fetch_user_metadata(zap['key'], self.client)
splitted_amount = math.floor(
(int(zap['split']) / overallsplit) * int(jsonevent['overall_amount']) / 1000)
# invoice = create_bolt11_lud16(lud16, splitted_amount)
# TODO add details about DVM in message
invoice = zaprequest(lud16, splitted_amount, "DVM subscription", None,
PublicKey.parse(zap['key']), self.keys, DVMConfig.RELAY_LIST)
print(invoice)
if invoice is not None:
nwc_event_id = nwc_zap(nwc, invoice, self.keys, zap['relay'])
if nwc_event_id is None:
print("error zapping " + lud16)
else:
zapped_amount = zapped_amount + (splitted_amount * 1000)
print(str(zapped_amount) + "/" + str(overall_amount))
if zapped_amount < overall_amount * 0.8: # TODO how do we handle failed zaps for some addresses? we are ok with 80% for now
success = False
else:
print("Zapped successfully")
# if no active subscription exists OR the subscription ended, pay
if success:
message = "payed by subscription service"
pTag = Tag.parse(["p", recipient])
PTag = Tag.parse(["P", subscriber])
eTag = Tag.parse(["e", event7001])
validTag = Tag.parse(["valid", str(start), str(end)])
tierTag = Tag.parse(["tier", tier_dtag])
alttag = Tag.parse(["alt", "This is a NIP90 DVM Subscription Payment Recipe"])
tags = [pTag, PTag, eTag, validTag, tierTag, alttag]
event = EventBuilder(EventDefinitions.KIND_NIP88_PAYMENT_RECIPE,
message, tags).to_event(self.keys)
dvmconfig = DVMConfig()
signer = NostrSigner.keys(self.keys)
client = Client(signer)
for relay in dvmconfig.RELAY_LIST:
client.add_relay(relay)
client.connect()
recipeid = client.send_event(event)
recipe = recipeid.to_hex()
recipe = make_subscription_zap_recipe(event7001, recipient, subscriber, start, end, tier_dtag)
print("RECIPE " + recipe)
isactivesubscription = True
if subscription is None:
add_to_subscription_sql_table("db/subscriptions", event7001, recipient, subscriber, nwc,
add_to_subscription_sql_table(dvm_config.DB, event7001, recipient, subscriber, nwc,
cadence, overall_amount, start, end, tier_dtag,
zapsstr, recipe, isactivesubscription)
zapsstr, recipe, isactivesubscription, Timestamp.now().as_secs())
print("new subscription entry")
else:
update_subscription_sql_table("db/subscriptions", event7001, recipient, subscriber, nwc,
update_subscription_sql_table(dvm_config.DB, event7001, recipient, subscriber, nwc,
cadence, overall_amount, start, end,
tier_dtag, zapsstr, recipe, isactivesubscription)
tier_dtag, zapsstr, recipe, isactivesubscription,
Timestamp.now().as_secs())
print("updated subscription entry")
except Exception as e:
print(e)
except Exception as e:
print("Error in Subscriber " + str(e))
@ -230,7 +256,66 @@ class Subscription:
try:
while True:
time.sleep(60.0)
print("Checking Subscription")
subscriptions = get_all_subscriptions_from_sql_table(dvm_config.DB)
for subscription in subscriptions:
if subscription.active:
if subscription.end < Timestamp.now().as_secs():
# We could directly zap, but let's make another check if our subscription expired
subscription_status = nip88_has_active_subscription(
PublicKey.parse(subscription.subscriber),
subscription.tier_dtag, self.client, subscription.recipent)
if not subscription_status["isActive"] or subscription_status["expires"]:
update_subscription_sql_table(dvm_config.DB, subscription.id,
subscription.recipent,
subscription.subscriber, subscription.nwc,
subscription.cadence, subscription.amount,
subscription.begin, subscription.end,
subscription.tier_dtag, subscription.zaps,
subscription.recipe,
False,
Timestamp.now().as_secs())
else:
zaps = json.loads(subscription.zaps)
success = pay_zap_split(subscription.nwc, subscription.amount, zaps)
if success:
end = infer_subscription_end_time(Timestamp.now().as_secs(), subscription.cadence)
recipe = make_subscription_zap_recipe(subscription.id, subscription.recipent,
subscription.subscriber, subscription.begin,
end, subscription.tier_dtag)
else:
end = Timestamp.now().as_secs()
recipe = subscription.recipe
update_subscription_sql_table(dvm_config.DB, subscription.id,
subscription.recipent,
subscription.subscriber, subscription.nwc,
subscription.cadence, subscription.amount,
subscription.begin, end,
subscription.tier_dtag, subscription.zaps, recipe,
success,
Timestamp.now().as_secs())
print("updated subscription entry")
else:
delete_threshold = 60 * 60 * 24 * 365
if subscription.cadence == "daily":
delete_threshold = 60 * 60 * 24 * 3 # After 3 days, delete the subscription, user can make a new one
elif subscription.cadence == "weekly":
delete_threshold = 60 * 60 * 24 * 21 # After 21 days, delete the subscription, user can make a new one
elif subscription.cadence == "monthly":
delete_threshold = 60 * 60 * 24 * 60 # After 60 days, delete the subscription, user can make a new one
elif subscription.cadence == "yearoy":
delete_threshold = 60 * 60 * 24 * 500 # After 500 days, delete the subscription, user can make a new one
if subscription.end < (Timestamp.now().as_secs() - delete_threshold):
delete_from_subscription_sql_table(dvm_config.DB, subscription.id)
print("Delete expired subscription")
print(str(Timestamp.now().as_secs()) + ": Checking " + str(len(subscriptions)) + " Subscription entries..")
except KeyboardInterrupt:
print('Stay weird!')
os.kill(os.getpid(), signal.SIGTERM)

View File

@ -1,12 +1,13 @@
import json
import os
from datetime import timedelta
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind, RelayOptions
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_events
@ -19,15 +20,16 @@ Params: None
class AdvancedSearch(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_CONTENT_SEARCH
KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_SEARCH
TASK: str = "search-content"
FIX_COST: float = 0
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
@ -93,7 +95,9 @@ class AdvancedSearch(DVMTaskInterface):
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
cli.add_relay(options["relay"])
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts(options["relay"], ropts)
cli.connect()
#earch_since_seconds = int(options["since"]) * 24 * 60 * 60
@ -121,12 +125,12 @@ class AdvancedSearch(DVMTaskInterface):
userkeys.append(userkey)
if not options["users"]:
notes_filter = Filter().kind(1).search(options["search"]).since(search_since).until(search_until).limit(options["max_results"])
notes_filter = Filter().kind(Kind(1)).search(options["search"]).since(search_since).until(search_until).limit(options["max_results"])
elif options["search"] == "":
notes_filter = Filter().kind(1).authors(userkeys).since(search_since).until(
notes_filter = Filter().kind(Kind(1)).authors(userkeys).since(search_since).until(
search_until).limit(options["max_results"])
else:
notes_filter = Filter().kind(1).authors(userkeys).search(options["search"]).since(
notes_filter = Filter().kind(Kind(1)).authors(userkeys).search(options["search"]).since(
search_since).until(search_until).limit(options["max_results"])
@ -163,7 +167,7 @@ def build_example(name, identifier, admin_config):
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/a99ab925084029d9468fef8330ff3d9be2cf67da473b024f2a6d48b5cd77197f.jpg",
"image": "https://nostr.band/android-chrome-192x192.png",
"about": "I search notes on Nostr.band.",
"encryptionSupported": True,
"cashuAccepted": True,

View File

@ -3,12 +3,13 @@ import os
from datetime import timedelta
import requests
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Event
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Event, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_events
@ -21,15 +22,16 @@ Params: None
class AdvancedSearchWine(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_CONTENT_SEARCH
KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_SEARCH
TASK: str = "search-content"
FIX_COST: float = 0
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -2,12 +2,14 @@ import json
import os
from datetime import timedelta
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, NostrDatabase, \
ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Event, EventId
ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Event, EventId, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils import definitions
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config, check_and_set_d_tag_nip88, check_and_set_tiereventid_nip88
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_events, post_process_list_to_users
@ -20,23 +22,24 @@ Params: None
class DicoverContentCurrentlyPopular(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY
KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY
TASK: str = "discover-content"
FIX_COST: float = 0
dvm_config: DVMConfig
last_schedule: int
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
self.last_schedule = Timestamp.now().as_secs()
use_logger = False
if use_logger:
init_logger(LogLevel.DEBUG)
super().__init__(name, dvm_config, nip89config, admin_config, options)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
@ -94,20 +97,21 @@ class DicoverContentCurrentlyPopular(DVMTaskInterface):
# Query events from database
timestamp_hour_ago = Timestamp.now().as_secs() - 3600
lasthour = Timestamp.from_secs(timestamp_hour_ago)
filter1 = Filter().kind(1).since(lasthour)
filter1 = Filter().kind(definitions.EventDefinitions.KIND_NOTE).since(lasthour)
events = cli.database().query([filter1])
ns.finallist = {}
for event in events:
if event.created_at().as_secs() > timestamp_hour_ago:
ns.finallist[event.id().to_hex()] = 0
filt = Filter().kinds([9735, 7, 1]).event(event.id()).since(lasthour)
filt = Filter().kinds([definitions.EventDefinitions.KIND_ZAP, definitions.EventDefinitions.KIND_REACTION, definitions.EventDefinitions.KIND_NOTE]).event(event.id()).since(lasthour)
reactions = cli.database().query([filt])
ns.finallist[event.id().to_hex()] = len(reactions)
result_list = []
finallist_sorted = sorted(ns.finallist.items(), key=lambda x: x[1], reverse=True)[:int(options["max_results"])]
for entry in finallist_sorted:
print(EventId.parse(entry[0]).to_bech32() + "/" + EventId.parse(entry[0]).to_hex() + ": " + str(entry[1]))
#print(EventId.parse(entry[0]).to_bech32() + "/" + EventId.parse(entry[0]).to_hex() + ": " + str(entry[1]))
e_tag = Tag.parse(["e", entry[0]])
result_list.append(e_tag.as_vec())
@ -146,12 +150,16 @@ class DicoverContentCurrentlyPopular(DVMTaskInterface):
timestamp_hour_ago = Timestamp.now().as_secs() - 3600
lasthour = Timestamp.from_secs(timestamp_hour_ago)
filter1 = Filter().kinds([1, 7, 9735]).since(lasthour) # Notes, reactions, zaps
filter1 = Filter().kinds([definitions.EventDefinitions.KIND_NOTE, definitions.EventDefinitions.KIND_REACTION, definitions.EventDefinitions.KIND_ZAP]).since(lasthour) # Notes, reactions, zaps
# filter = Filter().author(keys.public_key())
print("Syncing Notes of last hour.. this might take a while..")
dbopts = NegentropyOptions().direction(NegentropyDirection.DOWN)
cli.reconcile(filter1, dbopts)
database.delete(Filter().until(Timestamp.from_secs(
Timestamp.now().as_secs() - 3600))) # Clear old events so db doesnt get too full.
print("Done Syncing Notes of Last hour.")
@ -163,13 +171,20 @@ def build_example(name, identifier, admin_config):
dvm_config.USE_OWN_VENV = False
dvm_config.SHOWLOG = True
dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 minutes
# Activate these to use a subscription based model instead
# dvm_config.SUBSCRIPTION_REQUIRED = True
# dvm_config.SUBSCRIPTION_DAILY_COST = 1
dvm_config.FIX_COST = 0
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/f720192abfbfbcc21ce78281aca4bbd1ccf89ee7c90b54ae16b71ae9c1ad88e0.png",
"about": "I show popular content",
"image": "https://image.nostr.build/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg",
"about": "I show notes that are currently popular",
"lud16": dvm_config.LN_ADDRESS,
"encryptionSupported": True,
"cashuAccepted": True,
"amount": "free",
"nip90Params": {
"max_results": {
"required": False,
@ -183,8 +198,68 @@ def build_example(name, identifier, admin_config):
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = True
return DicoverContentCurrentlyPopular(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
admin_config=admin_config)
def build_example_subscription(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
dvm_config.SHOWLOG = True
dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 minutes
# Activate these to use a subscription based model instead
# dvm_config.SUBSCRIPTION_DAILY_COST = 1
dvm_config.FIX_COST = 0
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg",
"about": "I show notes that are currently popular, just like the free DVM, I'm also used for testing subscriptions. (beta, might break"
")",
"lud16": dvm_config.LN_ADDRESS,
"encryptionSupported": True,
"cashuAccepted": True,
"subscription": True,
"nip90Params": {
"max_results": {
"required": False,
"values": [],
"description": "The number of maximum results to return (default currently 100)"
}
}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
nip88config = NIP88Config()
nip88config.DTAG = check_and_set_d_tag_nip88(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip88config.TIER_EVENT = check_and_set_tiereventid_nip88(identifier, "1")
nip89config.NAME = name
nip88config.IMAGE = nip89info["image"]
nip88config.TITLE = name
nip88config.AMOUNT_DAILY = 100
nip88config.AMOUNT_MONTHLY = 2000
nip88config.CONTENT = "Subscribe to the DVM for unlimited use during your subscription"
nip88config.PERK1DESC = "Unlimited requests"
nip88config.PAYMENT_VERIFIER_PUBKEY = "5b5c045ecdf66fb540bdf2049fe0ef7f1a566fa427a4fe50d400a011b65a3a7e"
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = True
admin_config.REBROADCAST_NIP88 = False
# admin_config.FETCH_NIP88 = True
# admin_config.EVENTID = "63a791cdc7bf78c14031616963105fce5793f532bb231687665b14fb6d805fdb"
# admin_config.PRIVKEY = dvm_config.PRIVATE_KEY
return DicoverContentCurrentlyPopular(name=name, dvm_config=dvm_config, nip89config=nip89config,
nip88config=nip88config,
admin_config=admin_config)
if __name__ == '__main__':

View File

@ -1,9 +1,13 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config
from nostr_dvm.utils.mediasource_utils import organize_input_media_data
from nostr_dvm.utils.output_utils import upload_media_to_hoster
@ -18,15 +22,16 @@ Params: -language The target language
class MediaConverter(DVMTaskInterface):
KIND = EventDefinitions.KIND_NIP90_CONVERT_VIDEO
KIND: Kind = EventDefinitions.KIND_NIP90_CONVERT_VIDEO
TASK = "convert"
FIX_COST = 20
PER_UNIT_COST = 0.1
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -3,12 +3,13 @@ import os
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_users
@ -22,16 +23,17 @@ Params: None
class DiscoverInactiveFollows(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "inactive-follows"
FIX_COST: float = 50
client: Client
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
# no input required
@ -78,7 +80,7 @@ class DiscoverInactiveFollows(DVMTaskInterface):
options = DVMTaskInterface.set_options(request_form)
step = 20
followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(3).limit(1)
followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(Kind(3)).limit(1)
followers = cli.get_events_of([followers_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
if len(followers) > 0:

View File

@ -3,12 +3,13 @@ import os
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_users
@ -22,16 +23,17 @@ Params: None
class DiscoverNonFollowers(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "non-followers"
FIX_COST: float = 50
client: Client
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
# no input required
@ -66,15 +68,15 @@ class DiscoverNonFollowers(DVMTaskInterface):
keys = Keys.parse(sk.to_hex())
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
#cli.add_relay("wss://relay.nostr.band")
# cli.add_relay("wss://relay.nostr.band")
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
cli.add_relay(relay)
cli.connect()
options = DVMTaskInterface.set_options(request_form)
step = 20
followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(3)
followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(Kind(3))
followers = cli.get_events_of([followers_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
if len(followers) > 0:
@ -96,7 +98,6 @@ class DiscoverNonFollowers(DVMTaskInterface):
ns.dic[following] = "True"
print("Followings: " + str(len(followings)))
def scanList(users, instance, i, st):
from nostr_sdk import Filter
@ -109,10 +110,9 @@ class DiscoverNonFollowers(DVMTaskInterface):
cli.add_relay(relay)
cli.connect()
for i in range(i, i + st):
filters = []
filter1 = Filter().author(PublicKey.from_hex(users[i])).kind(3)
filter1 = Filter().author(PublicKey.from_hex(users[i])).kind(Kind(3))
filters.append(filter1)
followers = cli.get_events_of(filters, timedelta(seconds=3))
@ -130,12 +130,12 @@ class DiscoverNonFollowers(DVMTaskInterface):
if tag.as_vec()[0] == "p":
if len(tag.as_vec()) > 1:
if tag.as_vec()[1] == options["user"]:
foundfollower = True
break
foundfollower = True
break
if not foundfollower:
instance.dic[best_entry.author().to_hex()] = "False"
print( "DIDNT FIND " + best_entry.author().to_nostr_uri())
print("DIDNT FIND " + best_entry.author().to_nostr_uri())
print(str(i) + "/" + str(len(users)))
cli.disconnect()
@ -147,7 +147,7 @@ class DiscoverNonFollowers(DVMTaskInterface):
args = [followings, ns, begin, step]
t = Thread(target=scanList, args=args)
threads.append(t)
begin = begin + step -1
begin = begin + step - 1
# last to step size
missing_scans = (len(followings) - begin)
@ -216,7 +216,7 @@ def build_example(name, identifier, admin_config):
nip89config.CONTENT = json.dumps(nip89info)
return DiscoverNonFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
admin_config=admin_config)
if __name__ == '__main__':

View File

@ -1 +1,299 @@
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, EventId
import json
import os
import random
import re
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, EventId, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_users
"""
This File contains a Module to find inactive follows for a user on nostr
Accepted Inputs: None needed
Outputs: A list of users that have been inactive
Params: None
"""
class DiscoverInactiveFollows(DVMTaskInterface):
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "inactive-follows"
FIX_COST: float = 50
client: Client
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
# no input required
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
self.dvm_config = dvm_config
request_form = {"jobID": event.id().to_hex()}
# default values
user = event.author().to_hex()
since_days = 90
for tag in event.tags():
if tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "user": # check for param type
user = tag.as_vec()[2]
elif param == "since_days": # check for param type
since_days = int(tag.as_vec()[2])
options = {
"user": user,
"since_days": since_days
}
request_form['options'] = json.dumps(options)
return request_form
def process(self, request_form):
from nostr_sdk import Filter
from types import SimpleNamespace
ns = SimpleNamespace()
opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)))
sk = SecretKey.from_hex(self.dvm_config.PRIVATE_KEY)
keys = Keys.parse(sk.to_hex())
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
cli.connect()
options = DVMTaskInterface.set_options(request_form)
inactivefollowerslist = ""
relay_list = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine",
"wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev",
"wss://relay.nostr.bg", "wss://relay.f7z.io"]
relaytimeout = 5
step = 20
keys = Keys.parse(os.getenv(env.NOSTR_PRIVATE_KEY))
opts = Options().wait_for_send(False).send_timeout(timedelta(seconds=5)).skip_disconnected_relays(
True)
cl = Client.with_opts(keys, opts)
for relay in relay_list:
cl.add_relay(relay)
cl.connect()
timeinseconds = 3 * 24 * 60 * 60
dif = Timestamp.now().as_secs() - timeinseconds
considernotessince = Timestamp.from_secs(dif)
filt = Filter().author(user).kind(1).since(considernotessince)
reactions = cl.get_events_of([filt], timedelta(seconds=relaytimeout))
list = []
random.shuffle(reactions)
for reaction in reactions:
if reaction.kind() == 1:
list.append(reaction.content())
all = json.dumps(list)
all = all.replace("\n", " ").replace("\n\n", " ")
cleared = ""
tokens = all.split()
for item in tokens:
item = item.replace("\n", " ").lstrip("\"").rstrip(",").rstrip(("."))
# print(item)
if item.__contains__("http") or item.__contains__("\nhttp") or item.__contains__(
"\n\nhttp") or item.lower().__contains__("nostr:") or item.lower().__contains__(
"nevent") or item.__contains__("\\u"):
cleareditem = ""
else:
cleareditem = item
cleared = cleared + " " + cleareditem
cleared = cleared.replace("\n", " ")
# res = re.sub(r"[^ a-zA-Z0-9.!?/\\:,]+", '', all)
# print(cleared)
try:
answer = LLAMA2(
"Give me the 15 most important substantives as keywords of the following input: " + cleared,
"nostruser",
"Reply only with a comma-seperated keywords. return topics starting with a *", clear=True)
except:
answer = ""
promptarr = answer.split(":")
if len(promptarr) > 1:
# print(promptarr[1])
prompt = promptarr[1].lstrip("\n").replace("\n", ",").replace("*", ",").replace("", ",")
else:
prompt = promptarr[0].replace("\n", ",").replace("*", "")
pattern = r"[^a-zA-Z,#'\s]"
text = re.sub(pattern, "", prompt) + ","
# text = (text.replace("Let's,", "").replace("Why,", "").replace("GM,", "")
# .replace("Remember,", "").replace("I,", "").replace("Think,", "")
# .replace("Already,", ""))
# print(text)
keywords = text.split(', ')
keywords = [x.lstrip().rstrip(',') for x in keywords if x]
print(keywords)
# answer = LLAMA2("Extent the given list with 5 synonyms per entry " + str(keywords), user,
# "Reply only with a comma-seperated keywords. return topics starting with a *")
# answer.replace(" - Alternatives:", ",")
# print(answer)
# promptarr = answer.split(":")
# if len(promptarr) > 1:
# # print(promptarr[1])
# prompt = promptarr[1].lstrip("\n").replace("\n", ",").replace("*", "").replace("•", "")
# else:
# prompt = promptarr[0].replace("\n", ",").replace("*", "")
# pattern = r"[^a-zA-Z,'\s]"
# text = re.sub(pattern, "", prompt) + ","
# keywords = text.split(', ')
# print(keywords)
timeinseconds = 30 * 60 # last 30 min?
dif = Timestamp.now().as_secs() - timeinseconds
looksince = Timestamp.from_secs(dif)
filt2 = Filter().kind(1).since(looksince)
notes = cl.get_events_of([filt2], timedelta(seconds=6))
# finallist = []
ns.finallist = {}
print("Notes found: " + str(len(notes)))
def scanList(noteid: EventId, instance, i, length):
relay_list = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol",
"wss://nostr.wine",
"wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev",
"wss://relay.nostr.bg", "wss://relay.f7z.io"]
keys = Keys.parse(os.getenv(env.NOSTR_PRIVATE_KEY))
opts = Options().wait_for_send(wait_for_send).send_timeout(
timedelta(seconds=5)).skip_disconnected_relays(True)
cli = Client.with_opts(keys, opts)
for relay in relay_list:
cli.add_relay(relay)
cli.connect()
filters = []
instance.finallist[noteid.to_hex()] = 0
filt = Filter().kinds([9735, 7, 1]).event(noteid)
reactions = cl.get_events_of([filt], timedelta(seconds=5))
print(str(len(reactions)) + " " + str(j) + "/" + str(len(notes)))
instance.finallist[noteid.to_hex()] = len(reactions)
print(str(i) + "/" + str(length))
cli.disconnect()
j = 0
threads = []
for note in notes:
j = j + 1
res = [ele for ele in keywords if (ele.replace(',', "") in note.content())]
if bool(res):
if not note.id().to_hex() in ns.finallist and note.pubkey().to_hex() != user:
args = [note.id(), ns, j, len(notes)]
t = Thread(target=scanList, args=args)
threads.append(t)
# Start all threads
for x in threads:
x.start()
# Wait for all of them to finish
for x in threads:
x.join()
finallist_sorted = sorted(ns.finallist.items(), key=lambda x: x[1], reverse=True)
converted_dict = dict(finallist_sorted)
print(json.dumps(converted_dict))
notelist = ""
resultlist = []
i = 0
notelist = "Based on topics: " + json.dumps(keywords).lstrip("[").rstrip(("]")) + "\n\n"
for k in converted_dict:
# print(k)
if is_bot:
i = i + 1
notelist = notelist + "nostr:" + EventId.from_hex(k).to_bech32() + "\n\n"
if i == 25:
break
else:
p_tag = Tag.parse(["p", k])
resultlist.append(p_tag.as_vec())
else:
return json.dumps(resultlist[:25])
def post_process(self, result, event):
"""Overwrite the interface function to return a social client readable format, if requested"""
for tag in event.tags():
if tag.as_vec()[0] == 'output':
format = tag.as_vec()[1]
if format == "text/plain": # check for output type
result = post_process_list_to_users(result)
# if not text/plain, don't post-process
return result
# We build an example here that we can call by either calling this file directly from the main directory,
# or by adding it to our playground. You can call the example and adjust it to your needs or redefine it in the
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/b29b6ec4bf9b6184f69d33cb44862db0d90a2dd9a506532e7ba5698af7d36210.jpg",
"about": "I discover users you follow, but that have been inactive on Nostr",
"encryptionSupported": True,
"cashuAccepted": True,
"amount": "Free",
"nip90Params": {
"user": {
"required": False,
"values": [],
"description": "Do the task for another user"
},
"since_days": {
"required": False,
"values": [],
"description": "The number of days a user has not been active to be considered inactive"
}
}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
return DiscoverInactiveFollows(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
if __name__ == '__main__':
process_venv(DiscoverInactiveFollows)

View File

@ -1,11 +1,12 @@
import json
import os
from nostr_sdk import Tag
from nostr_sdk import Tag, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_events
@ -18,15 +19,16 @@ Params: None
class TrendingNotesNostrBand(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY
KIND: Kind = EventDefinitions.KIND_NIP90_CONTENT_DISCOVERY
TASK: str = "trending-content"
FIX_COST: float = 0
dvm_config: DVMConfig
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
@ -76,8 +78,9 @@ class TrendingNotesNostrBand(DVMTaskInterface):
return json.dumps(result_list)
except:
return "error"
except Exception as e:
print(e)
return json.dumps([])
def post_process(self, result, event):
"""Overwrite the interface function to return a social client readable format, if requested"""
@ -102,7 +105,7 @@ def build_example(name, identifier, admin_config):
nip89info = {
"name": name,
"image": "https://image.nostr.build/4dc758923c7bfc5ba92030e6419272ec7470c3809d36e88e99f3a9daece88bac.png",
"image": "https://nostr.band/android-chrome-192x192.png",
"about": "I show trending notes from nostr.band",
"amount": "Free",
"encryptionSupported": True,

View File

@ -5,11 +5,13 @@ from io import BytesIO
import requests
from PIL import Image
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import upload_media_to_hoster
from nostr_dvm.utils.zap_utils import get_price_per_sat
@ -23,16 +25,17 @@ Outputs: An url to an Image
class ImageGenerationDALLE(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image"
FIX_COST: float = 120
dependencies = [("nostr-dvm", "nostr-dvm"),
("openai", "openai==1.3.5")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -3,11 +3,13 @@ import os
from io import BytesIO
import requests
from PIL import Image
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import upload_media_to_hoster
from nostr_dvm.utils.zap_utils import get_price_per_sat
@ -22,16 +24,17 @@ Params:
class ImageGenerationReplicateSDXL(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image"
FIX_COST: float = 120
dependencies = [("nostr-dvm", "nostr-dvm"),
("replicate", "replicate")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,12 +1,14 @@
import json
import os
from PIL import Image
from nostr_sdk import Kind
from tqdm import tqdm
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import upload_media_to_hoster
from nostr_dvm.utils.zap_utils import get_price_per_sat
@ -21,7 +23,7 @@ Params:
class ImageGenerationMLX(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image"
FIX_COST: float = 120
dependencies = [("nostr-dvm", "nostr-dvm"),
@ -32,10 +34,11 @@ class ImageGenerationMLX(DVMTaskInterface):
("tqdm", "tqdm"),
]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,13 @@
import json
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -19,13 +22,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea
class ImageGenerationSDXL(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "text-to-image"
FIX_COST: float = 50
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,13 @@
import json
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -19,13 +22,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea
class ImageGenerationSDXLIMG2IMG(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "image-to-image"
FIX_COST: float = 70
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
hasurl = False

View File

@ -1,10 +1,13 @@
import json
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -18,13 +21,14 @@ Outputs: An textual description of the image
class ImageInterrogator(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
TASK: str = "image-to-text"
FIX_COST: float = 80
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
hasurl = False

View File

@ -1,10 +1,13 @@
import json
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -18,13 +21,14 @@ Params: -upscale 2,3,4
class ImageUpscale(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_IMAGE
TASK: str = "image-to-image"
FIX_COST: float = 20
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
hasurl = False

View File

@ -2,12 +2,13 @@ import json
import os
from datetime import timedelta
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, NostrDatabase, \
ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel
ClientBuilder, Filter, NegentropyOptions, NegentropyDirection, init_logger, LogLevel, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import post_process_list_to_events, post_process_list_to_users
@ -20,23 +21,22 @@ Params: None
class SearchUser(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_USER_SEARCH
KIND: Kind = EventDefinitions.KIND_NIP90_USER_SEARCH
TASK: str = "search-user"
FIX_COST: float = 0
dvm_config: DVMConfig
last_schedule: int
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
self.last_schedule = Timestamp.now().as_secs()
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
use_logger = False
if use_logger:
init_logger(LogLevel.DEBUG)
super().__init__(name, dvm_config, nip89config, admin_config, options)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
@ -95,7 +95,7 @@ class SearchUser(DVMTaskInterface):
# Query events from database
filter1 = Filter().kind(0)
filter1 = Filter().kind(Kind(0))
events = cli.database().query([filter1])
# for event in events:
# print(event.as_json())
@ -153,7 +153,7 @@ class SearchUser(DVMTaskInterface):
cli.add_relay("wss://relay.damus.io")
cli.connect()
filter1 = Filter().kind(0)
filter1 = Filter().kind(Kind(0))
# filter = Filter().author(keys.public_key())
print("Syncing Profile Database.. this might take a while..")

View File

@ -5,9 +5,10 @@ from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id, get_events_by_ids
from nostr_sdk import Tag
from nostr_sdk import Tag, Kind
"""
This File contains a Module to summarize Text, based on a prompt using a the HuggingChat LLM on Huggingface
@ -18,16 +19,17 @@ Outputs: Generated text
class TextSummarizationHuggingChat(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT
TASK: str = "summarization"
FIX_COST: float = 0
dependencies = [("nostr-dvm", "nostr-dvm"),
("hugchat", "hugchat")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,11 +1,12 @@
import json
import os
import re
from nostr_sdk import Tag
from nostr_sdk import Tag, Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_events_by_ids, get_event_by_id
@ -18,16 +19,17 @@ Outputs: Generated text
class SummarizationUnleashedChat(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_SUMMARIZE_TEXT
TASK: str = "text-to-text"
FIX_COST: float = 10
dependencies = [("nostr-dvm", "nostr-dvm"),
("openai", "openai")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
@ -107,7 +109,7 @@ class SummarizationUnleashedChat(DVMTaskInterface):
for model in client.models.list():
print('- ' + model.id)
content = "Summarize the following notes: " + str(options["prompt"])[:4000]
content = "Summarize the following notes: " + str(options["prompt"])[:3500]
normal_stream = client.chat.completions.create(
messages=[
{

View File

@ -2,10 +2,13 @@ import json
import os
import time
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.mediasource_utils import organize_input_media_data
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -19,17 +22,18 @@ Outputs: Transcribed text
class SpeechToTextGoogle(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
TASK: str = "speech-to-text"
FIX_COST: float = 10
PER_UNIT_COST: float = 0.1
dependencies = [("nostr-dvm", "nostr-dvm"),
("speech_recognition", "SpeechRecognition==3.10.0")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
if options is None:
self.options = {}

View File

@ -2,10 +2,13 @@ import json
import os
import re
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.nostr_utils import get_event_by_id
@ -19,16 +22,17 @@ Params: None
class TextExtractionPDF(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
TASK: str = "pdf-to-text"
FIX_COST: float = 0
dependencies = [("nostr-dvm", "nostr-dvm"),
("pypdf", "pypdf==3.17.1")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -2,11 +2,15 @@ import json
import os
import time
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server, send_file_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.mediasource_utils import organize_input_media_data
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -20,14 +24,16 @@ Outputs: Transcribed text
class SpeechToTextWhisperX(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_EXTRACT_TEXT
TASK: str = "speech-to-text"
FIX_COST: float = 10
PER_UNIT_COST: float = 0.1
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,13 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
"""
@ -16,16 +19,17 @@ Outputs: Generated text
class TextGenerationHuggingChat(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT
TASK: str = "text-to-text"
FIX_COST: float = 0
dependencies = [("nostr-dvm", "nostr-dvm"),
("hugchat", "hugchat")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,13 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
"""
@ -16,16 +19,17 @@ Outputs: Generated text
class TextGenerationLLMLite(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT
TASK: str = "text-to-text"
FIX_COST: float = 0
dependencies = [("nostr-dvm", "nostr-dvm"),
("litellm", "litellm==1.12.3")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,13 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
"""
@ -16,16 +19,17 @@ Outputs: Generated text
class TextGenerationUnleashedChat(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_TEXT
TASK: str = "text-to-text"
FIX_COST: float = 10
dependencies = [("nostr-dvm", "nostr-dvm"),
("openai", "openai")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,6 +1,10 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.utils.nip88_utils import NIP88Config
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
from pathlib import Path
import urllib.request
@ -22,17 +26,18 @@ Outputs: Generated Audiofile
class TextToSpeech(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_TEXT_TO_SPEECH
KIND: Kind = EventDefinitions.KIND_NIP90_TEXT_TO_SPEECH
TASK: str = "text-to-speech"
FIX_COST: float = 50
PER_UNIT_COST = 0.5
dependencies = [("nostr-dvm", "nostr-dvm"),
("TTS", "TTS==0.22.0")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,9 +1,13 @@
import json
import os
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id
@ -17,16 +21,17 @@ Params: -language The target language
class TranslationGoogle(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT
TASK: str = "translation"
FIX_COST: float = 0
dependencies = [("nostr-dvm", "nostr-dvm"),
("translatepy", "translatepy==2.3")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,11 +1,13 @@
import json
import os
import requests
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.nostr_utils import get_referenced_event_by_id, get_event_by_id
@ -21,14 +23,15 @@ Requires API key or self-hosted instance
class TranslationLibre(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT
KIND: Kind = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT
TASK: str = "translation"
FIX_COST: float = 0
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
admin_config: AdminConfig = None, options=None, task=None):
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options, task)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -4,11 +4,13 @@ from io import BytesIO
import requests
import urllib.request
from PIL import Image
from nostr_sdk import Kind
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.output_utils import upload_media_to_hoster
from nostr_dvm.utils.zap_utils import get_price_per_sat
@ -23,16 +25,17 @@ Params:
class VideoGenerationReplicateSVD(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_VIDEO
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_VIDEO
TASK: str = "image-to-video"
FIX_COST: float = 120
dependencies = [("nostr-dvm", "nostr-dvm"),
("replicate", "replicate")]
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
dvm_config.SCRIPT = os.path.abspath(__file__)
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -1,10 +1,14 @@
import json
import os
from multiprocessing.pool import ThreadPool
from nostr_sdk import Kind
from nostr_dvm.backends.nova_server.utils import check_server_status, send_request_to_server
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface, process_venv
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import DVMConfig, build_default_config
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
from nostr_dvm.utils.definitions import EventDefinitions
@ -18,13 +22,14 @@ Outputs: An url to a video
class VideoGenerationSVD(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_GENERATE_VIDEO
KIND: Kind = EventDefinitions.KIND_NIP90_GENERATE_VIDEO
TASK: str = "image-to-video"
FIX_COST: float = 120
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config,
def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, nip88config: NIP88Config = None,
admin_config: AdminConfig = None, options=None):
super().__init__(name, dvm_config, nip89config, admin_config, options)
super().__init__(name=name, dvm_config=dvm_config, nip89config=nip89config, nip88config=nip88config,
admin_config=admin_config, options=options)
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:

View File

@ -5,23 +5,28 @@ from nostr_sdk import Keys, PublicKey, Client
from nostr_dvm.utils.database_utils import get_from_sql_table, list_db, delete_from_sql_table, update_sql_table, \
get_or_add_user, clean_db
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nip89_utils import nip89_announce_tasks, fetch_nip89_paramters_for_deletion
from nostr_dvm.utils.nip88_utils import nip88_announce_tier, fetch_nip88_parameters_for_deletion, fetch_nip88_event, \
check_and_set_tiereventid_nip88
from nostr_dvm.utils.nip89_utils import nip89_announce_tasks, fetch_nip89_parameters_for_deletion
from nostr_dvm.utils.nostr_utils import update_profile
class AdminConfig:
REBROADCAST_NIP89: bool = False
REBROADCAST_NIP88: bool = False
UPDATE_PROFILE: bool = False
DELETE_NIP89: bool = False
DELETE_NIP88: bool = False
FETCH_NIP88: bool = False
WHITELISTUSER: bool = False
UNWHITELISTUSER: bool = False
BLACKLISTUSER: bool = False
DELETEUSER: bool = False
LISTDATABASE: bool = False
ClEANDB: bool = False
INDEX: str = "1"
USERNPUBS: list = []
LUD16: str = ""
EVENTID: str = ""
PRIVKEY: str = ""
@ -56,17 +61,17 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC
if adminconfig.WHITELISTUSER:
user = get_or_add_user(db, publickey, client=client, config=dvmconfig)
update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive)
update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed)
user = get_from_sql_table(db, publickey)
print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted))
if adminconfig.UNWHITELISTUSER:
user = get_from_sql_table(db, publickey)
update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive)
update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed)
if adminconfig.BLACKLISTUSER:
user = get_from_sql_table(db, publickey)
update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive)
update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive, user.subscribed)
if adminconfig.DELETEUSER:
delete_from_sql_table(db, publickey)
@ -80,11 +85,27 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC
if adminconfig.REBROADCAST_NIP89:
nip89_announce_tasks(dvmconfig, client=client)
if adminconfig.REBROADCAST_NIP88:
annotier_id = nip88_announce_tier(dvmconfig, client=client)
check_and_set_tiereventid_nip88(dvmconfig.IDENTIFIER, adminconfig.INDEX, annotier_id.to_hex())
if adminconfig.DELETE_NIP89:
event_id = adminconfig.EVENTID
keys = Keys.parse(
adminconfig.PRIVKEY) # Private key from sender of Event (e.g. the key of an nip89 announcement you want to delete)
fetch_nip89_paramters_for_deletion(keys, event_id, client, dvmconfig)
fetch_nip89_parameters_for_deletion(keys, event_id, client, dvmconfig)
if adminconfig.DELETE_NIP88:
event_id = adminconfig.EVENTID
keys = Keys.parse(
adminconfig.PRIVKEY) # Private key from sender of Event (e.g. the key of an nip89 announcement you want to delete)
fetch_nip88_parameters_for_deletion(keys, event_id, client, dvmconfig)
if adminconfig.FETCH_NIP88:
event_id = adminconfig.EVENTID
keys = Keys.parse(
adminconfig.PRIVKEY)
fetch_nip88_event(keys, event_id, client, dvmconfig)
if adminconfig.UPDATE_PROFILE:
update_profile(dvmconfig, client, lud16=adminconfig.LUD16)
update_profile(dvmconfig, client, lud16=dvmconfig.LN_ADDRESS)

View File

@ -12,7 +12,7 @@ from nostr_dvm.utils.nostr_utils import get_event_by_id, get_referenced_event_by
def get_task(event, client, dvm_config):
try:
if event.kind() == EventDefinitions.KIND_NIP90_GENERIC: # use this for events that have no id yet, inclufr j tag
if event.kind().as_u64() == EventDefinitions.KIND_NIP90_GENERIC.as_u64(): # use this for events that have no id yet, inclufr j tag
for tag in event.tags():
if tag.as_vec()[0] == 'j':
return tag.as_vec()[1]
@ -26,7 +26,7 @@ def get_task(event, client, dvm_config):
return "unknown job: " + event.as_json()
# This looks a bit more complicated, but we do several tasks for text-extraction in the future
elif event.kind() == EventDefinitions.KIND_NIP90_EXTRACT_TEXT:
elif event.kind().as_u64() == EventDefinitions.KIND_NIP90_EXTRACT_TEXT.as_u64():
for tag in event.tags():
if tag.as_vec()[0] == "i":
if tag.as_vec()[2] == "url":
@ -57,7 +57,7 @@ def get_task(event, client, dvm_config):
return "unknown type"
else:
return "unknown job"
elif event.kind() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE:
elif event.kind().as_u64() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE.as_u64():
has_image_tag = False
has_text_tag = False
for tag in event.tags():
@ -92,7 +92,7 @@ def get_task(event, client, dvm_config):
else:
for dvm in dvm_config.SUPPORTED_DVMS:
if dvm.KIND == event.kind():
if dvm.KIND.as_u64() == event.kind().as_u64():
return dvm.TASK
except Exception as e:
print("Get task: " + str(e))

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from datetime import timedelta
from logging import Filter
from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter
from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter, Kind
from nostr_dvm.utils.nostr_utils import send_event
@ -21,6 +21,7 @@ class User:
nip05: str
lud16: str
lastactive: int
subscribed: int
def create_sql_table(db):
@ -40,7 +41,8 @@ def create_sql_table(db):
nip05 text,
lud16 text,
name text,
lastactive integer
lastactive integer,
subscribed integer
); """)
cur.execute("SELECT name FROM sqlite_master")
con.close()
@ -53,29 +55,29 @@ def add_sql_table_column(db):
try:
con = sqlite3.connect(db)
cur = con.cursor()
cur.execute(""" ALTER TABLE users ADD COLUMN lastactive 'integer' """)
cur.execute(""" ALTER TABLE users ADD COLUMN subscribed 'integer' """)
con.close()
except Error as e:
print(e)
def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive):
def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive)
cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?)", data)
data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed)
cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", data)
con.commit()
con.close()
except Error as e:
print("Error when Adding to DB: " + str(e))
def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive):
def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, npub)
data = (balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, subscribed, npub)
cur.execute(""" UPDATE users
SET sats = ? ,
@ -84,7 +86,8 @@ def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud
nip05 = ? ,
lud16 = ? ,
name = ? ,
lastactive = ?
lastactive = ?,
subscribed = ?
WHERE npub = ?""", data)
con.commit()
con.close()
@ -102,6 +105,12 @@ def get_from_sql_table(db, npub):
if row is None:
return None
else:
if len(row) < 9:
add_sql_table_column(db)
# Migrate
user = User
user.npub = row[0]
user.balance = row[1]
@ -111,6 +120,9 @@ def get_from_sql_table(db, npub):
user.lud16 = row[5]
user.name = row[6]
user.lastactive = row[7]
user.subscribed = row[8]
if user.subscribed is None:
user.subscribed = 0
return user
@ -162,28 +174,57 @@ def update_user_balance(db, npub, additional_sats, client, config):
if user is None:
name, nip05, lud16 = fetch_user_metadata(npub, client)
add_to_sql_table(db, npub, (int(additional_sats) + config.NEW_USER_BALANCE), False, False,
nip05, lud16, name, Timestamp.now().as_secs())
nip05, lud16, name, Timestamp.now().as_secs(), 0)
print("Adding User: " + npub + " (" + npub + ")")
else:
user = get_from_sql_table(db, npub)
new_balance = int(user.balance) + int(additional_sats)
update_sql_table(db, npub, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16,
user.name,
Timestamp.now().as_secs())
Timestamp.now().as_secs(), user.subscribed)
print("Updated user balance for: " + str(user.name) +
" Zap amount: " + str(additional_sats) + " Sats. New balance: " + str(new_balance) +" Sats")
" Zap amount: " + str(additional_sats) + " Sats. New balance: " + str(new_balance) + " Sats")
if config is not None:
keys = Keys.parse(config.PRIVATE_KEY)
#time.sleep(1.0)
# time.sleep(1.0)
message = ("Added " + str(additional_sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats.")
message = ("Added " + str(additional_sats) + " Sats to balance. New balance is " + str(
new_balance) + " Sats.")
evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.from_hex(npub), message,
None).to_event(keys)
None).to_event(keys)
send_event(evt, client=client, dvm_config=config)
def update_user_subscription(npub, subscribed_until, client, dvm_config):
user = get_from_sql_table(dvm_config.DB, npub)
if user is None:
name, nip05, lud16 = fetch_user_metadata(npub, client)
add_to_sql_table(dvm_config.DB, npub, dvm_config.NEW_USER_BALANCE, False, False,
nip05, lud16, name, Timestamp.now().as_secs(), 0)
print("Adding User: " + npub + " (" + npub + ")")
else:
user = get_from_sql_table(dvm_config.DB, npub)
update_sql_table(dvm_config.DB, npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05,
user.lud16,
user.name,
Timestamp.now().as_secs(), subscribed_until)
print("Updated user subscription for: " + str(user.name))
if dvm_config is not None:
keys = Keys.parse(dvm_config.PRIVATE_KEY)
# time.sleep(1.0)
message = ("Subscribed to DVM " + dvm_config.NIP89.NAME + " until: " + str(
Timestamp.from_secs(subscribed_until).to_human_datetime().replace("Z", " ").replace("T", " ")))
evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.from_hex(npub), message,
None).to_event(keys)
send_event(evt, client=client, dvm_config=dvm_config)
def get_or_add_user(db, npub, client, config, update=False):
user = get_from_sql_table(db, npub)
if user is None:
@ -191,7 +232,7 @@ def get_or_add_user(db, npub, client, config, update=False):
name, nip05, lud16 = fetch_user_metadata(npub, client)
print("Adding User: " + npub + " (" + npub + ")")
add_to_sql_table(db, npub, config.NEW_USER_BALANCE, False, False, nip05,
lud16, name, Timestamp.now().as_secs())
lud16, name, Timestamp.now().as_secs(), 0)
user = get_from_sql_table(db, npub)
return user
except Exception as e:
@ -201,7 +242,7 @@ def get_or_add_user(db, npub, client, config, update=False):
name, nip05, lud16 = fetch_user_metadata(npub, client)
print("Updating User: " + npub + " (" + npub + ")")
update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, nip05,
lud16, name, Timestamp.now().as_secs())
lud16, name, Timestamp.now().as_secs(), user.subscribed)
user = get_from_sql_table(db, npub)
return user
except Exception as e:
@ -214,9 +255,9 @@ def fetch_user_metadata(npub, client):
name = ""
nip05 = ""
lud16 = ""
pk = PublicKey.from_hex(npub)
pk = PublicKey.parse(npub)
print(f"\nGetting profile metadata for {pk.to_bech32()}...")
profile_filter = Filter().kind(0).author(pk).limit(1)
profile_filter = Filter().kind(Kind(0)).author(pk).limit(1)
events = client.get_events_of([profile_filter], timedelta(seconds=1))
if len(events) > 0:
latest_entry = events[0]

View File

@ -1,41 +1,49 @@
import os
from dataclasses import dataclass
from nostr_sdk import Event
from nostr_sdk import Event, Kind
class EventDefinitions:
KIND_DM = 4
KIND_ZAP = 9735
KIND_ANNOUNCEMENT = 31990
KIND_NIP94_METADATA = 1063
KIND_FEEDBACK = 7000
KIND_NIP90_EXTRACT_TEXT = 5000
KIND_NIP90_RESULT_EXTRACT_TEXT = KIND_NIP90_EXTRACT_TEXT + 1000
KIND_NIP90_SUMMARIZE_TEXT = 5001
KIND_NIP90_RESULT_SUMMARIZE_TEXT = KIND_NIP90_SUMMARIZE_TEXT + 1000
KIND_NIP90_TRANSLATE_TEXT = 5002
KIND_NIP90_RESULT_TRANSLATE_TEXT = KIND_NIP90_TRANSLATE_TEXT + 1000
KIND_NIP90_GENERATE_TEXT = 5050
KIND_NIP90_RESULT_GENERATE_TEXT = KIND_NIP90_GENERATE_TEXT + 1000
KIND_NIP90_GENERATE_IMAGE = 5100
KIND_NIP90_RESULT_GENERATE_IMAGE = KIND_NIP90_GENERATE_IMAGE + 1000
KIND_NIP90_CONVERT_VIDEO = 5200
KIND_NIP90_RESULT_CONVERT_VIDEO = KIND_NIP90_CONVERT_VIDEO + 1000
KIND_NIP90_GENERATE_VIDEO = 5202
KIND_NIP90_RESULT_GENERATE_VIDEO = KIND_NIP90_GENERATE_VIDEO + 1000
KIND_NIP90_TEXT_TO_SPEECH = 5250
KIND_NIP90_RESULT_TEXT_TO_SPEECH = KIND_NIP90_TEXT_TO_SPEECH + 1000
KIND_NIP90_CONTENT_DISCOVERY = 5300
KIND_NIP90_RESULT_CONTENT_DISCOVERY = KIND_NIP90_CONTENT_DISCOVERY + 1000
KIND_NIP90_PEOPLE_DISCOVERY = 5301
KIND_NIP90_RESULT_PEOPLE_DISCOVERY = KIND_NIP90_PEOPLE_DISCOVERY + 1000
KIND_NIP90_CONTENT_SEARCH = 5302
KIND_NIP90_RESULTS_CONTENT_SEARCH = KIND_NIP90_CONTENT_SEARCH + 1000
KIND_NIP90_USER_SEARCH = 5303
KIND_NIP90_RESULTS_USER_SEARCH = KIND_NIP90_USER_SEARCH + 1000
KIND_NIP90_GENERIC = 5999
KIND_NIP90_RESULT_GENERIC = KIND_NIP90_GENERIC + 1000
KIND_NOTE = Kind(1)
KIND_DM = Kind(4)
KIND_REACTION = Kind(7)
KIND_ZAP = Kind(9735)
KIND_ANNOUNCEMENT = Kind(31990)
KIND_NIP94_METADATA = Kind(1063)
KIND_FEEDBACK = Kind(7000)
KIND_NIP90_EXTRACT_TEXT = Kind(5000)
KIND_NIP90_RESULT_EXTRACT_TEXT = Kind(6000)
KIND_NIP90_SUMMARIZE_TEXT = Kind(5001)
KIND_NIP90_RESULT_SUMMARIZE_TEXT = Kind(6001)
KIND_NIP90_TRANSLATE_TEXT = Kind(5002)
KIND_NIP90_RESULT_TRANSLATE_TEXT = Kind(6002)
KIND_NIP90_GENERATE_TEXT = Kind(5050)
KIND_NIP90_RESULT_GENERATE_TEXT = Kind(6050)
KIND_NIP90_GENERATE_IMAGE = Kind(5100)
KIND_NIP90_RESULT_GENERATE_IMAGE = Kind(6100)
KIND_NIP90_CONVERT_VIDEO = Kind(5200)
KIND_NIP90_RESULT_CONVERT_VIDEO = Kind(6200)
KIND_NIP90_GENERATE_VIDEO = Kind(5202)
KIND_NIP90_RESULT_GENERATE_VIDEO =Kind(6202)
KIND_NIP90_TEXT_TO_SPEECH = Kind(5250)
KIND_NIP90_RESULT_TEXT_TO_SPEECH = Kind(5650)
KIND_NIP90_CONTENT_DISCOVERY = Kind(5300)
KIND_NIP90_RESULT_CONTENT_DISCOVERY = Kind(6300)
KIND_NIP90_PEOPLE_DISCOVERY = Kind(5301)
KIND_NIP90_RESULT_PEOPLE_DISCOVERY = Kind(6301)
KIND_NIP90_CONTENT_SEARCH = Kind(5302)
KIND_NIP90_RESULTS_CONTENT_SEARCH = Kind(6302)
KIND_NIP90_USER_SEARCH = Kind(5303)
KIND_NIP90_RESULTS_USER_SEARCH = Kind(6303)
KIND_NIP90_GENERIC = Kind(5999)
KIND_NIP90_RESULT_GENERIC = Kind(6999)
KIND_NIP88_SUBSCRIBE_EVENT = Kind(7001)
KIND_NIP88_STOP_SUBSCRIPTION_EVENT = Kind(7002)
KIND_NIP88_PAYMENT_RECIPE = Kind(7003)
KIND_NIP88_TIER_EVENT = Kind(37001)
ANY_RESULT = [KIND_NIP90_RESULT_EXTRACT_TEXT,
KIND_NIP90_RESULT_SUMMARIZE_TEXT,
KIND_NIP90_RESULT_TRANSLATE_TEXT,

View File

@ -2,6 +2,7 @@ import os
from nostr_sdk import Keys
from nostr_dvm.utils.nip88_utils import NIP88Config
from nostr_dvm.utils.nip89_utils import NIP89Config
from nostr_dvm.utils.nostr_utils import check_and_set_private_key
from nostr_dvm.utils.output_utils import PostProcessFunctionType
@ -32,12 +33,12 @@ class DVMConfig:
USE_OWN_VENV = True # Make an own venv for each dvm's process function.Disable if you want to install packages into main venv. Only recommended if you dont want to run dvms with different dependency versions
DB: str
NEW_USER_BALANCE: int = 0 # Free credits for new users
NIP88: NIP88Config
NIP89: NIP89Config
SEND_FEEDBACK_EVENTS = True
SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess
SCHEDULE_UPDATES_SECONDS = 0
def build_default_config(identifier):
dvm_config = DVMConfig()
dvm_config.PRIVATE_KEY = check_and_set_private_key(identifier)

View File

@ -0,0 +1,211 @@
import os
from datetime import timedelta
from hashlib import sha256
from pathlib import Path
import dotenv
from nostr_sdk import Filter, Tag, Keys, EventBuilder, Client, EventId, PublicKey, Event, Timestamp, SingleLetterTag, \
Alphabet
from nostr_sdk.nostr_sdk import Duration
from nostr_dvm.utils import definitions
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.nostr_utils import send_event
class NIP88Config:
DTAG: str = ""
TITLE: str = ""
CONTENT: str = ""
IMAGE: str = ""
TIER_EVENT: str = ""
PERK1DESC: str = ""
PERK2DESC: str = ""
PERK3DESC: str = ""
PERK4DESC: str = ""
PAYMENT_VERIFIER_PUBKEY: str = ""
AMOUNT_DAILY: int = None
AMOUNT_MONTHLY: int = None
AMOUNT_YEARLY: int = None
def nip88_create_d_tag(name, pubkey, image):
key_str = str(name + image + pubkey)
d_tag = sha256(key_str.encode('utf-8')).hexdigest()[:16]
return d_tag
def fetch_nip88_parameters_for_deletion(keys, eventid, client, dvmconfig):
idfilter = Filter().id(EventId.from_hex(eventid)).limit(1)
nip88events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT))
d_tag = ""
if len(nip88events) == 0:
print("Event not found. Potentially gone.")
for event in nip88events:
print(event.as_json())
for tag in event.tags():
if tag.as_vec()[0] == "d":
d_tag = tag.as_vec()[1]
if d_tag == "":
print("No dtag found")
return
if event.author().to_hex() == keys.public_key().to_hex():
nip88_delete_announcement(event.id().to_hex(), keys, d_tag, client, dvmconfig)
print("NIP88 announcement deleted from known relays!")
else:
print("Privatekey does not belong to event")
def fetch_nip88_event(keys, eventid, client, dvmconfig):
idfilter = Filter().id(EventId.parse(eventid)).limit(1)
nip88events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT))
d_tag = ""
if len(nip88events) == 0:
print("Event not found. Potentially gone.")
for event in nip88events:
for tag in event.tags():
if tag.as_vec()[0] == "d":
d_tag = tag.as_vec()[1]
if d_tag == "":
print("No dtag found")
return
if event.author().to_hex() == keys.public_key().to_hex():
print(event.as_json())
else:
print("Privatekey does not belong to event")
def nip88_delete_announcement(eid: str, keys: Keys, dtag: str, client: Client, config):
e_tag = Tag.parse(["e", eid])
a_tag = Tag.parse(
["a", str(EventDefinitions.KIND_NIP88_TIER_EVENT) + ":" + keys.public_key().to_hex() + ":" + dtag])
event = EventBuilder(5, "", [e_tag, a_tag]).to_event(keys)
send_event(event, client, config)
def nip88_has_active_subscription(user: PublicKey, tiereventdtag, client: Client, receiver_public_key_hex):
subscription_status = {
"isActive": False,
"validUntil": 0,
"subscriptionId": "",
"expires": False,
}
subscriptionfilter = Filter().kind(definitions.EventDefinitions.KIND_NIP88_PAYMENT_RECIPE).pubkey(
PublicKey.parse(receiver_public_key_hex)).custom_tag(SingleLetterTag.uppercase(Alphabet.P),
[user.to_hex()]).limit(1)
evts = client.get_events_of([subscriptionfilter], timedelta(seconds=5))
if len(evts) > 0:
print(evts[0].as_json())
matchesdtag = False
for tag in evts[0].tags():
if tag.as_vec()[0] == "valid":
subscription_status["validUntil"] = int(tag.as_vec()[2])
elif tag.as_vec()[0] == "e":
subscription_status["subscriptionId"] = tag.as_vec()[1]
elif tag.as_vec()[0] == "tier":
if tag.as_vec()[1] == tiereventdtag:
matchesdtag = True
if (subscription_status["validUntil"] > Timestamp.now().as_secs()) & matchesdtag:
subscription_status["isActive"] = True
if subscription_status["isActive"]:
# if subscription seems active, check if it has been canceled, and if so mark it as expiring.
cancel_filter = Filter().kind(EventDefinitions.KIND_NIP88_STOP_SUBSCRIPTION_EVENT).author(
user).pubkey(PublicKey.parse(receiver_public_key_hex)).event(
EventId.parse(subscription_status["subscriptionId"])).limit(1)
cancel_events = client.get_events_of([cancel_filter], timedelta(seconds=5))
if len(cancel_events) > 0:
if cancel_events[0].created_at().as_secs() > evts[0].created_at().as_secs():
subscription_status["expires"] = True
return subscription_status
def nip88_announce_tier(dvm_config, client):
title_tag = Tag.parse(["title", str(dvm_config.NIP88.TITLE)])
image_tag = Tag.parse(["image", str(dvm_config.NIP88.IMAGE)])
d_tag = Tag.parse(["d", dvm_config.NIP88.DTAG])
# may todo
zaptag1 = Tag.parse(["zap", dvm_config.PUBLIC_KEY, "wss://damus.io", "19"])
zaptag2 = Tag.parse(["zap", "", "wss://damus.io", "1"])
p_tag = Tag.parse(["p", dvm_config.NIP88.PAYMENT_VERIFIER_PUBKEY])
tags = [title_tag, image_tag, zaptag1, zaptag2, d_tag, p_tag]
if dvm_config.NIP88.AMOUNT_DAILY is not None:
amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_DAILY * 1000), "msats", "daily"])
tags.append(amount_tag)
if dvm_config.NIP88.AMOUNT_MONTHLY is not None:
amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_MONTHLY * 1000), "msats", "monthly"])
tags.append(amount_tag)
if dvm_config.NIP88.AMOUNT_YEARLY is not None:
amount_tag = Tag.parse(["amount", str(dvm_config.NIP88.AMOUNT_YEARLY * 1000), "msats", "yearly"])
tags.append(amount_tag)
if dvm_config.NIP88.PERK1DESC != "":
perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK1DESC)])
tags.append(perk_tag)
if dvm_config.NIP88.PERK2DESC != "":
perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK2DESC)])
tags.append(perk_tag)
if dvm_config.NIP88.PERK3DESC != "":
perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK3DESC)])
tags.append(perk_tag)
if dvm_config.NIP88.PERK4DESC != "":
perk_tag = Tag.parse(["perk", str(dvm_config.NIP88.PERK4DESC)])
tags.append(perk_tag)
keys = Keys.parse(dvm_config.NIP89.PK)
content = dvm_config.NIP88.CONTENT
event = EventBuilder(EventDefinitions.KIND_NIP88_TIER_EVENT, content, tags).to_event(keys)
annotier_id = send_event(event, client=client, dvm_config=dvm_config)
print("Announced NIP 88 Tier for " + dvm_config.NIP89.NAME)
return annotier_id
# Relay and payment-verification
# ["r", "wss://my-subscribers-only-relay.com"],
# ["p", "<payment-verifier-pubkey>"],
def check_and_set_d_tag_nip88(identifier, name, pk, imageurl):
if not os.getenv("NIP88_DTAG_" + identifier.upper()):
new_dtag = nip88_create_d_tag(name, Keys.parse(pk).public_key().to_hex(),
imageurl)
nip88_add_dtag_to_env_file("NIP88_DTAG_" + identifier.upper(), new_dtag)
print("Some new dtag:" + new_dtag)
return new_dtag
else:
return os.getenv("NIP88_DTAG_" + identifier.upper())
def check_and_set_tiereventid_nip88(identifier, index="1", eventid=None):
if eventid is None:
if not os.getenv("NIP88_TIEREVENT_" + index + identifier.upper()):
print("No Tier Event ID set")
return None
else:
return os.getenv("NIP88_TIEREVENT_" + index + identifier.upper())
else:
nip88_add_dtag_to_env_file("NIP88_TIEREVENT_" + index + identifier.upper(), eventid)
return eventid
def nip88_add_dtag_to_env_file(dtag, oskey):
env_path = Path('.env')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
dotenv.set_key(env_path, dtag, oskey)

View File

@ -25,7 +25,7 @@ def nip89_create_d_tag(name, pubkey, image):
def nip89_announce_tasks(dvm_config, client):
k_tag = Tag.parse(["k", str(dvm_config.NIP89.KIND)])
k_tag = Tag.parse(["k", str(dvm_config.NIP89.KIND.as_u64())])
d_tag = Tag.parse(["d", dvm_config.NIP89.DTAG])
keys = Keys.parse(dvm_config.NIP89.PK)
content = dvm_config.NIP89.CONTENT
@ -34,7 +34,7 @@ def nip89_announce_tasks(dvm_config, client):
print("Announced NIP 89 for " + dvm_config.NIP89.NAME)
def fetch_nip89_paramters_for_deletion(keys, eventid, client, dvmconfig):
def fetch_nip89_parameters_for_deletion(keys, eventid, client, dvmconfig):
idfilter = Filter().id(EventId.from_hex(eventid)).limit(1)
nip89events = client.get_events_of([idfilter], timedelta(seconds=dvmconfig.RELAY_TIMEOUT))
d_tag = ""

View File

@ -1,44 +1,84 @@
import json
import os
from datetime import timedelta
import requests
from nostr_sdk import Keys, PublicKey, Client, nip04_encrypt, EventBuilder, Tag, NostrSigner
from nostr_sdk import Keys, PublicKey, Client, nip04_encrypt, EventBuilder, Tag, NostrSigner, Filter, Timestamp, \
NostrWalletConnectUri, Nwc
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import check_and_set_private_key
from nostr_dvm.utils.zap_utils import zaprequest
def nwc_zap(connectionstr, bolt11, keys):
target_pubkey, relay, secret = parse_connection_str(connectionstr)
SecretSK = Keys.parse(secret)
def nwc_zap(connectionstr, bolt11, keys, externalrelay=None):
uri = NostrWalletConnectUri.parse(connectionstr)
content = {
"method": "pay_invoice",
"params": {
"invoice": bolt11
}
}
# Initialize NWC client
nwc = Nwc(uri)
signer = NostrSigner.keys(keys)
client = Client(signer)
client.add_relay(relay)
client.connect()
info = nwc.get_info()
print(info)
client_public_key = PublicKey.from_hex(target_pubkey)
encrypted_content = nip04_encrypt(SecretSK.secret_key(), client_public_key, json.dumps(content))
balance = nwc.get_balance()
print(f"Balance: {balance} SAT")
pTag = Tag.parse(["p", client_public_key.to_hex()])
event = EventBuilder(23194, encrypted_content,
[pTag]).to_event(keys)
event_id = nwc.pay_invoice(bolt11)
print("NWC event: " + event_id)
event_id = client.send_event(event)
print(event_id.to_hex())
#target_pubkey, relay, secret = parse_connection_str(connectionstr)
#print(target_pubkey)
#print(relay)
#print(secret)
#SecretSK = Keys.parse(secret)
#content = {
# "method": "pay_invoice",
# "params": {
# "invoice": bolt11
# }
#}
#signer = NostrSigner.keys(keys)
#client = Client(signer)
#client.add_relay(relay)
#if externalrelay is not None:
# client.add_relay(externalrelay)
#client.connect()
#client_public_key = PublicKey.from_hex(target_pubkey)
#encrypted_content = nip04_encrypt(SecretSK.secret_key(), client_public_key, json.dumps(content))
#pTag = Tag.parse(["p", client_public_key.to_hex()])
#event = EventBuilder(23194, encrypted_content,
# [pTag]).to_event(keys)
#ts = Timestamp.now()
#event_id = client.send_event(event)
#nwc_response_filter = Filter().kind(23195).since(ts)
#events = client.get_events_of([nwc_response_filter], timedelta(seconds=5))
#if len(events) > 0:
# for evt in events:
# print(evt.as_json())
#else:
# print("No response found")
return event_id
def parse_connection_str(connectionstring):
split = connectionstring.split("?")
targetpubkey = split[0].split(":")[1]
targetpubkey = split[0].split(":")[1].replace("//", "")
split2 = split[1].split("&")
relay = split2[0].split("=")[1]
relay = relay.replace("%3A%2F%2F", "://")

View File

@ -206,6 +206,12 @@ def build_status_reaction(status, task, amount, content, dvm_config):
amount) + " Sats. "
reaction = alt_description + emoji.emojize(":orange_heart:")
elif status == "subscription-required":
alt_description = "NIP90 DVM AI task " + task + " requires payment for subscription"
reaction = alt_description + emoji.emojize(":orange_heart:")
elif status == "payment-rejected":
alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str(
amount) + " Sats. "

View File

@ -0,0 +1,181 @@
import sqlite3
from dataclasses import dataclass
from sqlite3 import Error
@dataclass
class Subscription:
id: str
recipent: str
subscriber: str
nwc: str
cadence: str
amount: int
begin: int
end: int
tier_dtag: str
zaps: str
recipe: str
active: bool
lastupdate: int
def create_subscription_sql_table(db):
try:
import os
if not os.path.exists(r'db'):
os.makedirs(r'db')
if not os.path.exists(r'outputs'):
os.makedirs(r'outputs')
con = sqlite3.connect(db)
cur = con.cursor()
cur.execute(""" CREATE TABLE IF NOT EXISTS subscriptions (
id text PRIMARY KEY,
recipient text,
subscriber text,
nwc text NOT NULL,
cadence text,
amount int,
begin int,
end int,
tier_dtag text,
zaps text,
recipe text,
active boolean,
lastupdate int
); """)
cur.execute("SELECT name FROM sqlite_master")
con.close()
except Error as e:
print(e)
def add_to_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps,
recipe, active, lastupdate):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, lastupdate)
print(id)
print(recipient)
print(subscriber)
print(nwc)
cur.execute("INSERT or IGNORE INTO subscriptions VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", data)
con.commit()
con.close()
except Error as e:
print("Error when Adding to DB: " + str(e))
def get_from_subscription_sql_table(db, id):
try:
con = sqlite3.connect(db)
cur = con.cursor()
cur.execute("SELECT * FROM subscriptions WHERE id=?", (id,))
row = cur.fetchone()
con.close()
if row is None:
return None
else:
subscription = Subscription
subscription.id = row[0]
subscription.recipent = row[1]
subscription.subscriber = row[2]
subscription.nwc = row[3]
subscription.cadence = row[4]
subscription.amount = row[5]
subscription.begin = row[6]
subscription.end = row[7]
subscription.tier_dtag = row[8]
subscription.zaps = row[9]
subscription.recipe = row[10]
subscription.active = row[11]
subscription.lastupdate = row[12]
return subscription
except Error as e:
print("Error Getting from DB: " + str(e))
return None
def get_all_subscriptions_from_sql_table(db):
try:
con = sqlite3.connect(db)
cursor = con.cursor()
sqlite_select_query = """SELECT * from subscriptions"""
cursor.execute(sqlite_select_query)
records = cursor.fetchall()
subscriptions = []
for row in records:
subscription = Subscription
subscription.id = row[0]
subscription.recipent = row[1]
subscription.subscriber = row[2]
subscription.nwc = row[3]
subscription.cadence = row[4]
subscription.amount = row[5]
subscription.begin = row[6]
subscription.end = row[7]
subscription.tier_dtag = row[8]
subscription.zaps = row[9]
subscription.recipe = row[10]
subscription.active = row[11]
subscription.lastupdate = row[12]
subscriptions.append(subscription)
cursor.close()
return subscriptions
except sqlite3.Error as error:
print("Failed to read data from sqlite table", error)
finally:
if con:
con.close()
#print("The SQLite connection is closed")
def delete_from_subscription_sql_table(db, id):
try:
con = sqlite3.connect(db)
cur = con.cursor()
cur.execute("DELETE FROM subscriptions WHERE id=?", (id,))
con.commit()
con.close()
except Error as e:
print(e)
def update_subscription_sql_table(db, id, recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps,
recipe, active, lastupdate):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (recipient, subscriber, nwc, cadence, amount, begin, end, tier_dtag, zaps, recipe, active, lastupdate, id)
cur.execute(""" UPDATE subscriptions
SET recipient = ? ,
subscriber = ? ,
nwc = ? ,
cadence = ? ,
amount = ? ,
begin = ? ,
end = ?,
tier_dtag = ?,
zaps = ?,
recipe = ?,
active = ?,
lastupdate = ?
WHERE id = ?""", data)
con.commit()
con.close()
except Error as e:
print("Error Updating DB: " + str(e))

View File

@ -8,7 +8,7 @@ import requests
from Crypto.Cipher import AES
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, generate_shared_key
from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event, EventBuilder, Tag, Keys, generate_shared_key, Kind
from nostr_dvm.utils.nostr_utils import get_event_by_id, check_and_decrypt_own_tags
import lnurl
@ -50,7 +50,7 @@ def parse_zap_event_tags(zap_event, keys, name, client, config):
keys.secret_key(),
zap_request_event.author())
decrypted_private_event = Event.from_json(decrypted_content)
if decrypted_private_event.kind() == 9733:
if decrypted_private_event.kind().as_u64() == 9733:
sender = decrypted_private_event.author().to_hex()
message = decrypted_private_event.content()
# if message != "":
@ -132,7 +132,7 @@ def create_bolt11_lud16(lud16, amount):
def create_lnbits_account(name):
if os.getenv("LNBITS_ADMIN_ID") is None or os.getenv("LNBITS_ADMIN_ID") == "":
print("No admin id set, no wallet created.")
return "","","","", "failed"
return "", "", "", "", "failed"
data = {
'admin_id': os.getenv("LNBITS_ADMIN_ID"),
'wallet_name': name,
@ -240,6 +240,10 @@ def decrypt_private_zap_message(msg: str, privkey: SecretKey, pubkey: PublicKey)
def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys, relay_list, zaptype="public"):
print(lud16)
print(str(amount))
print(content)
print(zapped_user.to_hex())
if lud16.startswith("LNURL") or lud16.startswith("lnurl"):
url = lnurl.decode(lud16)
elif '@' in lud16: # LNaddress
@ -250,6 +254,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys
response = requests.get(url)
ob = json.loads(response.content)
callback = ob["callback"]
print(ob["callback"])
encoded_lnurl = lnurl.encode(url)
amount_tag = Tag.parse(['amount', str(amount * 1000)])
relays_tag = Tag.parse(['relays', str(relay_list)])
@ -262,12 +267,11 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys
p_tag = Tag.parse(['p', zapped_user.to_hex()])
tags = [amount_tag, relays_tag, p_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,
zap_request = EventBuilder(Kind(9733), content,
[p_tag, e_tag]).to_event(keys).as_json()
keys = Keys.parse(encryption_key)
encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_event.author())
@ -275,7 +279,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys
tags.append(anon_tag)
content = ""
zap_request = EventBuilder(9734, content,
zap_request = EventBuilder(Kind(9734), content,
tags).to_event(keys).as_json()
response = requests.get(callback + "?amount=" + str(int(amount) * 1000) + "&nostr=" + urllib.parse.quote_plus(
@ -287,6 +291,7 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys
print("ZAP REQUEST: " + e)
return None
def get_price_per_sat(currency):
import requests
@ -334,8 +339,6 @@ def make_ln_address_nostdress(identifier, npub, pin, nostdressdomain):
return "", ""
def check_and_set_ln_bits_keys(identifier, npub):
if not os.getenv("LNBITS_INVOICE_KEY_" + identifier.upper()):
invoicekey, adminkey, walletid, userid, success = create_lnbits_account(identifier)
@ -365,4 +368,4 @@ def add_key_to_env_file(value, oskey):
env_path = Path('.env')
if env_path.is_file():
dotenv.load_dotenv(env_path, verbose=True, override=True)
dotenv.set_key(env_path, value, oskey)
dotenv.set_key(env_path, value, oskey)

View File

@ -1,6 +1,6 @@
from setuptools import setup, find_packages
VERSION = '0.2.6'
VERSION = '0.3.0'
DESCRIPTION = 'A framework to build and run Nostr NIP90 Data Vending Machines'
LONG_DESCRIPTION = ('A framework to build and run Nostr NIP90 Data Vending Machines. '
'This is an early stage release. Interfaces might change/brick')
@ -15,7 +15,7 @@ setup(
long_description=LONG_DESCRIPTION,
packages=find_packages(include=['nostr_dvm', 'nostr_dvm.*']),
install_requires=["nostr-sdk==0.9.1",
install_requires=["nostr-sdk=>0.10.0",
"bech32",
"pycryptodome==3.20.0",
"python-dotenv==1.0.0",

View File

@ -1,12 +1,17 @@
import os
import threading
from pathlib import Path
import dotenv
from nostr_sdk import Keys
from nostr_dvm.subscription import Subscription
from nostr_dvm.tasks import content_discovery_currently_popular
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.backend_utils import keep_alive
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import check_and_set_private_key
from nostr_dvm.utils.zap_utils import check_and_set_ln_bits_keys
def playground():
@ -19,10 +24,24 @@ def playground():
admin_config.REBROADCAST_NIP89 = False
admin_config.UPDATE_PROFILE = False
discovery_test = content_discovery_currently_popular.build_example("Currently Popular Notes DVM", "discovery_content_test", admin_config)
discovery_test.run()
discovery_test_sub = content_discovery_currently_popular.build_example_subscription("Currently Popular Notes DVM (with Subscriptions)", "discovery_content_test", admin_config)
discovery_test_sub.run()
keep_alive()
#discovery_test = content_discovery_currently_popular.build_example("Currently Popular Notes DVM",
# "discovery_content_test", admin_config)
#discovery_test.run()
subscription_config = DVMConfig()
subscription_config.PRIVATE_KEY = check_and_set_private_key("dvm_subscription")
npub = Keys.parse(subscription_config.PRIVATE_KEY).public_key().to_bech32()
invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys("dvm_subscription", npub)
subscription_config.LNBITS_INVOICE_KEY = invoice_key
subscription_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back
subscription_config.LNBITS_URL = os.getenv("LNBITS_HOST")
x = threading.Thread(target=Subscription, args=(Subscription(subscription_config),))
x.start()
#keep_alive()
if __name__ == '__main__':

View File

@ -757,7 +757,7 @@ export default {
if(!jsonentry.amount){
jsonentry.amount = ""
}
if(jsonentry.amount === "subscription"){
if(jsonentry.subscription === true){
// if(susbcrition_tier) {
const filter = new Filter().kind(37001).author(entry.author)
let tiers = await client.getEventsOf([filter], Duration.fromSecs(5))
@ -817,11 +817,13 @@ export default {
}
console.log("hello")
let subscription_status = await hasActiveSubscription(store.state.pubkey.toHex(), nip88.d, evt.author.toHex(), nip88.amounts)
nip88.hasActiveSubscription = subscription_status.isActive
nip88.subscribedUntil = subscription_status.validUntil
nip88.subscriptionId = subscription_status.subscriptionId
nip88.expires = subscription_status.expires
console.log(subscription_status)
jsonentry.nip88 = nip88

View File

@ -90,8 +90,8 @@
<div style="margin-left: auto; margin-right: 10px;">
<p v-if="dvm.amount.toString().toLowerCase()==='free'" class="badge bg-nostr">Free</p>
<p v-if="dvm.amount.toString().toLowerCase()==='flexible'" class="badge bg-nostr2" >Flexible</p>
<p v-if="dvm.subscription" class="badge text-white bg-gradient-to-br from-pink-500 to-orange-400">Subscription</p>
<p v-if="dvm.amount.toString().toLowerCase()==='subscription'" class="badge bg-orange-500">Subscription</p>
<p v-if="dvm.amount.toString()===''" ></p>
<p v-if="!isNaN(parseInt(dvm.amount))" class="text-sm text-gray-600 rounded" ><div class="flex"><svg style="margin-top:3px" xmlns="http://www.w3.org/2000/svg" width="14" height="16" fill="currentColor" class="bi bi-lightning" viewBox="0 0 16 20">
<path d="M5.52.359A.5.5 0 0 1 6 0h4a.5.5 0 0 1 .474.658L8.694 6H12.5a.5.5 0 0 1 .395.807l-7 9a.5.5 0 0 1-.873-.454L6.823 9.5H3.5a.5.5 0 0 1-.48-.641zM6.374 1 4.168 8.5H7.5a.5.5 0 0 1 .478.647L6.78 13.04 11.478 7H8a.5.5 0 0 1-.474-.658L9.306 1z"/></svg> {{dvm.amount/1000}}</div></p>

File diff suppressed because one or more lines are too long

View File

@ -1,574 +0,0 @@
<script setup>
import {
Client,
Filter,
Timestamp,
Event,
Metadata,
PublicKey,
EventBuilder,
Tag,
EventId,
Nip19Event, Alphabet, Keys, nip04_decrypt, SecretKey, Duration
} from "@rust-nostr/nostr-sdk";
import store from '../store';
import miniToastr from "mini-toastr";
import VueNotifications from "vue-notifications";
import {computed, watch} from "vue";
import deadnip89s from "@/components/data/deadnip89s.json";
import {data} from "autoprefixer";
import {requestProvider} from "webln";
import Newnote from "@/components/Newnote.vue";
import SummarizationGeneration from "@/components/SummarizationGeneration.vue"
import {post_note, schedule, copyurl, copyinvoice, sleep, getEvents, get_user_infos, nextInput, createBolt11Lud16, getEventsOriginalOrder} from "../components/helper/Helper.vue"
import amberSignerService from "./android-signer/AndroidSigner";
import StringUtil from "@/components/helper/string.ts";
let dvms =[]
async function generate_feed() {
try {
if(store.state.pubkey === undefined || localStorage.getItem('nostr-key-method') === "anon"){
miniToastr.showMessage("Some algorithms may need your profile to give personalized recommendations. Sign-in for a better experience.", "Not signed in.", VueNotifications.types.warn)
}
dvms = []
store.commit('set_recommendation_dvms', dvms)
let client = store.state.client
let content = "NIP 90 Content Discovery request"
let kind = 5300
let tags = []
let res;
let requestid;
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: content,
kind: kind,
pubkey: store.state.pubkey.toHex(),
tags: tags,
createdAt: Date.now()
};
res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = res.id;
}
else {
let tags_t = []
for (let tag of tags){
tags_t.push(Tag.parse(tag))
}
let evt = new EventBuilder(kind, content, tags_t)
res = await client.sendEventBuilder(evt);
requestid = res.toHex();
}
store.commit('set_current_request_id_recommendation', requestid)
if (!store.state.recommendationehasEventListener){
store.commit('set_recommendationEventListener', true)
listen()
}
else{
console.log("Already has event listener")
}
} catch (error) {
console.log(error);
}
}
async function listen() {
let client = store.state.client
let pubkey = store.state.pubkey
const filter = new Filter().kinds([7000, 6300]).pubkey(pubkey).since(Timestamp.now());
await client.subscribe([filter]);
const handle = {
// Handle event
handleEvent: async (relayUrl, event) => {
/* if (store.state.recommendationehasEventListener === false){
return true
}*/
//const dvmname = getNamefromId(event.author.toHex())
console.log("Received new event from", relayUrl);
//console.log(event.asJson())
let resonsetorequest = false
sleep(1200).then(async () => {
for (let tag in event.tags) {
if (event.tags[tag].asVec()[0] === "e") {
//console.log(event.tags[tag].asVec()[1])
//console.log(test)
if (event.tags[tag].asVec()[1] === store.state.requestidRecommendation) {
resonsetorequest = true
}
}
}
if (resonsetorequest === true) {
if (event.kind === 7000) {
try {
console.log("7000: ", event.content);
// console.log("DVM: " + event.author.toHex())
//miniToastr.showMessage("DVM: " + dvmname, event.content, VueNotifications.types.info)
await addDVM(event)
} catch (error) {
console.log("Error: ", error);
}
}
else if (event.kind === 6300) {
let entries = []
//console.log("6300:", event.content);
let event_etags = JSON.parse(event.content)
if (event_etags.length > 0) {
for (let etag of event_etags) {
const eventid = EventId.fromHex(etag[1]).toHex()
entries.push(eventid)
}
const events = await getEventsOriginalOrder(entries)
let authors = []
for (const evt of events) {
authors.push(evt.author)
}
if (authors.length > 0) {
let profiles = await get_user_infos(authors)
let items = []
let index = 0
for (const evt of events) {
let p = profiles.find(record => record.author === evt.author.toHex())
let bech32id = evt.id.toBech32()
let nip19 = new Nip19Event(evt.id, evt.author, store.state.relays)
let nip19bech32 = nip19.toBech32()
let picture = p === undefined ? "../assets/nostr-purple.svg" : p["profile"]["picture"]
let name = p === undefined ? bech32id : p["profile"]["name"]
let highlighterurl = "https://highlighter.com/e/" + nip19bech32
let njumpurl = "https://njump.me/" + nip19bech32
let nostrudelurl = "https://nostrudel.ninja/#/n/" + bech32id
let uri = "nostr:" + bech32id // nip19.toNostrUri()
if (items.find(e => e.id.toHex() === evt.id.toHex()) === undefined) {
items.push({
id: evt.id,
content: evt.content,
author: name,
authorurl: "https://njump.me/" + evt.author.toBech32(),
links: {
"uri": uri,
"highlighter": highlighterurl,
"njump": njumpurl,
"nostrudel": nostrudelurl
},
avatar: picture,
index: index,
indicator: {"time": evt.createdAt.toHumanDatetime(), "index": index}
})
index = index+1
}
}
if (dvms.find(i => i.id === event.author.toHex()) === undefined){
await addDVM(event)
console.log("add dvm because of bug")
}
dvms.find(i => i.id === event.author.toHex()).result.length = 0
dvms.find(i => i.id === event.author.toHex()).result.push.apply(dvms.find(i => i.id === event.author.toHex()).result, items)
dvms.find(i => i.id === event.author.toHex()).status = "finished"
}
}
store.commit('set_recommendation_dvms', dvms)
}
}
})
},
// Handle relay message
handleMsg: async (relayUrl, message) => {
//console.log("Received message from", relayUrl, message.asJson());
}
};
client.handleNotifications(handle);
}
const urlinput = ref("");
async function addDVM(event){
let status = "unknown"
let jsonentry = {
id: event.author.toHex(),
kind: "",
status: status,
result: [],
name: event.author.toBech32(),
about: "",
image: "",
amount: 0,
bolt11: ""
}
for (const tag in event.tags) {
if (event.tags[tag].asVec()[0] === "status") {
status = event.tags[tag].asVec()[1]
}
if (event.tags[tag].asVec()[0] === "amount") {
jsonentry.amount = event.tags[tag].asVec()[1]
if (event.tags[tag].asVec().length > 2) {
jsonentry.bolt11 = event.tags[tag].asVec()[2]
}
else{
let profiles = await get_user_infos([event.author])
let created = 0
if (profiles.length > 0){
// for (const profile of profiles){
console.log(profiles[0].profile)
let current = profiles[0]
// if (profiles[0].profile.createdAt > created){
// created = profile.profile.createdAt
// current = profile
// }
let lud16 = current.profile.lud16
if (lud16 !== null && lud16 !== ""){
console.log("LUD16: " + lud16)
jsonentry.bolt11 = await createBolt11Lud16(lud16, jsonentry.amount)
console.log(jsonentry.bolt11)
if(jsonentry.bolt11 === ""){
status = "error"
}
}
else {
console.log("NO LNURL")
}
}
else {
console.log("PROFILE NOT FOUND")
}
}
}
}
//let dvm = store.state.nip89dvms.find(x => JSON.parse(x.event).pubkey === event.author.toHex())
for (const el of store.state.nip89dvms) {
if (JSON.parse(el.event).pubkey === event.author.toHex().toString()) {
jsonentry.name = el.name
jsonentry.about = el.about
jsonentry.image = el.image
console.log(jsonentry)
}
}
if (event.content !== "" && status !== "payment-required" && status !== "error" && status !== "finished" && status !== "paid"){
status = event.content
}
jsonentry.status = status
console.log(dvms)
if (dvms.filter(i => i.id === jsonentry.id).length === 0) {
dvms.push(jsonentry)
}
//dvms.find(i => i.id === jsonentry.id).status = status
store.commit('set_recommendation_dvms', dvms)
}
async function zap_local(invoice) {
let success = await zap(invoice)
if (success){
dvms.find(i => i.bolt11 === invoice).status = "paid"
store.commit('set_recommendation_results', dvms)
}
}
defineProps({
msg: {
type: String,
required: false
},
})
import { ref } from "vue";
import ModalComponent from "../components/Newnote.vue";
import VueDatePicker from "@vuepic/vue-datepicker";
import {timestamp} from "@vueuse/core";
import NoteTable from "@/components/NoteTable.vue";
import zap from "@/components/helper/Zap.vue";
const isModalOpened = ref(false);
const modalcontent = ref("");
const datetopost = ref(Date.now());
const openModal = result => {
datetopost.value = Date.now();
isModalOpened.value = true;
//let resevents = ""
//for (let evt of result){
// resevents = resevents + "nostr:" + (evt.id.toBech32()) + "\n"
//}
modalcontent.value = result
};
const closeModal = () => {
isModalOpened.value = false;
};
const submitHandler = async () => {
}
</script>
<!-- font-thin bg-gradient-to-r from-white to-nostr bg-clip-text text-transparent -->
<template>
<div class="greetings">
<br>
<br>
<h1 class="text-7xl font-black tracking-wide">Noogle</h1>
<h1 class="text-7xl font-black tracking-wide">Content</h1>
<h1 class="text-7xl font-black tracking-wide">Discovery</h1>
<h2 class="text-base-200-content text-center tracking-wide text-2xl font-thin ">
Algorithms, but you are the one in control.</h2>
<h3>
<br>
<button class="v-Button" @click="generate_feed()">Recommend me Notes</button>
</h3>
</div>
<br>
<ModalComponent :isOpen="isModalOpened" @modal-close="closeModal" @submit="submitHandler" name="first-modal">
<template #header>Summarize Results <br></template>
<template #content>
<SummarizationGeneration :events="modalcontent"></SummarizationGeneration>
</template>
<template #footer>
<!-- <div>
<VueDatePicker :min-date="new Date()" :teleport="false" :dark="true" position="right" className="bg-base-200 inline-flex flex-none" style="width: 220px;" v-model="datetopost"></VueDatePicker>
<button className="v-Button" @click="schedule(modalcontent, datetopost)" @click.stop="closeModal"><img width="25px" style="margin-right: 5px" src="../../public/shipyard.ico"/>Schedule Note with Shipyard DVM</button>
<br>
or
<br>
<button className="v-Button" style="margin-bottom: 0px" @click="post_note(modalcontent)" @click.stop="closeModal"><img width="25px" style="margin-right: 5px;" src="../../public/favicon.ico"/>Post Note now</button>
</div> -->
</template>
</ModalComponent>
<div class=" relative space-y-3">
<div class="grid grid-cols-1 gap-6">
<div className="card w-70 bg-base-100 shadow-xl flex flex-col" v-for="dvm in store.state.recommendationdvms"
:key="dvm.id">
<div className="card-body">
<div className="playeauthor-wrapper">
<figure className="w-20">
<img className="avatar" v-if="dvm.image" :src="dvm.image" alt="DVM Picture" />
<img class="avatar" v-else src="@/assets/nostr-purple.svg" />
</figure>
<h2 className="card-title">{{ dvm.name }}</h2>
</div>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
<div className="card-actions justify-end mt-auto" >
<div className="tooltip mt-auto">
<button v-if="dvm.status !== 'finished' && dvm.status !== 'paid' && dvm.status !== 'payment-required' && dvm.status !== 'error'" className="btn">{{dvm.status}}</button>
<button v-if="dvm.status === 'finished'" className="btn">Done</button>
<button v-if="dvm.status === 'paid'" className="btn">Paid, waiting for DVM..</button>
<button v-if="dvm.status === 'error'" className="btn">Error</button>
<button v-if="dvm.status === 'payment-required'" className="zap-Button" @click="zap_local(dvm.bolt11);">{{ dvm.amount/1000 }} Sats</button>
</div>
</div>
<!-- <div v-if="dvm.result.length > 0" class="collapse bg-base-200">
<input type="checkbox" class="peer" />
<div class="collapse-title bg-primary text-primary-content peer-checked:bg-secondary peer-checked:text-secondary-content">
Click me to show/hide content
</div>
<div class="collapse-content bg-primary text-primary-content peer-checked:bg-base-200 peer-checked:text-accent">
</div>
</div> -->
<!-- <details open ></details> -->
<details v-if="dvm.status === 'finished'" class="collapse bg-base">
<summary class="collapse-title "><div class="btn">Show/Hide Results</div></summary>
<div class="collapse-content font-size-0" className="z-10" id="collapse">
<NoteTable :data="dvm.result" ></NoteTable>
</div>
</details>
<div data-tip="Make Summarization" v-if="dvm.result.length > 0 && store.state.pubkey.toHex() !== Keys.parse(store.state.nooglekey).publicKey.toHex()" >
<button @click="openModal(dvm.result)" class="w-8 h-8 rounded-full bg-nostr border-white border-1 text-white flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black tooltip" data-top='Share' aria-label="make note" role="button">
<svg class="w-4 h-4 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M9 19V.352A3.451 3.451 0 0 0 7.5 0a3.5 3.5 0 0 0-3.261 2.238A3.5 3.5 0 0 0 2.04 6.015a3.518 3.518 0 0 0-.766 1.128c-.042.1-.064.209-.1.313a3.34 3.34 0 0 0-.106.344 3.463 3.463 0 0 0 .02 1.468A4.016 4.016 0 0 0 .3 10.5l-.015.036a3.861 3.861 0 0 0-.216.779A3.968 3.968 0 0 0 0 12a4.032 4.032 0 0 0 .107.889 4 4 0 0 0 .2.659c.006.014.015.027.021.041a3.85 3.85 0 0 0 .417.727c.105.146.219.284.342.415.072.076.148.146.225.216.1.091.205.179.315.26.11.081.2.14.308.2.02.013.039.028.059.04v.053a3.506 3.506 0 0 0 3.03 3.469 3.426 3.426 0 0 0 4.154.577A.972.972 0 0 1 9 19Zm10.934-7.68a3.956 3.956 0 0 0-.215-.779l-.017-.038a4.016 4.016 0 0 0-.79-1.235 3.417 3.417 0 0 0 .017-1.468 3.387 3.387 0 0 0-.1-.333c-.034-.108-.057-.22-.1-.324a3.517 3.517 0 0 0-.766-1.128 3.5 3.5 0 0 0-2.202-3.777A3.5 3.5 0 0 0 12.5 0a3.451 3.451 0 0 0-1.5.352V19a.972.972 0 0 1-.184.546 3.426 3.426 0 0 0 4.154-.577A3.506 3.506 0 0 0 18 15.5v-.049c.02-.012.039-.027.059-.04.106-.064.208-.13.308-.2s.214-.169.315-.26c.077-.07.153-.14.225-.216a4.007 4.007 0 0 0 .459-.588c.115-.176.215-.361.3-.554.006-.014.015-.027.021-.041.087-.213.156-.434.205-.659.013-.057.024-.115.035-.173.046-.237.07-.478.073-.72a3.948 3.948 0 0 0-.066-.68Z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.zap-Button{
@apply btn hover:bg-amber-400 border-amber-400 text-base;
bottom: 0;
}
.v-Button {
@apply bg-nostr hover:bg-nostr2 focus:ring-white mb-2 inline-flex flex-none items-center rounded-lg border border-black px-3 py-1.5 text-sm leading-4 text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
height: 48px;
margin: 5px;
}
.c-Input {
@apply bg-base-200 text-accent dark:bg-black dark:text-white focus:ring-white mb-2 inline-flex flex-none items-center rounded-lg border border-transparent px-3 py-1.5 text-sm leading-4 text-accent-content transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
width: 350px;
height: 48px;
}
.d-Input {
@apply bg-black hover:bg-gray-900 focus:ring-white mb-2 inline-flex flex-none items-center rounded-lg border border-transparent px-3 py-1.5 text-sm leading-4 text-white transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
width: 300px;
color: white;
background: black;
}
.playeauthor-wrapper {
padding: 6px;
display: flex;
align-items: center;
justify-items: center;
}
.logo {
display: flex;
width:100%;
height:125px;
justify-content: center;
align-items: center;
}
h3 {
font-size: 1.0rem;
text-align: left;
}
.avatar {
margin-right: 10px;
margin-left: 0px;
display: inline-block;
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
box-shadow: inset 0 4px 4px 0 rgb(0 0 0 / 10%);
}
.greetings h1,
.greetings h3 {
text-align: left;
}
.center {
text-align: center;
justify-content: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: center;
}
}
</style>

View File

@ -382,7 +382,7 @@ export async function hasActiveSubscription(pubkeystring, tiereventdtag, tieraut
let subscriptionfilter = new Filter().kind(7003).pubkey(PublicKey.parse(tierauthorid)).customTag(SingleLetterTag.uppercase(Alphabet.P), [pubkeystring]).limit(1)
let evts = await client.getEventsOf([subscriptionfilter], Duration.fromSecs(5))
console.log(evts)
if (evts.length > 0){
console.log(evts[0].asJson())
let matchesdtag = false
@ -419,7 +419,7 @@ export async function hasActiveSubscription(pubkeystring, tiereventdtag, tieraut
return subscriptionstatus
}
return subscriptionstatus
}