This commit is contained in:
greenart7c3
2024-04-29 10:05:51 -03:00
109 changed files with 11331 additions and 1935 deletions

View File

@@ -17,7 +17,7 @@ LIBRE_TRANSLATE_API_KEY = "" # API Key, if required (You can host your own inst
REPLICATE_API_TOKEN = "" #API Key to run models on replicate.com
HUGGINGFACE_EMAIL = ""
HUGGINGFACE_PASSWORD = ""
COINSTATSOPENAPI_KEY = ""
# We will automatically create dtags and private keys based on the identifier variable in main.
# If your DVM already has a dtag and private key you can replace it here before publishing the DTAG to not create a new one.

3
.gitignore vendored
View File

@@ -181,3 +181,6 @@ ui/noogle/node_modules
ui/noogle/.idea/
ui/noogle/package-lock.json
search.py
identifier.sqlite
.idea/dataSources.xml
todo.txt

45
.idea/dataSources.xml generated
View File

@@ -68,5 +68,50 @@
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="Profiles" uuid="77eda71f-1c66-4b3d-bc34-dfe34fb45fc2">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/nostr_profiles.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar</url>
</library>
</libraries>
</data-source>
<data-source source="LOCAL" name="identifier.sqlite" uuid="7b0e6d22-6530-4715-8aa9-b7e1707839e9">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="identifier.sqlite [2]" uuid="6d783b7b-9c23-46fa-9057-ac42e5e18a1e">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="identifier.sqlite [3]" uuid="4d771519-8188-464e-ba40-636a043fda3e">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:identifier.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="subscriptions" uuid="ccd96349-b12f-47d5-8caf-c0c8c359d831">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/subscriptions</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar</url>
</library>
</libraries>
</data-source>
</component>
</project>

View File

@@ -1,4 +1,4 @@
# NostrAI: Nostr NIP90 Data Vending Machine Framework
# NostrDVM: Nostr NIP90 Data Vending Machine Framework
This framework provides a way to easily build and/or run `Nostr NIP90 DVMs in Python`.

View File

@@ -4,7 +4,7 @@ from pathlib import Path
from threading import Thread
import dotenv
from nostr_sdk import Keys, Client, ClientSigner, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt
from nostr_sdk import Keys, Client, NostrSigner, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key
@@ -12,7 +12,7 @@ from nostr_dvm.utils.definitions import EventDefinitions
def nostr_client_test_llm(prompt):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", prompt, "text"])
relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
@@ -24,7 +24,7 @@ def nostr_client_test_llm(prompt):
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
@@ -35,7 +35,7 @@ def nostr_client_test_llm(prompt):
return event.as_json()
def nostr_client():
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
sk = keys.secret_key()
pk = keys.public_key()
print(f"Nostr Test Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ")

View File

@@ -5,7 +5,7 @@ from threading import Thread
import dotenv
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \
ClientSigner
NostrSigner
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key
@@ -13,7 +13,7 @@ from nostr_dvm.utils.definitions import EventDefinitions
def nostr_client_test_tts(prompt):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", prompt, "text"])
paramTag1 = Tag.parse(["param", "language", "en"])
@@ -30,7 +30,7 @@ def nostr_client_test_tts(prompt):
"wss://nostr-pub.wellorder.net"]
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
@@ -40,11 +40,11 @@ def nostr_client_test_tts(prompt):
return event.as_json()
def nostr_client():
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
sk = keys.secret_key()
pk = keys.public_key()
print(f"Nostr Test Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ")
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
dvmconfig = DVMConfig()

View File

@@ -0,0 +1,18 @@
#Create an account with a lnbits instance of your choice, add the admin key and id here. This account will be used to create a new lnbits wallet for each dvm/bot
LNBITS_ADMIN_KEY = ""
LNBITS_ADMIN_ID = ""
LNBITS_HOST = "https://lnbits.com" #Use your own/a trusted instance ideally.
# In order to create a zappable lightning address, host nostdress on your domain or use this preinstalled domain.
# We will use the api to create and manage zapable lightning addresses
NOSTDRESS_DOMAIN = "nostrdvm.com"
UNLEASHED_API_KEY = ""
# We will automatically create dtags and private keys based on the identifier variable in main.
# If your DVM already has a dtag and private key you can replace it here before publishing the DTAG to not create a new one.
# The name and NIP90 info of the DVM can be changed but the identifier must stay the same in order to not create a different dtag.
# We will also create new wallets on the given lnbits instance for each dvm. If you want to use an existing wallet, you can replace the parameters here as well.
# Make sure you backup this file to keep access to your wallets

View File

@@ -0,0 +1,26 @@
# NostrAI: Nostr NIP90 Data Vending Machine Framework Example
Projects in this folder contain ready-to-use DVMs. To tun the DVM following the next steps:
## To get started:
- Install Python (tested on 3.10/3.11)
Create a new venv in this directory by opening the terminal here, or navigate to this directory and type: `"python -m venv venv"`
- Place .env file (based on .env_example) in this folder.
- Recommended but optional:
- Create a `LNbits` account on an accessible instance of your choice, enter one account's id and admin key (this account will create other accounts for the dvms) Open the .env file and enter this info to `LNBITS_ADMIN_KEY`, `LNBITS_ADMIN_ID`, `LNBITS_HOST`.
- If you are running an own instance of `Nostdress` enter `NOSTDRESS_DOMAIN` or use the default one.
- Activate the venv with
- MacOS/Linux: source ./venv/bin/activate
- Windows: .\venv\Scripts\activate
- Type: `pip install nostr-dvm`
- Run `python3 main.py` (or python main.py)
- The framework will then automatically create keys, nip89 tags and zapable NIP57 `lightning addresses` for your dvms in this file.
- Check the .env file if these values look correct.
- Check the `main.py` file. You can update the image/description/name of your DVM before announcing it.
- You can then in main.py set `admin_config.REBROADCAST_NIP89` and
`admin_config.UPDATE_PROFILE` to `True` to announce the NIP89 info and update the npubs profile automatically.
- After this was successful you can set these back to False until the next time you want to update the NIP89 or profile.
You are now running your own DVM.

View File

@@ -0,0 +1,55 @@
import json
from pathlib import Path
import dotenv
from nostr_dvm.tasks.textgeneration_unleashed_chat import TextGenerationUnleashedChat
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.dvmconfig import build_default_config
from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag
def main():
identifier = "unleashed"
name = "Unleashed Chat"
dvm_config = build_default_config(identifier)
dvm_config.SEND_FEEDBACK_EVENTS = False
dvm_config.USE_OWN_VENV = False
dvm_config.FIX_COST = 0
admin_config = AdminConfig()
admin_config.LUD16 = dvm_config.LN_ADDRESS
admin_config.REBROADCAST_NIP89 = False
nip89info = {
"name": name,
"image": "https://unleashed.chat/_app/immutable/assets/hero.pehsu4x_.jpeg",
"about": "I generate Text with Unleashed.chat",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
unleashed = TextGenerationUnleashedChat(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config)
unleashed.run()
if __name__ == '__main__':
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
main()

View File

@@ -0,0 +1,103 @@
import json
import time
from datetime import timedelta
from pathlib import Path
from threading import Thread
import dotenv
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \
NostrSigner, Options
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key
from nostr_dvm.utils.definitions import EventDefinitions
def nostr_client_test(prompt):
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", prompt, "text"])
relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"])
alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to generate TTS"])
event = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_TEXT, str("Answer to prompt"),
[iTag, relaysTag, alttag]).to_event(keys)
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=5)))
signer = NostrSigner.keys(keys)
client = Client.with_opts(signer,opts)
for relay in relay_list:
client.add_relay(relay)
client.connect()
config = DVMConfig
send_event(event, client=client, dvm_config=config)
return event.as_json()
def nostr_client():
keys = Keys.parse(check_and_set_private_key("test_client"))
sk = keys.secret_key()
pk = keys.public_key()
print(f"Nostr Test Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ")
signer = NostrSigner.keys(keys)
client = Client(signer)
dvmconfig = DVMConfig()
for relay in dvmconfig.RELAY_LIST:
client.add_relay(relay)
client.connect()
dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM,
EventDefinitions.KIND_ZAP]).since(
Timestamp.now()) # events to us specific
dvm_filter = (Filter().kinds([EventDefinitions.KIND_NIP90_RESULT_GENERATE_TEXT,
EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events
client.subscribe([dm_zap_filter, dvm_filter])
#nostr_client_test("What has Pablo been up to?")
nostr_client_test("What is Gigi talking about recently?")
print("Sending Job Request")
class NotificationHandler(HandleNotification):
def handle(self, relay_url, event):
print(f"Received new event from {relay_url}: {event.as_json()}")
if event.kind() == 7000:
print("[Nostr Client]: " + event.as_json())
elif 6000 < event.kind() < 6999:
print("[Nostr Client " + event.author().to_bech32() + "]: " + event.as_json())
print("[Nostr Client " + event.author().to_bech32() + "]: " + event.content())
elif event.kind() == 4:
dec_text = nip04_decrypt(sk, event.author(), event.content())
print("[Nostr Client]: " + f"Received new msg: {dec_text}")
elif event.kind() == 9735:
print("[Nostr Client]: " + f"Received new zap:")
print(event.as_json())
def handle_msg(self, relay_url, msg):
return
client.handle_notifications(NotificationHandler())
while True:
time.sleep(1)
if __name__ == '__main__':
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)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
nostr_dvm_thread = Thread(target=nostr_client())
nostr_dvm_thread.start()

51
main.py
View File

@@ -5,9 +5,10 @@ from sys import platform
from nostr_dvm.bot import Bot
from nostr_dvm.tasks import videogeneration_replicate_svd, imagegeneration_replicate_sdxl, textgeneration_llmlite, \
trending_notes_nostrband, discovery_inactive_follows, translation_google, textextraction_pdf, \
discovery_trending_notes_nostrband, discovery_inactive_follows, translation_google, textextraction_pdf, \
translation_libretranslate, textextraction_google, convert_media, imagegeneration_openai_dalle, texttospeech, \
imagegeneration_sd21_mlx, advanced_search, textgeneration_huggingchat, summarization_huggingchat
imagegeneration_sd21_mlx, advanced_search, textgeneration_huggingchat, summarization_huggingchat, \
discovery_nonfollowers, search_users
from nostr_dvm.utils.admin_utils import AdminConfig
from nostr_dvm.utils.backend_utils import keep_alive
from nostr_dvm.utils.definitions import EventDefinitions
@@ -24,13 +25,12 @@ def playground():
# Note this is very basic for now and still under development
bot_config = DVMConfig()
bot_config.PRIVATE_KEY = check_and_set_private_key("bot")
npub = Keys.from_sk_str(bot_config.PRIVATE_KEY).public_key().to_bech32()
npub = Keys.parse(bot_config.PRIVATE_KEY).public_key().to_bech32()
invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys("bot", npub)
bot_config.LNBITS_INVOICE_KEY = invoice_key
bot_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back
bot_config.LNBITS_URL = os.getenv("LNBITS_HOST")
# Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast
# their NIP89 announcement
# You can create individual admins configs and hand them over when initializing the dvm,
@@ -39,13 +39,15 @@ def playground():
admin_config = AdminConfig()
admin_config.REBROADCAST_NIP89 = False
admin_config.LUD16 = lnaddress
# Set rebroadcast to true once you have set your NIP89 descriptions and d tags. You only need to rebroadcast once you
# want to update your NIP89 descriptions
# Update the DVMs (not the bot) profile. For example after you updated the NIP89 or the lnaddress, you can automatically update profiles here.
admin_config.UPDATE_PROFILE = False
# Spawn some DVMs in the playground and run them
# You can add arbitrary DVMs there and instantiate them here
@@ -62,11 +64,11 @@ def playground():
# Spawn DVM3 Kind 5002 Local Text TranslationLibre, calling the free LibreTranslateApi, as an alternative.
# This will only run and appear on the bot if an endpoint is set in the .env
if os.getenv("LIBRE_TRANSLATE_ENDPOINT") is not None and os.getenv("LIBRE_TRANSLATE_ENDPOINT") != "":
libre_translator = translation_libretranslate.build_example("Libre Translator", "libre_translator", admin_config)
libre_translator = translation_libretranslate.build_example("Libre Translator", "libre_translator",
admin_config)
bot_config.SUPPORTED_DVMS.append(libre_translator) # We add translator to the bot
libre_translator.run()
# Spawn DVM4, this one requires an OPENAI API Key and balance with OpenAI, you will move the task to them and pay
# per call. Make sure you have enough balance and the DVM's cost is set higher than what you pay yourself, except, you know,
# you're being generous.
@@ -76,17 +78,18 @@ def playground():
dalle.run()
if os.getenv("REPLICATE_API_TOKEN") is not None and os.getenv("REPLICATE_API_TOKEN") != "":
sdxlreplicate = imagegeneration_replicate_sdxl.build_example("Stable Diffusion XL", "replicate_sdxl", admin_config)
sdxlreplicate = imagegeneration_replicate_sdxl.build_example("Stable Diffusion XL", "replicate_sdxl",
admin_config)
bot_config.SUPPORTED_DVMS.append(sdxlreplicate)
sdxlreplicate.run()
if os.getenv("REPLICATE_API_TOKEN") is not None and os.getenv("REPLICATE_API_TOKEN") != "":
svdreplicate = videogeneration_replicate_svd.build_example("Stable Video Diffusion", "replicate_svd", admin_config)
svdreplicate = videogeneration_replicate_svd.build_example("Stable Video Diffusion", "replicate_svd",
admin_config)
bot_config.SUPPORTED_DVMS.append(svdreplicate)
svdreplicate.run()
#Let's define a function so we can add external DVMs to our bot, we will instanciate it afterwards
# Let's define a function so we can add external DVMs to our bot, we will instanciate it afterwards
# Spawn DVM5.. oh wait, actually we don't spawn a new DVM, we use the dvmtaskinterface to define an external dvm by providing some info about it, such as
# their pubkey, a name, task, kind etc. (unencrypted)
@@ -97,7 +100,6 @@ def playground():
bot_config.SUPPORTED_DVMS.append(tasktiger_external)
# Don't run it, it's on someone else's machine, and we simply make the bot aware of it.
# DVM: 6 Another external dvm for recommendations:
ymhm_external = build_external_dvm(pubkey="6b37d5dc88c1cbd32d75b713f6d4c2f7766276f51c9337af9d32c8d715cc1b93",
task="content-discovery",
@@ -107,28 +109,30 @@ def playground():
# If we get back a list of people or events, we can post-process it to make it readable in social clients
bot_config.SUPPORTED_DVMS.append(ymhm_external)
# Spawn DVM 7 Find inactive followers
googleextractor = textextraction_google.build_example("Extractor", "speech_recognition",
admin_config)
bot_config.SUPPORTED_DVMS.append(googleextractor)
googleextractor.run()
# Spawn DVM 8 A Media Grabber/Converter
media_bringer = convert_media.build_example("Media Bringer", "media_converter", admin_config)
bot_config.SUPPORTED_DVMS.append(media_bringer)
media_bringer.run()
# Spawn DVM9 Find inactive followers
discover_inactive = discovery_inactive_follows.build_example("Bygones", "discovery_inactive_follows",
admin_config)
bot_config.SUPPORTED_DVMS.append(discover_inactive)
discover_inactive.run()
trending = trending_notes_nostrband.build_example("Trending Notes on nostr.band", "trending_notes_nostrband", admin_config)
discover_nonfollowers = discovery_nonfollowers.build_example("Not Refollowing", "discovery_non_followers",
admin_config)
bot_config.SUPPORTED_DVMS.append(discover_nonfollowers)
discover_nonfollowers.run()
trending = discovery_trending_notes_nostrband.build_example("Trending Notes on nostr.band", "trending_notes_nostrband",
admin_config)
bot_config.SUPPORTED_DVMS.append(trending)
trending.run()
@@ -144,8 +148,12 @@ def playground():
bot_config.SUPPORTED_DVMS.append(search)
search.run()
profile_search = search_users.build_example("Profile Searcher", "profile_search", admin_config)
bot_config.SUPPORTED_DVMS.append(profile_search)
profile_search.run()
inactive = discovery_inactive_follows.build_example("Inactive People you follow", "discovery_inactive_follows", admin_config)
inactive = discovery_inactive_follows.build_example("Inactive People you follow", "discovery_inactive_follows",
admin_config)
bot_config.SUPPORTED_DVMS.append(inactive)
inactive.run()
@@ -156,16 +164,15 @@ def playground():
mlx.run()
if os.getenv("HUGGINGFACE_EMAIL") is not None and os.getenv("HUGGINGFACE_EMAIL") != "":
hugginchat = textgeneration_huggingchat.build_example("Huggingchat", "huggingchat",admin_config)
hugginchat = textgeneration_huggingchat.build_example("Huggingchat", "huggingchat", admin_config)
bot_config.SUPPORTED_DVMS.append(hugginchat)
hugginchat.run()
hugginchatsum = summarization_huggingchat.build_example("Huggingchat Summarizer", "huggingchatsum", admin_config)
hugginchatsum = summarization_huggingchat.build_example("Huggingchat Summarizer", "huggingchatsum",
admin_config)
bot_config.SUPPORTED_DVMS.append(hugginchatsum)
hugginchatsum.run()
# Run the bot
Bot(bot_config)
# Keep the main function alive for libraries that require it, like openai

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, ClientSigner, EventId, Nip19Event)
Options, Tag, Event, nip04_encrypt, NostrSigner, EventId, Nip19Event, Kind, KindEnum,
UnsignedEvent, UnwrappedGift)
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
@@ -30,12 +31,12 @@ class Bot:
nip89config.NAME = self.NAME
self.dvm_config.NIP89 = nip89config
self.admin_config = admin_config
self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
self.keys = Keys.parse(dvm_config.PRIVATE_KEY)
wait_for_send = True
skip_disconnected_relays = True
opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
.skip_disconnected_relays(skip_disconnected_relays))
signer = ClientSigner.keys(self.keys)
signer = NostrSigner.keys(self.keys)
self.client = Client.with_opts(signer, opts)
pk = self.keys.public_key()
@@ -52,145 +53,210 @@ class Bot:
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now())
dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since(Timestamp.now())
nip59_filter = Filter().pubkey(pk).kind(Kind.from_enum(KindEnum.GIFT_WRAP())).since(
Timestamp.from_secs(Timestamp.now().as_secs() - 60 * 60 * 24 * 7))
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, nip59_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:
handle_nip90_feedback(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_DM:
handle_dm(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_ZAP:
handle_zap(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_DM:
try:
handle_dm(nostr_event, False)
except Exception as e:
print(f"Error during content NIP04 decryption: {e}")
elif nostr_event.kind().match_enum(KindEnum.GIFT_WRAP()):
try:
handle_dm(nostr_event, True)
except Exception as e:
print(f"Error during content NIP59 decryption: {e}")
def handle_msg(self, relay_url, msg):
return
def handle_dm(nostr_event):
def handle_dm(nostr_event, giftwrap):
sender = nostr_event.author().to_hex()
if sender == self.keys.public_key().to_hex():
return
decrypted_text = ""
try:
decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.author(), nostr_event.content())
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config)
print("[" + self.NAME + "] Message from " + user.name + ": " + decrypted_text)
sealed = " "
if giftwrap:
try:
# Extract rumor
unwrapped_gift = UnwrappedGift.from_gift_wrap(self.keys, nostr_event)
sender = unwrapped_gift.sender().to_hex()
rumor: UnsignedEvent = unwrapped_gift.rumor()
# if user selects an index from the overview list...
if decrypted_text[0].isdigit():
split = decrypted_text.split(' ')
index = int(split[0]) - 1
# if user sends index info, e.g. 1 info, we fetch the nip89 information and reply with it.
if len(split) > 1 and split[1].lower() == "info":
answer_nip89(nostr_event, index)
# otherwise we probably have to do some work, so build an event from input and send it to the DVM
else:
task = self.dvm_config.SUPPORTED_DVMS[index].TASK
print("[" + self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) +
", Balance: " + str(user.balance) + " Sats) Task: " + str(task))
if user.isblacklisted:
# If users are blacklisted for some reason, tell them.
answer_blacklisted(nostr_event)
else:
# Parse inputs to params
tags = build_params(decrypted_text, nostr_event, index)
p_tag = Tag.parse(['p', self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY])
if self.dvm_config.SUPPORTED_DVMS[index].SUPPORTS_ENCRYPTION:
tags_str = []
for tag in tags:
tags_str.append(tag.as_vec())
params_as_str = json.dumps(tags_str)
print(params_as_str)
# and encrypt them
encrypted_params = nip04_encrypt(self.keys.secret_key(),
PublicKey.from_hex(
self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY),
params_as_str)
# add encrypted and p tag on the outside
encrypted_tag = Tag.parse(['encrypted'])
# add the encrypted params to the content
nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND,
encrypted_params, [p_tag, encrypted_tag]).
to_event(self.keys))
# Check timestamp of rumor
if rumor.created_at().as_secs() >= Timestamp.now().as_secs():
if rumor.kind().match_enum(KindEnum.SEALED_DIRECT()):
decrypted_text = rumor.content()
print(f"Received new msg [sealed]: {decrypted_text}")
sealed = " [sealed] "
# client.send_sealed_msg(sender, f"Echo: {msg}", None)
else:
tags.append(p_tag)
nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND,
"", tags).
to_event(self.keys))
# remember in the job_list that we have made an event, if anybody asks for payment,
# we know we actually sent the request
entry = {"npub": user.npub, "event_id": nip90request.id().to_hex(),
"dvm_key": self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY, "is_paid": False}
self.job_list.append(entry)
# send the event to the DVM
send_event(nip90request, client=self.client, dvm_config=self.dvm_config)
# print(nip90request.as_json())
elif decrypted_text.lower().startswith("balance"):
time.sleep(3.0)
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
"Your current balance is " + str(
user.balance) + " Sats. Zap me to add to your balance. I will use your balance interact with the DVMs for you.\n"
"I support both public and private Zaps, as well as Zapplepay.\n"
"Alternativly you can add a #cashu token with \"-cashu cashuASomeToken\" to your command.\n Make sure the token is worth the requested amount + "
"mint fees (at least 3 sat).\n Not all DVMs might accept Cashu tokens."
, None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
elif decrypted_text.startswith("cashuA"):
print("Received Cashu token:" + decrypted_text)
cashu_redeemed, cashu_message, total_amount, fees = redeem_cashu(decrypted_text, self.dvm_config,
self.client)
print(cashu_message)
if cashu_message == "success":
update_user_balance(self.dvm_config.DB, sender, total_amount, client=self.client,
config=self.dvm_config)
else:
time.sleep(2.0)
message = "Error: " + cashu_message + ". Token has not been redeemed."
evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.from_hex(sender), message,
None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=self.dvm_config)
elif decrypted_text.lower().startswith("what's the second best"):
time.sleep(3.0)
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
"No, there is no second best.\n\nhttps://cdn.nostr.build/p/mYLv.mp4",
nostr_event.id()).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=self.dvm_config)
print(f"{rumor.as_json()}")
except Exception as e:
print(f"Error during content NIP59 decryption: {e}")
else:
# Build an overview of known DVMs and send it to the user
answer_overview(nostr_event)
try:
decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.author(),
nostr_event.content())
except Exception as e:
print(f"Error during content NIP04 decryption: {e}")
if decrypted_text != "":
user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client,
config=self.dvm_config)
print("[" + self.NAME + "]" + sealed + "Message from " + user.name + ": " + decrypted_text)
# if user selects an index from the overview list...
if decrypted_text != "" and decrypted_text[0].isdigit():
split = decrypted_text.split(' ')
index = int(split[0]) - 1
# if user sends index info, e.g. 1 info, we fetch the nip89 information and reply with it.
if len(split) > 1 and split[1].lower() == "info":
answer_nip89(nostr_event, index, giftwrap, sender)
# otherwise we probably have to do some work, so build an event from input and send it to the DVM
else:
task = self.dvm_config.SUPPORTED_DVMS[index].TASK
print("[" + self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) +
", Balance: " + str(user.balance) + " Sats) Task: " + str(task))
if user.isblacklisted:
# If users are blacklisted for some reason, tell them.
answer_blacklisted(nostr_event, giftwrap, sender)
else:
# Parse inputs to params
tags = build_params(decrypted_text, sender, index)
p_tag = Tag.parse(['p', self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY])
if self.dvm_config.SUPPORTED_DVMS[index].SUPPORTS_ENCRYPTION:
tags_str = []
for tag in tags:
tags_str.append(tag.as_vec())
params_as_str = json.dumps(tags_str)
print(params_as_str)
# and encrypt them
encrypted_params = nip04_encrypt(self.keys.secret_key(),
PublicKey.from_hex(
self.dvm_config.SUPPORTED_DVMS[
index].PUBLIC_KEY),
params_as_str)
# add encrypted and p tag on the outside
encrypted_tag = Tag.parse(['encrypted'])
# add the encrypted params to the content
nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND,
encrypted_params, [p_tag, encrypted_tag]).
to_event(self.keys))
else:
tags.append(p_tag)
nip90request = (EventBuilder(self.dvm_config.SUPPORTED_DVMS[index].KIND,
"", tags).
to_event(self.keys))
# remember in the job_list that we have made an event, if anybody asks for payment,
# we know we actually sent the request
entry = {"npub": user.npub, "event_id": nip90request.id().to_hex(),
"dvm_key": self.dvm_config.SUPPORTED_DVMS[index].PUBLIC_KEY, "is_paid": False, "giftwrap": giftwrap}
self.job_list.append(entry)
# send the event to the DVM
send_event(nip90request, client=self.client, dvm_config=self.dvm_config)
# print(nip90request.as_json())
elif decrypted_text.lower().startswith("balance"):
time.sleep(3.0)
message = "Your current balance is " + str(user.balance) + ("Sats. Zap me to add to your "
"balance. I will use your "
"balance interact with the DVMs "
"for you.\n I support both "
"public and private Zaps, "
"as well as "
"Zapplepay.\nAlternativly you "
"can add a #cashu token with "
"\"-cashu cashuASomeToken\" to "
"your command.\n Make sure the "
"token is worth the requested "
"amount mint fees (at least 3 "
"sat).\n Not all DVMs might "
"accept Cashu tokens.")
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), message, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.parse(sender),
message,None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
elif decrypted_text.startswith("cashuA"):
print("Received Cashu token:" + decrypted_text)
cashu_redeemed, cashu_message, total_amount, fees = redeem_cashu(decrypted_text,
self.dvm_config,
self.client)
print(cashu_message)
if cashu_message == "success":
update_user_balance(self.dvm_config.DB, sender, total_amount, client=self.client,
config=self.dvm_config)
else:
time.sleep(2.0)
message = "Error: " + cashu_message + ". Token has not been redeemed."
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), message, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.from_hex(sender), message,
None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=self.dvm_config)
elif decrypted_text.lower().startswith("what's the second best"):
time.sleep(3.0)
message = "No, there is no second best.\n\nhttps://cdn.nostr.build/p/mYLv.mp4"
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), message, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.parse(sender),
message,
nostr_event.id()).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=self.dvm_config)
else:
# Build an overview of known DVMs and send it to the user
answer_overview(nostr_event, giftwrap, sender)
except Exception as e:
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 = ""
@@ -211,7 +277,7 @@ class Bot:
if is_encrypted:
if ptag == self.keys.public_key().to_hex():
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
tags_str = nip04_decrypt(Keys.parse(dvm_config.PRIVATE_KEY).secret_key(),
nostr_event.author(), nostr_event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", ptag]).as_vec())
@@ -240,14 +306,18 @@ class Bot:
user = get_or_add_user(db=self.dvm_config.DB, npub=entry['npub'],
client=self.client, config=self.dvm_config)
time.sleep(2.0)
reply_event = EventBuilder.encrypted_direct_msg(self.keys,
if entry["giftwrap"]:
self.client.send_sealed_msg(PublicKey.parse(entry["npub"]), content, None)
else:
reply_event = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.from_hex(entry['npub']),
content,
None).to_event(self.keys)
send_event(reply_event, client=self.client, dvm_config=dvm_config)
print(status + ": " + content)
print(
"[" + self.NAME + "] Received reaction from " + nostr_event.author().to_hex() + " message to orignal sender " + user.name)
send_event(reply_event, client=self.client, dvm_config=dvm_config)
elif status == "payment-required" or status == "partial":
for tag in nostr_event.tags():
@@ -265,29 +335,31 @@ 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())
evt = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.from_hex(entry["npub"]),
"Paid " + str(
amount) + " Sats from balance to DVM. New balance is " +
str(balance)
+ " Sats.\n",
None).to_event(self.keys)
lastactive=Timestamp.now().as_secs(), subscribed=user.subscribed)
message = "Paid " + str(amount) + " Sats from balance to DVM. New balance is " + str(balance) + " Sats.\n"
if entry["giftwrap"]:
self.client.send_sealed_msg(PublicKey.parse(entry["npub"]), message, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.parse(entry["npub"]),
message,
None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
print(
"[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation")
send_event(evt, client=self.client, dvm_config=dvm_config)
else:
print("Bot payment-required")
time.sleep(2.0)
evt = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.from_hex(entry["npub"]),
"Current balance: " + str(
user.balance) + " Sats. Balance of " + str(
amount) + " Sats required. Please zap me with at least " +
str(int(amount - user.balance))
+ " Sats, then try again.",
None).to_event(self.keys)
PublicKey.parse(entry["npub"]),
"Current balance: " + str(
user.balance) + " Sats. Balance of " + str(
amount) + " Sats required. Please zap me with at least " +
str(int(amount - user.balance))
+ " Sats, then try again.",
None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
return
@@ -346,7 +418,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:
@@ -357,11 +429,14 @@ class Bot:
print("[" + self.NAME + "] Received results, message to orignal sender " + user.name)
time.sleep(1.0)
reply_event = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.from_hex(user.npub),
if entry["giftwrap"]:
self.client.send_sealed_msg(PublicKey.parse(user.npub), content, None)
else:
reply_event = EventBuilder.encrypted_direct_msg(self.keys,
PublicKey.parse(user.npub),
content,
None).to_event(self.keys)
send_event(reply_event, client=self.client, dvm_config=dvm_config)
send_event(reply_event, client=self.client, dvm_config=dvm_config)
except Exception as e:
print(e)
@@ -412,7 +487,7 @@ class Bot:
except Exception as e:
print("[" + self.NAME + "] Error during content decryption:" + str(e))
def answer_overview(nostr_event):
def answer_overview(nostr_event, giftwrap, sender):
message = "DVMs that I support:\n\n"
index = 1
for p in self.dvm_config.SUPPORTED_DVMS:
@@ -425,37 +500,41 @@ class Bot:
index += 1
time.sleep(3.0)
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
message + "\nSelect an Index and provide an input ("
"e.g. \"2 A purple ostrich\")\nType \"index info\" to learn "
"more about each DVM. (e.g. \"2 info\")\n\n"
"Type \"balance\" to see your current balance",
text = message + "\nSelect an Index and provide an input (e.g. \"2 A purple ostrich\")\nType \"index info\" to learn more about each DVM. (e.g. \"2 info\")\n\n Type \"balance\" to see your current balance"
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), text, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys, PublicKey.parse(sender),
text,
nostr_event.id()).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
send_event(evt, client=self.client, dvm_config=dvm_config)
def answer_blacklisted(nostr_event):
# For some reason an admin might blacklist npubs, e.g. for abusing the service
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
"Your are currently blocked from all "
"services.", None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
def answer_nip89(nostr_event, index):
info = print_dvm_info(self.client, index)
time.sleep(2.0)
if info is not None:
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
info, None).to_event(self.keys)
def answer_blacklisted(nostr_event, giftwrap, sender):
message = "Your are currently blocked from this service."
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), message, None)
else:
# For some reason an admin might blacklist npubs, e.g. for abusing the service
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
"No NIP89 Info found for " +
self.dvm_config.SUPPORTED_DVMS[index].NAME,
None).to_event(self.keys)
message, None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
send_event(evt, client=self.client, dvm_config=dvm_config)
def answer_nip89(nostr_event, index, giftwrap, sender):
info = print_dvm_info(self.client, index)
if info is None:
info = "No NIP89 Info found for " + self.dvm_config.SUPPORTED_DVMS[index].NAME
time.sleep(2.0)
def build_params(decrypted_text, nostr_event, index):
if giftwrap:
self.client.send_sealed_msg(PublicKey.parse(sender), info, None)
else:
evt = EventBuilder.encrypted_direct_msg(self.keys, nostr_event.author(),
info, None).to_event(self.keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
def build_params(decrypted_text, author, index):
tags = []
splitzero = decrypted_text.split(' -')
split = splitzero[0].split(' ')
@@ -463,7 +542,7 @@ class Bot:
if len(split) == 1:
remaining_text = decrypted_text.replace(split[0], "")
params = remaining_text.split(" -")
tag = Tag.parse(["param", "user", nostr_event.author().to_hex()])
tag = Tag.parse(["param", "user", author])
tags.append(tag)
for i in params:
print(i)

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, ClientSigner
init_logger, LogLevel, Options, nip04_encrypt, NostrSigner, Kind
import time
@@ -13,18 +13,16 @@ 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, \
parse_amount_from_bolt11_invoice, zaprequest, pay_bolt11_ln_bits, create_bolt11_lud16
from nostr_dvm.utils.cashu_utils import redeem_cashu
use_logger = False
if use_logger:
init_logger(LogLevel.DEBUG)
class DVM:
dvm_config: DVMConfig
@@ -37,15 +35,14 @@ class DVM:
def __init__(self, dvm_config, admin_config=None):
self.dvm_config = dvm_config
self.admin_config = admin_config
self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
wait_for_send = True
self.keys = Keys.parse(dvm_config.PRIVATE_KEY)
wait_for_send = False
skip_disconnected_relays = True
opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
.skip_disconnected_relays(skip_disconnected_relays))
signer = ClientSigner.keys(self.keys)
self.client = Client.with_opts(signer,opts)
signer = NostrSigner.keys(self.keys)
self.client = Client.with_opts(signer, opts)
self.job_list = []
self.jobs_on_hold_list = []
@@ -64,7 +61,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)
@@ -74,48 +72,103 @@ 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, skip_meta=False)
# 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:
send_job_status_reaction(nip90_event, "subscription-required", True, amount, self.client,
"Checking Subscription Status, please wait..",self.dvm_config)
# 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
send_job_status_reaction(nip90_event, "subscription-required", True, amount,
self.client, "User subscripton active until " +
Timestamp.from_secs(int(user.subscribed)).to_human_datetime().replace("Z", " ").replace("T", " ") + " GMT", self.dvm_config)
# otherwise we check for an active subscription by checking recipie events
else:
print("[" + self.dvm_config.NIP89.NAME + "] Checking Subscription status")
send_job_status_reaction(nip90_event, "subscription-required", True, amount, self.client,
"I Don't have information about subscription status, checking on the Nostr. This might take a few seconds",
self.dvm_config)
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"]:
send_job_status_reaction(nip90_event, "subscription-required", True, amount, self.client,
"User subscripton active until " + Timestamp.from_secs(int(subscription_status["validUntil"])).to_human_datetime().replace("Z", " ").replace("T", " ") + " GMT",
self.dvm_config)
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")
send_job_status_reaction(nip90_event, "subscription-required", True, amount, self.client,
"No active subscription found. Manage your subscription at: " + self.dvm_config.SUBSCRIPTION_MANAGEMENT,
self.dvm_config)
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
@@ -129,31 +182,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)
@@ -162,27 +224,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:
@@ -202,6 +277,7 @@ class DVM:
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)
@@ -213,41 +289,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() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT:
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() == EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT:
print("new subscription, doing nothing")
elif zapped_event.kind() in EventDefinitions.ANY_RESULT:
print("[" + self.dvm_config.NIP89.NAME + "] "
@@ -259,7 +347,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,
@@ -328,6 +416,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",
@@ -355,7 +444,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
@@ -376,18 +465,19 @@ 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)
alt_description, reaction = build_status_reaction(status, task, amount, content, dvm_config)
e_tag = Tag.parse(["e", original_event.id().to_hex()])
p_tag = Tag.parse(["p", original_event.author().to_hex()])
@@ -405,9 +495,12 @@ class DVM:
if encrypted:
encryption_tags.append(p_tag)
encryption_tags.append(e_tag)
else:
reply_tags.append(p_tag)
if status == "success" or status == "error": #
for x in self.job_list:
if x.event == original_event:
@@ -418,18 +511,19 @@ 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)
bolt11, payment_hash = create_bolt11_ln_bits(amount, dvm_config)
except Exception as e:
print(e)
try:
bolt11, payment_hash = create_bolt11_lud16(dvm_config.LN_ADDRESS,
amount)
amount)
except Exception as e:
print(e)
bolt11 = None
print(e)
bolt11 = None
elif dvm_config.LN_ADDRESS != "":
try:
bolt11, payment_hash = create_bolt11_lud16(dvm_config.LN_ADDRESS, amount)
@@ -451,7 +545,7 @@ class DVM:
status == "processing" and not is_paid)
or (status == "success" and not is_paid)):
if dvm_config.LNBITS_INVOICE_KEY != "":
if dvm_config.LNBITS_INVOICE_KEY != "" and bolt11 is not None:
amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11])
else:
amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats
@@ -472,16 +566,17 @@ class DVM:
else:
content = reaction
keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
keys = Keys.parse(dvm_config.PRIVATE_KEY)
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)
@@ -520,9 +615,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)
@@ -531,7 +628,8 @@ class DVM:
user = get_or_add_user(self.dvm_config.DB, job_event.author().to_hex(),
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,
bolt11 = zaprequest(user.lud16, amount, "Couldn't finish job, returning sats", job_event,
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,18 +643,23 @@ class DVM:
self.client.handle_notifications(NotificationHandler())
while True:
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.is_paid:
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,17 +31,18 @@ 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:
dvm_config.PUBLIC_KEY = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_hex()
dvm_config.PUBLIC_KEY = Keys.parse(dvm_config.PRIVATE_KEY).public_key().to_hex()
self.PUBLIC_KEY = dvm_config.PUBLIC_KEY
if dvm_config.FIX_COST is not None:
self.FIX_COST = dvm_config.FIX_COST
@@ -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
@@ -86,6 +94,10 @@ class DVMTaskInterface:
nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config])
nostr_dvm_thread.start()
def schedule(self, dvm_config):
"""schedule something, e.g. define some time to update or to post, does nothing by default"""
pass
def NIP89_announcement(self, nip89config: NIP89Config):
nip89 = NIP89Config()
nip89.NAME = self.NAME
@@ -146,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)

