Last active
March 14, 2023 00:45
-
-
Save davestgermain/12974fef590fc1edf12e9a25b9dfa7b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Program that demostrates the protocol negotiation explained in https://github.com/nostr-protocol/nips/pull/351 | |
requires aionostr: pip install aionostr | |
""" | |
import asyncio | |
from aionostr.key import PrivateKey, PublicKey | |
from aionostr.event import Event | |
from aionostr import Manager | |
from hashlib import sha256 | |
import secrets | |
import time | |
import json | |
async def main(relays, to_pubkey=None): | |
me = PrivateKey() | |
print(f"My pubkey {me.public_key.hex()}\n") | |
try: | |
if to_pubkey: | |
print(f"Intiating private DM with {to_pubkey}") | |
await request_private_chat( | |
me, | |
to_pubkey, | |
relays=relays, | |
) | |
else: | |
print(f"In another terminal, run: dm.py {me.public_key.hex()}") | |
await wait_for_dm_request(me, relays=relays) | |
except KeyboardInterrupt: | |
return | |
def make_dm( | |
sender_privatekey, recipient_pubkey, content, kind=4, tags=None, expiration=86400 | |
): | |
tags = tags or [] | |
if expiration: | |
tags.append(["expiration", str(int(time.time() + expiration))]) | |
dm = Event( | |
pubkey=sender_privatekey.public_key.hex(), | |
kind=kind, | |
tags=tags, | |
content=sender_privatekey.encrypt_message(content, recipient_pubkey), | |
) | |
dm.sign(sender_privatekey.hex()) | |
return dm | |
def make_safe_dm_request( | |
sender_privatekey, | |
recipient_pubkey, | |
relays=["wss://nos.lol"], | |
kind=20004, | |
token=None, | |
request_private_key=None, | |
request_dm_kind=4, | |
print_request=False, | |
): | |
tags = [["r", relay] for relay in relays] | |
tags.append(["p", recipient_pubkey]) | |
request = Event( | |
pubkey=sender_privatekey.public_key.hex(), | |
tags=tags, | |
content=token or secrets.token_hex(), | |
kind=kind, | |
) | |
request.sign(sender_privatekey.hex()) | |
if print_request: | |
print("\n-------\nContents of request (will be encrypted):") | |
print(json.dumps(request.to_json_object(), indent=4)) | |
print("\n------") | |
if not request_private_key: | |
tags = [["p", recipient_pubkey]] | |
else: | |
tags = [] | |
request_private_key = request_private_key or PrivateKey() | |
envelope = make_dm( | |
request_private_key, | |
recipient_pubkey, | |
str(request), | |
tags=tags, | |
kind=request_dm_kind, | |
) | |
return envelope, get_conversation_key(request.content, recipient_pubkey) | |
def get_conversation_key(token, pubkey): | |
# the request content is a token used to create a new private key | |
return PrivateKey(sha256((token + pubkey).encode()).digest()) | |
def receive_dm_request(recipient_privatekey, envelope): | |
try: | |
request_data = recipient_privatekey.decrypt_message( | |
envelope.content, envelope.pubkey | |
) | |
except ValueError: | |
# print(f"Failed to decrypt from {envelope.pubkey}") | |
return | |
try: | |
request_event = Event(**json.loads(request_data)) | |
except json.JSONEncoder: | |
# not a valid request | |
return | |
if request_event.verify(): | |
# this ensures that the request is addressed to me | |
if all(request_event.has_tag("p", recipient_privatekey.public_key.hex())): | |
conversation_key = get_conversation_key( | |
request_event.content, recipient_privatekey.public_key.hex() | |
) | |
return ( | |
conversation_key, | |
request_event.pubkey, | |
request_event.kind, | |
[tag[1] for tag in request_event.tags if tag[0] == "r"], | |
) | |
async def send_private_dm(relays, queue, from_private_key, to_pubkey, to_kind): | |
print(f"Sender connecting to {relays}") | |
async with Manager(relays, private_key=from_private_key.hex()) as manager: | |
while True: | |
dm_content = await queue.get() | |
dm = make_dm(from_private_key, to_pubkey, content=dm_content, kind=to_kind) | |
await manager.add_event(dm) | |
async def receive_private_dm(relays, private_key, receive_pubkey, kind, since=1): | |
query = {"kinds": [kind], "limit": 10, "since": since} | |
if kind == 4: | |
query["#p"] = [private_key.public_key.hex()] | |
else: | |
query["authors"] = [private_key.public_key.hex()] | |
print(f"Waiting for events on {relays} {query}") | |
async with Manager(relays, private_key=private_key.hex()) as manager: | |
async for event in manager.get_events(query, only_stored=False): | |
dm_request = receive_dm_request(private_key, event) | |
if dm_request: | |
to_key, pub_key, to_kind, to_relays = dm_request | |
print( | |
f"Received chat request from {pub_key} -> {to_kind} @ {to_relays} {to_key.public_key.hex()}" | |
) | |
if input("Continue conversation? [Y/n] ").strip() != "n": | |
return to_key, pub_key, to_kind, to_relays | |
else: | |
try: | |
message = ( | |
private_key.decrypt_message(event.content, receive_pubkey) | |
.strip() | |
.rjust(80) | |
) | |
# message = f"{event.pubkey[:4]}: {private_key.decrypt_message(event.content, receive_pubkey)}" | |
print(message) | |
except ValueError: | |
print(f"Failed to decrypt {event}") | |
continue | |
async def request_private_chat( | |
from_private_key, | |
to_public_key, | |
relays=["ws://localhost:6969"], | |
conversation_kind=20004, | |
): | |
envelope, receive_key = make_safe_dm_request( | |
from_private_key, | |
to_public_key, | |
relays=relays, | |
request_dm_kind=4, | |
kind=conversation_kind, | |
) | |
async with Manager(relays, private_key=from_private_key.hex()) as manager: | |
print(f"Sending request to {envelope.pubkey} {envelope.kind}") | |
await manager.add_event(envelope) | |
send_key, _, send_kind, send_relays = await receive_private_dm( | |
relays, receive_key, None, conversation_kind, since=envelope.created_at | |
) | |
await start_private_chat( | |
receive_key, send_key, relays, send_relays, conversation_kind, send_kind | |
) | |
async def start_private_chat( | |
receive_key, send_key, receive_relays, send_relays, receive_kind, send_kind | |
): | |
send_queue = asyncio.Queue() | |
receive_task = asyncio.create_task( | |
receive_private_dm( | |
receive_relays, | |
receive_key, | |
send_key.public_key.hex(), | |
receive_kind, | |
since=int(time.time()) - 1, | |
) | |
) | |
send_task = asyncio.create_task( | |
send_private_dm( | |
send_relays, send_queue, send_key, receive_key.public_key.hex(), send_kind | |
) | |
) | |
await asyncio.sleep(0.5) | |
prompt = Prompt() | |
print( | |
f"Connected {receive_key.public_key.hex()} to {send_key.public_key.hex()}\n========================\n" | |
) | |
while True: | |
response = await prompt("") | |
if response is None: | |
break | |
elif response: | |
await send_queue.put(response) | |
async def wait_for_dm_request(from_private_key, relays=["wss://nostr.mom"]): | |
conversation_kind = 20044 | |
send_key, to_pubkey, send_kind, send_relays = await receive_private_dm( | |
relays, from_private_key, None, 4 | |
) | |
envelope, receive_key = make_safe_dm_request( | |
from_private_key, | |
send_key.public_key.hex(), | |
relays=relays, | |
request_dm_kind=send_kind, | |
request_private_key=send_key, | |
kind=conversation_kind, | |
) | |
async with Manager(send_relays, private_key=send_key.hex()) as manager: | |
print(f"Sending response to {envelope.pubkey} {envelope.kind}") | |
await manager.add_event(envelope) | |
await start_private_chat( | |
receive_key, send_key, relays, send_relays, conversation_kind, send_kind | |
) | |
class Prompt: | |
def __init__(self, loop=None): | |
import sys | |
self.loop = loop or asyncio.get_event_loop() | |
self.q = asyncio.Queue() | |
self.loop.add_reader(sys.stdin, self.got_input) | |
def got_input(self): | |
asyncio.ensure_future(self.q.put(sys.stdin.readline()), loop=self.loop) | |
async def __call__(self, msg, end="", flush=True): | |
print(msg, end=end, flush=flush) | |
try: | |
return (await self.q.get()).rstrip("\n") | |
except: | |
return | |
def demo(): | |
alice = PrivateKey( | |
bytes.fromhex( | |
"58483cd4ca00512e24ae55d1938b5e488a6a12cda576dc315793baa5f65642f1" | |
) | |
) | |
alice_pubkey = alice.public_key.hex() | |
bob = PrivateKey( | |
bytes.fromhex( | |
"3b450ab159dfbd9cce9e384a03a54587d92d494c5bf64d1f29c5e02a5ae81ff1" | |
) | |
) | |
bob_pubkey = bob.public_key.hex() | |
# alice wants to send to bob | |
# alice will listen on wss://relayalice.com | |
relays = ["wss://relayalice.com"] | |
envelope, bob_alice_key = make_safe_dm_request(alice, bob_pubkey, relays) | |
print(f"Alice pubkey: {alice_pubkey}") | |
print(f"Bob pubkey: {bob_pubkey}") | |
print("\nFirst, Alice sends Bob a DM request:") | |
print(json.dumps(envelope.to_json_object(), indent=4)) | |
print("\nBob receives the DM...") | |
bob_alice_key, requestor_pubkey, convo_kind, convo_relays = receive_dm_request( | |
bob, envelope | |
) | |
print( | |
f"Alice ({requestor_pubkey}) wants to talk to Bob using {bob_alice_key.public_key.hex()} at {convo_kind} -> {convo_relays}" | |
) | |
print(f"Bob sends a request to Alice at her chosen location...") | |
print(f"Bob would like to use kind=20044 at wss://relaybob.com") | |
envelope, alice_bob_key = make_safe_dm_request( | |
bob, | |
alice.public_key.hex(), | |
["wss://relaybob.com"], | |
kind=20044, | |
ephemeral_privatekey=bob_alice_key, | |
conversation_kind=20004, | |
print_request=True, | |
) | |
print(json.dumps(envelope.to_json_object(), indent=4)) | |
print(f"\nAlice receives and validates the request at wss://relayalice.com...") | |
alice_bob_key, requestor_pubkey, convo_kind2, convo_relays2 = receive_dm_request( | |
alice, envelope | |
) | |
print( | |
f"\nBob ({requestor_pubkey}) wants to talk to Alice using {alice_bob_key.public_key.hex()} at {convo_kind2} -> {convo_relays2}" | |
) | |
print(f"\nFinally, Alice starts the conversation, posting to wss://relaybob.com:") | |
print( | |
json.dumps( | |
make_dm( | |
alice_bob_key, | |
bob_alice_key.public_key.hex(), | |
content="Hello Bob!", | |
kind=convo_kind2, | |
).to_json_object(), | |
indent=4, | |
) | |
) | |
if __name__ == "__main__": | |
import sys | |
# relays = ["wss://relay-pub.deschooling.us/"] | |
relays = ["wss://nostr.mom/"] | |
# relays = ["wss://nos.lol/"] | |
# relays = ["ws://localhost:6969"] | |
if len(sys.argv) >= 2: | |
to_pubkey = sys.argv[1] | |
else: | |
to_pubkey = None | |
asyncio.run(main(relays, to_pubkey)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment