Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 33 additions & 16 deletions helpers.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
from bech32 import bech32_decode, convertbits
from nostr_sdk import (
ClientMessage,
EventBuilder,
Keys,
Kind,
PublicKey,
SecretKey,
Tag,
nip04_encrypt,
)


def normalize_public_key(pubkey: str) -> str:
if pubkey.startswith("npub1"):
_, decoded_data = bech32_decode(pubkey)
if not decoded_data:
raise ValueError("Public Key is not valid npub")
def create_encrypted_dm_message(
sender_private_key: str | None, recipient_public_key: str, message: str
) -> tuple[str, str, str]:
try:
keys = (
Keys(SecretKey.parse(sender_private_key))
if sender_private_key
else Keys.generate()
)
recipient = PublicKey.parse(recipient_public_key)
content = nip04_encrypt(keys.secret_key(), recipient, message)
event = (
EventBuilder(Kind(4), content)
.tags([Tag.public_key(recipient)])
.sign_with_keys(keys)
)
except Exception as ex:
raise ValueError("Cannot generate encrypted direct message event") from ex

decoded_data_bits = convertbits(decoded_data, 5, 8, False)
if not decoded_data_bits:
raise ValueError("Public Key is not valid npub")
return bytes(decoded_data_bits).hex()

# check if valid hex
if len(pubkey) != 64:
raise ValueError("Public Key is not valid hex")
int(pubkey, 16)
return pubkey
return (
keys.secret_key().to_hex(),
recipient.to_hex(),
ClientMessage.event(event).as_json(),
)
46 changes: 6 additions & 40 deletions nostr/bech32.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,7 @@
# Copyright (c) 2017, 2020 Pieter Wuille
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Reference implementation for Bech32/Bech32m and segwit addresses."""


from enum import Enum


class Encoding(Enum):
"""Enumeration type to list the various supported encodings."""

BECH32 = 1
BECH32M = 2

Expand All @@ -36,7 +11,6 @@ class Encoding(Enum):


def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for value in values:
Expand All @@ -48,12 +22,10 @@ def bech32_polymod(values):


def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def bech32_verify_checksum(hrp, data):
"""Verify a checksum given HRP and converted data characters."""
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
if const == 1:
return Encoding.BECH32
Expand All @@ -63,21 +35,18 @@ def bech32_verify_checksum(hrp, data):


def bech32_create_checksum(hrp, data, spec):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]


def bech32_encode(hrp, data, spec):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data, spec)
return hrp + "1" + "".join([CHARSET[d] for d in combined])


def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
Expand All @@ -97,7 +66,6 @@ def bech32_decode(bech):


def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
Expand All @@ -120,29 +88,27 @@ def convertbits(data, frombits, tobits, pad=True):


def decode(hrp, addr):
"""Decode a segwit address."""
hrpgot, data, spec = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
decoded = convertbits(data[1:], 5, 8, False) # type: ignore
decoded = convertbits(data[1:], 5, 8, False) # type: ignore[index]
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return (None, None)
if data[0] > 16: # type: ignore
if data[0] > 16: # type: ignore[index]
return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: # type: ignore
if data[0] == 0 and len(decoded) not in {20, 32}: # type: ignore[index]
return (None, None)
if (
data[0] == 0 # type: ignore
data[0] == 0 # type: ignore[index]
and spec != Encoding.BECH32
or data[0] != 0 # type: ignore
or data[0] != 0 # type: ignore[index]
and spec != Encoding.BECH32M
):
return (None, None)
return (data[0], decoded) # type: ignore
return (data[0], decoded) # type: ignore[index]


def encode(hrp, witver, witprog):
"""Encode a segwit address."""
spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
wit_prog = convertbits(witprog, 8, 5)
assert wit_prog
Expand Down
1 change: 1 addition & 0 deletions nostr/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def close(self):
try:
self.relay_manager.close_all_subscriptions()
self.relay_manager.close_connections()
self.relay_manager.shutdown()

self.running = False
except Exception as e:
Expand Down
59 changes: 22 additions & 37 deletions nostr/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from dataclasses import dataclass, field
from enum import IntEnum
from hashlib import sha256
from typing import Optional

import coincurve
from nostr_sdk import Event as SdkEvent

from .message_type import ClientMessageType

Expand All @@ -21,18 +20,15 @@ class EventKind(IntEnum):

@dataclass
class Event:
content: Optional[str] = None
public_key: Optional[str] = None
created_at: Optional[int] = None
content: str | None = None
public_key: str | None = None
created_at: int | None = None
kind: int = EventKind.TEXT_NOTE
tags: list[list[str]] = field(
default_factory=list
) # Dataclasses require special handling when the default value is a mutable type
signature: Optional[str] = None
tags: list[list[str]] = field(default_factory=list)
signature: str | None = None

def __post_init__(self):
if self.content is not None and not isinstance(self.content, str):
# DMs initialize content to None but all other kinds should pass in a str
raise TypeError("Argument 'content' must be of type str")

if self.created_at is None:
Expand All @@ -56,7 +52,6 @@ def compute_id(

@property
def id(self) -> str:
# Always recompute the id to reflect the up-to-date state of the Event
assert self.public_key
assert self.created_at
assert self.content
Expand All @@ -65,41 +60,34 @@ def id(self) -> str:
)

def add_pubkey_ref(self, pubkey: str):
"""Adds a reference to a pubkey as a 'p' tag"""
self.tags.append(["p", pubkey])

def add_event_ref(self, event_id: str):
"""Adds a reference to an event_id as an 'e' tag"""
self.tags.append(["e", event_id])

def verify(self) -> bool:
assert self.public_key
assert self.signature
pub_key = coincurve.PublicKeyXOnly(bytes.fromhex(self.public_key))
return pub_key.verify(bytes.fromhex(self.signature), bytes.fromhex(self.id))
return SdkEvent.from_json(json.dumps(self.to_dict())).verify()

def to_dict(self) -> dict:
return {
"id": self.id,
"pubkey": self.public_key,
"created_at": self.created_at,
"kind": self.kind,
"tags": self.tags,
"content": self.content,
"sig": self.signature,
}

def to_message(self) -> str:
return json.dumps(
[
ClientMessageType.EVENT,
{
"id": self.id,
"pubkey": self.public_key,
"created_at": self.created_at,
"kind": self.kind,
"tags": self.tags,
"content": self.content,
"sig": self.signature,
},
]
)
return json.dumps([ClientMessageType.EVENT, self.to_dict()])


@dataclass
class EncryptedDirectMessage(Event):
recipient_pubkey: Optional[str] = None
cleartext_content: Optional[str] = None
reference_event_id: Optional[str] = None
recipient_pubkey: str | None = None
cleartext_content: str | None = None
reference_event_id: str | None = None

def __post_init__(self):
if self.content is not None:
Expand All @@ -111,11 +99,8 @@ def __post_init__(self):

self.kind = EventKind.ENCRYPTED_DIRECT_MESSAGE
super().__post_init__()

# Must specify the DM recipient's pubkey in a 'p' tag
self.add_pubkey_ref(self.recipient_pubkey)

# Optionally specify a reference event (DM) this is a reply to
if self.reference_event_id is not None:
self.add_event_ref(self.reference_event_id)

Expand Down
Loading