446
nostr_dvm/subscription.py Normal file
View File

@@ -0,0 +1,446 @@
import json
import math
import os
import signal
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, nip44_decrypt, Kind)
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.nostr_utils import send_event
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_all_subscriptions_from_sql_table, \
delete_from_subscription_sql_table
from nostr_dvm.utils.zap_utils import create_bolt11_lud16, zaprequest
class Subscription:
job_list: list
# This is a simple list just to keep track which events we created and manage, so we don't pay for other requests
def __init__(self, dvm_config, admin_config=None):
self.NAME = "Subscription Handler"
dvm_config.DB = "db/" + "subscriptions" + ".db"
self.dvm_config = dvm_config
nip89config = NIP89Config()
nip89config.NAME = self.NAME
self.dvm_config.NIP89 = nip89config
self.admin_config = admin_config
self.keys = Keys.parse(dvm_config.PRIVATE_KEY)
wait_for_send = False
skip_disconnected_relays = True
opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
.skip_disconnected_relays(skip_disconnected_relays))
signer = NostrSigner.keys(self.keys)
self.client = Client.with_opts(signer, opts)
pk = self.keys.public_key()
self.job_list = []
print("Nostr Subscription Handler 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)
self.client.connect()
zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now())
cancel_subscription_filter = Filter().kinds([EventDefinitions.KIND_NIP88_STOP_SUBSCRIPTION_EVENT]).since(
Timestamp.now())
authors = []
if admin_config is not None and len(admin_config.USERNPUBS) > 0:
#we might want to limit which services can connect to the subscription handler
for key in admin_config.USERNPUBS:
authors.append(PublicKey.parse(key))
dvm_filter = Filter().authors(authors).pubkey(pk).kinds([EventDefinitions.KIND_NIP90_DVM_SUBSCRIPTION]).since(
Timestamp.now())
else:
# or we don't
dvm_filter = Filter().pubkey(pk).kinds(
[EventDefinitions.KIND_NIP90_DVM_SUBSCRIPTION]).since(
Timestamp.now())
self.client.subscribe([zap_filter, dvm_filter, cancel_subscription_filter], None)
create_subscription_sql_table(dvm_config.DB)
class NotificationHandler(HandleNotification):
client = self.client
dvm_config = self.dvm_config
keys = self.keys
def handle(self, relay_url, subscription_id, nostr_event: Event):
if nostr_event.kind() == EventDefinitions.KIND_NIP90_DVM_SUBSCRIPTION:
handle_nwc_request(nostr_event)
elif nostr_event.kind() == EventDefinitions.KIND_NIP88_STOP_SUBSCRIPTION_EVENT:
handle_cancel(nostr_event)
def handle_msg(self, relay_url, msg):
return
def handle_cancel(nostr_event):
print(nostr_event.as_json())
sender = nostr_event.author().to_hex()
kind7001eventid = ""
recipient = ""
if sender == self.keys.public_key().to_hex():
return
for tag in nostr_event.tags():
if tag.as_vec()[0] == "p":
recipient = tag.as_vec()[1]
elif tag.as_vec()[0] == "e":
kind7001eventid = tag.as_vec()[1]
if kind7001eventid != "":
subscription = get_from_subscription_sql_table(dvm_config.DB, kind7001eventid)
if subscription is not None:
update_subscription_sql_table(dvm_config.DB, kind7001eventid, recipient,
subscription.subscriber, subscription.nwc, subscription.cadence,
subscription.amount, subscription.unit, subscription.begin,
subscription.end,
subscription.tier_dtag, subscription.zaps, subscription.recipe,
False, Timestamp.now().as_secs(), subscription.tier)
# send_status_canceled(kind7001eventid, nostr_event) # TODO, waiting for spec
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 send_status_success(original_event, domain):
e_tag = Tag.parse(["e", original_event.id().to_hex()])
p_tag = Tag.parse(["p", original_event.author().to_hex()])
status_tag = Tag.parse(["status", "success", "Job has been scheduled, you can manage it on " + domain])
reply_tags = [status_tag]
encryption_tags = []
encrypted_tag = Tag.parse(["encrypted"])
encryption_tags.append(encrypted_tag)
encryption_tags.append(p_tag)
encryption_tags.append(e_tag)
str_tags = []
for element in reply_tags:
str_tags.append(element.as_vec())
content = json.dumps(str_tags)
content = nip04_encrypt(self.keys.secret_key(), PublicKey.from_hex(original_event.author().to_hex()),
content)
reply_tags = encryption_tags
keys = Keys.parse(dvm_config.PRIVATE_KEY)
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.as_u64()) + " Reaction: " + "success" + " " + reaction_event.as_json())
def pay_zap_split(nwc, overall_amount, zaps, tier, unit="msats"):
overallsplit = 0
for zap in zaps:
print(zap)
overallsplit += int(zap[3])
print(overallsplit)
zapped_amount = 0
for zap in zaps:
if zap[1] == "":
#If the client did decide to not add itself to the zap split, or if a slot is left we add the subscription service in the empty space
zap[1] = Keys.parse(self.dvm_config.PRIVATE_KEY).public_key().to_hex()
name, nip05, lud16 = fetch_user_metadata(zap[1], self.client)
splitted_amount = math.floor(
(int(zap[3]) / overallsplit) * int(overall_amount) / 1000)
# invoice = create_bolt11_lud16(lud16, splitted_amount)
# TODO add details about DVM in message
invoice = zaprequest(lud16, splitted_amount, tier, None,
PublicKey.parse(zap[1]), self.keys, DVMConfig.RELAY_LIST)
print(invoice)
if invoice is not None:
nwc_event_id = nwc_zap(nwc, invoice, self.keys, zap[2])
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:
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_nwc_request(nostr_event):
print(nostr_event.as_json())
sender = nostr_event.author().to_hex()
if sender == self.keys.public_key().to_hex():
return
try:
decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.author(), nostr_event.content())
try:
jsonevent = json.loads(decrypted_text)
for entry in jsonevent:
if entry[1] == "nwc":
nwc = entry[2]
elif entry[1] == "p":
subscriber = entry[2]
subscriptionfilter = Filter().kind(EventDefinitions.KIND_NIP88_SUBSCRIBE_EVENT).author(
PublicKey.parse(subscriber)).limit(1)
evts = self.client.get_events_of([subscriptionfilter], timedelta(seconds=3))
print(evts)
if len(evts) > 0:
event7001id = evts[0].id().to_hex()
print(evts[0].as_json())
tier_dtag = ""
recipient = ""
cadence = ""
unit = "msats"
zaps = []
tier = "DVM"
overall_amount = 0
for tag in evts[0].tags():
if tag.as_vec()[0] == "amount":
overall_amount = int(tag.as_vec()[1])
unit = tag.as_vec()[2]
cadence = tag.as_vec()[3]
print(str(overall_amount) + " " + unit + " " + cadence)
elif tag.as_vec()[0] == "p":
recipient = tag.as_vec()[1]
elif tag.as_vec()[0] == "e":
subscription_event_id = tag.as_vec()[1]
elif tag.as_vec()[0] == "event":
jsonevent = json.loads(tag.as_vec()[1])
subscription_event = Event.from_json(jsonevent)
for tag in subscription_event.tags():
if tag.as_vec()[0] == "d":
tier_dtag = tag.as_vec()[1]
elif tag.as_vec()[0] == "zap":
zaps.append(tag.as_vec())
elif tag.as_vec()[0] == "title":
tier = tag.as_vec()[1]
if tier_dtag == "" or len(zaps) == 0:
tierfilter = Filter().id(EventId.parse(subscription_event_id))
evts = self.client.get_events_of([tierfilter], timedelta(seconds=3))
if len(evts) > 0:
for tag in evts[0].tags():
if tag.as_vec()[0] == "d":
tier_dtag = tag.as_vec()[0]
isactivesubscription = False
recipe = ""
subscription = get_from_subscription_sql_table(dvm_config.DB, event7001id)
zapsstr = json.dumps(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, checkCanceled=False)
if not subscription_status["isActive"]:
start = Timestamp.now().as_secs()
end = infer_subscription_end_time(start, cadence)
# we add or update the subscription in the db, with non-active subscription to avoid double payments
if subscription is None:
add_to_subscription_sql_table(dvm_config.DB, event7001id, recipient, subscriber,
nwc,
cadence, overall_amount, unit, start, end, tier_dtag,
zapsstr, recipe, isactivesubscription,
Timestamp.now().as_secs(), tier)
print("new subscription entry before payment")
else:
update_subscription_sql_table(dvm_config.DB, event7001id, recipient, subscriber,
nwc,
cadence, overall_amount, unit, start, end,
tier_dtag, zapsstr, recipe, isactivesubscription,
Timestamp.now().as_secs(), tier)
print("updated subscription entry before payment")
# we attempt to pay the subscription
success = pay_zap_split(nwc, overall_amount, zaps, tier, unit)
else:
start = Timestamp.now().as_secs()
end = subscription_status["validUntil"]
else:
start = subscription.begin
end = subscription.end
if success:
# we create a payment recipe
recipe = make_subscription_zap_recipe(event7001id, recipient, subscriber, start, end,
tier_dtag)
print("RECIPE " + recipe)
isactivesubscription = True
# we then update the subscription based on payment success
update_subscription_sql_table(dvm_config.DB, event7001id, recipient, subscriber, nwc,
cadence, overall_amount, unit, start, end,
tier_dtag, zapsstr, recipe, isactivesubscription,
Timestamp.now().as_secs(), tier)
print("updated subscription entry after payment")
send_status_success(nostr_event, "noogle.lol")
keys = Keys.parse(dvm_config.PRIVATE_KEY)
message = ("Subscribed to DVM " + tier + ". Renewing on: " + str(
Timestamp.from_secs(end).to_human_datetime().replace("Z", " ").replace("T", " ") + " GMT"))
evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.parse(subscriber), message,
None).to_event(keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
except Exception as e:
print(e)
except Exception as e:
print("Error in Subscriber " + str(e))
def handle_expired_subscription(subscription):
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 == "yearly":
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")
def handle_subscription_renewal(subscription):
zaps = json.loads(subscription.zaps)
success = pay_zap_split(subscription.nwc, subscription.amount, zaps, subscription.tier,
subscription.unit)
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.unit,
subscription.begin, end,
subscription.tier_dtag, subscription.zaps, recipe,
success,
Timestamp.now().as_secs(), subscription.tier)
print("updated subscription entry")
keys = Keys.parse(dvm_config.PRIVATE_KEY)
message = (
"Renewed Subscription to DVM " + subscription.tier + ". Next renewal: " + str(
Timestamp.from_secs(end).to_human_datetime().replace("Z", " ").replace("T",
" ")))
evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.parse(subscription.subscriber),
message,
None).to_event(keys)
send_event(evt, client=self.client, dvm_config=dvm_config)
def check_subscriptions():
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 subscription_status["expires"]:
# if subscription expires, just note it as not active
update_subscription_sql_table(dvm_config.DB, subscription_status["subscriptionId"],
subscription.recipent,
subscription.subscriber, subscription.nwc,
subscription.cadence, subscription.amount,
subscription.unit,
subscription.begin, subscription.end,
subscription.tier_dtag, subscription.zaps,
subscription.recipe,
False,
Timestamp.now().as_secs(), subscription.tier)
else:
handle_subscription_renewal(subscription)
else:
handle_expired_subscription(subscription)
print(str(Timestamp.now().as_secs()) + ": Checking " + str(
len(subscriptions)) + " Subscription entries..")
self.client.handle_notifications(NotificationHandler())
try:
while True:
time.sleep(60.0)
check_subscriptions()
except KeyboardInterrupt:
print('Stay weird!')
os.kill(os.getpid(), signal.SIGTERM)

View File

