mirror of
https://github.com/aljazceru/nostrdvm.git
synced 2026-02-07 15:24:20 +01:00
Merge branch 'main' of https://github.com/greenart7c3/nostrdvm
This commit is contained in:
@@ -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
3
.gitignore
vendored
@@ -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
45
.idea/dataSources.xml
generated
@@ -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>
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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()} ")
|
||||
|
||||
@@ -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()
|
||||
|
||||
18
examples/unleashed_dvm/.env_example
Normal file
18
examples/unleashed_dvm/.env_example
Normal 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
|
||||
26
examples/unleashed_dvm/README.md
Normal file
26
examples/unleashed_dvm/README.md
Normal 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.
|
||||
55
examples/unleashed_dvm/main.py
Normal file
55
examples/unleashed_dvm/main.py
Normal 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()
|
||||
103
examples/unleashed_dvm/test_client.py
Normal file
103
examples/unleashed_dvm/test_client.py
Normal 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
51
main.py
@@ -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
|
||||
|
||||
389
nostr_dvm/bot.py
389
nostr_dvm/bot.py
@@ -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)
|
||||
|
||||
315
nostr_dvm/dvm.py
315
nostr_dvm/dvm.py
@@ -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)
|
||||
|
||||
@@ -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
446
nostr_dvm/subscription.py
Normal 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)
|
||||
@@ -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.
|
||||
@@ -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__':
|
||||
|
||||
@@ -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,
|
||||
|
||||
266
nostr_dvm/tasks/content_discovery_currently_popular.py
Normal file
266
nostr_dvm/tasks/content_discovery_currently_popular.py
Normal 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)
|
||||
307
nostr_dvm/tasks/content_discovery_currently_popular_followers.py
Normal file
307
nostr_dvm/tasks/content_discovery_currently_popular_followers.py
Normal 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)
|
||||
@@ -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 = {
|
||||
|
||||
203
nostr_dvm/tasks/discovery_bot_farms.py
Normal file
203
nostr_dvm/tasks/discovery_bot_farms.py
Normal 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)
|
||||
197
nostr_dvm/tasks/discovery_censor_wot.py
Normal file
197
nostr_dvm/tasks/discovery_censor_wot.py
Normal 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)
|
||||
@@ -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": {
|
||||
|
||||
228
nostr_dvm/tasks/discovery_nonfollowers.py
Normal file
228
nostr_dvm/tasks/discovery_nonfollowers.py
Normal 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)
|
||||
@@ -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": {}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
215
nostr_dvm/tasks/search_users.py
Normal file
215
nostr_dvm/tasks/search_users.py
Normal 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)
|
||||
@@ -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__':
|
||||
|
||||
170
nostr_dvm/tasks/summarization_unleashed_chat.py
Normal file
170
nostr_dvm/tasks/summarization_unleashed_chat.py
Normal 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)
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
134
nostr_dvm/tasks/textgeneration_unleashed_chat.py
Normal file
134
nostr_dvm/tasks/textgeneration_unleashed_chat.py
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
218
nostr_dvm/utils/nip88_utils.py
Normal file
218
nostr_dvm/utils/nip88_utils.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:")
|
||||
|
||||
@@ -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 + "/"
|
||||
|
||||
|
||||
173
nostr_dvm/utils/subscription_utils.py
Normal file
173
nostr_dvm/utils/subscription_utils.py
Normal 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))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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
54
tests/bot.py
Normal 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
63
tests/discovery.py
Normal 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
52
tests/filter.py
Normal 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()
|
||||
@@ -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
33
tests/nwc.py
Normal 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()
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
2
ui/noogle/.env_example
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_NOOGLE_PK=""
|
||||
VITE_SUBSCRIPTIPON_VERIFIER_PUBKEY=""
|
||||
@@ -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>
|
||||
|
||||
@@ -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
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
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
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
BIN
ui/noogle/public/NWC.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
ui/noogle/public/shipyard.ico
Normal file
BIN
ui/noogle/public/shipyard.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
1627
ui/noogle/src/components/FilterGeneration.vue
Normal file
1627
ui/noogle/src/components/FilterGeneration.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
1116
ui/noogle/src/components/Login.vue
Normal file
1116
ui/noogle/src/components/Login.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
415
ui/noogle/src/components/NoteTable.vue
Normal file
415
ui/noogle/src/components/NoteTable.vue
Normal 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>
|
||||
128
ui/noogle/src/components/ProfileResultTable.vue
Normal file
128
ui/noogle/src/components/ProfileResultTable.vue
Normal 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>
|
||||
1432
ui/noogle/src/components/RecommendationGeneration.vue
Normal file
1432
ui/noogle/src/components/RecommendationGeneration.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
469
ui/noogle/src/components/SummarizationGeneration.vue
Normal file
469
ui/noogle/src/components/SummarizationGeneration.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
62
ui/noogle/src/components/android-signer/helpers/nip49.ts
Normal file
62
ui/noogle/src/components/android-signer/helpers/nip49.ts
Normal 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;
|
||||
@@ -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"}
|
||||
]
|
||||
@@ -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
Reference in New Issue
Block a user