Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with npub key signing for DM messages #101

Open
wants to merge 55 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
77af490
client
callebtc Dec 26, 2022
269032d
clean
callebtc Dec 26, 2022
1be4a85
Merge pull request #1 from callebtc/client
callebtc Dec 26, 2022
6c572e1
relative imports
callebtc Dec 26, 2022
dfca8bf
two relays
callebtc Dec 26, 2022
13c8a8d
print hex as default
callebtc Dec 26, 2022
3c27539
silent
callebtc Dec 26, 2022
d7fb45f
connect optional
callebtc Dec 26, 2022
06362a4
error checking and reconnect
callebtc Jan 24, 2023
56deff4
Merge pull request #2 from callebtc/connection_state
callebtc Jan 24, 2023
479b776
error checking and reconnect
callebtc Jan 24, 2023
09f4b67
nostr close
callebtc Jan 25, 2023
c5a050f
Merge pull request #3 from callebtc/fix/nostr_close
callebtc Jan 25, 2023
3794ef2
Do not reconnect on close
callebtc Jan 25, 2023
3aee210
Merge branch 'main' into track-connection-state
callebtc Jan 30, 2023
76815b7
count events
callebtc Jan 31, 2023
1d6ee33
subscribe global
callebtc Feb 1, 2023
36768d3
add filter and ping
callebtc Feb 1, 2023
c22a3e0
rename to ping
callebtc Feb 1, 2023
0c9dcc7
enable ping
callebtc Feb 1, 2023
f9e1a53
ping is a property
callebtc Feb 1, 2023
fa802d7
verbose stuff
callebtc Feb 1, 2023
9520aab
Merge branch 'main' of https://github.com/jeffthibault/python-nostr i…
callebtc Feb 2, 2023
fb3bbca
Merge branch 'jeffthibault-main'
callebtc Feb 2, 2023
88d4367
also reuse proxy
callebtc Feb 2, 2023
21b3d66
Merge branch 'main' into feat/count_events
callebtc Feb 2, 2023
b1b4e71
Merge branch 'track-connection-state'
callebtc Feb 2, 2023
4a56c00
update client to main
callebtc Feb 2, 2023
15b16da
not working client
callebtc Feb 7, 2023
15e291e
Merge remote-tracking branch 'upstream/main'
callebtc Feb 7, 2023
2dfb24a
fix: Events initialization
callebtc Feb 7, 2023
c9c0000
udpate client
callebtc Feb 7, 2023
455d44b
Merge pull request #5 from callebtc/fix/dataclass_update
callebtc Feb 7, 2023
8e39666
update client
callebtc Feb 7, 2023
1812b01
message queues
callebtc Feb 7, 2023
6d5e2b3
Merge branch 'main' into feat/count_events
callebtc Feb 7, 2023
5054fea
fix filter
callebtc Feb 7, 2023
1331ca1
count events and queue
callebtc Feb 7, 2023
123cfb5
Merge pull request #6 from callebtc:feat/count_events
callebtc Feb 7, 2023
c4fbeb8
update client
callebtc Feb 8, 2023
557a016
relative import
callebtc Feb 8, 2023
826dca7
refactor
callebtc Feb 8, 2023
d56f9ca
update client
callebtc Feb 9, 2023
56fff73
connection state on connect
callebtc Feb 9, 2023
be948b6
subscribe
callebtc Feb 9, 2023
f598039
fix
callebtc Feb 9, 2023
0328fc3
bech32 working
callebtc Feb 13, 2023
383f49e
fix default pubkey
callebtc Feb 13, 2023
2872fe3
filter for DM
callebtc Feb 13, 2023
1f5c6f6
threads are daemon mode
callebtc Feb 25, 2023
59ddc1b
Merge pull request #7 from callebtc/threads-daemon
callebtc Feb 25, 2023
880dd11
add default relays
callebtc Feb 25, 2023
8086e4d
update client for debugging
callebtc Apr 14, 2023
4891923
Update relay.py
vicariousdrama Oct 12, 2023
012d1e2
Merge pull request #9 from vicariousdrama/patch-1
callebtc Dec 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added __init__.py
Empty file.
118 changes: 118 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from nostr.client.client import NostrClient
from nostr.event import Event
from nostr.key import PublicKey
import asyncio
import threading
import time
import datetime