@@ -6,30 +6,32 @@ Reusable backend functions can be defined in backends (e.g. API calls)
Current List of Tasks:
| Module | Kind | Description | Backend |
|------------------------------|--------|------------------------------------------------------------|------------------|
| TextExtractionPDF | 5000 | Extracts Text from a PDF file | local |
| SpeechToTextGoogle | 5000 | Extracts Speech from Media files via Google Services | googleAPI |
| SpeechToTextWhisperX | 5000 | Extracts Speech from Media files via local WhisperX | nserver |
| ImageInterrogator | 5000 | Extracts Prompts from Images | nserver |
| TextSummarizationHuggingChat | 5001 | Summarizes given Input | huggingface |
| TranslationGoogle | 5002 | Translates Inputs to another language | googleAPI |
| TranslationLibre | 5002 | Translates Inputs to another language | libreAPI |
| TextGenerationLLMLite | 5050 | Chat with LLM backends like Ollama, ChatGPT etc | local/api/openai |
| TextGenerationHuggingChat | 5050 | Chat with LLM backend on Huggingface | huggingface |
| ImageGenerationSDXL | 5100 | Generates an Image from Prompt with Stable Diffusion XL | nserver |
| ImageGenerationSDXLIMG2IMG | 5100 | Generates an Image from an Image with Stable Diffusion XL | nserver |
| ImageGenerationReplicateSDXL | 5100 | Generates an Image from Prompt with Stable Diffusion XL | replicate |
| ImageGenerationMLX | 5100 | Generates an Image with Stable Diffusion 2.1 on M1/2/3 Mac | mlx |
| ImageGenerationDALLE | 5100 | Generates an Image with OpenAI's Dall-E | openAI |
| ImageUpscale | 5100 | Upscales an Image | nserver |
| MediaConverter | 5200 | Converts a link of a media file and uploads it | openAI |
| VideoGenerationReplicateSVD | 5202 | Generates a Video from an Image | replicate |
| VideoGenerationSVD | 5202 | Generates a Video from an Image | nserver |
| TextToSpeech | 5250 | Generate Audio from a prompt | local |
| TrendingNotesNostrBand | 5300 | Show trending notes on nostr.band | nostr.band api |
| DiscoverInactiveFollows | 5301 | Find inactive Nostr users | local |
| AdvancedSearch | 5302 | Search Content on nostr.band | local/nostr.band |
| Module | Kind | Description | Backend |
|--------------------------------|--------|------------------------------------------------------------|------------------|
| TextExtractionPDF | 5000 | Extracts Text from a PDF file | local |
| SpeechToTextGoogle | 5000 | Extracts Speech from Media files via Google Services | googleAPI |
| SpeechToTextWhisperX | 5000 | Extracts Speech from Media files via local WhisperX | nserver |
| ImageInterrogator | 5000 | Extracts Prompts from Images | nserver |
| TextSummarizationHuggingChat | 5001 | Summarizes given Input | huggingface |
| TranslationGoogle | 5002 | Translates Inputs to another language | googleAPI |
| TranslationLibre | 5002 | Translates Inputs to another language | libreAPI |
| TextGenerationLLMLite | 5050 | Chat with LLM backends like Ollama, ChatGPT etc | local/api/openai |
| TextGenerationHuggingChat | 5050 | Chat with LLM backend on Huggingface | huggingface |
| TextGenerationLLMUnleashedChat | 5050 | Chat with unleashed.chat LLMs | unleashed api |
| ImageGenerationSDXL | 5100 | Generates an Image from Prompt with Stable Diffusion XL | nserver |
| ImageGenerationSDXLIMG2IMG | 5100 | Generates an Image from an Image with Stable Diffusion XL | nserver |
| ImageGenerationReplicateSDXL | 5100 | Generates an Image from Prompt with Stable Diffusion XL | replicate |
| ImageGenerationMLX | 5100 | Generates an Image with Stable Diffusion 2.1 on M1/2/3 Mac | mlx |
| ImageGenerationDALLE | 5100 | Generates an Image with OpenAI's Dall-E | openAI |
| ImageUpscale | 5100 | Upscales an Image | nserver |
| MediaConverter | 5200 | Converts a link of a media file and uploads it | openAI |
| VideoGenerationReplicateSVD | 5202 | Generates a Video from an Image | replicate |
| VideoGenerationSVD | 5202 | Generates a Video from an Image | nserver |
| TextToSpeech | 5250 | Generate Audio from a prompt | local |
| TrendingNotesNostrBand | 5300 | Show trending notes on nostr.band | nostr.band api |
| DiscoverInactiveFollows | 5301 | Find inactive Nostr users | local |
| AdvancedSearch | 5302 | Search Content on relays (nostr.band) | local/nostr.band |
| AdvancedSearchWine | 5302 | Search Content on nostr.wine | api/nostr.wine |
Kinds with (inoff) are suggestions and not merged yet and might change in the future.
Backends might require to add an API key to the .env file or run an external server/framework the dvm will communicate with.

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, ClientSigner
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,16 +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
dependencies = [("nostr-dvm", "nostr-dvm")]
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:
@@ -48,11 +49,12 @@ class AdvancedSearch(DVMTaskInterface):
# default values
user = ""
users = []
since_days = 800 # days ago
until_days = 0 # days ago
since_seconds = Timestamp.now().as_secs() - (365 * 24 * 60 * 60)
until_seconds = Timestamp.now().as_secs()
search = ""
max_results = 20
max_results = 100
relay = "wss://relay.nostr.band"
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
@@ -61,23 +63,24 @@ class AdvancedSearch(DVMTaskInterface):
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "user": # check for param type
user = tag.as_vec()[2]
#user = tag.as_vec()[2]
users.append(Tag.parse(["p", tag.as_vec()[2]]))
elif param == "users": # check for param type
users = json.loads(tag.as_vec()[2])
elif param == "since": # check for param type
since_days = int(tag.as_vec()[2])
since_seconds = int(tag.as_vec()[2])
elif param == "until": # check for param type
until_days = int(tag.as_vec()[2])
until_seconds = min(int(tag.as_vec()[2]), until_seconds)
elif param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
options = {
"search": search,
"user": user,
"users": users,
"since": since_days,
"until": until_days,
"max_results": max_results
"since": since_seconds,
"until": until_seconds,
"max_results": max_results,
"relay": relay
}
request_form['options'] = json.dumps(options)
return request_form
@@ -88,23 +91,29 @@ class AdvancedSearch(DVMTaskInterface):
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.from_sk_str(sk.to_hex())
signer = ClientSigner.keys(keys)
keys = Keys.parse(sk.to_hex())
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
cli.add_relay("wss://relay.nostr.band")
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts(options["relay"], ropts)
cli.connect()
search_since_seconds = int(options["since"]) * 24 * 60 * 60
dif = Timestamp.now().as_secs() - search_since_seconds
search_since = Timestamp.from_secs(dif)
#earch_since_seconds = int(options["since"]) * 24 * 60 * 60
#dif = Timestamp.now().as_secs() - search_since_seconds
#search_since = Timestamp.from_secs(dif)
search_since = Timestamp.from_secs(int(options["since"]))
search_until_seconds = int(options["until"]) * 24 * 60 * 60
dif = Timestamp.now().as_secs() - search_until_seconds
search_until = Timestamp.from_secs(dif)
#search_until_seconds = int(options["until"]) * 24 * 60 * 60
#dif = Timestamp.now().as_secs() - search_until_seconds
#search_until = Timestamp.from_secs(dif)
search_until = Timestamp.from_secs(int(options["until"]))
userkeys = []
for user in options["users"]:
user = user[1]
tag = Tag.parse(user)
user = tag.as_vec()[1]
#user = user[1]
user = str(user).lstrip("@")
if str(user).startswith('npub'):
userkey = PublicKey.from_bech32(user)
@@ -115,22 +124,13 @@ class AdvancedSearch(DVMTaskInterface):
userkeys.append(userkey)
if not options["users"] and options["user"] == "":
notes_filter = Filter().kind(1).search(options["search"]).since(search_since).until(search_until).limit(
options["max_results"])
if not options["users"]:
notes_filter = Filter().kind(Kind(1)).search(options["search"]).since(search_since).until(search_until).limit(options["max_results"])
elif options["search"] == "":
if options["users"]:
notes_filter = Filter().kind(1).authors(userkeys).since(search_since).until(
search_until).limit(options["max_results"])
else:
notes_filter = Filter().kind(1).authors([PublicKey.from_hex(options["user"])]).since(search_since).until(
notes_filter = Filter().kind(Kind(1)).authors(userkeys).since(search_since).until(
search_until).limit(options["max_results"])
else:
if options["users"]:
notes_filter = Filter().kind(1).authors(userkeys).search(options["search"]).since(
search_since).until(search_until).limit(options["max_results"])
else:
notes_filter = Filter().kind(1).authors([PublicKey.from_hex(options["user"])]).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"])
@@ -141,7 +141,7 @@ class AdvancedSearch(DVMTaskInterface):
for event in events:
e_tag = Tag.parse(["e", event.id().to_hex()])
print(e_tag.as_vec())
#print(e_tag.as_vec())
result_list.append(e_tag.as_vec())
return json.dumps(result_list)
@@ -163,29 +163,29 @@ class AdvancedSearch(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
admin_config.LUD16 = dvm_config.LN_ADDRESS
dvm_config.USE_OWN_VENV = False
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"about": "I search notes",
"image": "https://nostr.band/android-chrome-192x192.png",
"about": "I search notes on Nostr.band.",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {
"user": {
"users": {
"required": False,
"values": [],
"description": "Do the task for another user"
"description": "Search for content from specific users"
},
"since": {
"required": False,
"values": [],
"description": "The number of days in the past from now the search should include"
"description": "A unix timestamp in the past from where the search should start"
},
"until": {
"required": False,
"values": [],
"description": "The number of days in the past from now the search should include up to"
"description": "A unix timestamp that tells until the search should include results"
},
"max_results": {
"required": False,
@@ -199,8 +199,10 @@ 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)
options = {"relay": "wss://relay.nostr.band"}
return AdvancedSearch(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
admin_config=admin_config, options=options)
if __name__ == '__main__':

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, ClientSigner, 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,16 +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
dependencies = [("nostr-dvm", "nostr-dvm")]
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:
@@ -50,8 +51,8 @@ class AdvancedSearchWine(DVMTaskInterface):
# default values
user = ""
users = []
since_days = 800 # days ago
until_days = 365 # days ago
since_seconds = Timestamp.now().as_secs() - (365 * 24 * 60 * 60)
until_seconds = Timestamp.now().as_secs()
search = ""
max_results = 100
@@ -68,9 +69,9 @@ class AdvancedSearchWine(DVMTaskInterface):
elif param == "users": # check for param type
users = json.loads(tag.as_vec()[2])
elif param == "since": # check for param type
since_days = int(tag.as_vec()[2])
since_seconds = int(tag.as_vec()[2])
elif param == "until": # check for param type
until_days = int(tag.as_vec()[2])
until_seconds = int(tag.as_vec()[2])
elif param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
@@ -78,8 +79,8 @@ class AdvancedSearchWine(DVMTaskInterface):
"search": search,
"user": user,
"users": users,
"since": since_days,
"until": until_days,
"since": since_seconds,
"until": until_seconds,
"max_results": max_results,
}
@@ -91,7 +92,8 @@ class AdvancedSearchWine(DVMTaskInterface):
options = DVMTaskInterface.set_options(request_form)
userkeys = []
for user in options["users"]:
user = user[1]
tag = Tag.parse(user)
user = tag.as_vec()[1]
user = str(user).lstrip("@")
if str(user).startswith('npub'):
userkey = PublicKey.from_bech32(user)
@@ -102,23 +104,27 @@ class AdvancedSearchWine(DVMTaskInterface):
userkeys.append(userkey)
print("Sending job to Server")
print("Sending job to nostr.wine API")
if options["users"]:
url = ('https://api.nostr.wine/search?query=' + options["search"] + '&kind=1' + '&pubkey=' + options["users"][0] + "&limit=100" + "&sort=time")
url = ('https://api.nostr.wine/search?query=' + options["search"] + '&kind=1' + '&pubkey=' + options["users"][0][1] + "&limit=100" + "&sort=time" + "&until=" + str(options["until"]) + "&since=" + str(options["since"]))
else:
url = ('https://api.nostr.wine/search?query=' + options["search"] + '&kind=1' + "&limit=100" + "&sort=time")
url = ('https://api.nostr.wine/search?query=' + options["search"] + '&kind=1' + "&limit=100" + "&sort=time" + "&until=" + str(options["until"]) + "&since=" + str(options["since"]))
headers = {'Content-type': 'application/x-www-form-urlencoded'}
response = requests.get(url, headers=headers)
print(response.text)
ob = json.loads(response.text)
data = ob['data']
print(data)
#print(response.text)
result_list = []
for el in data:
e_tag = Tag.parse(["e", el['id']])
print(e_tag.as_vec())
result_list.append(e_tag.as_vec())
try:
ob = json.loads(response.text)
data = ob['data']
for el in data:
try:
e_tag = Tag.parse(["e", el['id']])
result_list.append(e_tag.as_vec())
except Exception as e:
print("ERROR: " + str(e))
except Exception as e:
print(e)
return json.dumps(result_list)
@@ -140,6 +146,7 @@ class AdvancedSearchWine(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
# Add NIP89
nip89info = {
"name": name,

View File

@@ -0,0 +1,266 @@
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, 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
"""
This File contains a Module to discover popular notes
Accepted Inputs: none
Outputs: A list of events
Params: None
"""
class DicoverContentCurrentlyPopular(DVMTaskInterface):
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, 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)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
self.dvm_config = dvm_config
print(self.dvm_config.PRIVATE_KEY)
request_form = {"jobID": event.id().to_hex()}
# default values
search = ""
max_results = 100
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
options = {
"max_results": max_results,
}
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()
options = DVMTaskInterface.set_options(request_form)
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)
database = NostrDatabase.sqlite("db/nostr_recent_notes.db")
cli = ClientBuilder().database(database).signer(signer).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.connect()
# Negentropy reconciliation
# Query events from database
timestamp_hour_ago = Timestamp.now().as_secs() - 3600
lasthour = Timestamp.from_secs(timestamp_hour_ago)
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([definitions.EventDefinitions.KIND_ZAP, definitions.EventDefinitions.KIND_REPOST, 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]))
e_tag = Tag.parse(["e", entry[0]])
result_list.append(e_tag.as_vec())
return json.dumps(result_list)
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
def schedule(self, dvm_config):
if dvm_config.SCHEDULE_UPDATES_SECONDS == 0:
return 0
else:
if Timestamp.now().as_secs() >= self.last_schedule + dvm_config.SCHEDULE_UPDATES_SECONDS:
self.sync_db()
self.last_schedule = Timestamp.now().as_secs()
return 1
def sync_db(self):
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)
database = NostrDatabase.sqlite("db/nostr_recent_notes.db")
cli = ClientBuilder().signer(signer).database(database).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.connect()
timestamp_hour_ago = Timestamp.now().as_secs() - 3600
lasthour = Timestamp.from_secs(timestamp_hour_ago)
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.")
# 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)
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/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,
"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)
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = False
return DicoverContentCurrentlyPopular(name=name, dvm_config=dvm_config, nip89config=nip89config,
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 all over Nostr. I'm also used for testing subscriptions.",
"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.PERK2DESC = "Support NostrDVM & NostrSDK development"
nip88config.PAYMENT_VERIFIER_PUBKEY = "5b5c045ecdf66fb540bdf2049fe0ef7f1a566fa427a4fe50d400a011b65a3a7e"
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = False
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__':
process_venv(DicoverContentCurrentlyPopular)

View File

@@ -0,0 +1,307 @@
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, Kind, \
RelayOptions, RelayLimits
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
"""
This File contains a Module to discover popular notes
Accepted Inputs: none
Outputs: A list of events
Params: None
"""
class DicoverContentCurrentlyPopularFollowers(DVMTaskInterface):
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, 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)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event: Event, client=None, dvm_config=None):
self.dvm_config = dvm_config
print(self.dvm_config.PRIVATE_KEY)
request_form = {"jobID": event.id().to_hex()}
# default values
user = event.author().to_hex()
max_results = 100
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
elif param == "user": # check for param type
user = tag.as_vec()[2]
options = {
"max_results": max_results,
"user": user,
}
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()
options = DVMTaskInterface.set_options(request_form)
relaylimits = RelayLimits.disable()
opts = (Options().wait_for_send(True).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)).relay_limits(relaylimits))
sk = SecretKey.from_hex(self.dvm_config.PRIVATE_KEY)
keys = Keys.parse(sk.to_hex())
signer = NostrSigner.keys(keys)
database = NostrDatabase.sqlite("db/nostr_recent_notes2.db")
cli = ClientBuilder().database(database).signer(signer).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.add_relay("wss://nos.lol")
cli.add_relay("wss://pablof7z.nostr1.com")
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts("wss://nostr.band", ropts)
cli.connect()
user = PublicKey.parse(options["user"])
followers_filter = Filter().author(user).kinds([Kind(3)])
followers = cli.get_events_of([followers_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
print(followers)
# Negentropy reconciliation
# Query events from database
timestamp_hour_ago = Timestamp.now().as_secs() - 7200
lasthour = Timestamp.from_secs(timestamp_hour_ago)
result_list = []
if len(followers) > 0:
newest = 0
best_entry = followers[0]
for entry in followers:
if entry.created_at().as_secs() > newest:
newest = entry.created_at().as_secs()
best_entry = entry
print(best_entry.as_json())
followings = []
for tag in best_entry.tags():
if tag.as_vec()[0] == "p":
following = PublicKey.parse(tag.as_vec()[1])
followings.append(following)
filter1 = Filter().kind(definitions.EventDefinitions.KIND_NOTE).authors(followings).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(
[definitions.EventDefinitions.KIND_ZAP, definitions.EventDefinitions.KIND_REACTION, definitions.EventDefinitions.KIND_REPOST,
definitions.EventDefinitions.KIND_NOTE]).event(event.id()).since(lasthour)
reactions = cli.database().query([filt])
ns.finallist[event.id().to_hex()] = len(reactions)
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]))
e_tag = Tag.parse(["e", entry[0]])
result_list.append(e_tag.as_vec())
return json.dumps(result_list)
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
def schedule(self, dvm_config):
if dvm_config.SCHEDULE_UPDATES_SECONDS == 0:
return 0
# We simply use the db from the other dvm that contains all notes
else:
if Timestamp.now().as_secs() >= self.last_schedule + dvm_config.SCHEDULE_UPDATES_SECONDS:
self.sync_db()
self.last_schedule = Timestamp.now().as_secs()
return 1
def sync_db(self):
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)
database = NostrDatabase.sqlite("db/nostr_recent_notes2.db")
cli = ClientBuilder().signer(signer).database(database).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.connect()
timestamp_hour_ago = Timestamp.now().as_secs() - 7200
lasthour = Timestamp.from_secs(timestamp_hour_ago)
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.. 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() - 7200))) # Clear old events so db doesnt get too full.
print("Done Syncing Notes of Last hour.")
# 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)
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/d92652a6a07677e051d647dcf9f0f59e265299b3335a939d008183a911513f4a.jpg",
"about": "I show notes that are currently popular from people you follow",
"lud16": dvm_config.LN_ADDRESS,
"encryptionSupported": True,
"cashuAccepted": True,
"amount": "free",
"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)
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = False
return DicoverContentCurrentlyPopularFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config,
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)",
"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.PERK2DESC = "Support NostrDVM & NostrSDK development"
nip88config.PAYMENT_VERIFIER_PUBKEY = "5b5c045ecdf66fb540bdf2049fe0ef7f1a566fa427a4fe50d400a011b65a3a7e"
admin_config.UPDATE_PROFILE = False
admin_config.REBROADCAST_NIP89 = False
admin_config.REBROADCAST_NIP88 = False
# admin_config.FETCH_NIP88 = True
# admin_config.EVENTID = "63a791cdc7bf78c14031616963105fce5793f532bb231687665b14fb6d805fdb"
# admin_config.PRIVKEY = dvm_config.PRIVATE_KEY
return DicoverContentCurrentlyPopularFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config,
nip88config=nip88config,
admin_config=admin_config)
if __name__ == '__main__':
process_venv(DicoverContentCurrentlyPopularFollowers)

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:
@@ -79,6 +84,7 @@ class MediaConverter(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {

View File

@@ -0,0 +1,203 @@
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, 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
"""
This File contains a Module to search for notes
Accepted Inputs: a search query
Outputs: A list of events
Params: None
"""
class DiscoveryBotFarms(DVMTaskInterface):
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "discover-bot-farms"
FIX_COST: float = 0
dvm_config: DVMConfig
last_schedule: int = 0
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)
use_logger = False
if use_logger:
init_logger(LogLevel.DEBUG)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
self.dvm_config = dvm_config
print(self.dvm_config.PRIVATE_KEY)
request_form = {"jobID": event.id().to_hex()}
# default values
search = "@nostrich.house"
max_results = 500
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
if input_type == "text":
search = tag.as_vec()[1]
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
options = {
"search": search,
"max_results": max_results,
}
request_form['options'] = json.dumps(options)
return request_form
def process(self, request_form):
from nostr_sdk import Filter
options = DVMTaskInterface.set_options(request_form)
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)
database = NostrDatabase.sqlite("db/nostr_profiles.db")
cli = ClientBuilder().database(database).signer(signer).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
# cli.add_relay("wss://atl.purplerelay.com")
cli.connect()
# Negentropy reconciliation
# Query events from database
filter1 = Filter().kind(Kind(0))
events = cli.database().query([filter1])
# for event in events:
# print(event.as_json())
# events = cli.get_events_of([notes_filter], timedelta(seconds=5))
result_list = []
print("Events: " + str(len(events)))
searchterms = str(options["search"]).split(";")
index = 0
if len(events) > 0:
for event in events:
if index < options["max_results"]:
try:
if any(ext in event.content().lower() for ext in searchterms):
p_tag = Tag.parse(["p", event.author().to_hex()])
print(event.as_json())
result_list.append(p_tag.as_vec())
index += 1
except Exception as exp:
print(str(exp) + " " + event.author().to_hex())
else:
break
return json.dumps(result_list)
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
def schedule(self, dvm_config):
if dvm_config.SCHEDULE_UPDATES_SECONDS == 0:
return 0
else:
if Timestamp.now().as_secs() >= self.last_schedule + dvm_config.SCHEDULE_UPDATES_SECONDS:
self.sync_db()
self.last_schedule = Timestamp.now().as_secs()
return 1
def sync_db(self):
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)
database = NostrDatabase.sqlite("db/nostr_profiles.db")
cli = ClientBuilder().signer(signer).database(database).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.connect()
filter1 = Filter().kind(Kind(0))
# filter = Filter().author(keys.public_key())
print("Syncing Profile Database.. this might take a while..")
dbopts = NegentropyOptions().direction(NegentropyDirection.DOWN)
cli.reconcile(filter1, dbopts)
print("Done Syncing Profile Database.")
# 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)
dvm_config.USE_OWN_VENV = False
dvm_config.SHOWLOG = True
dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 seconds
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/981b560820bc283c58de7989b7abc6664996b487a531d852e4ef7322586a2122.jpg",
"about": "I hunt down bot farms.",
"encryptionSupported": True,
"cashuAccepted": True,
"action": "mute", # follow, unfollow, mute, unmute
"nip90Params": {
"max_results": {
"required": False,
"values": [],
"description": "The number of maximum results to return (default currently 20)"
}
}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
options = {"relay": "wss://relay.damus.io"}
return DiscoveryBotFarms(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config, options=options)
if __name__ == '__main__':
process_venv(DiscoveryBotFarms)

View File

@@ -0,0 +1,197 @@
import json
import os
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind, RelayOptions, \
RelayLimits, Event
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 DiscoverReports(DVMTaskInterface):
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "people to mute"
FIX_COST: float = 0
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):
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
users = []
sender = event.author().to_hex()
since_days = 90
# users.append(event.author().to_hex())
for tag in event.tags():
if tag.as_vec()[0] == 'i':
users.append(tag.as_vec()[1])
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "since_days": # check for param type
since_days = int(tag.as_vec()[2])
options = {
"users": users,
"sender": sender,
"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()
relaylimits = RelayLimits.disable()
opts = (
Options().wait_for_send(False).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)).relay_limits(
relaylimits))
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)
# cli.add_relay("wss://relay.nostr.band")
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
# add nostr band, too.
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts("wss://nostr.band", ropts)
cli.connect()
options = DVMTaskInterface.set_options(request_form)
step = 20
pubkeys = []
for user in options["users"]:
pubkeys.append(PublicKey.parse(user))
# if we don't add users, e.g. by a wot, we check all our followers.
if len(pubkeys) == 0:
followers_filter = Filter().author(PublicKey.parse(options["sender"])).kind(Kind(3))
followers = cli.get_events_of([followers_filter], timedelta(seconds=5))
if len(followers) > 0:
result_list = []
newest = 0
best_entry = followers[0]
for entry in followers:
print(len(best_entry.tags()))
print(best_entry.created_at().as_secs())
if entry.created_at().as_secs() > newest:
newest = entry.created_at().as_secs()
best_entry = entry
for tag in best_entry.tags():
if tag.as_vec()[0] == "p":
following = PublicKey.parse(tag.as_vec()[1])
pubkeys.append(following)
ago = Timestamp.now().as_secs() - 60*60*24*int(options["since_days"]) #TODO make this an option, 180 days for now
since = Timestamp.from_secs(ago)
kind1984_filter = Filter().authors(pubkeys).kind(Kind(1984)).since(since)
reports = cli.get_events_of([kind1984_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
bad_actors = []
ns.dic = {}
reasons = ["spam", "illegal", "impersonation"]
# init
for report in reports:
for tag in report.tags():
if tag.as_vec()[0] == "p":
ns.dic[tag.as_vec()[1]] = 0
for report in reports:
#print(report.as_json())
for tag in report.tags():
if tag.as_vec()[0] == "p":
if len(tag.as_vec()) > 2 and tag.as_vec()[2] in reasons or len(tag.as_vec()) <= 2:
ns.dic[tag.as_vec()[1]] += 1
#print(ns.dic.items())
# result = {k for (k, v) in ns.dic.items() if v > 0}
# result = sorted(ns.dic.items(), key=lambda x: x[1], reverse=True)
finallist_sorted = sorted(ns.dic.items(), key=lambda x: x[1], reverse=True)
converted_dict = dict(finallist_sorted)
print(json.dumps(converted_dict))
for k, v in converted_dict.items():
print(k)
p_tag = Tag.parse(["p", k, str(v)])
bad_actors.append(p_tag.as_vec())
print(json.dumps(bad_actors))
return json.dumps(bad_actors)
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)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/19872a2edd866258fa9eab137631efda89310d52b2c6ea8f99ef057325aa1c7b.jpg",
"about": "I show users that have been reported by either your followers or your Web of Trust. Note: Anyone can report, so you might double check and decide for yourself who to mute. Considers spam, illegal and impersonation reports. Notice: This works with NIP51 mute lists. Not all clients support the new mute list format.",
"encryptionSupported": True,
"cashuAccepted": True,
"action": "mute", # follow, unfollow, mute, unmute
"nip90Params": {
"since_days": {
"required": False,
"values": [],
"description": "The number of days a report is ago in order to be considered "
}
}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
return DiscoverReports(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
if __name__ == '__main__':
process_venv(DiscoverReports)

View File

@@ -3,12 +3,14 @@ import os
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, ClientSigner
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind, RelayOptions, \
RelayLimits
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 +24,17 @@ Params: None
class DiscoverInactiveFollows(DVMTaskInterface):
KIND: int = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "inactive-follows"
FIX_COST: float = 50
KIND: Kind = EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY
TASK: str = "inactive-followings"
FIX_COST: float = 100
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,39 +69,58 @@ class DiscoverInactiveFollows(DVMTaskInterface):
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.from_sk_str(sk.to_hex())
signer = ClientSigner.keys(keys)
keys = Keys.parse(sk.to_hex())
signer = NostrSigner.keys(keys)
#relaylimits = RelayLimits().event_max_num_tags(max_num_tags=10000)
#relaylimits.event_max_size(None)
relaylimits = RelayLimits.disable()
opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))).relay_limits(relaylimits)
cli = Client.with_opts(signer, opts)
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts("wss://nostr.band", ropts)
cli.connect()
options = DVMTaskInterface.set_options(request_form)
step = 20
followers_filter = Filter().author(PublicKey.from_hex(options["user"])).kind(3).limit(1)
followers = cli.get_events_of([followers_filter], timedelta(seconds=self.dvm_config.RELAY_TIMEOUT))
followers_filter = Filter().author(PublicKey.parse(options["user"])).kind(Kind(3))
followers = cli.get_events_of([followers_filter], timedelta(seconds=5))
if len(followers) > 0:
result_list = []
newest = 0
best_entry = followers[0]
for entry in followers:
print(len(best_entry.tags()))
print(best_entry.created_at().as_secs())
if entry.created_at().as_secs() > newest:
newest = entry.created_at().as_secs()
best_entry = entry
print(best_entry.as_json())
print(len(best_entry.tags()))
print(best_entry.created_at().as_secs())
print(Timestamp.now().as_secs())
followings = []
ns.dic = {}
tagcount = 0
for tag in best_entry.tags():
tagcount += 1
if tag.as_vec()[0] == "p":
following = tag.as_vec()[1]
followings.append(following)
ns.dic[following] = "False"
print("Followings: " + str(len(followings)))
print("Followings: " + str(len(followings)) + " Tags: " + str(tagcount))
not_active_since_seconds = int(options["since_days"]) * 24 * 60 * 60
dif = Timestamp.now().as_secs() - not_active_since_seconds
@@ -107,10 +129,10 @@ class DiscoverInactiveFollows(DVMTaskInterface):
def scanList(users, instance, i, st, notactivesince):
from nostr_sdk import Filter
keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY)
keys = Keys.parse(self.dvm_config.PRIVATE_KEY)
opts = Options().wait_for_send(True).send_timeout(
timedelta(seconds=5)).skip_disconnected_relays(True)
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
@@ -122,7 +144,7 @@ class DiscoverInactiveFollows(DVMTaskInterface):
filters.append(filter1)
event_from_authors = cli.get_events_of(filters, timedelta(seconds=10))
for author in event_from_authors:
instance.dic[author.pubkey().to_hex()] = "True"
instance.dic[author.author().to_hex()] = "True"
print(str(i) + "/" + str(len(users)))
cli.disconnect()
@@ -175,12 +197,14 @@ class DiscoverInactiveFollows(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"about": "I discover users you follow, but that have been inactive on Nostr",
"action": "unfollow", #follow, mute, unmute
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {

View File

@@ -0,0 +1,228 @@
import json
import os
from datetime import timedelta
from threading import Thread
from nostr_sdk import Client, Timestamp, PublicKey, Tag, Keys, Options, SecretKey, NostrSigner, Kind, RelayOptions, \
RelayLimits
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 DiscoverNonFollowers(DVMTaskInterface):
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, 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()
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]
options = {
"user": user,
}
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()
relaylimits = RelayLimits.disable()
opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)).relay_limits(relaylimits))
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)
# cli.add_relay("wss://relay.nostr.band")
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
#add nostr band, too.
ropts = RelayOptions().ping(False)
cli.add_relay_with_opts("wss://nostr.band", ropts)
cli.connect()
options = DVMTaskInterface.set_options(request_form)
step = 20
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:
result_list = []
newest = 0
best_entry = followers[0]
for entry in followers:
if entry.created_at().as_secs() > newest:
newest = entry.created_at().as_secs()
best_entry = entry
print(best_entry.as_json())
followings = []
ns.dic = {}
for tag in best_entry.tags():
if tag.as_vec()[0] == "p":
following = tag.as_vec()[1]
followings.append(following)
ns.dic[following] = "True"
print("Followings: " + str(len(followings)))
def scanList(users, instance, i, st):
from nostr_sdk import Filter
keys = Keys.parse(self.dvm_config.PRIVATE_KEY)
opts = Options().wait_for_send(True).send_timeout(
timedelta(seconds=5)).skip_disconnected_relays(True)
signer = NostrSigner.keys(keys)
cli = Client.with_opts(signer, opts)
for relay in self.dvm_config.RELAY_LIST:
cli.add_relay(relay)
cli.connect()
for i in range(i, i + st):
filters = []
filter1 = Filter().author(PublicKey.from_hex(users[i])).kind(Kind(3))
filters.append(filter1)
followers = cli.get_events_of(filters, timedelta(seconds=3))
if len(followers) > 0:
result_list = []
newest = 0
best_entry = followers[0]
for entry in followers:
if entry.created_at().as_secs() > newest:
newest = entry.created_at().as_secs()
best_entry = entry
foundfollower = False
for tag in best_entry.tags():
if tag.as_vec()[0] == "p":
if len(tag.as_vec()) > 1:
if tag.as_vec()[1] == options["user"]:
foundfollower = True
break
if not foundfollower:
instance.dic[best_entry.author().to_hex()] = "False"
print("DIDNT FIND " + best_entry.author().to_nostr_uri())
print(str(i) + "/" + str(len(users)))
cli.disconnect()
threads = []
begin = 0
# Spawn some threads to speed things up
while begin < len(followings) - step:
args = [followings, ns, begin, step]
t = Thread(target=scanList, args=args)
threads.append(t)
begin = begin + step - 1
# last to step size
missing_scans = (len(followings) - begin)
args = [followings, ns, begin, missing_scans]
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()
result = {k for (k, v) in ns.dic.items() if v == "False"}
print("Non backfollowing accounts found: " + str(len(result)))
for k in result:
p_tag = Tag.parse(["p", k])
result_list.append(p_tag.as_vec())
return json.dumps(result_list)
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)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"about": "I discover users you follow, but that don't follow you back.",
"encryptionSupported": True,
"cashuAccepted": True,
"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 DiscoverNonFollowers(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
if __name__ == '__main__':
process_venv(DiscoverNonFollowers)

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"""
@@ -96,13 +99,15 @@ class TrendingNotesNostrBand(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"image": "https://nostr.band/android-chrome-192x192.png",
"about": "I show trending notes from nostr.band",
"amount": "Free",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {}

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 = 70
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
@@ -55,12 +59,12 @@ class ImageUpscale(DVMTaskInterface):
elif tag.as_vec()[0] == 'param':
print("Param: " + tag.as_vec()[1] + ": " + tag.as_vec()[2])
if tag.as_vec()[1] == "upscale":
out_scale = tag.as_vec()[2]
out_scale = int(tag.as_vec()[2])
io_input_image = {
"id": "input_image",
"type": "input",
"src": "url:Image",
"src": "url:image",
"uri": url
}
@@ -72,10 +76,15 @@ class ImageUpscale(DVMTaskInterface):
request_form['data'] = json.dumps([io_input_image, io_output])
options = {
"outscale": out_scale,
options = {"model": "RealESRGAN_x4plus",
"outscale": out_scale,
"denoise_strength": 0.5,
"tile": 0,
"tile_pad": 10,
"pre_pad": 0,
"compute_type": "fp32",
"face_enhance": False}
}
request_form['options'] = json.dumps(options)
return request_form

View File

@@ -0,0 +1,215 @@
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, 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
"""
This File contains a Module to search for notes
Accepted Inputs: a search query
Outputs: A list of events
Params: None
"""
class SearchUser(DVMTaskInterface):
KIND: Kind = EventDefinitions.KIND_NIP90_USER_SEARCH
TASK: str = "search-user"
FIX_COST: float = 0
dvm_config: DVMConfig
last_schedule: int = 0
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)
use_logger = False
if use_logger:
init_logger(LogLevel.DEBUG)
self.sync_db()
def is_input_supported(self, tags, client=None, dvm_config=None):
for tag in tags:
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
self.dvm_config = dvm_config
print(self.dvm_config.PRIVATE_KEY)
request_form = {"jobID": event.id().to_hex()}
# default values
search = ""
max_results = 100
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
if input_type == "text":
search = tag.as_vec()[1]
elif tag.as_vec()[0] == 'param':
param = tag.as_vec()[1]
if param == "max_results": # check for param type
max_results = int(tag.as_vec()[2])
options = {
"search": search,
"max_results": max_results,
}
request_form['options'] = json.dumps(options)
return request_form
def process(self, request_form):
from nostr_sdk import Filter
options = DVMTaskInterface.set_options(request_form)
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)
database = NostrDatabase.sqlite("db/nostr_profiles.db")
cli = ClientBuilder().database(database).signer(signer).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
# cli.add_relay("wss://atl.purplerelay.com")
cli.connect()
# Negentropy reconciliation
# Query events from database
filter1 = Filter().kind(Kind(0))
events = cli.database().query([filter1])
# for event in events:
# print(event.as_json())
# events = cli.get_events_of([notes_filter], timedelta(seconds=5))
result_list = []
print("Events: " + str(len(events)))
index = 0
if len(events) > 0:
for event in events:
if index < options["max_results"]:
try:
if options["search"].lower() in event.content().lower():
p_tag = Tag.parse(["p", event.author().to_hex()])
print(event.as_json())
result_list.append(p_tag.as_vec())
index += 1
except Exception as exp:
print(str(exp) + " " + event.author().to_hex())
else:
break
return json.dumps(result_list)
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
def schedule(self, dvm_config):
if dvm_config.SCHEDULE_UPDATES_SECONDS == 0:
return 0
else:
if Timestamp.now().as_secs() >= self.last_schedule + dvm_config.SCHEDULE_UPDATES_SECONDS:
self.sync_db()
self.last_schedule = Timestamp.now().as_secs()
return 1
def sync_db(self):
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)
database = NostrDatabase.sqlite("db/nostr_profiles.db")
cli = ClientBuilder().signer(signer).database(database).opts(opts).build()
cli.add_relay("wss://relay.damus.io")
cli.connect()
filter1 = Filter().kind(Kind(0))
# filter = Filter().author(keys.public_key())
print("Syncing Profile Database.. this might take a while..")
dbopts = NegentropyOptions().direction(NegentropyDirection.DOWN)
cli.reconcile(filter1, dbopts)
print("Done Syncing Profile Database.")
# 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)
dvm_config.USE_OWN_VENV = False
dvm_config.SHOWLOG = True
dvm_config.SCHEDULE_UPDATES_SECONDS = 600 # Every 10 seconds
# Add NIP89
nip89info = {
"name": name,
"image": "https://image.nostr.build/a99ab925084029d9468fef8330ff3d9be2cf67da473b024f2a6d48b5cd77197f.jpg",
"about": "I search users.",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {
"users": {
"required": False,
"values": [],
"description": "Search for content from specific users"
},
"since": {
"required": False,
"values": [],
"description": "A unix timestamp in the past from where the search should start"
},
"until": {
"required": False,
"values": [],
"description": "A unix timestamp that tells until the search should include results"
},
"max_results": {
"required": False,
"values": [],
"description": "The number of maximum results to return (default currently 20)"
}
}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
options = {"relay": "wss://relay.damus.io"}
return SearchUser(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config, options=options)
if __name__ == '__main__':
process_venv(SearchUser)

View File

@@ -1,13 +1,15 @@
import json
import os
import re
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
from nostr_sdk import 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, Kind
"""
This File contains a Module to summarize Text, based on a prompt using a the HuggingChat LLM on Huggingface
@@ -18,16 +20,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:
@@ -42,15 +45,17 @@ class TextSummarizationHuggingChat(DVMTaskInterface):
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")}
prompt = ""
collect_events = []
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
if input_type == "text":
prompt = tag.as_vec()[1]
prompt += tag.as_vec()[1] + "\n"
elif input_type == "event":
evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvm_config)
prompt = evt.content()
collect_events.append(tag.as_vec()[1])
# evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvm_config)
# prompt += evt.content() + "\n"
elif input_type == "job":
evt = get_referenced_event_by_id(event_id=tag.as_vec()[1], client=client,
kinds=[EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT,
@@ -61,7 +66,7 @@ class TextSummarizationHuggingChat(DVMTaskInterface):
if evt is None:
print("Event not found")
raise Exception
if evt.kind() == EventDefinitions.KIND_NIP90_RESULT_CONTENT_DISCOVERY:
result_list = json.loads(evt.content())
prompt = ""
@@ -72,9 +77,17 @@ class TextSummarizationHuggingChat(DVMTaskInterface):
else:
prompt = evt.content()
evts = get_events_by_ids(collect_events, client=client, config=dvm_config)
if evts is not None:
for evt in evts:
prompt += evt.content() + "\n"
clean_prompt = re.sub(r'^https?:\/\/.*[\r\n]*', '', prompt, flags=re.MULTILINE)
options = {
"prompt": prompt,
"prompt": clean_prompt[:4000],
}
request_form['options'] = json.dumps(options)
return request_form
@@ -91,12 +104,11 @@ class TextSummarizationHuggingChat(DVMTaskInterface):
cookies = sign.login()
sign.saveCookiesToDir(cookie_path_dir)
options = DVMTaskInterface.set_options(request_form)
try:
chatbot = hugchat.ChatBot(cookies=cookies.get_dict()) # or cookie_path="usercookies/<email>.json"
query_result = chatbot.query("Summarize the following text in maximum 5 sentences: " + options["prompt"])
query_result = chatbot.query("Summarize the following notes: " + str(options["prompt"]))
print(query_result["text"]) # or query_result.text or query_result["text"]
return str(query_result["text"]).lstrip()
@@ -105,7 +117,6 @@ class TextSummarizationHuggingChat(DVMTaskInterface):
raise Exception(e)
# 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
@@ -115,8 +126,8 @@ def build_example(name, identifier, admin_config):
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"about": "I use a LLM connected via Huggingchat",
"image": "https://image.nostr.build/720eadc9af89084bb09de659af43ad17fec1f4b0887084e83ac0ae708dfa83a6.png",
"about": "I use a LLM connected via Huggingchat to summarize Inputs",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {}
@@ -127,7 +138,7 @@ def build_example(name, identifier, admin_config):
nip89config.CONTENT = json.dumps(nip89info)
return TextSummarizationHuggingChat(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config)
admin_config=admin_config)
if __name__ == '__main__':

View File

@@ -0,0 +1,170 @@
import json
import os
import re
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
"""
This File contains a Module to generate Text, based on a prompt using the Unleashed.chat API.
Accepted Inputs: Prompt (text)
Outputs: Generated text
"""
class SummarizationUnleashedChat(DVMTaskInterface):
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, 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):
for tag in tags:
if tag.as_vec()[0] == 'i':
print(tag.as_vec())
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "event" and input_type != "job" and input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")}
prompt = ""
collect_events = []
nostr_mode = True
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
if input_type == "text":
prompt += tag.as_vec()[1] + "\n"
elif input_type == "event":
collect_events.append(tag.as_vec()[1])
# evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvm_config)
# prompt += evt.content() + "\n"
elif input_type == "job":
evt = get_referenced_event_by_id(event_id=tag.as_vec()[1], client=client,
kinds=[EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT,
EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT,
EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT,
EventDefinitions.KIND_NIP90_RESULT_CONTENT_DISCOVERY],
dvm_config=dvm_config)
if evt is None:
print("Event not found")
raise Exception
if evt.kind() == EventDefinitions.KIND_NIP90_RESULT_CONTENT_DISCOVERY:
result_list = json.loads(evt.content())
prompt = ""
for tag in result_list:
e_tag = Tag.parse(tag)
evt = get_event_by_id(e_tag.as_vec()[1], client=client, config=dvm_config)
prompt += evt.content() + "\n"
else:
prompt = evt.content()
evts = get_events_by_ids(collect_events, client=client, config=dvm_config)
if evts is not None:
for evt in evts:
prompt += evt.content() + "\n"
clean_prompt = re.sub(r'^https?:\/\/.*[\r\n]*', '', prompt, flags=re.MULTILINE)
options = {
"prompt": clean_prompt[:4000],
"nostr": nostr_mode,
}
request_form['options'] = json.dumps(options)
return request_form
def process(self, request_form):
from openai import OpenAI
temp_open_ai_api_key = os.environ["OPENAI_API_KEY"]
os.environ["OPENAI_API_KEY"] = os.getenv("UNLEASHED_API_KEY")
options = DVMTaskInterface.set_options(request_form)
try:
client = OpenAI(
base_url='https://unleashed.chat/api/v1',
)
print('Models:\n')
for model in client.models.list():
print('- ' + model.id)
content = "Summarize the following notes: " + str(options["prompt"])
normal_stream = client.chat.completions.create(
messages=[
{
'role': 'user',
'content': content,
}
],
model='dolphin-2.2.1-mistral-7b',
stream=True,
extra_body={
'nostr_mode': options["nostr"],
},
)
print('\nChat response: ', end='')
result = ""
for chunk in normal_stream:
result += chunk.choices[0].delta.content
print(chunk.choices[0].delta.content, end='')
os.environ["OPENAI_API_KEY"] = temp_open_ai_api_key
return result
except Exception as e:
print("Error in Module: " + str(e))
raise Exception(e)
# 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)
dvm_config.SEND_FEEDBACK_EVENTS = True
admin_config.LUD16 = dvm_config.LN_ADDRESS
nip89info = {
"name": name,
"image": "https://unleashed.chat/_app/immutable/assets/hero.pehsu4x_.jpeg",
"about": "I summarize Text with https://unleashed.chat",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
admin_config2 = AdminConfig()
admin_config2.REBROADCAST_NIP89 = False
return SummarizationUnleashedChat(name=name, dvm_config=dvm_config, nip89config=nip89config,
admin_config=admin_config2)
if __name__ == '__main__':
process_venv(SummarizationUnleashedChat)

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:
@@ -89,7 +93,7 @@ def build_example(name, identifier, admin_config):
nip89info = {
"name": name,
"image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg",
"image": "https://image.nostr.build/720eadc9af89084bb09de659af43ad17fec1f4b0887084e83ac0ae708dfa83a6.png",
"about": "I use a LLM connected via Huggingchat",
"encryptionSupported": True,
"cashuAccepted": True,

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

@@ -0,0 +1,134 @@
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
"""
This File contains a Module to generate Text, based on a prompt using the Unleashed.chat API.
Accepted Inputs: Prompt (text)
Outputs: Generated text
"""
class TextGenerationUnleashedChat(DVMTaskInterface):
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, 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):
for tag in tags:
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type != "text":
return False
return True
def create_request_from_nostr_event(self, event, client=None, dvm_config=None):
request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")}
prompt = ""
nostr_mode= True
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_type = tag.as_vec()[2]
if input_type == "text":
prompt = tag.as_vec()[1]
options = {
"prompt": prompt,
"nostr": nostr_mode,
}
request_form['options'] = json.dumps(options)
return request_form
def process(self, request_form):
from openai import OpenAI
temp_open_ai_api_key = os.environ["OPENAI_API_KEY"]
os.environ["OPENAI_API_KEY"] = os.getenv("UNLEASHED_API_KEY")
options = DVMTaskInterface.set_options(request_form)
try:
client = OpenAI(
base_url='https://unleashed.chat/api/v1',
)
print('Models:\n')
for model in client.models.list():
print('- ' + model.id)
normal_stream = client.chat.completions.create(
messages=[
{
'role': 'user',
'content': options["prompt"],
}
],
model='dolphin-2.2.1-mistral-7b',
stream=True,
extra_body={
'nostr_mode': options["nostr"],
},
)
print('\nChat response: ', end='')
result = ""
for chunk in normal_stream:
result += chunk.choices[0].delta.content
print(chunk.choices[0].delta.content, end='')
os.environ["OPENAI_API_KEY"] = temp_open_ai_api_key
return result
except Exception as e:
print("Error in Module: " + str(e))
raise Exception(e)
# 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)
dvm_config.SEND_FEEDBACK_EVENTS = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
nip89info = {
"name": name,
"image": "https://unleashed.chat/_app/immutable/assets/hero.pehsu4x_.jpeg",
"about": "I generate Text with Unleashed.chat",
"encryptionSupported": True,
"cashuAccepted": True,
"nip90Params": {}
}
nip89config = NIP89Config()
nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"])
nip89config.CONTENT = json.dumps(nip89info)
return TextGenerationUnleashedChat(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, )
if __name__ == '__main__':
process_venv(TextGenerationUnleashedChat)

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:
@@ -106,6 +109,7 @@ class TranslationLibre(DVMTaskInterface):
# playground or elsewhere
def build_example(name, identifier, admin_config):
dvm_config = build_default_config(identifier)
dvm_config.USE_OWN_VENV = False
admin_config.LUD16 = dvm_config.LN_ADDRESS
options = {'libre_end_point': os.getenv("LIBRE_TRANSLATE_ENDPOINT"),

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"
USERNPUB: str = ""
LUD16: str = ""
USERNPUBS: list = []
EVENTID: str = ""
PRIVKEY: str = ""
@@ -37,7 +42,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC
if ((
adminconfig.WHITELISTUSER is True or adminconfig.UNWHITELISTUSER is True or adminconfig.BLACKLISTUSER is True or adminconfig.DELETEUSER is True)
and adminconfig.USERNPUB == ""):
and adminconfig.USERNPUBS == []):
return
if adminconfig.UPDATE_PROFILE and (dvmconfig.NIP89 is None):
@@ -48,27 +53,28 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC
db = dvmconfig.DB
if str(adminconfig.USERNPUB).startswith("npub"):
publickey = PublicKey.from_bech32(adminconfig.USERNPUB).to_hex()
else:
publickey = adminconfig.USERNPUB
for npub in adminconfig.USERNPUBS:
if str(npub).startswith("npub"):
publickey = PublicKey.from_bech32(npub).to_hex()
else:
publickey = npub
if adminconfig.WHITELISTUSER:
user = get_or_add_user(db, publickey, client=client, config=dvmconfig)
update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive)
user = get_from_sql_table(db, publickey)
print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted))
if adminconfig.WHITELISTUSER:
user = get_or_add_user(db, publickey, client=client, config=dvmconfig)
update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive, user.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)
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, 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)
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, user.subscribed)
if adminconfig.DELETEUSER:
delete_from_sql_table(db, publickey)
if adminconfig.DELETEUSER:
delete_from_sql_table(db, publickey)
if adminconfig.ClEANDB:
clean_db(db)
@@ -79,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.from_sk_str(
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

@@ -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))
@@ -111,11 +111,11 @@ def is_input_supported_generic(tags, client, dvm_config) -> bool:
else:
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
if input_type == "event":
evt = get_event_by_id(input_value, client=client, config=dvm_config)
if evt is None:
print("Event not found")
return False
#if input_type == "event":
# evt = get_event_by_id(input_value, client=client, config=dvm_config)
# if evt is None:
# print("Event not found")
# return False
# TODO check_url_is_readable might be more relevant per task in the future
# if input_type == 'url' and check_url_is_readable(input_value) is None:
# print("Url not readable / supported")

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
@@ -157,41 +169,70 @@ def list_db(db):
print(e)
def update_user_balance(db, npub, additional_sats, client, config):
def update_user_balance(db, npub, additional_sats, client, config, giftwrap=False):
user = get_from_sql_table(db, npub)
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.from_sk_str(config.PRIVATE_KEY)
#time.sleep(1.0)
keys = Keys.parse(config.PRIVATE_KEY)
# 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,
if giftwrap:
client.send_sealed_msg(PublicKey.parse(npub), message, None)
else:
evt = EventBuilder.encrypted_direct_msg(keys, PublicKey.parse(npub), message,
None).to_event(keys)
send_event(evt, client=client, dvm_config=config)
send_event(evt, client=client, dvm_config=config)
def get_or_add_user(db, npub, client, config, update=False):
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))
def get_or_add_user(db, npub, client, config, update=False, skip_meta = False):
user = get_from_sql_table(db, npub)
if user is None:
try:
name, nip05, lud16 = fetch_user_metadata(npub, client)
if skip_meta:
name = npub
nip05 = ""
lud16 = ""
else:
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,39 +1,53 @@
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_TEXT_TO_SPEECH = 5250
KIND_NIP90_RESULT_TEXT_TO_SPEECH = KIND_NIP90_TEXT_TO_SPEECH + 1000
KIND_NIP90_RESULT_GENERATE_VIDEO = KIND_NIP90_GENERATE_VIDEO + 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_GENERIC = 5999
KIND_NIP90_RESULT_GENERIC = KIND_NIP90_GENERIC + 1000
KIND_NOTE = Kind(1)
KIND_DM = Kind(4)
KIND_REPOST = Kind(6)
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_DVM_SUBSCRIPTION = Kind(5906)
KIND_NIP90_RESULT_DVM_SUBSCRIPTION = Kind(6906)
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
@@ -15,32 +16,35 @@ class DVMConfig:
FIX_COST: float = None
PER_UNIT_COST: float = None
RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine",
RELAY_LIST = ["wss://relay.damus.io", "wss://nos.lol", "wss://nostr.wine",
"wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg",
"wss://relay.f7z.io", "wss://pablof7z.nostr1.com", "wss://relay.nostr.net", "wss://140.f7z.io",
"wss://relay.snort.social", "wss://offchain.pub/", "wss://relay.nostr.band"]
]
RELAY_TIMEOUT = 5
EXTERNAL_POST_PROCESS_TYPE = PostProcessFunctionType.NONE # Leave this on None, except the DVM is external
LNBITS_INVOICE_KEY = '' # Will all automatically generated by default, or read from .env
LNBITS_INVOICE_KEY = '' # Will all automatically generated by default, or read from .env
LNBITS_ADMIN_KEY = '' # In order to pay invoices, e.g. from the bot to DVMs, or reimburse users.
LNBITS_URL = 'https://lnbits.com'
LN_ADDRESS = ''
SCRIPT = ''
IDENTIFIER = ''
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
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
SUBSCRIPTION_MANAGEMENT = 'https://noogle.lol/discovery'
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)
dvm_config.IDENTIFIER = identifier
npub = Keys.from_sk_str(dvm_config.PRIVATE_KEY).public_key().to_bech32()
npub = Keys.parse(dvm_config.PRIVATE_KEY).public_key().to_bech32()
invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub)
dvm_config.LNBITS_INVOICE_KEY = invoice_key
dvm_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back

View File

@@ -1,7 +1,7 @@
import json
from datetime import timedelta
from nostr_sdk import PublicKey, Options, Keys, Client, ClientSigner
from nostr_sdk import PublicKey, Options, Keys, Client, NostrSigner
from nostr_dvm.interfaces.dvmtaskinterface import DVMTaskInterface
from nostr_dvm.utils.dvmconfig import DVMConfig
@@ -19,8 +19,8 @@ def build_external_dvm(pubkey, task, kind, fix_cost, per_unit_cost, config,
opts = (Options().wait_for_send(True).send_timeout(timedelta(seconds=config.RELAY_TIMEOUT))
.skip_disconnected_relays(True))
keys = Keys.from_sk_str(config.PRIVATE_KEY)
signer = ClientSigner.keys(keys)
keys = Keys.parse(config.PRIVATE_KEY)
signer = NostrSigner.keys(keys)
client = Client.with_opts(signer, opts)

View File

@@ -14,15 +14,19 @@ def input_data_file_duration(event, dvm_config, client, start=0, end=0):
# print("[" + dvm_config.NIP89.NAME + "] Getting Duration of the Media file..")
input_value = ""
input_type = ""
count = 0
for tag in event.tags():
if tag.as_vec()[0] == 'i':
input_value = tag.as_vec()[1]
input_type = tag.as_vec()[2]
count = count+1
if input_type == "text":
return len(input_value)
if input_type == "event": # NIP94 event
if count > 1:
return 1 # we ignore length for multiple event inputs for now
evt = get_event_by_id(input_value, client=client, config=dvm_config)
if evt is not None:
input_value, input_type = check_nip94_event_for_media(evt, input_value, input_type)

View File

@@ -0,0 +1,218 @@
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, checkCanceled = True):
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=3))
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"] and checkCanceled:
# 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=3))
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])
# zap splits. Feel free to change this for your DVM
# By default, 80% of subscription go to the current's DVM lightning address (make sure to update profile for it to work)
# 5% go to NostrDVM developers
# 5% go to NostrSDK developers
# 10% optionally go to clients that support this subscription DVM
zaptag1 = Tag.parse(["zap", dvm_config.PUBLIC_KEY, "wss://damus.io", "16"])
zaptag2 = Tag.parse(["zap", "npub1nxa4tywfz9nqp7z9zp7nr7d4nchhclsf58lcqt5y782rmf2hefjquaa6q8", "wss://damus.io", "1"]) # NostrDVM
zaptag3 = Tag.parse(["zap", "npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet", "wss://damus.io", "1"]) # NostrSDK
zaptag4 = Tag.parse(["zap", "", "wss://damus.io", "2"]) # Client might use this for splits
p_tag = Tag.parse(["p", dvm_config.NIP88.PAYMENT_VERIFIER_PUBKEY])
tags = [title_tag, image_tag, zaptag1, zaptag2, zaptag3, zaptag4, 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

@@ -4,7 +4,7 @@ from hashlib import sha256
from pathlib import Path
import dotenv
from nostr_sdk import Tag, Keys, EventBuilder, Filter, Alphabet, PublicKey, Client, EventId
from nostr_sdk import Tag, Keys, EventBuilder, Filter, Alphabet, PublicKey, Client, EventId, SingleLetterTag, Kind
from nostr_dvm.utils.definitions import EventDefinitions
from nostr_dvm.utils.nostr_utils import send_event
@@ -25,16 +25,16 @@ 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.from_sk_str(dvm_config.NIP89.PK)
keys = Keys.parse(dvm_config.NIP89.PK)
content = dvm_config.NIP89.CONTENT
event = EventBuilder(EventDefinitions.KIND_ANNOUNCEMENT, content, [k_tag, d_tag]).to_event(keys)
send_event(event, client=client, dvm_config=dvm_config)
print("Announced NIP 89 for " + dvm_config.NIP89.NAME)
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 = ""
@@ -59,8 +59,8 @@ def fetch_nip89_paramters_for_deletion(keys, eventid, client, dvmconfig):
def nip89_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_ANNOUNCEMENT) + ":" + keys.public_key().to_hex() + ":" + dtag])
event = EventBuilder(5, "", [e_tag, a_tag]).to_event(keys)
a_tag = Tag.parse(["a", str(EventDefinitions.KIND_ANNOUNCEMENT.as_u64()) + ":" + keys.public_key().to_hex() + ":" + dtag])
event = EventBuilder(Kind(5), "", [e_tag, a_tag]).to_event(keys)
send_event(event, client, config)
@@ -69,19 +69,18 @@ def nip89_fetch_all_dvms(client):
for i in range(5000, 5999):
ktags.append(str(i))
filter = Filter().kind(EventDefinitions.KIND_ANNOUNCEMENT).custom_tag(Alphabet.K, ktags)
filter = Filter().kind(EventDefinitions.KIND_ANNOUNCEMENT).custom_tag(SingleLetterTag.lowercase(Alphabet.K), ktags)
events = client.get_events_of([filter], timedelta(seconds=5))
for event in events:
print(event.as_json())
def nip89_fetch_events_pubkey(client, pubkey, kind):
ktags = [str(kind)]
# for i in range(5000, 5999):
# ktags.append(str(i))
nip89filter = (Filter().kind(EventDefinitions.KIND_ANNOUNCEMENT).author(PublicKey.from_hex(pubkey)).
custom_tag(Alphabet.K, ktags))
events = client.get_events_of([nip89filter], timedelta(seconds=2))
ktags = [str(kind.as_u64())]
nip89filter = (Filter().kind(EventDefinitions.KIND_ANNOUNCEMENT).author(PublicKey.parse(pubkey)).
custom_tag(SingleLetterTag.lowercase(Alphabet.K), ktags))
events = client.get_events_of([nip89filter], timedelta(seconds=4))
dvms = {}
for event in events:
@@ -98,7 +97,7 @@ def nip89_fetch_events_pubkey(client, pubkey, kind):
def check_and_set_d_tag(identifier, name, pk, imageurl):
if not os.getenv("NIP89_DTAG_" + identifier.upper()):
new_dtag = nip89_create_d_tag(name, Keys.from_sk_str(pk).public_key().to_hex(),
new_dtag = nip89_create_d_tag(name, Keys.parse(pk).public_key().to_hex(),
imageurl)
nip89_add_dtag_to_env_file("NIP89_DTAG_" + identifier.upper(), new_dtag)
print("Some new dtag:" + new_dtag)

View File

@@ -6,14 +6,14 @@ from typing import List
import dotenv
from nostr_sdk import Filter, Client, Alphabet, EventId, Event, PublicKey, Tag, Keys, nip04_decrypt, Metadata, Options, \
Nip19Event
Nip19Event, SingleLetterTag
def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None:
split = event_id.split(":")
if len(split) == 3:
pk = PublicKey.from_hex(split[1])
id_filter = Filter().author(pk).custom_tag(Alphabet.D, [split[2]])
id_filter = Filter().author(pk).custom_tag(SingleLetterTag.lowercase(Alphabet.D), [split[2]])
events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT))
else:
if str(event_id).startswith('note'):
@@ -37,6 +37,37 @@ def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None:
return None
def get_events_by_ids(event_ids, client: Client, config=None) -> List | None:
search_ids = []
for event_id in event_ids:
split = event_id.split(":")
if len(split) == 3:
pk = PublicKey.from_hex(split[1])
id_filter = Filter().author(pk).custom_tag(SingleLetterTag.lowercase(Alphabet.D), [split[2]])
events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT))
else:
if str(event_id).startswith('note'):
event_id = EventId.from_bech32(event_id)
elif str(event_id).startswith("nevent"):
event_id = Nip19Event.from_bech32(event_id).event_id()
elif str(event_id).startswith('nostr:note'):
event_id = EventId.from_nostr_uri(event_id)
elif str(event_id).startswith("nostr:nevent"):
event_id = Nip19Event.from_nostr_uri(event_id).event_id()
else:
event_id = EventId.from_hex(event_id)
search_ids.append(event_id)
id_filter = Filter().ids(search_ids)
events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT))
if len(events) > 0:
return events
else:
return None
def get_events_by_id(event_ids: list, client: Client, config=None) -> list[Event] | None:
id_filter = Filter().ids(event_ids)
events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT))
@@ -116,7 +147,7 @@ def check_and_decrypt_tags(event, dvm_config):
return None
elif p == dvm_config.PUBLIC_KEY:
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
tags_str = nip04_decrypt(Keys.parse(dvm_config.PRIVATE_KEY).secret_key(),
event.author(), event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", p]).as_vec())
@@ -148,7 +179,7 @@ def check_and_decrypt_own_tags(event, dvm_config):
return None
elif event.author().to_hex() == dvm_config.PUBLIC_KEY:
tags_str = nip04_decrypt(Keys.from_sk_str(dvm_config.PRIVATE_KEY).secret_key(),
tags_str = nip04_decrypt(Keys.parse(dvm_config.PRIVATE_KEY).secret_key(),
PublicKey.from_hex(p), event.content())
params = json.loads(tags_str)
params.append(Tag.parse(["p", p]).as_vec())
@@ -164,7 +195,7 @@ def check_and_decrypt_own_tags(event, dvm_config):
def update_profile(dvm_config, client, lud16=""):
keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY)
keys = Keys.parse(dvm_config.PRIVATE_KEY)
nip89content = json.loads(dvm_config.NIP89.CONTENT)
if nip89content.get("name"):
name = nip89content.get("name")

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, ClientSigner
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.from_sk_str(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 = ClientSigner.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", "://")
@@ -47,7 +87,7 @@ def parse_connection_str(connectionstring):
def make_nwc_account(identifier, nwcdomain):
pubkey = Keys.from_sk_str(os.getenv("DVM_PRIVATE_KEY_" + identifier.upper())).public_key().to_hex()
pubkey = Keys.parse(os.getenv("DVM_PRIVATE_KEY_" + identifier.upper())).public_key().to_hex()
data = {
'name': identifier,
'host': os.getenv("LNBITS_HOST"),
@@ -77,7 +117,7 @@ def nwc_test(nwc_server):
# connectionstring = "nostr+walletconnect:..."
if connectionstring != "":
# we use the keys from a test user
keys = Keys.from_sk_str(check_and_set_private_key("test"))
keys = Keys.parse(check_and_set_private_key("test"))
# we zap npub1nxa4tywfz9nqp7z9zp7nr7d4nchhclsf58lcqt5y782rmf2hefjquaa6q8's profile 21 sats and say Cool stuff
pubkey = PublicKey.from_bech32("npub1nxa4tywfz9nqp7z9zp7nr7d4nchhclsf58lcqt5y782rmf2hefjquaa6q8")

View File

@@ -182,36 +182,51 @@ def upload_media_to_hoster(filepath: str):
raise Exception("Upload not possible, all hosters didn't work or couldn't generate output")
def build_status_reaction(status, task, amount, content):
def build_status_reaction(status, task, amount, content, dvm_config):
alt_description = "This is a reaction to a NIP90 DVM AI task. "
if status == "processing":
alt_description = "NIP90 DVM AI task " + task + " started processing. "
reaction = alt_description + emoji.emojize(":thumbs_up:")
if content is not None and content != "":
alt_description = content
reaction = alt_description
else:
alt_description = "NIP90 DVM task " + task + " started processing. "
reaction = alt_description + emoji.emojize(":thumbs_up:")
elif status == "success":
alt_description = "NIP90 DVM AI task " + task + " finished successfully. "
alt_description = "NIP90 DVM task " + task + " finished successfully. "
reaction = alt_description + emoji.emojize(":call_me_hand:")
elif status == "chain-scheduled":
alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled"
alt_description = "NIP90 DVM task " + task + " Chain Task scheduled"
reaction = alt_description + emoji.emojize(":thumbs_up:")
elif status == "error":
alt_description = "NIP90 DVM AI task " + task + " had an error. "
alt_description = "NIP90 DVM task " + task + " had an error. "
if content is None:
reaction = alt_description + emoji.emojize(":thumbs_down:")
else:
reaction = alt_description + emoji.emojize(":thumbs_down:") + " " + content
elif status == "payment-required":
alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str(
alt_description = "NIP90 DVM task " + task + " requires payment of min " + str(
amount) + " Sats. "
reaction = alt_description + emoji.emojize(":orange_heart:")
elif status == "subscription-required":
if content is not None and content != "":
alt_description = content
reaction = alt_description
else:
alt_description = "NIP90 DVM 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(
alt_description = "NIP90 DVM task " + task + " payment is below required amount of " + str(
amount) + " Sats. "
reaction = alt_description + emoji.emojize(":thumbs_down:")
elif status == "user-blocked-from-service":
alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. "
alt_description = "NIP90 DVM task " + task + " can't be performed. User has been blocked from Service. "
reaction = alt_description + emoji.emojize(":thumbs_down:")
else:
reaction = emoji.emojize(":thumbs_down:")

View File

@@ -165,7 +165,7 @@ def XitterDownload(source_url, target_location):
return details
def get_tweet_status_id(tweet_url):
sid_patern = r"https://x\.com/[^/]+/status/(\d+)"
sid_patern = r'https://(?:x\.com|twitter\.com)/[^/]+/status/(\d+)'
if tweet_url[len(tweet_url) - 1] != "/":
tweet_url = tweet_url + "/"

View File

@@ -0,0 +1,173 @@
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
unit: str
begin: int
end: int
tier_dtag: str
zaps: str
recipe: str
active: bool
lastupdate: int
tier: str
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,
unit text,
begin int,
end int,
tier_dtag text,
zaps text,
recipe text,
active boolean,
lastupdate int,
tier text
); """)
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, unit, begin, end, tier_dtag, zaps,
recipe, active, lastupdate, tier):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (id, recipient, subscriber, nwc, cadence, amount, unit, begin, end, tier_dtag, zaps, recipe, active, lastupdate, tier)
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.unit = row[6]
subscription.begin = row[7]
subscription.end = row[8]
subscription.tier_dtag = row[9]
subscription.zaps = row[10]
subscription.recipe = row[11]
subscription.active = row[12]
subscription.lastupdate = row[13]
subscription.tier = row[14]
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:
subscriptions.append(Subscription(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10], row[11], row[12], row[13], row[14]))
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, unit, begin, end, tier_dtag, zaps,
recipe, active, lastupdate, tier):
try:
con = sqlite3.connect(db)
cur = con.cursor()
data = (recipient, subscriber, nwc, cadence, amount, unit, begin, end, tier_dtag, zaps, recipe, active, lastupdate, tier, id)
cur.execute(""" UPDATE subscriptions
SET recipient = ? ,
subscriber = ? ,
nwc = ? ,
cadence = ? ,
amount = ? ,
unit = ? ,
begin = ? ,
end = ?,
tier_dtag = ?,
zaps = ?,
recipe = ?,
active = ?,
lastupdate = ?,
tier = ?
WHERE id = ?""", data)
con.commit()
con.close()
except Error as e:
print("Error Updating DB: " + str(e))

View File

@@ -8,7 +8,8 @@ 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, \
Timestamp
from nostr_dvm.utils.nostr_utils import get_event_by_id, check_and_decrypt_own_tags
import lnurl
@@ -50,7 +51,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 +133,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 +241,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 +255,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,20 +268,31 @@ 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()
if zapped_event is not None:
key_str = keys.secret_key().to_hex() + zapped_event.id().to_hex() + str(
zapped_event.created_at().as_secs())
else:
key_str = keys.secret_key().to_hex() + str(Timestamp.now().as_secs())
encryption_key = sha256(key_str.encode('utf-8')).hexdigest()
tags = [p_tag]
if zapped_event is not None:
tags.append(e_tag)
zap_request = EventBuilder(Kind(9733), content,
tags).to_event(keys).as_json()
keys = Keys.parse(encryption_key)
if zapped_event is not None:
encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_event.author())
else:
encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_user)
zap_request = EventBuilder(9733, content,
[p_tag, e_tag]).to_event(keys).as_json()
keys = Keys.from_sk_str(encryption_key)
encrypted_content = enrypt_private_zap_message(zap_request, keys.secret_key(), zapped_event.author())
anon_tag = Tag.parse(['anon', encrypted_content])
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(
@@ -284,22 +301,27 @@ def zaprequest(lud16: str, amount: int, content, zapped_event, zapped_user, keys
return ob["pr"]
except Exception as e:
print("ZAP REQUEST: " + e)
print("ZAP REQUEST: " + str(e))
return None
def get_price_per_sat(currency):
import requests
url = "https://api.coinstats.app/public/v1/coins"
url = "https://openapiv1.coinstats.app/coins/bitcoin"
params = {"skip": 0, "limit": 1, "currency": currency}
try:
response = requests.get(url, params=params)
response_json = response.json()
price_currency_per_sat = 0.0004
if os.getenv("COINSTATSOPENAPI_KEY"):
bitcoin_price = response_json["coins"][0]["price"]
price_currency_per_sat = bitcoin_price / 100000000.0
except:
price_currency_per_sat = 0.0004
header = {'accept': 'application/json', 'X-API-KEY': os.getenv("COINSTATSOPENAPI_KEY")}
try:
response = requests.get(url, headers=header, params=params)
response_json = response.json()
bitcoin_price = response_json["price"]
price_currency_per_sat = bitcoin_price / 100000000.0
except:
price_currency_per_sat = 0.0004
return price_currency_per_sat
@@ -330,8 +352,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)
@@ -361,4 +381,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.1'
VERSION = '0.3.3'
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.8.0",
install_requires=["nostr-sdk==0.11.0",
"bech32",
"pycryptodome==3.20.0",
"python-dotenv==1.0.0",

54
tests/bot.py Normal file
View File

@@ -0,0 +1,54 @@
import json
import os
import threading
from pathlib import Path
import dotenv
from nostr_sdk import Keys
from nostr_dvm.bot import Bot
from nostr_dvm.tasks import textextraction_pdf
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.nip89_utils import NIP89Config
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():
bot_config = DVMConfig()
identifier = "bot_test"
bot_config.PRIVATE_KEY = check_and_set_private_key(identifier)
npub = Keys.parse(bot_config.PRIVATE_KEY).public_key().to_bech32()
invoice_key, admin_key, wallet_id, user_id, lnaddress = check_and_set_ln_bits_keys(identifier, npub)
bot_config.LNBITS_INVOICE_KEY = invoice_key
bot_config.LNBITS_ADMIN_KEY = admin_key # The dvm might pay failed jobs back
bot_config.LNBITS_URL = os.getenv("LNBITS_HOST")
admin_config = AdminConfig()
pdfextractor = textextraction_pdf.build_example("PDF Extractor", "pdf_extractor", admin_config)
# If we don't add it to the bot, the bot will not provide access to the DVM
pdfextractor.run()
bot_config.SUPPORTED_DVMS.append(pdfextractor) # We add translator to the bot
x = threading.Thread(target=Bot, args=([bot_config]))
x.start()
# Keep the main function alive for libraries that require it, like openai
# keep_alive()
if __name__ == '__main__':
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
playground()

63
tests/discovery.py Normal file
View File

@@ -0,0 +1,63 @@
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():
# Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast
# their NIP89 announcement
# You can create individual admins configs and hand them over when initializing the dvm,
# for example to whilelist users or add to their balance.
# If you use this global config, options will be set for all dvms that use it.
admin_config = AdminConfig()
admin_config.REBROADCAST_NIP89 = False
admin_config.UPDATE_PROFILE = False
#admin_config.DELETE_NIP89 = True
#admin_config.PRIVKEY = ""
#admin_config.EVENTID = ""
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()
#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")
sub_admin_config = AdminConfig()
#sub_admin_config.USERNPUBS = ["7782f93c5762538e1f7ccc5af83cd8018a528b9cd965048386ca1b75335f24c6"] #Add npubs of services that can contact the subscription handler
x = threading.Thread(target=Subscription, args=(Subscription(subscription_config, sub_admin_config),))
x.start()
#keep_alive()
if __name__ == '__main__':
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
playground()

52
tests/filter.py Normal file
View File

@@ -0,0 +1,52 @@
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, discovery_censor_wot, discovery_inactive_follows, \
discovery_bot_farms
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():
# Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast
# their NIP89 announcement
# You can create individual admins configs and hand them over when initializing the dvm,
# for example to whilelist users or add to their balance.
# If you use this global config, options will be set for all dvms that use it.
admin_config = AdminConfig()
admin_config.REBROADCAST_NIP89 = False
admin_config.UPDATE_PROFILE = False
#discovery_test_sub = discovery_censor_wot.build_example("Censorship", "discovery_censor", admin_config)
#discovery_test_sub.run()
discovery_test_sub = discovery_bot_farms.build_example("Bot Hunter", "discovery_botfarms", admin_config)
discovery_test_sub.run()
#discovery_test_sub = discovery_inactive_follows.build_example("Inactive Followings", "discovery_inactive", admin_config)
#discovery_test_sub.run()
#keep_alive()
if __name__ == '__main__':
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
playground()

View File

@@ -3,7 +3,7 @@ import time
from datetime import timedelta
from nicegui import run, ui
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, \
Options, Timestamp, ClientSigner, EventId, Nip19Event, PublicKey
Options, Timestamp, NostrSigner, EventId, Nip19Event, PublicKey
from nostr_dvm.utils import dvmconfig
from nostr_dvm.utils.dvmconfig import DVMConfig
@@ -13,11 +13,11 @@ from nostr_dvm.utils.definitions import EventDefinitions
@ui.page('/', dark=True)
def init():
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
opts = (Options().wait_for_send(False).send_timeout(timedelta(seconds=2))
.skip_disconnected_relays(True))
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client.with_opts(signer, opts)
relay_list = dvmconfig.DVMConfig.RELAY_LIST

33
tests/nwc.py Normal file
View File

@@ -0,0 +1,33 @@
import os
from pathlib import Path
import dotenv
from nostr_sdk import Keys, PublicKey
from nostr_dvm.utils import dvmconfig
from nostr_dvm.utils.nwc_tools import nwc_zap
from nostr_dvm.utils.zap_utils import create_bolt11_lud16, zaprequest
def playground():
connectionstr = os.getenv("TEST_NWC")
keys = Keys.parse(os.getenv("TEST_USER"))
bolt11 = zaprequest("bot@nostrdvm.com", 5, "test", None, PublicKey.parse("npub1cc79kn3phxc7c6mn45zynf4gtz0khkz59j4anew7dtj8fv50aqrqlth2hf"), keys, dvmconfig.DVMConfig.RELAY_LIST, zaptype="private")
print(bolt11)
result = nwc_zap(connectionstr, bolt11, keys, externalrelay=None)
print(result)
if __name__ == '__main__':
env_path = Path('.env')
if not env_path.is_file():
with open('.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)
else:
raise FileNotFoundError(f'.env file not found at {env_path} ')
playground()

View File

@@ -5,7 +5,7 @@ from threading import Thread
import dotenv
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \
nip04_encrypt, ClientSigner
nip04_encrypt, NostrSigner, PublicKey, Event, Kind, RelayOptions
from nostr_dvm.utils.dvmconfig import DVMConfig
from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key
@@ -14,7 +14,7 @@ from nostr_dvm.utils.definitions import EventDefinitions
# TODO HINT: Best use this path with a previously whitelisted privkey, as zapping events is not implemented in the lib/code
def nostr_client_test_translation(input, kind, lang, sats, satsmax):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
if kind == "text":
iTag = Tag.parse(["i", input, "text"])
elif kind == "event":
@@ -31,7 +31,32 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax):
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
client.connect()
config = DVMConfig
send_event(event, client=client, dvm_config=config)
return event.as_json()
def nostr_client_test_search_profile(input):
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", input, "text"])
relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"])
alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to translate a given Input"])
event = EventBuilder(EventDefinitions.KIND_NIP90_USER_SEARCH, str("Search for user"),
[iTag, relaysTag, alttag]).to_event(keys)
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
@@ -43,7 +68,7 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax):
def nostr_client_test_image(prompt):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", prompt, "text"])
outTag = Tag.parse(["output", "image/png;format=url"])
@@ -60,7 +85,7 @@ def nostr_client_test_image(prompt):
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
@@ -70,8 +95,63 @@ def nostr_client_test_image(prompt):
return event.as_json()
def nostr_client_test_censor_filter(users):
keys = Keys.parse(check_and_set_private_key("test_client"))
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
]
relaysTag = Tag.parse(relay_list)
alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to find people to ignore based on people the user trusts"])
# pTag = Tag.parse(["p", user, "text"])
tags = [relaysTag, alttag]
for user in users:
iTag = Tag.parse(["i", user, "text"])
tags.append(iTag)
event = EventBuilder(EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY, str("Give me bad actors"),
tags).to_event(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
client.connect()
config = DVMConfig
send_event(event, client=client, dvm_config=config)
return event.as_json()
def nostr_client_test_inactive_filter(user):
keys = Keys.parse(check_and_set_private_key("test_client"))
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz",
]
relaysTag = Tag.parse(relay_list)
alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to find people that are inactive"])
paramTag = Tag.parse(["param", "user", user])
paramTag2 = Tag.parse(["param", "since_days", "120"])
tags = [relaysTag, alttag, paramTag, paramTag2]
event = EventBuilder(EventDefinitions.KIND_NIP90_PEOPLE_DISCOVERY, str("Give me inactive users"),
tags).to_event(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
ropts = RelayOptions().ping(False)
client.add_relay_with_opts("wss://nostr.band", ropts)
client.connect()
config = DVMConfig
send_event(event, client=client, dvm_config=config)
return event.as_json()
def nostr_client_test_tts(prompt):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
iTag = Tag.parse(["i", prompt, "text"])
paramTag1 = Tag.parse(["param", "language", "en"])
@@ -84,9 +164,9 @@ def nostr_client_test_tts(prompt):
[iTag, paramTag1, bidTag, relaysTag, alttag]).to_event(keys)
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
]
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
@@ -97,10 +177,8 @@ def nostr_client_test_tts(prompt):
def nostr_client_test_image_private(prompt, cashutoken):
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
receiver_keys = Keys.from_sk_str(check_and_set_private_key("replicate_sdxl"))
# TODO more advanced logic, more parsing, params etc, just very basic test functions for now
keys = Keys.parse(check_and_set_private_key("test_client"))
receiver_keys = Keys.parse(check_and_set_private_key("replicate_sdxl"))
relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org",
"wss://nostr-pub.wellorder.net"]
@@ -125,7 +203,7 @@ def nostr_client_test_image_private(prompt, cashutoken):
nip90request = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, encrypted_params,
[pTag, encrypted_tag]).to_event(keys)
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
for relay in relay_list:
client.add_relay(relay)
@@ -136,11 +214,11 @@ def nostr_client_test_image_private(prompt, cashutoken):
def nostr_client():
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
sk = keys.secret_key()
pk = keys.public_key()
print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ")
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client(signer)
dvmconfig = DVMConfig()
@@ -151,33 +229,42 @@ def nostr_client():
dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM,
EventDefinitions.KIND_ZAP]).since(
Timestamp.now()) # events to us specific
dvm_filter = (Filter().kinds([EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT,
EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events
client.subscribe([dm_zap_filter, dvm_filter])
kinds = [EventDefinitions.KIND_NIP90_GENERIC]
SUPPORTED_KINDS = [Kind(6301)]
for kind in SUPPORTED_KINDS:
if kind not in kinds:
kinds.append(kind)
dvm_filter = (Filter().kinds(kinds).since(Timestamp.now()))
client.subscribe([dm_zap_filter, dvm_filter], None)
# nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20)
# nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20)
# nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20)
nostr_client_test_image("a beautiful purple ostrich watching the sunset")
# nostr_client_test_image("a beautiful purple ostrich watching the sunset")
# nostr_client_test_search_profile("dontbelieve")
wot = ["99bb5591c9116600f845107d31f9b59e2f7c7e09a1ff802e84f1d43da557ca64"]
#nostr_client_test_censor_filter(wot)
nostr_client_test_inactive_filter("99bb5591c9116600f845107d31f9b59e2f7c7e09a1ff802e84f1d43da557ca64")
# nostr_client_test_tts("Hello, this is a test. Mic check one, two.")
# cashutoken = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MSwiQyI6IjAyNWU3ODZhOGFkMmExYTg0N2YxMzNiNGRhM2VhMGIyYWRhZGFkOTRiYzA4M2E2NWJjYjFlOTgwYTE1NGIyMDA2NCIsInNlY3JldCI6InQ1WnphMTZKMGY4UElQZ2FKTEg4V3pPck5rUjhESWhGa291LzVzZFd4S0U9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6NCwiQyI6IjAyOTQxNmZmMTY2MzU5ZWY5ZDc3MDc2MGNjZmY0YzliNTMzMzVmZTA2ZGI5YjBiZDg2Njg5Y2ZiZTIzMjVhYWUwYiIsInNlY3JldCI6IlRPNHB5WE43WlZqaFRQbnBkQ1BldWhncm44UHdUdE5WRUNYWk9MTzZtQXM9In0seyJpZCI6InZxc1VRSVorb0sxOSIsImFtb3VudCI6MTYsIkMiOiIwMmRiZTA3ZjgwYmMzNzE0N2YyMDJkNTZiMGI3ZTIzZTdiNWNkYTBhNmI3Yjg3NDExZWYyOGRiZDg2NjAzNzBlMWIiLCJzZWNyZXQiOiJHYUNIdHhzeG9HM3J2WWNCc0N3V0YxbU1NVXczK0dDN1RKRnVwOHg1cURzPSJ9XSwibWludCI6Imh0dHBzOi8vbG5iaXRzLmJpdGNvaW5maXhlc3RoaXMub3JnL2Nhc2h1L2FwaS92MS9ScDlXZGdKZjlxck51a3M1eVQ2SG5rIn1dfQ=="
# nostr_client_test_image_private("a beautiful ostrich watching the sunset")
class NotificationHandler(HandleNotification):
def handle(self, relay_url, event):
def handle(self, relay_url, subscription_id, event: Event):
print(f"Received new event from {relay_url}: {event.as_json()}")
if event.kind() == 7000:
if event.kind().as_u64() == 7000:
print("[Nostr Client]: " + event.as_json())
elif 6000 < event.kind() < 6999:
elif 6000 < event.kind().as_u64() < 6999:
print("[Nostr Client]: " + event.as_json())
print("[Nostr Client]: " + event.content())
elif event.kind() == 4:
elif event.kind().as_u64() == 4:
dec_text = nip04_decrypt(sk, event.author(), event.content())
print("[Nostr Client]: " + f"Received new msg: {dec_text}")
elif event.kind() == 9735:
elif event.kind().as_u64() == 9735:
print("[Nostr Client]: " + f"Received new zap:")
print(event.as_json())

View File

@@ -4,20 +4,20 @@ from pathlib import Path
import dotenv
import nostr_sdk
from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt, \
nip04_encrypt, EventId, Options, PublicKey, Event, ClientSigner, Nip19Event
nip04_encrypt, EventId, Options, PublicKey, Event, NostrSigner, Nip19Event
from nostr_dvm.utils import definitions, dvmconfig
from nostr_dvm.utils.nostr_utils import check_and_set_private_key
relay_list = dvmconfig.DVMConfig.RELAY_LIST
keys = Keys.from_sk_str(check_and_set_private_key("test_client"))
keys = Keys.parse(check_and_set_private_key("test_client"))
wait_for_send = False
skip_disconnected_relays = True
opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=5))
.skip_disconnected_relays(skip_disconnected_relays))
signer = ClientSigner.keys(keys)
signer = NostrSigner.keys(keys)
client = Client.with_opts(signer, opts)
for relay in relay_list:

2
ui/noogle/.env_example Normal file
View File

@@ -0,0 +1,2 @@
VITE_NOOGLE_PK=""
VITE_SUBSCRIPTIPON_VERIFIER_PUBKEY=""

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nooooooooogle</title>
<title>Nostr decentralized search and other stuff</title>
</head>
<body>
<div id="app"></div>

View File

@@ -11,13 +11,16 @@
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@rust-nostr/nostr-sdk": "^0.10.0",
"@getalby/sdk": "^3.4.0",
"@rust-nostr/nostr-sdk": "^0.13.1",
"@vuepic/vue-datepicker": "^7.4.1",
"@vueuse/core": "^10.7.2",
"bech32": "^2.0.0",
"bootstrap": "^5.3.2",
"daisyui": "^4.6.0",
"mini-toastr": "^0.8.1",
"nostr-tools": "^1.17.0",
"nostr-login": "^1.1.1",
"nostr-tools": "^2.4.0",
"vue": "^3.4.15",
"vue-notifications": "^1.0.2",
"vue3-easy-data-table": "^1.5.47",
@@ -25,9 +28,10 @@
"webln": "^0.3.2"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.10",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/tsconfig": "^0.5.1",
"@types/node": "^20.11.10",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"sass": "^1.70.0",
@@ -35,8 +39,7 @@
"typescript": "~5.3.0",
"vite": "^5.0.10",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.27",
"@tsconfig/node20": "^20.1.2"
"vue-tsc": "^1.8.27"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.6.1"

BIN
ui/noogle/public/Alby.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ui/noogle/public/Mutiny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
ui/noogle/public/NWA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
ui/noogle/public/NWC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,7 +1,9 @@
<script setup>
import Home from './components/Home.vue'
import ThreeColumnLayout from "./layouts/ThreeColumnLayout.vue";
import Nip07 from "@/components/Nip07.vue";
import ProfileResultsTable from "@/components/ProfileResultTable.vue";
import router from "@/router/index.js";
</script>
@@ -11,9 +13,11 @@ import Nip07 from "@/components/Nip07.vue";
<main>
<ThreeColumnLayout>
<template #aside>
<template #aside>
<ProfileResultsTable style="margin-top: 450px"/>
</template>
</ThreeColumnLayout>

View File

@@ -23,7 +23,14 @@ a,
}
.purple {
@apply text-nostr;
@apply text-nostr hover:text-orange-400;
text-decoration: none;
transition: 0.4s;
padding: 3px;
}
.white {
@apply text-white;
text-decoration: none;
transition: 0.4s;
padding: 3px;

File diff suppressed because one or more lines are too long

View File

@@ -1,27 +1,30 @@
<script>
import Search from "@/components/Search.vue";
import ResultsTable from "@/components/SearchResultTable.vue";
import Nip07 from "@/components/Nip07.vue";
import Donate from "@/components/Donate.vue";
import ProfileResultsTable from "@/components/ProfileResultTable.vue";
export default {
name: "Home",
components: {Donate, Nip07, ResultsTable, Search}
components: {ProfileResultsTable, ResultsTable, Search}
}
</script>
<template>
<div class="center">
<br>
<Search/>
<br>
<ResultsTable/>
</div>
</template>
<style scoped>
.center {

View File

@@ -1,11 +1,10 @@
<script>
import ImageGeneration from "@/components/ImageGeneration.vue";
import Nip07 from "@/components/Nip07.vue";
import Donate from "@/components/Donate.vue";
export default {
name: "Home",
components: {Donate, Nip07, ResultsTable, ImageGeneration}
name: "Image",
components: {ImageGeneration}
}
</script>

View File

@@ -11,106 +11,105 @@ import {
EventBuilder,
Tag,
EventId,
Nip19Event, Alphabet, Keys
Nip19Event, Alphabet, Keys, nip04_decrypt, SecretKey
} from "@rust-nostr/nostr-sdk";
import store from '../store';
import miniToastr from "mini-toastr";
import VueNotifications from "vue-notifications";
import searchdvms from './data/searchdvms.json'
import {computed, defineEmits, watch} from "vue";
import countries from "@/components/data/countries.json";
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 amberSignerService from "./android-signer/AndroidSigner";
import { ref } from "vue";
import ModalComponent from "../components/Newnote.vue";
import VueDatePicker from "@vuepic/vue-datepicker";
import {timestamp} from "@vueuse/core";
import {post_note, schedule, react_to_dvm, copyinvoice, copyurl, sleep, nextInput, get_user_infos, dvmreactions} from "../components/helper/Helper.vue"
import {zap, createBolt11Lud16, zaprequest} from "../components/helper/Zap.vue"
import StringUtil from "@/components/helper/string.ts";
let dvms =[]
let searching = false
let hasmultipleinputs = false
let requestids = []
let listener = false
function showDetails(user) {
this.$bvModal.show("modal-details");
this.modalData = user;
}
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function post_note(note){
let client = store.state.client
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
const draft = {
content: note,
kind: 1,
pubkey: store.state.pubkey.toHex(),
tags: [],
createdAt: Date.now()
};
const eventJson = await amberSignerService.signEvent(draft);
await client.sendEvent(Event.fromJson(JSON.stringify(eventJson)));
} else {
await client.publishTextNote(note, []);
}
}
async function generate_image(message) {
listen()
try {
if (message === undefined){
message = "A purple Ostrich"
}
if(store.state.pubkey === undefined){
miniToastr.showMessage("Please login first", "No pubkey set", VueNotifications.types.warn)
return
}
dvms = []
store.commit('set_imagedvm_results', dvms)
let client = store.state.client
let tags = []
console.log(message)
tags.push(Tag.parse(["i", message, "text"]))
let evt = new EventBuilder(5100, "NIP 90 Image Generation request", tags)
let content = "NIP 90 Image Generation request"
let kind = 5100
let tags = [
["i", message, "text"]
]
hasmultipleinputs = false
if (urlinput.value !== "" && urlinput.value.startsWith('http')){
let imagetag = ["i", urlinput.value, "url"]
tags.push(imagetag)
hasmultipleinputs = true
console.log(urlinput.value)
}
let res;
let requestid;
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: "NIP 90 Image Generation request",
kind: 5100,
content: content,
kind: kind,
pubkey: store.state.pubkey.toHex(),
tags: [
["i", message, "text"]
],
tags: tags,
createdAt: Date.now()
};
res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = res.id;
res = res.id;
} else {
res = await client.sendEventBuilder(evt);
requestid = res.toHex();
requestid = res.id
requestids.push(requestid)
store.commit('set_current_request_id_image', requestids)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
}
else {
let tags_t = []
for (let tag of tags){
tags_t.push(Tag.parse(tag))
}
let evt = new EventBuilder(kind, content, tags_t)
let unsigned = evt.toUnsignedEvent(store.state.pubkey)
let signedEvent = await (await client.signer()).signEvent(unsigned)
console.log(signedEvent.id.toHex())
requestid = signedEvent.id.toHex()
requestids.push(requestid)
store.commit('set_current_request_id_image', requestids)
await client.sendEvent(signedEvent)
}
store.commit('set_current_request_id_image', requestid)
//console.log("IMAGE EVENT SENT: " + res.toHex())
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
searching = true
if (!store.state.imagehasEventListener){
listen()
store.commit('set_imagehasEventListener', true)
}
else{
console.log("Already has event listener")
}
} catch (error) {
console.log(error);
@@ -118,44 +117,38 @@ async function generate_image(message) {
}
async function listen() {
listener = true
let client = store.state.client
let pubkey = store.state.pubkey
const filter = new Filter().kinds([7000, 6100]).pubkey(pubkey).since(Timestamp.now());
const filter = new Filter().kinds([7000, 6100, 6905]).pubkey(pubkey).since(Timestamp.now());
await client.subscribe([filter]);
const handle = {
// Handle event
handleEvent: async (relayUrl, event) => {
if (store.state.imagehasEventListener === false){
handleEvent: async (relayUrl, subscriptionId, event) => {
/* if (store.state.imagehasEventListener === false){
return true
}
}*/
//const dvmname = getNamefromId(event.author.toHex())
console.log("Received new event from", relayUrl);
// console.log(event.asJ())
let resonsetorequest = false
sleep(1000).then(async () => {
sleep(0).then(async () => {
for (let tag in event.tags) {
if (event.tags[tag].asVec()[0] === "e") {
console.log("IMAGE ETAG: " + event.tags[tag].asVec()[1])
console.log("IMAGE LISTEN TO : " + store.state.requestidImage)
if (event.tags[tag].asVec()[1] === store.state.requestidImage) {
if (store.state.requestidImage.includes(event.tags[tag].asVec()[1])){
resonsetorequest = true
}
}
}
if (resonsetorequest === true) {
if (event.kind === 7000) {
try {
console.log("7000: ", event.content);
console.log("DVM: " + event.author.toHex())
searching = false
//console.log("7000: ", event.content);
//console.log("DVM: " + event.author.toHex())
//miniToastr.showMessage("DVM: " + dvmname, event.content, VueNotifications.types.info)
let status = "unknown"
@@ -168,7 +161,9 @@ async function listen() {
about: "",
image: "",
amount: 0,
bolt11: ""
bolt11: "",
nip90params: {},
}
for (const tag in event.tags) {
@@ -181,25 +176,71 @@ async function listen() {
if (event.tags[tag].asVec().length > 2) {
jsonentry.bolt11 = event.tags[tag].asVec()[2]
}
// TODO else request invoice
else{
let profiles = await get_user_infos([event.author.toHex()])
let created = 0
let current
console.log("NUM KIND0 FOUND " + profiles.length)
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) //todo replace with zaprequest
jsonentry.bolt11 = await zaprequest(lud16, jsonentry.amount , "zapped from noogle.lol", event.id.toHex(), event.author.toHex(), store.state.relays) //Not working yet
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
jsonentry.nip90Params = el.nip90Params
jsonentry.reactions = await dvmreactions(PublicKey.parse(el.id), store.state.followings)
jsonentry.reactions.negativeUser = false
jsonentry.reactions.positiveUser = false
jsonentry.event = Event.fromJson(el.event)
console.log(jsonentry)
}
}
if (dvms.filter(i => i.id === jsonentry.id).length === 0) {
dvms.push(jsonentry)
}
if (!hasmultipleinputs ||
(hasmultipleinputs && jsonentry.id !== "04f74530a6ede6b24731b976b8e78fb449ea61f40ff10e3d869a3030c4edc91f")){
// DVM can not handle multiple inputs, straight up censorship until spec is fulfilled or requests are ignored.
dvms.push(jsonentry)
}
}
dvms.find(i => i.id === jsonentry.id).status = status
@@ -211,7 +252,12 @@ async function listen() {
}
} else if (event.kind === 6100) {
}
else if (event.kind === 6905) {
console.log(event.content)
}
else if (event.kind === 6100) {
let entries = []
console.log("6100:", event.content);
@@ -223,7 +269,7 @@ async function listen() {
}
})
},
// Handle relay message
handleMsg: async (relayUrl, message) => {
//console.log("Received message from", relayUrl, message.asJson());
}
@@ -233,84 +279,19 @@ async function listen() {
}
const urlinput = ref("");
function nextInput(e) {
const next = e.currentTarget.nextElementSibling;
if (next) {
next.focus();
}
async function zap_local(invoice) {
let success = await zap(invoice)
if (success){
dvms.find(i => i.bolt11 === invoice).status = "paid"
store.commit('set_imagedvm_results', dvms)
}
}
async function copyinvoice(invoice){
await navigator.clipboard.writeText(invoice)
window.open("lightning:" + invoice,"_blank")
miniToastr.showMessage("", "Copied Invoice to clipboard", VueNotifications.types.info)
}
async function copyurl(url){
await navigator.clipboard.writeText(url)
miniToastr.showMessage("", "Copied link to clipboard", VueNotifications.types.info)
}
async function zap(invoice) {
let webln;
//this.dvmpaymentaddr = `https://chart.googleapis.com/chart?cht=qr&chl=${invoice}&chs=250x250&chld=M|0`;
//this.dvminvoice = invoice
try {
webln = await requestProvider();
} catch (err) {
await copyinvoice(invoice)
}
if (webln) {
let response = await webln.sendPayment(invoice)
//console.log(response)
//for (const dvm of dvms){
// console.log(dvm.bolt11 + " " + invoice)
//}
dvms.find(i => i.bolt11 === invoice).status = "paid"
store.commit('set_imagedvm_results', dvms)
}
}
async function createBolt11Lud16(lud16, amount) {
let url;
if (lud16.includes('@')) { // LNaddress
const parts = lud16.split('@');
url = `https://${parts[1]}/.well-known/lnurlp/${parts[0]}`;
} else { // No lud16 set or format invalid
return null;
}
try {
console.log(url);
const response = await fetch(url);
const ob = await response.json();
const callback = ob.callback;
const amountInSats = parseInt(amount) * 1000;
const callbackResponse = await fetch(`${callback}?amount=${amountInSats}`);
const obCallback = await callbackResponse.json();
return obCallback.pr;
}
catch (e) {
console.log(`LUD16: ${e}`);
return null;
}
}
defineProps({
msg: {
type: String,
@@ -318,37 +299,34 @@ defineProps({
},
})
import { ref } from "vue";
import ModalComponent from "../components/Newnote.vue";
const isModalOpened = ref(false);
const modalcontent = ref("");
const datetopost = ref(Date.now());
const openModal = result => {
datetopost.value = Date.now();
isModalOpened.value = true;
modalcontent.value = result
};
const closeModal = () => {
isModalOpened.value = false;
console.log(datetopost.value)
};
const submitHandler = async () => {
console.log("hello")
await post_note(modalcontent)
}
</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">DVM</h1>
<h1 class="text-7xl font-black tracking-wide">Image Generation</h1>
<h2 class="text-base-200-content text-center tracking-wide text-2xl font-thin ">
Generate Images, the decentralized way</h2>
@@ -357,7 +335,16 @@ const submitHandler = async () => {
<input class="c-Input" autofocus placeholder="A purple ostrich..." v-model="message" @keyup.enter="generate_image(message)" @keydown.enter="nextInput">
<button class="v-Button" @click="generate_image(message)">Generate Image</button>
</h3>
<details class="collapse bg-base " className="advanced" >
<summary class="collapse-title font-thin bg">Advanced Options</summary>
<div class="collapse-content font-size-0" className="z-10" id="collapse-settings">
<div>
<h4 className="inline-flex flex-none font-thin">Url to existing image:</h4>
<div className="inline-flex flex-none" style="width: 10px;"></div>
<input class="c-Input" style="width: 300px;" placeholder="https://image.nostr.build/image123.jpg" v-model="urlinput">
</div>
</div>
</details>
</div>
<br>
@@ -365,8 +352,26 @@ const submitHandler = async () => {
<ModalComponent :isOpen="isModalOpened" @modal-close="closeModal" @submit="submitHandler" name="first-modal">
<template #header>Share your creation on Nostr <br> <br></template>
<template #content><textarea v-model="modalcontent" className="d-Input" style="height: 300px;">{{modalcontent}}</textarea></template>
<template #footer><button className="v-Button" @click="post_note(modalcontent)" @click.stop="closeModal">Create Note</button></template>
<template #content>
<textarea v-model="modalcontent" className="d-Input" style="height: 300px;">{{modalcontent}}</textarea>
</template>
<template #footer>
<div class="inline-flex flex-none">
<VueDatePicker :min-date="new Date()" :dark="true" style="max-width: 200px;" className="bg-base-200" teleport-center v-model="datetopost"></VueDatePicker>
</div>
<div class="content-center">
<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="max-w-5xl relative space-y-3">
@@ -388,20 +393,25 @@ const submitHandler = async () => {
<h2 className="card-title">{{ dvm.name }}</h2>
</div>
<h3 class="fa-cut" >{{ dvm.about }}</h3>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
<!-- <p>{{dvm.nip90Params}}</p> -->
<!-- <div v-for="param in dvm.nip90Params">
<p>{{param}}</p>
</div> -->
<div className="card-actions justify-end mt-auto" >
<div className="tooltip mt-auto" :data-tip="dvm.status">
<div className="tooltip mt-auto" >
<button v-if="dvm.status === 'processing'" className="btn">Processing</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(dvm.bolt11);">{{ dvm.amount/1000 }} Sats</button>
<button v-if="dvm.status === 'payment-required'" className="zap-Button" @click="zap_local(dvm.bolt11);">{{ dvm.amount/1000 }} Sats</button>
</div>
@@ -410,113 +420,137 @@ const submitHandler = async () => {
<figure className="w-full" >
<img v-if="dvm.result" :src="dvm.result" className="tooltip" data-top='Click to copy url' height="200" alt="DVM Picture" @click="copyurl(dvm.result)"/>
</figure>
<div v-if="dvm.result && store.state.pubkey.toHex() !== Keys.fromSkStr('ece3c0aa759c3e895ecb3c13ab3813c0f98430c6d4bd22160b9c2219efc9cf0e').publicKey.toHex()" >
<button @click="openModal('Look what I created on noogle.lol\n\n' + 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 xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pencil" width="20" height="20" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"></path>
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"></path>
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"></line>
</svg>
</button>
<div class="flex" >
<!-- <button class="w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-100 dark:text-gray-800 text-white flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black" aria-label="edit note" role="button">
<svg class="icon icon-tabler icon-tabler-pencil" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" /></svg></button>
-->
<div v-if="dvm.result && store.state.pubkey.toHex() !== Keys.parse(store.state.nooglekey).publicKey.toHex()" >
<button @click="openModal('Look what I created on noogle.lol\n\n' + dvm.result)" style="margin-right: 5px" 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 xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pencil" width="20" height="20" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"></path>
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4"></path>
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5"></line>
</svg>
</button>
</div>
<div v-if="dvm.result && store.state.pubkey.toHex() !== Keys.parse(store.state.nooglekey).publicKey.toHex() && !dvm.reactions.negativeUser && !dvm.reactions.positiveUser" style="margin-right: 5px">
<button @click="react_to_dvm(dvm, '👍')" 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 xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-hand-thumbs-up" viewBox="0 0 16 16">
<path d="M8.864.046C7.908-.193 7.02.53 6.956 1.466c-.072 1.051-.23 2.016-.428 2.59-.125.36-.479 1.013-1.04 1.639-.557.623-1.282 1.178-2.131 1.41C2.685 7.288 2 7.87 2 8.72v4.001c0 .845.682 1.464 1.448 1.545 1.07.114 1.564.415 2.068.723l.048.03c.272.165.578.348.97.484.397.136.861.217 1.466.217h3.5c.937 0 1.599-.477 1.934-1.064a1.86 1.86 0 0 0 .254-.912c0-.152-.023-.312-.077-.464.201-.263.38-.578.488-.901.11-.33.172-.762.004-1.149.069-.13.12-.269.159-.403.077-.27.113-.568.113-.857 0-.288-.036-.585-.113-.856a2 2 0 0 0-.138-.362 1.9 1.9 0 0 0 .234-1.734c-.206-.592-.682-1.1-1.2-1.272-.847-.282-1.803-.276-2.516-.211a10 10 0 0 0-.443.05 9.4 9.4 0 0 0-.062-4.509A1.38 1.38 0 0 0 9.125.111zM11.5 14.721H8c-.51 0-.863-.069-1.14-.164-.281-.097-.506-.228-.776-.393l-.04-.024c-.555-.339-1.198-.731-2.49-.868-.333-.036-.554-.29-.554-.55V8.72c0-.254.226-.543.62-.65 1.095-.3 1.977-.996 2.614-1.708.635-.71 1.064-1.475 1.238-1.978.243-.7.407-1.768.482-2.85.025-.362.36-.594.667-.518l.262.066c.16.04.258.143.288.255a8.34 8.34 0 0 1-.145 4.725.5.5 0 0 0 .595.644l.003-.001.014-.003.058-.014a9 9 0 0 1 1.036-.157c.663-.06 1.457-.054 2.11.164.175.058.45.3.57.65.107.308.087.67-.266 1.022l-.353.353.353.354c.043.043.105.141.154.315.048.167.075.37.075.581 0 .212-.027.414-.075.582-.05.174-.111.272-.154.315l-.353.353.353.354c.047.047.109.177.005.488a2.2 2.2 0 0 1-.505.805l-.353.353.353.354c.006.005.041.05.041.17a.9.9 0 0 1-.121.416c-.165.288-.503.56-1.066.56z"/>
</svg>
</button>
</div>
<div v-if="dvm.result && store.state.pubkey.toHex() !== Keys.parse(store.state.nooglekey).publicKey.toHex() && !dvm.reactions.negativeUser && !dvm.reactions.positiveUser" >
<button @click="react_to_dvm(dvm, '👎')" 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 xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-hand-thumbs-down" viewBox="0 0 16 16">
<path d="M8.864 15.674c-.956.24-1.843-.484-1.908-1.42-.072-1.05-.23-2.015-.428-2.59-.125-.36-.479-1.012-1.04-1.638-.557-.624-1.282-1.179-2.131-1.41C2.685 8.432 2 7.85 2 7V3c0-.845.682-1.464 1.448-1.546 1.07-.113 1.564-.415 2.068-.723l.048-.029c.272-.166.578-.349.97-.484C6.931.08 7.395 0 8 0h3.5c.937 0 1.599.478 1.934 1.064.164.287.254.607.254.913 0 .152-.023.312-.077.464.201.262.38.577.488.9.11.33.172.762.004 1.15.069.13.12.268.159.403.077.27.113.567.113.856s-.036.586-.113.856c-.035.12-.08.244-.138.363.394.571.418 1.2.234 1.733-.206.592-.682 1.1-1.2 1.272-.847.283-1.803.276-2.516.211a10 10 0 0 1-.443-.05 9.36 9.36 0 0 1-.062 4.51c-.138.508-.55.848-1.012.964zM11.5 1H8c-.51 0-.863.068-1.14.163-.281.097-.506.229-.776.393l-.04.025c-.555.338-1.198.73-2.49.868-.333.035-.554.29-.554.55V7c0 .255.226.543.62.65 1.095.3 1.977.997 2.614 1.709.635.71 1.064 1.475 1.238 1.977.243.7.407 1.768.482 2.85.025.362.36.595.667.518l.262-.065c.16-.04.258-.144.288-.255a8.34 8.34 0 0 0-.145-4.726.5.5 0 0 1 .595-.643h.003l.014.004.058.013a9 9 0 0 0 1.036.157c.663.06 1.457.054 2.11-.163.175-.059.45-.301.57-.651.107-.308.087-.67-.266-1.021L12.793 7l.353-.354c.043-.042.105-.14.154-.315.048-.167.075-.37.075-.581s-.027-.414-.075-.581c-.05-.174-.111-.273-.154-.315l-.353-.354.353-.354c.047-.047.109-.176.005-.488a2.2 2.2 0 0 0-.505-.804l-.353-.354.353-.354c.006-.005.041-.05.041-.17a.9.9 0 0 0-.121-.415C12.4 1.272 12.063 1 11.5 1"/>
</svg>
</button>
</div>
</div>
<div>
<p class="flex"> {{dvm.reactions.positive.length}}
<div>
<div className="dropdown">
<div tabIndex={0} role="button" class="button" >
<svg style="margin-left: 3px; margin-right: 10px; margin-top: 3px" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-hand-thumbs-up" viewBox="0 0 16 16">
<path d="M8.864.046C7.908-.193 7.02.53 6.956 1.466c-.072 1.051-.23 2.016-.428 2.59-.125.36-.479 1.013-1.04 1.639-.557.623-1.282 1.178-2.131 1.41C2.685 7.288 2 7.87 2 8.72v4.001c0 .845.682 1.464 1.448 1.545 1.07.114 1.564.415 2.068.723l.048.03c.272.165.578.348.97.484.397.136.861.217 1.466.217h3.5c.937 0 1.599-.477 1.934-1.064a1.86 1.86 0 0 0 .254-.912c0-.152-.023-.312-.077-.464.201-.263.38-.578.488-.901.11-.33.172-.762.004-1.149.069-.13.12-.269.159-.403.077-.27.113-.568.113-.857 0-.288-.036-.585-.113-.856a2 2 0 0 0-.138-.362 1.9 1.9 0 0 0 .234-1.734c-.206-.592-.682-1.1-1.2-1.272-.847-.282-1.803-.276-2.516-.211a10 10 0 0 0-.443.05 9.4 9.4 0 0 0-.062-4.509A1.38 1.38 0 0 0 9.125.111zM11.5 14.721H8c-.51 0-.863-.069-1.14-.164-.281-.097-.506-.228-.776-.393l-.04-.024c-.555-.339-1.198-.731-2.49-.868-.333-.036-.554-.29-.554-.55V8.72c0-.254.226-.543.62-.65 1.095-.3 1.977-.996 2.614-1.708.635-.71 1.064-1.475 1.238-1.978.243-.7.407-1.768.482-2.85.025-.362.36-.594.667-.518l.262.066c.16.04.258.143.288.255a8.34 8.34 0 0 1-.145 4.725.5.5 0 0 0 .595.644l.003-.001.014-.003.058-.014a9 9 0 0 1 1.036-.157c.663-.06 1.457-.054 2.11.164.175.058.45.3.57.65.107.308.087.67-.266 1.022l-.353.353.353.354c.043.043.105.141.154.315.048.167.075.37.075.581 0 .212-.027.414-.075.582-.05.174-.111.272-.154.315l-.353.353.353.354c.047.047.109.177.005.488a2.2 2.2 0 0 1-.505.805l-.353.353.353.354c.006.005.041.05.041.17a.9.9 0 0 1-.121.416c-.165.288-.503.56-1.066.56z"/>
</svg>
</div>
<div tabIndex={0} className="dropdown-content -start-56 z-[1] horizontal card card-compact w-64 p-2 shadow bg-nostr text-primary-content">
<div className="card-body">
<h3 className="card-title">Liked results by</h3>
<div class="flex" >
<div v-for="user in dvm.reactions.positive">
<div className="wotplayeauthor-wrapper">
<figure>
<img className="wotavatar" v-if="user.profile && user.profile.picture" :src="user.profile.picture" onerror="this.src='https://noogle.lol/favicon.ico'" alt="DVM Picture" />
<img class="wotavatar" v-else src="@/assets/nostr-purple.svg" />
</figure>
</div>
</div>
</div>
</div>
</div>
</div>
<!--<p>{{ this.current_user }}</p> -->
</div>
<div style="width: 10px"></div>
{{dvm.reactions.negative.length}}
<div>
<div className="dropdown">
<div tabIndex={0} role="button" class="button" >
<svg style="margin-left: 3px; margin-top: 3px" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-hand-thumbs-down" viewBox="0 0 16 16">
<path d="M8.864 15.674c-.956.24-1.843-.484-1.908-1.42-.072-1.05-.23-2.015-.428-2.59-.125-.36-.479-1.012-1.04-1.638-.557-.624-1.282-1.179-2.131-1.41C2.685 8.432 2 7.85 2 7V3c0-.845.682-1.464 1.448-1.546 1.07-.113 1.564-.415 2.068-.723l.048-.029c.272-.166.578-.349.97-.484C6.931.08 7.395 0 8 0h3.5c.937 0 1.599.478 1.934 1.064.164.287.254.607.254.913 0 .152-.023.312-.077.464.201.262.38.577.488.9.11.33.172.762.004 1.15.069.13.12.268.159.403.077.27.113.567.113.856s-.036.586-.113.856c-.035.12-.08.244-.138.363.394.571.418 1.2.234 1.733-.206.592-.682 1.1-1.2 1.272-.847.283-1.803.276-2.516.211a10 10 0 0 1-.443-.05 9.36 9.36 0 0 1-.062 4.51c-.138.508-.55.848-1.012.964zM11.5 1H8c-.51 0-.863.068-1.14.163-.281.097-.506.229-.776.393l-.04.025c-.555.338-1.198.73-2.49.868-.333.035-.554.29-.554.55V7c0 .255.226.543.62.65 1.095.3 1.977.997 2.614 1.709.635.71 1.064 1.475 1.238 1.977.243.7.407 1.768.482 2.85.025.362.36.595.667.518l.262-.065c.16-.04.258-.144.288-.255a8.34 8.34 0 0 0-.145-4.726.5.5 0 0 1 .595-.643h.003l.014.004.058.013a9 9 0 0 0 1.036.157c.663.06 1.457.054 2.11-.163.175-.059.45-.301.57-.651.107-.308.087-.67-.266-1.021L12.793 7l.353-.354c.043-.042.105-.14.154-.315.048-.167.075-.37.075-.581s-.027-.414-.075-.581c-.05-.174-.111-.273-.154-.315l-.353-.354.353-.354c.047-.047.109-.176.005-.488a2.2 2.2 0 0 0-.505-.804l-.353-.354.353-.354c.006-.005.041-.05.041-.17a.9.9 0 0 0-.121-.415C12.4 1.272 12.063 1 11.5 1"/>
</svg>
</div>
<div tabIndex={0} className="dropdown-content -start-56 z-[1] horizontal card card-compact w-64 p-2 shadow bg-nostr text-primary-content">
<div className="card-body">
<h3 className="card-title">Disliked results by</h3>
<div class="flex" >
<div v-for="user in dvm.reactions.negative">
<div className="wotplayeauthor-wrapper">
<figure>
<img className="wotavatar" v-if="user.profile && user.profile.picture" :src="user.profile.picture" onerror="this.src='https://noogle.lol/favicon.ico'" alt="DVM Picture" />
<img class="wotavatar" v-else src="@/assets/nostr-purple.svg" />
</figure>
</div>
</div>
</div>
</div>
</div>
</div>
<!--<p>{{ this.current_user }}</p> -->
</div>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="grid grid-cols-1 gap-6">
<div className="card h-60 bg-base-100 shadow-xl gap-6" v-for="dvm in store.state.imagedvmreplies"
:key="dvm.name">
<div className="card-body">
<div class="grid grid-cols-5 gap-6">
<div className="col-end-1">
<h2 className="card-title">{{ dvm.name }}</h2>
<figure v-if="dvm.image!==''" className="w-40"><img className="h-30" :src="dvm.image" alt="DVM Picture" /></figure>
</div>
<div className="col-span-2">
<h3>{{ dvm.about }}</h3>
<div>
</div>
</div>
<div className="mt-auto col-end-4" :data-tip="dvm.card ">
<button v-if="dvm.status === 'processing'" className="btn">Processing</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(dvm.bolt11);">{{ dvm.amount/1000 }} Sats</button>
</div>
<div className="mt-auto col-end-6" :data-tip="dvm.card ">
<figure v-if="dvm.result!==''" className="w-40"><img className="h-30" :src="dvm.result" alt="DVM Result" @click="copyurl(dvm.result)" /></figure>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<div className="card w-70 bg-base-100 shadow-xl flex flex-col" v-for="dvm in store.state.imagedvmreplies"
:key="dvm.id">
<figure class="w-full">
<img v-if="!dvm.result" :src="dvm.image" height="200" alt="DVM Picture" />
</figure>
<div className="card-body">
<h2 className="card-title">{{ dvm.name }}</h2>
<h3 class="fa-cut" >{{ dvm.about }}</h3>
<div className="card-actions justify-end mt-auto" >
<div className="tooltip mt-auto" :data-tip="dvm.card ">
<button v-if="dvm.status === 'processing'" className="btn">Processing</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(dvm.bolt11);">{{ dvm.amount/1000 }} Sats</button>
</div>
</div>
</div>
<br>
</div>
</div>
</div>-->
</template>
<style scoped>
.zap-Button{
@apply btn hover:bg-amber-400 border-amber-400 text-accent-content;
@apply btn hover:bg-amber-400 border-amber-400 text-base;
bottom: 0;
}
@@ -536,7 +570,7 @@ const submitHandler = async () => {
.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;
width: 500px;
color: white;
background: black;
@@ -574,6 +608,23 @@ h3 {
box-shadow: inset 0 4px 4px 0 rgb(0 0 0 / 10%);
}
.wotplayeauthor-wrapper {
padding: 0px;
display: flex;
;
}
.wotavatar {
margin-right: 0px;
margin-left: 0px;
width: 30px;
height: 30px;
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;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,8 @@
<script setup>
import { defineProps, defineEmits, ref } from "vue";
import { ref } from "vue";
import {onClickOutside} from '@vueuse/core'
import store from "@/store.js";
import {EventBuilder, PublicKey, Tag, Timestamp} from "@rust-nostr/nostr-sdk";
const props = defineProps({
isOpen: Boolean,
@@ -11,11 +13,13 @@ const emit = defineEmits(["modal-close"]);
const target = ref(null)
onClickOutside(target, ()=>emit('modal-close'))
</script>
<template>
<div v-if="isOpen" class="modal-mask">
<div class="modal-wrapper">
<div v-if="isOpen" class="modal-mask" >
<div class="modal-wrapper" >
<div class="modal-container" ref="target">
<div class="modal-header">
<slot name="header"> default header </slot>
@@ -26,7 +30,8 @@ onClickOutside(target, ()=>emit('modal-close'))
<div class="modal-footer">
<slot name="footer">
<div>
<button @click.stop="emit('modal-close')">Submit</button>
<button @click.stop="emit('modal-close')"></button>
<button @click.stop="schedule(Date.now())"></button>
</div>
</slot>
</div>
@@ -37,22 +42,26 @@ onClickOutside(target, ()=>emit('modal-close'))
<style scoped>
.modal-mask {
max-height: 100%;
overflow-y: scroll;
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-container {
@apply bg-base-100;
width: 400px;
margin: 200px auto;
@apply bg-base-200;
margin: 15% auto;
padding: 20px 30px;
//background-color: #181818;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
}
</style>

View File

@@ -1,382 +0,0 @@
<template>
<!--<label class="swap swap-rotate">
<input type="checkbox" class="theme-controller" value="synthwave" @click="toggleDark()" />
<svg class="swap-on fill-current w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/></svg>
<svg class="swap-off fill-current w-10 h-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/></svg>
</label> -->
<div>
<div class="playeauthor-wrapper" v-if="current_user">
<div className="dropdown">
<div tabIndex={0} role="button" class="button" >
<img class="avatar" :src="this.avatar" alt="" />
</div>
<div tabIndex={0} className="dropdown-content -start-44 z-[1] horizontal card card-compact w-64 p-2 shadow bg-primary text-primary-content">
<div className="card-body">
<h3 className="card-title">Sign out of your account</h3>
<!--<p>Sign out</p> -->
<button className="btn" @click="sign_out()">Sign Out</button>
</div>
</div>
</div>
<p>{{ this.current_user }}</p>
</div>
<template v-if="!current_user">
<div className="dropdown">
<div tabIndex={0} role="button" class="v-Button" >Sign in</div>
<div tabIndex={0} className="dropdown-content -start-44 z-[1] horizontal card card-compact w-64 p-2 shadow bg-primary text-primary-content">
<div className="card-body">
<h3 className="card-title">Nip07 Login</h3>
<p>Use a Browser Nip07 Extension like getalby or nos2x to login</p>
<button className="btn" @click="sign_in_nip07()">Browser Extension</button>
<template v-if="supports_android_signer">
<button className="btn" @click="sign_in_amber()">Amber Sign in</button>
</template>
</div>
</div>
</div>
<Nip89></Nip89>
</template>
</div>
</template>
<script>
import {
loadWasmAsync,
Client,
ClientSigner,
Nip07Signer,
Filter,
initLogger,
LogLevel,
Timestamp, Keys, NostrDatabase, ClientBuilder, ClientZapper, Alphabet, SingleLetterTag, Options, Duration, PublicKey
} from "@rust-nostr/nostr-sdk";
import VueNotifications from "vue-notifications";
import store from '../store';
import Nip89 from "@/components/Nip89.vue";
import miniToastr from "mini-toastr";
import deadnip89s from "@/components/data/deadnip89s.json";
import amberSignerService from "./android-signer/AndroidSigner";
import {useDark, useToggle} from "@vueuse/core";
const isDark = useDark();
//const toggleDark = useToggle(isDark);
let nip89dvms = []
let logger = true
export default {
data() {
return {
current_user: "",
avatar: "",
signer: "",
supports_android_signer: false,
};
},
async mounted() {
try{
if (amberSignerService.supported) {
this.supports_android_signer = true;
}
if (localStorage.getItem('nostr-key-method') === 'nip07')
{
await this.sign_in_nip07()
}
else {
await this.sign_in_anon()
}
await this.getnip89s()
}
catch (error){
console.log(error);
}
},
methods: {
toggleDark(){
isDark.value = !isDark.value
useToggle(isDark);
console.log(isDark.value)
if (localStorage.isDark === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
},
async sign_in_anon() {
try {
await loadWasmAsync();
if(logger){
try {
initLogger(LogLevel.debug());
} catch (error) {
console.log(error);
}
}
let keys = Keys.fromSkStr("ece3c0aa759c3e895ecb3c13ab3813c0f98430c6d4bd22160b9c2219efc9cf0e")
this.signer = ClientSigner.keys(keys) //TODO store keys
let opts = new Options().waitForSend(false).connectionTimeout(Duration.fromSecs(5));
let client = new ClientBuilder().signer(this.signer).opts(opts).build()
for (const relay of store.state.relays){
await client.addRelay(relay);
}
const pubkey = keys.publicKey
await client.connect();
/*
const filter = new Filter().kind(6302).limit(20)
await client.reconcile(filter);
const filterl = new Filter().author(pubkey)
let test = await client.database.query([filterl])
for (let ev of test){
console.log(ev.asJson())
}*/
store.commit('set_client', client)
store.commit('set_pubkey', pubkey)
store.commit('set_hasEventListener', false)
localStorage.setItem('nostr-key-method', "anon")
localStorage.setItem('nostr-key', "")
console.log("Client connected")
} catch (error) {
console.log(error);
}
},
async sign_in_nip07() {
try {
await loadWasmAsync();
if(logger){
try {
initLogger(LogLevel.debug());
} catch (error) {
console.log(error);
}
}
let nip07_signer = new Nip07Signer();
try{
this.signer = ClientSigner.nip07(nip07_signer);
console.log("SIGNER: " + this.signer)
} catch (error) {
console.log(error);
this.signer = ClientSigner.keys(Keys.generate())
}
//let zapper = ClientZapper.webln()
let opts = new Options().waitForSend(false).connectionTimeout(Duration.fromSecs(5));
let client = new ClientBuilder().signer(this.signer).opts(opts).build()
for (const relay of store.state.relays){
await client.addRelay(relay);
}
const pubkey = await nip07_signer.getPublicKey();
await client.connect();
/*
const filter = new Filter().kind(6302).limit(20)
await client.reconcile(filter);
const filterl = new Filter().author(pubkey)
let test = await client.database.query([filterl])
for (let ev of test){
console.log(ev.asJson())
}*/
store.commit('set_client', client)
store.commit('set_pubkey', pubkey)
store.commit('set_hasEventListener', false)
localStorage.setItem('nostr-key-method', "nip07")
localStorage.setItem('nostr-key', "")
console.log("Client connected")
await this.get_user_info(pubkey)
//miniToastr.showMessage("Login successful!", "Logged in as " + this.current_user, VueNotifications.types.success)
} catch (error) {
console.log(error);
}
},
async sign_in_amber() {
try {
await loadWasmAsync();
if(logger){
try {
initLogger(LogLevel.debug());
} catch (error) {
console.log(error);
}
}
if (!amberSignerService.supported) {
alert("android signer not supported")
return;
}
const hexKey = await amberSignerService.getPublicKey();
let publicKey = PublicKey.fromHex(hexKey);
let keys = Keys.fromPublicKey(publicKey)
this.signer = ClientSigner.keys(keys)
let opts = new Options().waitForSend(false).connectionTimeout(Duration.fromSecs(5));
let client = new ClientBuilder().signer(this.signer).opts(opts).build()
for (const relay of store.state.relays){
await client.addRelay(relay);
}
await client.connect();
store.commit('set_client', client)
store.commit('set_pubkey', publicKey)
store.commit('set_hasEventListener', false)
localStorage.setItem('nostr-key-method', "android-signer")
localStorage.setItem('nostr-key', "")
await this.get_user_info(publicKey)
//miniToastr.showMessage("Login successful!", "Logged in as " + publicKey.toHex(), VueNotifications.types.success)
} catch (error) {
console.log(error);
}
},
async getnip89s(){
//let keys = Keys.generate()
let keys = Keys.fromSkStr("ece3c0aa759c3e895ecb3c13ab3813c0f98430c6d4bd22160b9c2219efc9cf0e")
let signer = ClientSigner.keys(keys) //TODO store keys
let client = new ClientBuilder().signer(signer).build()
for (const relay of store.state.relays){
await client.addRelay(relay);
}
await client.connect();
let dvmkinds = []
for (let i = 5000; i < 6000; i++) {
dvmkinds.push((i.toString()))
}
//console.log(dvmkinds)
const filter = new Filter().kind(31990).customTag(SingleLetterTag.lowercase(Alphabet.K), dvmkinds)
//await client.reconcile(filter);
//const filterl = new Filter().kind(31990)
//let evts = await client.database.query([filterl])
let evts = await client.getEventsOf([filter], 3)
for (const entry of evts){
for (const tag in entry.tags){
if (entry.tags[tag].asVec()[0] === "k")
if(entry.tags[tag].asVec()[1] >= 5000 && entry.tags[tag].asVec()[1] <= 5999 && deadnip89s.filter(i => i.id === entry.id.toHex() ).length === 0) { // blocklist.indexOf(entry.id.toHex()) < 0){
// console.log(entry.tags[tag].asVec()[1])
try {
let jsonentry = JSON.parse(entry.content)
if (jsonentry.picture){
jsonentry.image = jsonentry.picture
}
jsonentry.event = entry.asJson()
jsonentry.kind = entry.tags[tag].asVec()[1]
nip89dvms.push(jsonentry);
}
catch (error){
//console.log(error)
}
}
}
}
store.commit('set_nip89dvms', nip89dvms)
return nip89dvms
},
async get_user_info(pubkey){
let client = store.state.client
const profile_filter = new Filter().kind(0).author(pubkey).limit(1)
let evts = await client.getEventsOf([profile_filter], 10)
console.log("PROFILES:" + evts.length)
if (evts.length > 0){
let latest_entry = evts[0]
let latest_time = 0
for (const entry of evts){
if (entry.createdAt.asSecs() > latest_time){
latest_time = entry.createdAt.asSecs();
latest_entry = entry
}
}
let profile = JSON.parse(latest_entry.content);
this.current_user = profile["name"]
this.avatar = profile["picture"]
}
},
async sign_out(){
this.current_user = ""
localStorage.setItem('nostr-key-method', "anon")
localStorage.setItem('nostr-key', "")
await this.state.client.shutdown();
await this.sign_in_anon()
}
},
};
</script>
<style scoped>
.operation-wrapper .operation-icon {
width: 20px;
cursor: pointer;
}
.playeauthor-wrapper {
padding: 5px;
display: flex;
align-items: center;
justify-items: center;
}
.avatar {
margin-right: 10px;
display: inline-block;
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 10%);
}
.v-Button {
@apply bg-black text-center hover:bg-nostr focus:ring-nostr mb-2 inline-flex flex-none items-center rounded-lg border border-nostr 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;
margin-right: 14px;
height: 44px;
width: 70px
}
</style>

View File

@@ -1,92 +0,0 @@
<template>
</template>
<script>
import {
ClientSigner,
Filter,
Keys, ClientBuilder, Alphabet, SingleLetterTag
} from "@rust-nostr/nostr-sdk";
import store from '../store';
import miniToastr from "mini-toastr";
import deadnip89s from "@/components/data/deadnip89s.json";
let nip89dvms = []
export default {
data() {
return {
current_user: "",
avatar: "",
signer: "",
};
},
async mounted() {
try{
await this.getnip89s()
}
catch (error){
console.log(error);
}
},
methods: {
async getnip89s(){
//let keys = Keys.generate()
let keys = Keys.fromSkStr("ece3c0aa759c3e895ecb3c13ab3813c0f98430c6d4bd22160b9c2219efc9cf0e")
let signer = ClientSigner.keys(keys) //TODO store keys
let client = new ClientBuilder().signer(signer).build()
for (const relay of store.state.relays){
await client.addRelay(relay);
}
await client.connect();
let dvmkinds = []
for (let i = 5000; i < 6000; i++) {
dvmkinds.push((i.toString()))
}
const filter = new Filter().kind(31990).customTag(SingleLetterTag.lowercase(Alphabet.K), dvmkinds)
//await client.reconcile(filter);
//const filterl = new Filter().kind(31990)
//let evts = await client.database.query([filterl])
let evts = await client.getEventsOf([filter], 3)
for (const entry of evts){
for (const tag in entry.tags){
if (entry.tags[tag].asVec()[0] === "k")
if(entry.tags[tag].asVec()[1] >= 5000 && entry.tags[tag].asVec()[1] <= 5999 && deadnip89s.filter(i => i.id === entry.id.toHex() ).length === 0) { // blocklist.indexOf(entry.id.toHex()) < 0){
try {
let jsonentry = JSON.parse(entry.content)
if (jsonentry.picture){
jsonentry.image = jsonentry.picture
}
jsonentry.event = entry.asJson()
jsonentry.createdAt = entry.createdAt.asSecs()
jsonentry.kind = entry.tags[tag].asVec()[1]
nip89dvms.push(jsonentry);
}
catch (error){
console.log(error)
}
}
}
}
store.commit('set_nip89dvms', nip89dvms)
return nip89dvms
},
},
};
</script>
<style scoped>
</style>

View File

@@ -1,65 +1,161 @@
<template>
<div class="max-w-5xl relative space-y-3">
<div class="grid grid-cols-1 gap-6">
<div class="flex flex-row gap-6 items-center">
<Logo />
<div class="flex flex-col gap-2">
<h1 class="text-7xl font-black tracking-wide">About</h1>
<h2 class="text-4xl font-black tracking-wide">Nostr NIP 90 Data Vending Machines</h2>
<div class="text-lg text-default">
<!-- There are many things that make using DVMs a bit of a magical experience. -->
</div>
</div>
</div>
<br><br>
<div className="card w-70 bg-base-100 shadow-xl flex flex-col" v-for="dvm in store.state.nip89dvms"
:key="dvm.id">
<div class="card card-compact rounded-box bg-black/30">
<div class="card-body !text-base">
<div class="card-title text-base-100-content font-bold">
What is this?
</div>
<p>Data Vending Machines are data-processing tools on top of the Nostr protocol.
</p>
<p>
You give them some data, sometimes a few sats, and they give you back some data.</p>
<p>
This page is just a demo client, showcasing a variety of DVM use-cases. Search Content, Search Profiles, Content Discovery, Summarization of events, Image Generation, Scheduling Notes.
</p>
<p>
There's an ever growing number of tasks added to the protocol. The current list of tasks can be found <a class="purple" target="_blank" href="https://www.data-vending-machines.org/">here</a>.
</p>
<p>
These DVMs are not running or being hosted on this site. Instead, the DVMs communicate via Nostr and are available to any App or Client that wants to interact with them.
Want your app or website to support any of these tasks? See <a class="purple" target="_blank" href="https://github.com/nostr-protocol/nips/blob/master/90.md">NIP90</a> for more details.
</p>
<p>
Got interested in building your own DVM and provide it to the whole world? There's OpenSource frameworks to start with, for example <a class="purple" target="_blank" href="https://github.com/believethehype/nostrdvm">NostrDVM</a> in Python.
</p>
<p>
A List of all DVMs that have a NIP89 announcement is available below, ordered by latest announcement.
</p>
</div>
</div>
<br><br>
<div class="grid gap-6 ">
<div className="card bg-base-200 shadow-xl" style="height: 300px" v-for="dvm in store.state.nip89dvms"
:key="dvm.id">
<!-- -->
<div className="card-body">
<!-- <div class="card bg-base-100 shadow-xl image-full" style="height: 400px">
<figure><img v-if="dvm.image" :src="dvm.image" style=" width: 100%; object-fit: cover;"
:alt="dvm.name" onerror="this.src='https://noogle.lol/favicon.ico'"/></figure>
<div class="card-body">
<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>
<div className="playeauthor-wrapper">
<figure className="w-20">
<img className="avatar" :src="dvm.image" alt="DVM Picture" />
</figure>
<h2 className="card-title">{{ dvm.name }}</h2>
<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>
</div>
<h3 class="fa-cut" >{{ dvm.about }}</h3>
<div class="">
<h2 class="card-title">{{ dvm.name }}</h2>
<h3 class="fa-cut text-gray" >Kind: {{ dvm.kind }}</h3>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
</div>
<div class="card-actions justify-end ">
<button className="btn " style="margin-bottom: 10px" @click="copyDoiToClipboard(dvm.event);">Copy Event Json</button>
</div>
</div>
</div> -->
<div class="card card-side bg-black/20 shadow-xl" style="height: 300px">
<figure style="max-width: 20%; flex: fit-content; background-size: cover;" >
<img v-if="dvm.image" style=" width: 100%; object-fit: cover;" :src="dvm.image" :alt="dvm.name" onerror="this.src='https://noogle.lol/favicon.ico'"/>
</figure>
<div class="card-body">
<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()===''" ></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>
</div>
<h2 class="card-title">{{ dvm.name }}</h2>
<h3 class="fa-cut text-gray" >Kind: {{ dvm.kind }}</h3>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
<div class="card-actions justify-end">
<button className="btn" @click="copyDoiToClipboard(dvm.event);">Copy Event Json</button>
</div>
</div>
</div>
<!--
<h2 className="card-title justify-center">{{ dvm.name }}</h2>
<div className="card-body">
<div className="playeauthor-wrapper flex align-top">
<figure className="w-40">
<img className="avatar" :src="dvm.image" alt="DVM Picture" />
</figure>
</div>
<br>
<h3 class="fa-cut" >Kind: {{ dvm.kind }}</h3>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
<div className="card-actions justify-end mt-auto" >
<div className="card-actions justify-end">
<div className="tooltip" :data-tip="dvm.event">
<button className="btn" @click="copyDoiToClipboard(dvm.event);">Copy Event</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<button className="btn glass" @click="copyDoiToClipboard(dvm.event);">Copy Event Json</button>
</div>
</div>
</div>-->
</div>
</div>
</template>
<script>
import '../app.css'
import store from "@/store.js";
import {Alphabet, ClientBuilder, ClientSigner, Filter, Keys, NostrDatabase, Tag} from "@rust-nostr/nostr-sdk";
import {Alphabet, ClientBuilder, NostrSigner, Filter, Keys, NostrDatabase, Tag} from "@rust-nostr/nostr-sdk";
import miniToastr from "mini-toastr";
import VueNotifications from "vue-notifications";
import StringUtil from "@/components/helper/string.ts";
import Donate from "@/components/Donate.vue"
import deadnip89s from './data/deadnip89s.json'
export default {
computed: {
StringUtil() {
return StringUtil
},
Keys() {
return Keys
},
@@ -84,4 +180,18 @@ async mounted(){
}
}
</script>
</script>
<style scoped>
donate{
position: fixed;
bottom:0;
background: rgba(0, 0, 0, 0.5);;
grid-area: footer;
width: 100vw;
height: 32px;
z-index: 10;
text-align: center;
}
</style>

View File

@@ -0,0 +1,415 @@
<template>
<EasyDataTable class="customize-table" header-text-direction="left" table-class-name="customize-table"
:headers="headers"
:items="data"
:sort-by="sortBy"
:sort-type="sortType">
<!--<template #expand="item">
<div style="padding: 15px">
<input class="c-Input" v-model="message">
<button class="v-Button" v-if="!item.replied" @click="reply(item.id, item.author, message)">Reply</button>
<button class="btn" v-if="item.replied" >Replied</button>
</div>
</template> -->
<template #item-content="{content, author, authorurl, avatar, indicator, links, lud16, id, authorid, zapped, zapAmount, reacted, reactions, boosts, boosted, event, replied}">
<div class="playeauthor-wrapper">
<img class="avatar" v-if="avatar" :src="avatar" alt="Avatar" onerror="this.src='https://noogle.lol/favicon.ico'" />
<img class="avatar" v-else src="@/assets/nostr-purple.svg" />
<a class="purple" :href="authorurl" target="_blank">{{ author }}</a>
<div class="time">
{{indicator.time.split("T")[1].split("Z")[0].trim()}}
{{indicator.time.split("T")[0].split("-")[2].trim()}}.{{indicator.time.split("T")[0].split("-")[1].trim()}}.{{indicator.time.split("T")[0].split("-")[0].trim().slice(2)}}
</div>
</div>
<!--.substr(0, 320) + "\u2026"}} -->
<h3 v-html="StringUtil.parseImages(content)"></h3>
<!-- <h3>{{StringUtil.parseImages(content)}}</h3> -->
<!--<p>{{content.substr(0, 320) + "\u2026"}}</p> -->
<div style="padding: 2px; text-align: left;" >
<a class="menusmall" :href="links.uri" target="_blank">Client</a>
<a class="menusmall" :href="links.njump" target="_blank">NJump</a>
<!--<a class="menusmall" :href="links.highlighter" target="_blank">Highlighter</a> -->
<a class="menusmall":href="links.nostrudel" target="_blank">Nostrudel</a>
<div class="flex" >
<div class="flex" style="margin-right: 5px;" v-if="!reacted" @click="react(id, authorid, event)">
<div style="margin-right: 5px;">
<svg style="margin-top:4px" width="14" height="12" xmlns="http://www.w3.org/2000/svg" class="bi bi-heart" fill-rule="evenodd" fill="currentColor" viewBox="0 0 20 25" clip-rule="evenodd"><path d="M12 21.593c-5.63-5.539-11-10.297-11-14.402 0-3.791 3.068-5.191 5.281-5.191 1.312 0 4.151.501 5.719 4.457 1.59-3.968 4.464-4.447 5.726-4.447 2.54 0 5.274 1.621 5.274 5.181 0 4.069-5.136 8.625-11 14.402m5.726-20.583c-2.203 0-4.446 1.042-5.726 3.238-1.285-2.206-3.522-3.248-5.719-3.248-3.183 0-6.281 2.187-6.281 6.191 0 4.661 5.571 9.429 12 15.809 6.43-6.38 12-11.148 12-15.809 0-4.011-3.095-6.181-6.274-6.181"/></svg>
</div>
<div>
<p style="float: left;">{{reactions}}</p>
</div>
</div>
<div class="flex" v-if="reacted" style="margin-right: 5px;" @click="react(id, authorid, event)">
<div style="margin-left: auto; margin-right: 5px; float: left;">
<svg style="margin-top:4px" xmlns="http://www.w3.org/2000/svg" width="14" height="12" class="bi bi-heart fill-red-500" viewBox="0 0 20 25"><path d="M12 4.419c-2.826-5.695-11.999-4.064-11.999 3.27 0 7.27 9.903 10.938 11.999 15.311 2.096-4.373 12-8.041 12-15.311 0-7.327-9.17-8.972-12-3.27z"/></svg> </div>
<div>
<p className="text-red-500" style="float: left;">{{reactions}}</p>
</div>
</div>
<div class="flex" v-if="lud16 != null && lud16 != '' && !zapped" style="margin-right: 5px;" @click="zap_local(lud16, id, authorid)">
<div style="margin-left: auto; margin-right: 5px; float: left;">
<svg style="margin-top:4px" 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> </div>
<div>
<p style="float: left;">{{zapAmount/1000}}</p>
</div>
</div>
<div class="flex" v-if="lud16 != null && lud16 != '' && zapped" style="margin-right: 5px;" @click="zap_local(lud16, id, authorid)" >
<div style="margin-left: auto; margin-right: 5px;">
<svg style="margin-top:4px" xmlns="http://www.w3.org/2000/svg" width="14" height="16" class="bi bi-lightning fill-amber-400" 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-.641z"/>
</svg></div>
<div>
<p style="float: left;" className="text-amber-400">{{zapAmount/1000}}</p>
</div>
</div>
<div class="flex" v-if="!boosted" @click="boost(id, authorid, event)">
<div style="margin-left: auto; margin-right: 5px; float: left;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="28" viewBox="0 0 20 34"><path class="bi" fill="currentColor" d="M19 7a1 1 0 0 0-1-1h-8v2h7v5h-3l3.969 5L22 13h-3zM5 17a1 1 0 0 0 1 1h8v-2H7v-5h3L6 6l-4 5h3z"/></svg> </div>
<div>
<p style="float: left;">{{boosts}}</p>
</div>
</div>
<div class="flex" v-if="boosted" @click="boost(id, authorid, event)">
<div style="margin-left: auto; margin-right: 5px; float: left;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="28" viewBox="0 0 20 34">
<path class="bi fill-green-700" d="M19 7a1 1 0 0 0-1-1h-8v2h7v5h-3l3.969 5L22 13h-3zM5 17a1 1 0 0 0 1 1h8v-2H7v-5h3L6 6l-4 5h3z"/>
</svg> </div>
<div>
<p className="text-green-700" style="float: left;">{{boosts}}</p>
</div>
</div>
<details>
<summary class="" style=" margin-right: 5px">
<div style="margin-right: 5px; margin-left: 10px;margin-top: 4px"> <svg id="Capa_1" fill="currentColor" height="14" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg"><g>
<g id="ad">
<path d="m113.241 463.222-4.3-88.312c-68.332-36.05-108.941-95.703-108.941-160.522 0-52.154 26.017-101.029 73.259-137.619 46.512-36.024 108.215-55.864 173.741-55.864s127.229 19.84 173.742 55.865c47.242 36.59 73.259 85.465 73.259 137.619s-26.017 101.03-73.259 137.621c-46.512 36.023-108.215 55.863-173.742 55.863-1.889 0-3.843-.021-6.01-.067l-113.406 63.371c-9.113 4.959-14.343-.411-14.343-7.955zm133.759-423.021c-125.556 0-227.703 78.141-227.703 174.189 0 58.936 38.629 113.466 103.33 145.859 3.116 1.56 5.148 4.679 5.317 8.159l3.814 78.334 102.12-57.064c1.514-.852 3.232-1.275 4.968-1.222 3.108.084 5.698.124 8.152.124 125.556 0 227.703-78.141 227.703-174.188s-102.144-174.191-227.701-174.191z"/> </g></g></svg> </div>
</summary>
<div class="collapse-content font-size-0" className="z-10" id="collapse">
<textarea class="c-Input" style="width: auto; margin-left: -100px" v-model="message"></textarea>
<br>
<button class="v-Button" v-if="!replied" @click="reply(id, author, message); message=''">Reply</button>
<button class="btn" v-if="replied" >Replied</button>
</div>
</details>
</div>
</div>
</template>
</EasyDataTable>
<p></p>
<!-- <p>{{data}}</p> -->
</template>
<script lang="ts" setup>
import type {Header, Item, SortType} from "vue3-easy-data-table";
import store from '../store';
import {types} from "sass";
import Null = types.Null;
import StringUtil from "@/components/helper/string";
import {copyinvoice, parseandreplacenpubs, } from "@/components/helper/Helper.vue";
import {requestProvider} from "webln";
import {Event, EventBuilder, EventId, PublicKey, Tag} from "@rust-nostr/nostr-sdk";
import amberSignerService from "@/components/android-signer/AndroidSigner";
import {zap, zap_lud16, createBolt11Lud16, zaprequest} from "@/components/helper/Zap.vue";
import {ref} from "vue";
const props = defineProps<{
data: any[]
}>()
const sortBy: String = "index";
const sortType: SortType = "asc";
const headers: Header[] = [
{ text: "Results:", value: "content", fixed: true},
// { text: "Time", value: "indicator.index", sortable: true, },
];
const message = ref("");
async function react(eventid, authorid, evt){
let event_id = EventId.parse(eventid)
let public_key = PublicKey.parse(authorid);
let signer = store.state.signer
let client = store.state.client
let objects = (props.data.find(x=> x.id === eventid))
if (objects !== undefined){
if(!objects.reacted ){
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: "🧡",
kind: 7,
pubkey: store.state.pubkey.toHex(),
tags: [["e", eventid]],
createdAt: Date.now()
};
let res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
let requestid = res.id;
}
else {
let event = EventBuilder.reaction(evt, "🧡")
let requestid = await client.sendEventBuilder(event);
}
objects.reacted = true
objects.reactions += 1
console.log("reacted")
}
}
}
async function reply (eventid, authorid, message){
console.log(eventid)
let signer = store.state.signer
let client = store.state.client
let objects = (props.data.find(x=> x.id === eventid))
if (objects !== undefined){
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: message,
kind: 1,
pubkey: store.state.pubkey.toHex(),
tags: [["e", eventid]],
createdAt: Date.now()
};
let res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
let requestid = res.id;
}
else {
let tags = [Tag.parse(["e", eventid])]
let event = EventBuilder.textNote(message, tags)
let requestid = await client.sendEventBuilder(event);
console.log(requestid.toHex())
}
objects.replied = true
console.log("replied")
}
}
async function boost(eventid, authorid, evt){
// TODO
let event_id = EventId.parse(eventid)
let public_key = PublicKey.parse(authorid);
let signer = store.state.signer
let client = store.state.client
let objects = (props.data.find(x=> x.id === eventid))
if (objects !== undefined){
if(!objects.boosted ){
console.log(evt.asJson())
let relay = "wss://relay.damus.io"
for (let tag of evt.tags){
if (tag.asVec()[0] == "relays"){
console.log(tag.asVec()[1])
}
}
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: evt.asJson(),
kind: 6,
pubkey: store.state.pubkey.toHex(),
tags: [["e", eventid]],
createdAt: Date.now()
};
let res = await amberSignerService.signEvent(draft)
await client.sendEvent(Event.fromJson(JSON.stringify(res)))
let requestid = res.id;
}
else {
let event = EventBuilder.repost(evt)
let requestid = await client.sendEventBuilder(event);
}
objects.boosted = true
objects.boosts += 1
//props.data.push.apply(props.data.find(x=> x.id === eventid), objects)
console.log("boosted")
}
}
}
async function zap_local(lud16, eventid, authorid) {
if (lud16 == undefined || lud16 == ""){
console.log("User has no lightning address")
return
}
let success = await zap_lud16(lud16, eventid, authorid)
try {
if (success) {
let objects = props.data.find(x => x.id === eventid)
console.log(objects)
if (objects !== undefined) {
objects.zapped = true
objects.zapAmount += 21000
let index = props.data.indexOf(x => x.id === eventid)
props.data[index] = objects
console.log("zapped")
}
}
}
catch (error)
{
console.log(error)
}
}
</script>
<style scoped>
.operation-wrapper .operation-icon {
width: 20px;
cursor: pointer;
}
.playeauthor-wrapper {
padding: 6px;
display: flex;
align-items: center;
justify-items: center;
}
.menusmall {
@apply btn text-gray-600 bg-transparent border-transparent tracking-wide ;
}
.vue3-easy-data-table__footer.previous-page__click-button{
height:100px
}
.time {
padding: 6px;
display: flex;
font-size: 1em;
align-items: center;
justify-items: center;
}
.avatar {
margin-right: 10px;
display: inline-block;
width: 30px;
height: 30px;
border-radius: 50%;
object-fit: cover;
box-shadow: inset 0 4px 4px 0 rgb(0 0 0 / 10%);
}
.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;
height: 180px;
margin-top: 15px;
}
.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-left: 10px;
}
.customize-table {
width: auto;
--easy-table-border: 2px solid bg-base;
--easy-table-row-border: 1px solid #000000;
--easy-table-header-font-size: 14px;
--easy-table-header-height: 50px;
--easy-table-header-font-color: bg-accent;
--easy-table-header-background-color: bg-base;
--easy-table-header-item-padding: 10px 15px;
--easy-table-body-even-row-font-color: bg-accent;
--easy-table-body-even-row-background-color: bg-base;
--easy-table-body-row-font-color: bg-accent;
--easy-table-body-row-background-color: bg-base;
--easy-table-body-row-height: 50px;
--easy-table-body-row-font-size: 14px;
--easy-table-body-row-hover-font-color: bg-accent;
--easy-table-body-row-hover-background-color: bg-base;
--easy-table-body-item-padding: 10px 15px;
--easy-table-footer-background-color: bg-base;
--easy-table-footer-font-color: bg-accent;
--easy-table-footer-font-size: 14px;
--easy-table-footer-padding: 0px 10px;
--easy-table-footer-height: 50px;
--easy-table-rows-per-page-selector-width: 70px;
--easy-table-rows-per-page-selector-option-padding: 10px;
--easy-table-rows-per-page-selector-z-index: 1;
--easy-table-scrollbar-track-color: bg-base;
--easy-table-scrollbar-color: bg-base;
--easy-table-scrollbar-thumb-color: bg-base;
--easy-table-scrollbar-corner-color: bg-base;
--easy-table-loading-mask-background-color: #2d3a4f;
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<EasyDataTable style="margin-top: 450px"
class="customize-table" header-text-direction="left" hide-rows-per-page=true rows-per-page=10 v-if="store.state.profile_results.length != 0 && router.currentRoute.value.path == '/'" table-class-name="customize-table"
:headers="headers"
:items="store.state.profile_results" >
<template #item-content="{ author, authorurl, avatar}">
<div class="playeauthor-wrapper" >
<img class="avatar" v-if="avatar" :src="avatar" alt="Avatar" onerror="this.src='https://noogle.lol/favicon.ico'" />
<img class="avatar" v-else src="@/assets/nostr-purple.svg" />
<a class="purple" :href="authorurl" target="_blank">{{ author }}</a>
</div>
<!-- <p>{{content}}</p> -->
</template>
<!--<template #expand="item">
<div style="padding: 15px; text-align: left;" >
<a class="menu" :href="item.links.uri" target="_blank">Nostr Client</a>
<a class="menu" :href="item.links.njump" target="_blank">NJump</a>
<a class="menu" :href="item.links.highlighter" target="_blank">Highlighter</a>
<a class="menu":href="item.links.nostrudel" target="_blank">Nostrudel</a>
</div>
</template> -->
</EasyDataTable>
</template>
<script lang="ts" setup>
import type {Header, Item, SortType} from "vue3-easy-data-table";
import store from '../store';
import router from "../router";
const headers: Header[] = [
{ text: "Relevant Profiles:", value: "content", fixed:true},
// { text: "Time", value: "indicator.time", sortable: true, },
];
</script>
<style scoped>
.operation-wrapper .operation-icon {
width: 20px;
cursor: pointer;
}
.playeauthor-wrapper {
padding: 6px;
display: flex;
align-items: center;
justify-items: center;
}
.menusmall {
@apply btn text-gray-600 bg-transparent border-transparent tracking-wide;
}
.vue3-easy-data-table__footer.previous-page__click-button{
height:100px
}
.time {
padding: 6px;
display: flex;
font-size: 1em;
align-items: center;
justify-items: center;
}
.avatar {
margin-right: 10px;
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
object-fit: cover;
box-shadow: inset 0 4px 4px 0 rgb(0 0 0 / 10%);
}
.customize-table {
width:auto;
--easy-table-border: 3px solid #000000;
--easy-table-row-border: 0px;
--easy-table-header-font-size: 14px;
--easy-table-header-height: 20px;
--easy-table-header-font-color: bg-accent;
--easy-table-header-background-color: bg-base;
--easy-table-header-item-padding: 10px 15px;
--easy-table-body-even-row-font-color: bg-accent;
--easy-table-body-even-row-background-color: bg-base;
--easy-table-body-row-font-color: bg-accent;
--easy-table-body-row-background-color: bg-base;
--easy-table-body-row-height: 20px;
--easy-table-body-row-font-size: 14px;
--easy-table-body-row-hover-font-color: bg-accent;
--easy-table-body-row-hover-background-color: bg-base;
--easy-table-body-item-padding: 10px 15px;
--easy-table-footer-background-color: bg-base;
--easy-table-footer-font-color: bg-accent;
--easy-table-footer-font-size: 14px;
--easy-table-footer-padding: 10px 10px;
--easy-table-footer-height: 20px;
--easy-table-rows-per-page-selector-width: 60px;
--easy-table-rows-per-page-selector-option-padding: 10px;
--easy-table-rows-per-page-selector-z-index: 1;
--easy-table-scrollbar-track-color: bg-base;
--easy-table-scrollbar-color: bg-base;
--easy-table-scrollbar-thumb-color: bg-base;
--easy-table-scrollbar-corner-color: bg-base;
--easy-table-loading-mask-background-color: #2d3a4f;
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,6 @@
<script setup>
import {
Client,
Filter,
@@ -9,24 +11,41 @@ import {
EventBuilder,
Tag,
EventId,
Nip19Event, Alphabet
Nip19Event,
Alphabet,
ClientBuilder,
Keys,
NostrDatabase,
NegentropyOptions,
NegentropyDirection,
Duration
} from "@rust-nostr/nostr-sdk";
import store from '../store';
import miniToastr from "mini-toastr";
import VueNotifications from "vue-notifications";
import searchdvms from './data/searchdvms.json'
import {computed, onMounted, ref} from "vue";
import countries from "@/components/data/countries.json";
import deadnip89s from "@/components/data/deadnip89s.json";
import Nip07 from "@/components/Nip07.vue";
import amberSignerService from "./android-signer/AndroidSigner";
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css'
import {post_note, schedule, copyurl, copyinvoice, sleep, getEvents, get_user_infos, nextInput} from "../components/helper/Helper.vue"
import StringUtil from "@/components/helper/string.ts";
let items = []
let profiles = []
let dvms =[]
let listener = false
let searching = false
const message = ref("");
const fromuser = ref("");
let usernames = []
const datefrom = ref(new Date().setFullYear(new Date().getFullYear() - 1));
const dateto = ref(Date.now());
onMounted(async () => {
let urlParams = new URLSearchParams(window.location.search);
@@ -35,20 +54,21 @@ onMounted(async () => {
await sleep(1000)
await send_search_request(message.value)
}
await sleep(2000)
})
// console.log(urlParams.has('search')); // true
// console.log(urlParams.get('search')); // "MyParam"
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function send_search_request(msg) {
if (!store.state.hasEventListener){
store.commit('set_hasEventListener', true)
listen()
}
else{
console.log("Already has event listener")
}
try {
if (msg === undefined){
msg = "Nostr"
@@ -59,11 +79,12 @@ async function send_search_request(msg) {
return
}
items = []
profiles = []
dvms =[]
store.commit('set_search_results', items)
store.commit('set_search_results_profiles', profiles)
let client = store.state.client
let tags = []
let users = [];
const taggedUsersFrom = msg.split(' ')
@@ -72,123 +93,105 @@ async function send_search_request(msg) {
// search
let search = msg;
// tags
for (let word of taggedUsersFrom) {
search = search.replace(word, "");
if(word === "me"){
word = store.state.pubkey.toBech32()
}
const userPubkey = PublicKey.fromBech32(word.replace("@", "")).toHex()
const pTag = Tag.parse(["p", userPubkey]);
users.push(pTag.asVec());
}
if (fromuser.value !== ""){
const userPubkey = PublicKey.fromBech32(fromuser.value.replace("@", "")).toHex()
const pTag = Tag.parse(["p", userPubkey]);
users.push(pTag.asVec());
}
msg = search.replace(/from:|to:|@/g, '').trim();
console.log(search);
tags.push(Tag.parse(["i", msg, "text"]))
tags.push(Tag.parse(["param", "max_results", "150"]))
tags.push(Tag.parse(['param', 'users', JSON.stringify(users)]))
let evt = new EventBuilder(5302, "NIP 90 Search request", tags)
let res;
let requestid;
if (localStorage.getItem('nostr-key-method') === 'android-signer') {
let draft = {
content: "NIP 90 Search request",
kind: 5302,
pubkey: store.state.pubkey.toHex(),
tags: [
let content = "NIP 90 Search request"
let kind = 5302
let kind_profiles = 5303
let tags = [
["i", msg, "text"],
["param", "max_results", "150"],
["param", "since", ((datefrom.value/1000).toFixed(0))],
["param", "until", ((dateto.value/1000).toFixed(0))],
['param', 'users', JSON.stringify(users)]
],
]
let res;
let requestid;
let requestid_profile;
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;
res = res.id;
} else {
res = await client.sendEventBuilder(evt)
requestid = res.toHex()
}
console.log("STORE: " +store.state.requestidSearch)
store.commit('set_current_request_id_search', requestid)
console.log("STORE AFTER: " + store.state.requestidSearch)
let result = await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = result.toHex()
//miniToastr.showMessage("Sent Request to DVMs", "Awaiting results", VueNotifications.types.warn)
if (!store.state.hasEventListener){
listen()
store.commit('set_hasEventListener', true)
}
else{
console.log("Already has event listener")
else {
let tags_t = []
for (let tag of tags){
tags_t.push(Tag.parse(tag))
}
let evt = new EventBuilder(kind, content, tags_t)
let evt_profiles = new EventBuilder(kind_profiles, "Profile Search request", [Tag.parse(["i", msg, "text"]), Tag.parse(["param", "max_results", "500"])])
try{
let res1 = await client.sendEventBuilder(evt_profiles)
requestid_profile = res1.toHex()
res = await client.sendEventBuilder(evt)
requestid = res.toHex()
}
catch(error){
console.log(error)
}
}
console.log(res)
store.commit('set_current_request_id_search', requestid)
store.commit('set_current_request_profile_id_search', requestid_profile)
} catch (error) {
console.log(error);
}
}
async function getEvents(eventids) {
const event_filter = new Filter().ids(eventids)
let client = store.state.client
return await client.getEventsOf([event_filter], 5)
}
async function get_user_infos(pubkeys){
let profiles = []
let client = store.state.client
const profile_filter = new Filter().kind(0).authors(pubkeys)
let evts = await client.getEventsOf([profile_filter], 10)
console.log("PROFILES:" + evts.length)
for (const entry of evts){
try{
let contentjson = JSON.parse(entry.content)
profiles.push({profile: contentjson, author: entry.author.toHex(), createdAt: entry.createdAt});
}
catch(error){
console.log("error")
}
}
return profiles
}
async function listen() {
listener = true
let client = store.state.client
let pubkey = store.state.pubkey
let originale = [store.state.requestidSearch]
const filter = new Filter().kinds([7000, 6302]).pubkey(pubkey).since(Timestamp.now());
const filter = new Filter().kinds([7000, 6302, 6303]).pubkey(pubkey).since(Timestamp.now());
await client.subscribe([filter]);
const handle = {
// Handle event
handleEvent: async (relayUrl, event) => {
if (store.state.hasEventListener === false){
handleEvent: async (relayUrl, subscriptionId, event) => {
/* if (store.state.hasEventListener === false){
return true
}
//const dvmname = getNamefromId(event.author.toHex())
}*/
console.log("Received new event from", relayUrl);
let resonsetorequest = false
sleep(1000).then(async () => {
sleep(500).then(async () => {
for (let tag in event.tags) {
if (event.tags[tag].asVec()[0] === "e") {
console.log("SEARCH ETAG: " + event.tags[tag].asVec()[1])
console.log("SEARCH LISTEN TO : " + store.state.requestidSearch)
if (event.tags[tag].asVec()[1] === store.state.requestidSearch) {
if (event.tags[tag].asVec()[1] === store.state.requestidSearch || event.tags[tag].asVec()[1] === store.state.requestidSearchProfile) {
resonsetorequest = true
}
}
@@ -201,8 +204,6 @@ async function listen() {
try {
console.log("7000: ", event.content);
console.log("DVM: " + event.author.toHex())
searching = false
//miniToastr.showMessage("DVM: " + dvmname, event.content, VueNotifications.types.info)
let status = "unknown"
let jsonentry = {
@@ -267,30 +268,29 @@ async function listen() {
else if (event.kind === 6302) {
let entries = []
console.log("6302:", event.content);
//miniToastr.showMessage("DVM: " + dvmname, "Received Results", VueNotifications.types.success)
try{
let event_etags = JSON.parse(event.content)
if (event_etags.length > 0) {
for (let etag of event_etags) {
const eventid = EventId.fromHex(etag[1])
const eventid = EventId.parse(etag[1]).toHex() //a bit unnecessary
entries.push(eventid)
}
const events = await getEvents(entries)
let authors = []
for (const evt of events) {
authors.push(evt.author)
authors.push(evt.author.toHex())
}
if (authors.length > 0) {
let profiles = await get_user_infos(authors)
for (const evt of events) {
let p = profiles.find(record => record.author === evt.author.toHex())
let bech32id = evt.id.toBech32()
let nip19 = new Nip19Event(event.id, event.author, store.state.relays)
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/a/" + bech32id
let njumpurl = "https://njump.me/" + bech32id
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()
@@ -310,8 +310,50 @@ async function listen() {
indicator: {"time": evt.createdAt.toHumanDatetime()}
})
}
}
}
}
const index = dvms.indexOf((dvms.find(i => i.id === event.author.toHex())));
if (index > -1) {
dvms.splice(index, 1);
}
store.commit('set_active_search_dvms', dvms)
console.log("Events from" + event.author.toHex())
store.commit('set_search_results', items)
}
catch{
}
}
else if (event.kind === 6303) {
let entries = []
console.log("6303:", event.content);
let event_ptags = JSON.parse(event.content)
let authors = []
if (event_ptags.length > 0) {
for (let ptag of event_ptags) {
authors.push(ptag[1])
}
if (authors.length > 0) {
let infos = await get_user_infos(authors)
for (const profile of infos) {
//console.log(profile["author"])
if (profiles.findIndex(e => e.id === profile["author"]) === -1 && profile["profile"]["name"] !== "" ) {
profiles.push({
id: profile["author"],
content: profile["profile"],
author: profile["profile"]["name"],
authorurl: "https://njump.me/" +PublicKey.parse(profile["author"]).toBech32(),
avatar: profile["profile"]["picture"]
})
}
}
}
}
@@ -323,8 +365,7 @@ async function listen() {
}
store.commit('set_active_search_dvms', dvms)
console.log("Events from" + event.author.toHex())
store.commit('set_search_results', items)
store.commit('set_search_results_profiles', profiles)
}
}
})
@@ -347,14 +388,42 @@ function getNamefromId(id){
else return elements[0].name
}
function nextInput(e) {
const next = e.currentTarget.nextElementSibling;
if (next) {
next.focus();
}
async function checkuser(msg){
usernames = []
let profiles = await get_user_from_search(msg)
for (let profile of profiles){
usernames.push(profile)
}
}
async function get_user_from_search(name){
name = "\"name\":" + name
if (store.state.dbclient.database === undefined){
console.log("not logged in, not getting profile suggestions")
return []
}
let client = store.state.dbclient
let profiles = []
let filter1 = new Filter().kind(0)
let evts = await client.database.query([filter1])
console.log(evts.length)
for (const entry of evts){
try{
let contentjson = JSON.parse(entry.content)
console.log(entry.content)
profiles.push({profile: contentjson, author: entry.author.toBech32(), createdAt: entry.createdAt});
}
catch(error){
console.log(error)
}
}
return profiles
}
defineProps({
msg: {
type: String,
@@ -364,7 +433,6 @@ defineProps({
</script>
<template>
<div class="greetings">
@@ -374,18 +442,45 @@ defineProps({
<h2 class="text-base-200-content text-center tracking-wide text-2xl">
Search the Nostr with Data Vending Machines</h2>
<h3>
<br>
<input class="c-Input" type="search" name="s" autofocus placeholder="Search..." v-model="message" @keyup.enter="send_search_request(message)" @keydown.enter="nextInput">
<button class="v-Button" @click="send_search_request(message)">Search the Nostr</button>
<br>
<input class="c-Input" type="search" name="s" autofocus placeholder="Search..." v-model="message" @keyup.enter="send_search_request(message)" @keydown.enter="nextInput">
<button class="v-Button" @click="send_search_request(message)">Search the Nostr</button>
</h3>
<!-- <details class="collapse bg-base">
<summary class="collapse-title font-thin bg ">Advanced Settings</summary>
<div class="collapse-content">
<p>content</p>
</div>
</details> -->
<details class="collapse bg-base " className="advanced" >
<summary class="collapse-title font-thin bg">Advanced Options</summary>
<div class="collapse-content font-size-0" className="z-10" id="collapse-settings">
<div>
<h4 className="inline-flex flex-none font-thin">by: </h4>
<div className="inline-flex flex-none" style="width: 10px;"></div>
<input list="users" id="user" class="u-Input" style="margin-left: 10px" type="search" name="user" autofocus placeholder="npub..." v-model="fromuser" @input="checkuser(fromuser)">
<datalist id="users">
<option v-for="profile in usernames" :value="profile.author">
{{profile.profile.name + ' (' + profile.profile.nip05 + ')'}}
</option>
</datalist>
</div>
</div>
<div className="inline-flex flex-none" style="width: 20px;"></div>
<div>
<h4 className="inline-flex flex-none font-thin">from:</h4>
<div className="inline-flex flex-none" style="width: 10px;"></div>
<VueDatePicker :teleport="true" :dark="true" position="left" className="bg-base-200 inline-flex flex-none" style="width: 220px;" v-model="datefrom"></VueDatePicker>
</div>
<div className="inline-flex flex-none" style="width: 20px;"></div>
<div>
<h4 className="inline-flex font-thin ">until: </h4>
<div className="inline-flex flex-none" style="width: 10px;"></div>
<VueDatePicker :teleport="true" :dark="true" position="left" className="bg-base-200 inline-flex flex-none" style="width: 220px;" v-model="dateto"></VueDatePicker>
</div>
</details>
</div>
<div class="max-w-5xl relative space-y-3">
<div class="grid grid-cols-1 gap-6">
@@ -400,7 +495,8 @@ defineProps({
</div>
<div className="col-end-2 w-auto card-body">
<p>{{ dvm.about }}</p>
<h3 class="fa-cut" v-html="StringUtil.parseHyperlinks(dvm.about)"></h3>
<div><br>
<span className="loading loading-dots loading-lg" ></span>
</div>
@@ -429,6 +525,15 @@ defineProps({
}
.u-Input {
@apply bg-base-200 text-accent dark:bg-base-200 dark:text-white focus:ring-white border border-transparent px-3 py-1.5 text-sm leading-4 text-accent-content transition-colors duration-300 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900;
width: 220px;
height: 35px;
}
.logo {
display: flex;
width:100%;
@@ -441,12 +546,19 @@ h3 {
font-size: 1.2rem;
}
h4 {
font-size: 1.0rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1000px) {
.greetings h1,

View File

@@ -1,12 +1,12 @@
<template>
<EasyDataTable class="customize-table" header-text-direction="left" v-if="store.state.results.length != 0" table-class-name="customize-table"
<EasyDataTable class="customize-table" header-text-direction="left" v-if="store.state.results.length != 0" table-class-name="customize-table"
:headers="headers"
:items="store.state.results" :sort-by="sortBy"
:sort-type="sortType">
<template #item-content="{ content, author, authorurl, avatar, indicator, links}">
<div class="playeauthor-wrapper">
<img class="avatar" v-if="avatar" :src="avatar" alt="Avatar" />
<img class="avatar" v-if="avatar" :src="avatar" alt="Avatar" onerror="this.src='https://noogle.lol/favicon.ico'" />
<img class="avatar" v-else src="@/assets/nostr-purple.svg" />
<a class="purple" :href="authorurl" target="_blank">{{ author }}</a>
@@ -21,21 +21,13 @@
<a class="menusmall" :href="links.uri" target="_blank">Nostr Client</a>
<a class="menusmall" :href="links.njump" target="_blank">NJump</a>
<a class="menusmall" :href="links.highlighter" target="_blank">Highlighter</a>
<!-- <a class="menusmall":href="links.nostrudel" target="_blank">Nostrudel</a> -->
<a class="menusmall":href="links.nostrudel" target="_blank">Nostrudel</a>
</div>
<!-- <p>{{content}}</p> -->
</template>
<!--<template #expand="item">
<div style="padding: 15px; text-align: left;" >
<a class="menu" :href="item.links.uri" target="_blank">Nostr Client</a>
<a class="menu" :href="item.links.njump" target="_blank">NJump</a>
<a class="menu" :href="item.links.highlighter" target="_blank">Highlighter</a>
<a class="menu":href="item.links.nostrudel" target="_blank">Nostrudel</a>
</div>
</template> -->
</EasyDataTable>
</template>
<script lang="ts" setup>
@@ -49,7 +41,6 @@ const sortType: SortType = "desc";
const headers: Header[] = [
{ text: "Results:", value: "content", fixed:true},
// { text: "Time", value: "indicator.time", sortable: true, },
];
@@ -95,7 +86,7 @@ const headers: Header[] = [
}
.customize-table {
width: auto;
--easy-table-border: 1px solid #000000;
--easy-table-border: 2px solid #000000;
--easy-table-row-border: 1px solid #000000;
--easy-table-header-font-size: 14px;
@@ -128,16 +119,10 @@ const headers: Header[] = [
--easy-table-rows-per-page-selector-option-padding: 10px;
--easy-table-rows-per-page-selector-z-index: 1;
--easy-table-scrollbar-track-color: #2d3a4f;
--easy-table-scrollbar-color: #2d3a4f;
--easy-table-scrollbar-thumb-color: #4c5d7a;;
--easy-table-scrollbar-corner-color: #2d3a4f;
--easy-table-scrollbar-track-color: bg-base;
--easy-table-scrollbar-color: bg-base;
--easy-table-scrollbar-thumb-color: bg-base;
--easy-table-scrollbar-corner-color: bg-base;
--easy-table-loading-mask-background-color: #2d3a4f;
}

View File

@@ -0,0 +1,469 @@
<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 {post_note, schedule, copyurl, copyinvoice, sleep, nextInput} from "../components/helper/Helper.vue"
import amberSignerService from "./android-signer/AndroidSigner";
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";
import index from "vuex";
let dvms =[]
let requestids = []
async function summarizefeed(eventids) {
listen()
let sortedIds = eventids.sort(function(a,b) {return (a.index > b.index) ? 1 : ((b.index > a.index) ? -1 : 0);} );
try {
if(store.state.pubkey === undefined || localStorage.getItem('nostr-key-method') === "anon"){
miniToastr.showMessage("In order to receive personalized recommendations, sign-in first.", "Not signed in.", VueNotifications.types.warn)
return
}
dvms = []
store.commit('set_summarization_dvms', dvms)
let client = store.state.client
let content = "NIP 90 Summarization request"
let kind = 5001
let tags = []
for (const tag of sortedIds){
try{
tags.push(["i", tag.id, "event"])
}
catch{}
}
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)
let result = await client.sendEvent(Event.fromJson(JSON.stringify(res)))
requestid = result.toHex()
}
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();
console.log(res)
}
requestids.push(requestid)
store.commit('set_current_request_id_summarization', requestids)
} catch (error) {
console.log(error);
}
}
async function listen() {
let client = store.state.client
let pubkey = store.state.pubkey
const filter = new Filter().kinds([7000, 6001]).pubkey(pubkey).since(Timestamp.now());
await client.subscribe([filter]);
const handle = {
// Handle event
handleEvent: async (relayUrl, subscriptionId, event) => {
/* if (store.state.summarizationhasEventListener === false){
return true
}*/
//const dvmname = getNamefromId(event.author.toHex())
console.log("Received new event from", relayUrl);
console.log(event.asJson())
let resonsetorequest = false
sleep(0).then(async () => {
for (let tag in event.tags) {
if (event.tags[tag].asVec()[0] === "e") {
if (store.state.requestidSummarization.includes(event.tags[tag].asVec()[1])){
resonsetorequest = true
}
}
}
if (resonsetorequest === true) {
if (event.kind === 7000) {
try {
console.log("7000: ", event.content);
console.log("DVM: " + event.author.toHex())
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.toHex()])
let created = 0
let current
console.log("NUM KIND0 FOUND " + profiles.length)
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 (dvms.filter(i => i.id === jsonentry.id).length === 0) {
dvms.push(jsonentry)
}
/*if (event.content !== ""){
status = event.content
}*/
dvms.find(i => i.id === jsonentry.id).status = status
store.commit('set_summarization_dvms', dvms)
} catch (error) {
console.log("Error: ", error);
}
}
else if (event.kind === 6001){
console.log(event.content)
dvms.find(i => i.id === event.author.toHex()).result = event.content
dvms.find(i => i.id === event.author.toHex()).status = "finished"
store.commit('set_summarization_dvms', dvms)
}
}
})
},
// Handle relay message
handleMsg: async (relayUrl, message) => {
//console.log("Received message from", relayUrl, message.asJson());
}
};
client.handleNotifications(handle);
}
async function zap_local(invoice) {
let success = await zap(invoice)
if (success){
dvms.find(i => i.bolt11 === invoice).status = "paid"
store.commit('set_summarization_dvms', dvms)
}
}
defineProps({
events: {
type: Array,
required: false
},
})
const isModalOpened = ref(false);
const modalcontent = ref("");
const datetopost = ref(Date.now());
const openModal = result => {
datetopost.value = Date.now();
isModalOpened.value = true;
modalcontent.value = resevents
};
const closeModal = () => {
isModalOpened.value = false;
};
const ttest = result => {
summarizefeed(result)
}
const submitHandler = async () => {
}
</script>
<!-- font-thin bg-gradient-to-r from-white to-nostr bg-clip-text text-transparent -->
<template>
<div class="greetings">
<h1 class="text-7xl font-black tracking-wide">Noogle</h1>
<h3 class="text-7xl font-black tracking-wide">Summarization</h3>
<h3>
<br>
<button class="v-Button" @click="summarizefeed($props.events)">Summarize Results</button>
</h3>
</div>
<br>
<div class=" relative space-y-2">
<div class="grid grid-cols-1 gap-2 " >
<div className="card w-70 bg-base-100 shadow-xl" v-for="dvm in store.state.summarizationdvms"
: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" >{{ 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> -->
<p v-if="dvm.status === 'finished'">{{dvm.result}}</p>
<!-- <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">
</div>
</details>-->
</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

@@ -1,9 +1,9 @@
// taken from https://github.com/hzrd149/nostrudel
import { nip19, verifySignature } from "nostr-tools";
import createDefer, { Deferred } from "./classes/deffered";
import { getPubkeyFromDecodeResult, isHexKey } from "./helpers/nip19";
import { NostrEvent } from "./types/nostr-event";
import {nip19, verifyEvent} from "nostr-tools";
import createDefer, {Deferred} from "./classes/deffered";
import {getPubkeyFromDecodeResult, isHexKey} from "./helpers/nip19";
import {NostrEvent} from "./types/nostr-event";
export function createGetPublicKeyIntent() {
return `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key`;
@@ -23,6 +23,26 @@ function rejectPending() {
}
}
export function createNip04EncryptIntent(pubkey: string, plainText: string) {
return `intent:${encodeURIComponent(
plainText,
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_encrypt;end`;
}
export function createNip04DecryptIntent(pubkey: string, data: string) {
return `intent:${encodeURIComponent(
data,
)}#Intent;scheme=nostrsigner;S.pubKey=${pubkey};S.compressionType=none;S.returnType=signature;S.type=nip04_decrypt;end`;
}
async function nip04Encrypt(pubkey: string, plaintext: string): Promise<string> {
return await intentRequest(createNip04EncryptIntent(pubkey, plaintext));
}
async function nip04Decrypt(pubkey: string, data: string): Promise<string> {
return await intentRequest(createNip04DecryptIntent(pubkey, data));
}
function onVisibilityChange() {
if (document.visibilityState === "visible") {
if (!pendingRequest || !navigator.clipboard) return;
@@ -66,15 +86,17 @@ async function getPublicKey() {
async function signEvent(draft): Promise<NostrEvent> {
const signedEventJson = await intentRequest(createSignEventIntent(draft));
const signedEvent = JSON.parse(signedEventJson) as NostrEvent;
if (!verifySignature(signedEvent)) throw new Error("Invalid signature");
if (!verifyEvent(signedEvent)) throw new Error("Invalid signature");
return signedEvent;
}
const amberSignerService = {
supported: navigator.userAgent.includes("Android") && navigator.clipboard,
getPublicKey,
signEvent
signEvent,
nip04Encrypt,
nip04Decrypt,
};
export default amberSignerService;

View File

@@ -1,9 +1,15 @@
import { getPublicKey, nip19 } from "nostr-tools";
import { bech32 } from '@scure/base'
export const Bech32MaxSize = 5000
export function isHexKey(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
return false;
}
export function isHex(str?: string) {
if (str?.match(/^[0-9a-f]+$/i)) return true;
return false;
}
export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
if (!result) return;
@@ -16,4 +22,12 @@ export function getPubkeyFromDecodeResult(result?: nip19.DecodeResult) {
case "nsec":
return getPublicKey(result.data);
}
}
}
function encodeBech32<Prefix extends string>(prefix: Prefix, data: Uint8Array): `${Prefix}1${string}` {
let words = bech32.toWords(data)
return bech32.encode(prefix, words, Bech32MaxSize) as `${Prefix}1${string}`
}
export function encodeBytes<Prefix extends string>(prefix: Prefix, bytes: Uint8Array): `${Prefix}1${string}` {
return encodeBech32(prefix, bytes)
}

View File

@@ -0,0 +1,62 @@
import { scrypt } from '@noble/hashes/scrypt'
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import { concatBytes, randomBytes } from '@noble/hashes/utils'
import { Bech32MaxSize, encodeBytes } from './nip19'
import { bech32 } from '@scure/base'
import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
export function encrypt(sec: Uint8Array, password: string, logn: number = 16, ksb: 0x00 | 0x01 | 0x02 = 0x02): string {
let salt = randomBytes(16)
let n = 2 ** logn
let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 })
let nonce = randomBytes(24)
let aad = Uint8Array.from([ksb])
let xc2p1 = xchacha20poly1305(key, nonce, aad)
let ciphertext = xc2p1.encrypt(sec)
let b = concatBytes(Uint8Array.from([0x02]), Uint8Array.from([logn]), salt, nonce, aad, ciphertext)
return encodeBytes('ncryptsec', b)
}
export function decrypt(ncryptsec: string, password: string): Uint8Array {
let { prefix, words } = bech32.decode(ncryptsec, Bech32MaxSize)
if (prefix !== 'ncryptsec') {
throw new Error(`invalid prefix ${prefix}, expected 'ncryptsec'`)
}
let b = new Uint8Array(bech32.fromWords(words))
let version = b[0]
if (version !== 0x02) {
throw new Error(`invalid version ${version}, expected 0x02`)
}
let logn = b[1]
let n = 2 ** logn
let salt = b.slice(2, 2 + 16)
let nonce = b.slice(2 + 16, 2 + 16 + 24)
let ksb = b[2 + 16 + 24]
let aad = Uint8Array.from([ksb])
let ciphertext = b.slice(2 + 16 + 24 + 1)
let key = scrypt(password, salt, { N: n, r: 8, p: 1, dkLen: 32 })
let xc2p1 = xchacha20poly1305(key, nonce, aad)
let sec = xc2p1.decrypt(ciphertext)
return sec
}
export function decryptwrapper(ncryptsec: string, password: string): String {
return bytesToHex(decrypt(ncryptsec, password))
}
const nip49 = {
encrypt,
decrypt,
decryptwrapper
};
export default nip49;

View File

@@ -1,250 +0,0 @@
[
{"name": "Albania"},
{"name": "Åland Islands"},
{"name": "Algeria"},
{"name": "American Samoa"},
{"name": "Andorra"},
{"name": "Angola"},
{"name": "Anguilla"},
{"name": "Antarctica"},
{"name": "Antigua and Barbuda"},
{"name": "Argentina"},
{"name": "Armenia"},
{"name": "Aruba"},
{"name": "Australia"},
{"name": "Austria"},
{"name": "Azerbaijan"},
{"name": "Bahamas (the)"},
{"name": "Bahrain"},
{"name": "Bangladesh"},
{"name": "Barbados"},
{"name": "Belarus"},
{"name": "Belgium"},
{"name": "Belize"},
{"name": "Benin"},
{"name": "Bermuda"},
{"name": "Bhutan"},
{"name": "Bolivia (Plurinational State of)"},
{"name": "Bonaire, Sint Eustatius and Saba"},
{"name": "Bosnia and Herzegovina"},
{"name": "Botswana"},
{"name": "Bouvet Island"},
{"name": "Brazil"},
{"name": "British Indian Ocean Territory (the)"},
{"name": "Brunei Darussalam"},
{"name": "Bulgaria"},
{"name": "Burkina Faso"},
{"name": "Burundi"},
{"name": "Cabo Verde"},
{"name": "Cambodia"},
{"name": "Cameroon"},
{"name": "Canada"},
{"name": "Cayman Islands (the)"},
{"name": "Central African Republic (the)"},
{"name": "Chad"},
{"name": "Chile"},
{"name": "China"},
{"name": "Christmas Island"},
{"name": "Cocos (Keeling) Islands (the)"},
{"name": "Colombia"},
{"name": "Comoros (the)"},
{"name": "Congo (the Democratic Republic of the)"},
{"name": "Congo (the)"},
{"name": "Cook Islands (the)"},
{"name": "Costa Rica"},
{"name": "Croatia"},
{"name": "Cuba"},
{"name": "Curaçao"},
{"name": "Cyprus"},
{"name": "Czechia"},
{"name": "Côte d'Ivoire"},
{"name": "Denmark"},
{"name": "Djibouti"},
{"name": "Dominica"},
{"name": "Dominican Republic (the)"},
{"name": "Ecuador"},
{"name": "Egypt"},
{"name": "El Salvador"},
{"name": "Equatorial Guinea"},
{"name": "Eritrea"},
{"name": "Estonia"},
{"name": "Eswatini"},
{"name": "Ethiopia"},
{"name": "Falkland Islands (the) [Malvinas]"},
{"name": "Faroe Islands (the)"},
{"name": "Fiji"},
{"name": "Finland"},
{"name": "France"},
{"name": "French Guiana"},
{"name": "French Polynesia"},
{"name": "French Southern Territories (the)"},
{"name": "Gabon"},
{"name": "Gambia (the)"},
{"name": "Georgia"},
{"name": "Germany"},
{"name": "Ghana"},
{"name": "Gibraltar"},
{"name": "Greece"},
{"name": "Greenland"},
{"name": "Grenada"},
{"name": "Guadeloupe"},
{"name": "Guam"},
{"name": "Guatemala"},
{"name": "Guernsey"},
{"name": "Guinea"},
{"name": "Guinea-Bissau"},
{"name": "Guyana"},
{"name": "Haiti"},
{"name": "Heard Island and McDonald Islands"},
{"name": "Holy See (the)"},
{"name": "Honduras"},
{"name": "Hong Kong"},
{"name": "Hungary"},
{"name": "Iceland"},
{"name": "India"},
{"name": "Indonesia"},
{"name": "Iran (Islamic Republic of)"},
{"name": "Iraq"},
{"name": "Ireland"},
{"name": "Isle of Man"},
{"name": "Israel"},
{"name": "Italy"},
{"name": "Jamaica"},
{"name": "Japan"},
{"name": "Jersey"},
{"name": "Jordan"},
{"name": "Kazakhstan"},
{"name": "Kenya"},
{"name": "Kiribati"},
{"name": "Korea (the Democratic People's Republic of)"},
{"name": "Korea (the Republic of)"},
{"name": "Kuwait"},
{"name": "Kyrgyzstan"},
{"name": "Lao People's Democratic Republic (the)"},
{"name": "Latvia"},
{"name": "Lebanon"},
{"name": "Lesotho"},
{"name": "Liberia"},
{"name": "Libya"},
{"name": "Liechtenstein"},
{"name": "Lithuania"},
{"name": "Luxembourg"},
{"name": "Macao"},
{"name": "Madagascar"},
{"name": "Malawi"},
{"name": "Malaysia"},
{"name": "Maldives"},
{"name": "Mali"},
{"name": "Malta"},
{"name": "Marshall Islands (the)"},
{"name": "Martinique"},
{"name": "Mauritania"},
{"name": "Mauritius"},
{"name": "Mayotte"},
{"name": "Mexico"},
{"name": "Micronesia (Federated States of)"},
{"name": "Moldova (the Republic of)"},
{"name": "Monaco"},
{"name": "Mongolia"},
{"name": "Montenegro"},
{"name": "Montserrat"},
{"name": "Morocco"},
{"name": "Mozambique"},
{"name": "Myanmar"},
{"name": "Namibia"},
{"name": "Nauru"},
{"name": "Nepal"},
{"name": "Netherlands (the)"},
{"name": "New Caledonia"},
{"name": "New Zealand"},
{"name": "Nicaragua"},
{"name": "Niger (the)"},
{"name": "Nigeria"},
{"name": "Niue"},
{"name": "Norfolk Island"},
{"name": "Northern Mariana Islands (the)"},
{"name": "Norway"},
{"name": "Oman"},
{"name": "Pakistan"},
{"name": "Palau"},
{"name": "Palestine, State of"},
{"name": "Panama"},
{"name": "Papua New Guinea"},
{"name": "Paraguay"},
{"name": "Peru"},
{"name": "Philippines (the)"},
{"name": "Pitcairn"},
{"name": "Poland"},
{"name": "Portugal"},
{"name": "Puerto Rico"},
{"name": "Qatar"},
{"name": "Republic of North Macedonia"},
{"name": "Romania"},
{"name": "Russian Federation (the)"},
{"name": "Rwanda"},
{"name": "Réunion"},
{"name": "Saint Barthélemy"},
{"name": "Saint Helena, Ascension and Tristan da Cunha"},
{"name": "Saint Kitts and Nevis"},
{"name": "Saint Lucia"},
{"name": "Saint Martin (French part)"},
{"name": "Saint Pierre and Miquelon"},
{"name": "Saint Vincent and the Grenadines"},
{"name": "Samoa"},
{"name": "San Marino"},
{"name": "Sao Tome and Principe"},
{"name": "Saudi Arabia"},
{"name": "Senegal"},
{"name": "Serbia"},
{"name": "Seychelles"},
{"name": "Sierra Leone"},
{"name": "Singapore"},
{"name": "Sint Maarten (Dutch part)"},
{"name": "Slovakia"},
{"name": "Slovenia"},
{"name": "Solomon Islands"},
{"name": "Somalia"},
{"name": "South Africa"},
{"name": "South Georgia and the South Sandwich Islands"},
{"name": "South Sudan"},
{"name": "Spain"},
{"name": "Sri Lanka"},
{"name": "Sudan (the)"},
{"name": "Suriname"},
{"name": "Svalbard and Jan Mayen"},
{"name": "Sweden"},
{"name": "Switzerland"},
{"name": "Syrian Arab Republic"},
{"name": "Taiwan (Province of China)"},
{"name": "Tajikistan"},
{"name": "Tanzania, United Republic of"},
{"name": "Thailand"},
{"name": "Timor-Leste"},
{"name": "Togo"},
{"name": "Tokelau"},
{"name": "Tonga"},
{"name": "Trinidad and Tobago"},
{"name": "Tunisia"},
{"name": "Turkey"},
{"name": "Turkmenistan"},
{"name": "Turks and Caicos Islands (the)"},
{"name": "Tuvalu"},
{"name": "Uganda"},
{"name": "Ukraine"},
{"name": "United Arab Emirates (the)"},
{"name": "United Kingdom of Great Britain and Northern Ireland (the)"},
{"name": "United States Minor Outlying Islands (the)"},
{"name": "United States of America (the)"},
{"name": "Uruguay"},
{"name": "Uzbekistan"},
{"name": "Vanuatu"},
{"name": "Venezuela (Bolivarian Republic of)"},
{"name": "Viet Nam"},
{"name": "Virgin Islands (British)"},
{"name": "Virgin Islands (U.S.)"},
{"name": "Wallis and Futuna"},
{"name": "Western Sahara"},
{"name": "Yemen"},
{"name": "Zambia"},
{"name": "Zimbabwe", "code": "ZW"}
]

View File

@@ -68,5 +68,7 @@
{"id": "91c0639025aba28c5af2178f49d653757bcc68e88d7cc461c86edc1ac2a61942"},
{"id": "aaf0b0846e265dec3dcf7b943ea2fc0331daf29a6114ac2eb971c10988e73f6d"},
{"id": "d26a9c5d89b9ce197e03bf91e2768df571cf04df796b5ae08742aea97be1c8c5"},
{"id": "490debe9303abe3c72fae49c62f8be15556a78c77c4d74e82305c5ce5723986a"}
{"id": "490debe9303abe3c72fae49c62f8be15556a78c77c4d74e82305c5ce5723986a"},
{"id": "52348da7537eb13da45277d755b7b26dfcb249b3b602b2c49b65ecd908c6cde0"},
{"id": "1f31fe5bfb3e75c5e984201bfd6be15632f266171ecaf8714829a503818865bf"}
]

Some files were not shown because too many files have changed in this diff Show More