def print_status(client):
print("")
for relay in client.relay_manager.relays.values():
connected_text = "🟢" if relay.connected else "🔴"
status_text = f"{connected_text} ⬆️ {relay.num_sent_events} ⬇️ {relay.num_received_events} ⚠️ {relay.error_counter} ⏱️ {relay.ping} ms - {relay.url.split('//')[1]}"
print(status_text)


async def dm():
print("This is an example NIP-04 DM flow")
pk = input("Enter your privatekey to post from (enter nothing for a random one): ")

def callback(event: Event, decrypted_content):
"""
Callback to trigger when a DM is received.
"""
print(
f"\nFrom {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
)

client = NostrClient(private_key=pk)
if not pk:
print(f"Your private key: {client.private_key.bech32()}")

print(f"Your public key: {client.public_key.bech32()}")

t = threading.Thread(
target=client.get_dm, args=(client.public_key, callback), daemon=True
)
t.start()

pubkey_to_str = (
input("Enter other pubkey to DM to (enter nothing to DM yourself): ")
or client.public_key.hex()
)
if pubkey_to_str.startswith("npub"):
pubkey_to = PublicKey().from_npub(pubkey_to_str)
else:
pubkey_to = PublicKey(bytes.fromhex(pubkey_to_str))
print(f"Sending DMs to {pubkey_to.bech32()}")
while True:
print_status(client)
await asyncio.sleep(1)
msg = input("\nEnter message: ")
client.dm(msg, pubkey_to)


async def post():
print("This posts and reads a nostr note")
pk = input("Enter your privatekey to post from (enter nothing for a random one): ")

def callback(event: Event):
"""
Callback to trigger when post appers.
"""
print(
f"\nFrom {event.public_key[:3]}..{event.public_key[-3:]}: {event.content}"
)

sender_client = NostrClient(private_key=pk)
# await asyncio.sleep(1)

pubkey_to_str = (
input(
"Enter other pubkey (enter nothing to read your own posts, enter * for all): "
)
or sender_client.public_key.hex()
)
if pubkey_to_str == "*":
pubkey_to = None
elif pubkey_to_str.startswith("npub"):
pubkey_to = PublicKey().from_npub(pubkey_to_str)
else:
pubkey_to = PublicKey(bytes.fromhex(pubkey_to_str))

print(f"Subscribing to posts by {pubkey_to.bech32() if pubkey_to else 'everyone'}")

filters = {
"since": int(
time.mktime(
(datetime.datetime.now() - datetime.timedelta(hours=1)).timetuple()
)
)
}

t = threading.Thread(
target=sender_client.get_post,
args=(
pubkey_to,
callback,
filters,
),
daemon=True,
)
t.start()

while True:
print_status(sender_client)
await asyncio.sleep(1)
msg = input("\nEnter post: ")
sender_client.post(msg)


if input("Enter '1' for DM, '2' for Posts (Default: 1): ") == "2":
# make a post and subscribe to posts
asyncio.run(post())
else:
# write a DM and receive DMs
asyncio.run(dm())
Empty file added nostr/client/__init__.py
Empty file.
41 changes: 41 additions & 0 deletions nostr/client/cbc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

from Cryptodome import Random
from Cryptodome.Cipher import AES

plain_text = "This is the text to encrypts"

# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode()
# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode()

key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795")

BLOCK_SIZE = 16

class AESCipher(object):
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')

"""
def __init__(self, key=None):
self.key = key

def pad(self, data):
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
return data + (chr(length) * length).encode()

def unpad(self, data):
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]

def encrypt(self, plain_text):
cipher = AES.new(self.key, AES.MODE_CBC)
b = plain_text.encode("UTF-8")
return cipher.iv, cipher.encrypt(self.pad(b))

def decrypt(self, iv, enc_text):
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))

if __name__ == "__main__":
aes = AESCipher(key=key)
iv, enc_text = aes.encrypt(plain_text)
dec_text = aes.decrypt(iv, enc_text)
print(dec_text)
167 changes: 167 additions & 0 deletions nostr/client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
from typing import *
import ssl
import time
import json
import os
import base64

from ..event import Event
from ..relay_manager import RelayManager
from ..message_type import ClientMessageType
from ..key import PrivateKey, PublicKey

from ..filter import Filter, Filters
from ..event import Event, EventKind, EncryptedDirectMessage
from ..relay_manager import RelayManager
from ..message_type import ClientMessageType

# from aes import AESCipher
from . import cbc


class NostrClient:
relays = [
# "wss://eagerporpoise9.lnbits.com/nostrclient/api/v1/relay",
"wss://localhost:5001/nostrclient/api/v1/relay",
# "wss://nostr-pub.wellorder.net",
# "wss://relay.damus.io",
# "wss://nostr.zebedee.cloud",
# "wss://relay.snort.social",
# "wss://nostr.fmt.wiz.biz",
# "wss://nos.lol",
# "wss://nostr.oxtr.dev",
# "wss://relay.current.fyi",
# "wss://relay.snort.social",
] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr"
relay_manager = RelayManager()
private_key: PrivateKey
public_key: PublicKey

def __init__(self, private_key: str = "", relays: List[str] = [], connect=True):
self.generate_keys(private_key)

if len(relays):
self.relays = relays
if connect:
self.connect()

def connect(self):
for relay in self.relays:
self.relay_manager.add_relay(relay)
self.relay_manager.open_connections(
{"cert_reqs": ssl.CERT_NONE}
) # NOTE: This disables ssl certificate verification

def close(self):
self.relay_manager.close_connections()

def generate_keys(self, private_key: str = None):
if private_key.startswith("nsec"):
self.private_key = PrivateKey.from_nsec(private_key)
elif private_key:
self.private_key = PrivateKey(bytes.fromhex(private_key))
else:
self.private_key = PrivateKey() # generate random key
self.public_key = self.private_key.public_key

def post(self, message: str):
event = Event(message, self.public_key.hex(), kind=EventKind.TEXT_NOTE)
self.private_key.sign_event(event)
event_json = event.to_message()
# print("Publishing message:")
# print(event_json)
self.relay_manager.publish_message(event_json)

def get_post(
self, sender_publickey: PublicKey = None, callback_func=None, filter_kwargs={}
):
filter = Filter(
authors=[sender_publickey.hex()] if sender_publickey else None,
kinds=[EventKind.TEXT_NOTE],
**filter_kwargs,
)
filters = Filters([filter])
subscription_id = os.urandom(4).hex()
self.relay_manager.add_subscription(subscription_id, filters)

request = [ClientMessageType.REQUEST, subscription_id]
request.extend(filters.to_json_array())
message = json.dumps(request)
# print(message)
self.relay_manager.publish_message(message)

while True:
while self.relay_manager.message_pool.has_events():
event_msg = self.relay_manager.message_pool.get_event()
if callback_func:
callback_func(event_msg.event)
time.sleep(0.1)

def dm(self, message: str, to_pubkey: PublicKey):
dm = EncryptedDirectMessage(
recipient_pubkey=to_pubkey.hex(), cleartext_content=message
)
self.private_key.sign_event(dm)
# print(dm)
self.relay_manager.publish_event(dm)

def get_dm(self, sender_publickey: PublicKey, callback_func=None, filter_kwargs={}):
filters = Filters(
[
Filter(
kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE],
pubkey_refs=[sender_publickey.hex()],
**filter_kwargs,
)
]
)
subscription_id = os.urandom(4).hex()
self.relay_manager.add_subscription(subscription_id, filters)

request = [ClientMessageType.REQUEST, subscription_id]
request.extend(filters.to_json_array())
message = json.dumps(request)
self.relay_manager.publish_message(message)
# print(message)
while True:
while self.relay_manager.message_pool.has_events():
event_msg = self.relay_manager.message_pool.get_event()
if "?iv=" in event_msg.event.content:
try:
shared_secret = self.private_key.compute_shared_secret(
event_msg.event.public_key
)
aes = cbc.AESCipher(key=shared_secret)
enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=")
iv = base64.decodebytes(iv_b64.encode("utf-8"))
enc_text = base64.decodebytes(enc_text_b64.encode("utf-8"))
dec_text = aes.decrypt(iv, enc_text)
if callback_func:
callback_func(event_msg.event, dec_text)
except:
pass
break
time.sleep(0.1)

def subscribe(
self,
callback_events_func=None,
callback_notices_func=None,
callback_eosenotices_func=None,
):
while True:
while self.relay_manager.message_pool.has_events():
event_msg = self.relay_manager.message_pool.get_event()
print(event_msg.event.content)
if callback_events_func:
callback_events_func(event_msg)
while self.relay_manager.message_pool.has_notices():
event_msg = self.relay_manager.message_pool.has_notices()
if callback_notices_func:
callback_notices_func(event_msg)
while self.relay_manager.message_pool.has_eose_notices():
event_msg = self.relay_manager.message_pool.get_eose_notice()
if callback_eosenotices_func:
callback_eosenotices_func(event_msg)

time.sleep(0.1)
Loading