buzzkit 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {buzzkit-0.1.0 → buzzkit-0.1.2}/.gitignore +2 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/Cargo.lock +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/Cargo.toml +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/PKG-INFO +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/claim_invite.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/list_channels.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/offline_sign.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/send_message.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/set_profile.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/subscribe.py +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/pyproject.toml +1 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/python/buzzkit/__init__.py +9 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/python/buzzkit/_native.pyi +13 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/python/buzzkit/client.py +19 -1
- {buzzkit-0.1.0 → buzzkit-0.1.2}/src/lib.rs +73 -3
- {buzzkit-0.1.0 → buzzkit-0.1.2}/tests/test_signing.py +34 -3
- {buzzkit-0.1.0 → buzzkit-0.1.2}/.github/workflows/CI.yml +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/.github/workflows/release.yml +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/LICENSE +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/LICENSE-APACHE +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/NOTICE +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/README.md +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/examples/_shared.py +0 -0
- {buzzkit-0.1.0 → buzzkit-0.1.2}/python/buzzkit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Redeem a relay invite so the agent becomes a community member.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BUZZ_RELAY_URL=wss://... python examples/claim_invite.py <invite_url_or_code>
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Post a message to a channel via the HTTP bridge.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BUZZ_RELAY_URL=wss://... python examples/send_message.py <channel-uuid> ["message"]
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Publish the agent's profile (kind 0) so it shows a display name in Buzz.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BUZZ_RELAY_URL=wss://... python examples/set_profile.py ["Display Name"]
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Real-time inbound: subscribe to a channel over the WebSocket and print messages.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
BUZZ_RELAY_URL=wss://... python examples/subscribe.py <channel-uuid>
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "buzzkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Python bindings + async client for Block's Buzz (Nostr) protocol, backed by Rust buzz-core/buzz-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -10,6 +10,7 @@ This is an independent, unofficial project — not affiliated with Block, Inc.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from ._native import (
|
|
13
|
+
KIND_ADD_MEMBER,
|
|
13
14
|
KIND_AUTH,
|
|
14
15
|
KIND_HTTP_AUTH,
|
|
15
16
|
KIND_PRESENCE_UPDATE,
|
|
@@ -17,8 +18,11 @@ from ._native import (
|
|
|
17
18
|
KIND_STREAM_MESSAGE,
|
|
18
19
|
KIND_STREAM_MESSAGE_V2,
|
|
19
20
|
build_auth_event,
|
|
21
|
+
build_join_channel_event,
|
|
20
22
|
build_message_event,
|
|
23
|
+
build_presence_event,
|
|
21
24
|
build_profile_event,
|
|
25
|
+
compute_auth_tag,
|
|
22
26
|
generate_keypair,
|
|
23
27
|
pubkey_from_secret,
|
|
24
28
|
sign_nip98,
|
|
@@ -26,9 +30,10 @@ from ._native import (
|
|
|
26
30
|
)
|
|
27
31
|
from .client import BuzzClient
|
|
28
32
|
|
|
29
|
-
__version__ = "0.1.
|
|
33
|
+
__version__ = "0.1.2"
|
|
30
34
|
|
|
31
35
|
__all__ = [
|
|
36
|
+
"KIND_ADD_MEMBER",
|
|
32
37
|
"KIND_AUTH",
|
|
33
38
|
"KIND_HTTP_AUTH",
|
|
34
39
|
"KIND_PRESENCE_UPDATE",
|
|
@@ -37,8 +42,11 @@ __all__ = [
|
|
|
37
42
|
"KIND_STREAM_MESSAGE_V2",
|
|
38
43
|
"BuzzClient",
|
|
39
44
|
"build_auth_event",
|
|
45
|
+
"build_join_channel_event",
|
|
40
46
|
"build_message_event",
|
|
47
|
+
"build_presence_event",
|
|
41
48
|
"build_profile_event",
|
|
49
|
+
"compute_auth_tag",
|
|
42
50
|
"generate_keypair",
|
|
43
51
|
"pubkey_from_secret",
|
|
44
52
|
"sign_nip98",
|
|
@@ -21,9 +21,20 @@ def build_profile_event(
|
|
|
21
21
|
) -> str:
|
|
22
22
|
"""Build + sign a profile event (kind 0); returns NIP-01 event JSON."""
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def build_join_channel_event(secret: str, channel_id: str) -> str:
|
|
25
|
+
"""Build + sign a NIP-29 channel self-join event (kind 9000, role=bot)."""
|
|
26
|
+
|
|
27
|
+
def build_presence_event(secret: str, status: str = ...) -> str:
|
|
28
|
+
"""Build + sign a presence event (kind 20001); status online/away/offline."""
|
|
29
|
+
|
|
30
|
+
def build_auth_event(
|
|
31
|
+
secret: str, challenge: str, relay_url: str, auth_tag: str | None = ...
|
|
32
|
+
) -> str:
|
|
25
33
|
"""Build + sign a NIP-42 AUTH event (kind 22242); returns event JSON."""
|
|
26
34
|
|
|
35
|
+
def compute_auth_tag(owner_secret: str, agent_pubkey_hex: str, conditions: str = ...) -> str:
|
|
36
|
+
"""Compute a NIP-OA owner-attestation tag JSON attesting an agent pubkey."""
|
|
37
|
+
|
|
27
38
|
def sign_nip98(secret: str, method: str, url: str, body: bytes | None = ...) -> str:
|
|
28
39
|
"""Return an ``Authorization: Nostr <base64>`` header value (NIP-98)."""
|
|
29
40
|
|
|
@@ -36,3 +47,4 @@ KIND_PRESENCE_UPDATE: int
|
|
|
36
47
|
KIND_AUTH: int
|
|
37
48
|
KIND_HTTP_AUTH: int
|
|
38
49
|
KIND_STREAM_MESSAGE_V2: int
|
|
50
|
+
KIND_ADD_MEMBER: int
|
|
@@ -108,6 +108,22 @@ class BuzzClient:
|
|
|
108
108
|
)
|
|
109
109
|
return await self.post_event(ev)
|
|
110
110
|
|
|
111
|
+
async def join_channel(self, channel_id: str) -> dict:
|
|
112
|
+
"""Self-add as a bot member of a channel (NIP-29 kind 9000).
|
|
113
|
+
|
|
114
|
+
Published over the WebSocket — NIP-29 management events are not accepted
|
|
115
|
+
on the HTTP bridge — so :meth:`connect` must be called first. Required
|
|
116
|
+
for the identity's messages to reach other channel members and for it to
|
|
117
|
+
appear in mention autocomplete.
|
|
118
|
+
"""
|
|
119
|
+
ev = _native.build_join_channel_event(self._secret, channel_id)
|
|
120
|
+
return await self.publish(ev)
|
|
121
|
+
|
|
122
|
+
async def publish_presence(self, status: str = "online") -> dict:
|
|
123
|
+
"""Announce presence (kind 20001, ephemeral) over the WebSocket."""
|
|
124
|
+
ev = _native.build_presence_event(self._secret, status)
|
|
125
|
+
return await self.publish(ev)
|
|
126
|
+
|
|
111
127
|
async def query(self, filters: list[dict]) -> list[dict]:
|
|
112
128
|
"""Run a NIP-01 REQ over the HTTP bridge; returns a list of events."""
|
|
113
129
|
url = f"{self._http}/query"
|
|
@@ -214,7 +230,9 @@ class BuzzClient:
|
|
|
214
230
|
q.put_nowait(("closed", "connection lost"))
|
|
215
231
|
|
|
216
232
|
async def _on_auth_challenge(self, challenge: str) -> None:
|
|
217
|
-
auth_ev = json.loads(
|
|
233
|
+
auth_ev = json.loads(
|
|
234
|
+
_native.build_auth_event(self._secret, challenge, self._ws_url, self._auth_tag)
|
|
235
|
+
)
|
|
218
236
|
self._auth_event_id = auth_ev["id"]
|
|
219
237
|
await self._ws.send(json.dumps(["AUTH", auth_ev]))
|
|
220
238
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
use base64::engine::general_purpose::STANDARD as B64;
|
|
9
9
|
use base64::Engine as _;
|
|
10
|
-
use nostr::{EventBuilder, JsonUtil, Keys, Kind, RelayUrl, Tag, ToBech32};
|
|
10
|
+
use nostr::{EventBuilder, JsonUtil, Keys, Kind, PublicKey, RelayUrl, Tag, ToBech32};
|
|
11
11
|
use pyo3::exceptions::PyValueError;
|
|
12
12
|
use pyo3::prelude::*;
|
|
13
13
|
use sha2::{Digest, Sha256};
|
|
@@ -69,17 +69,50 @@ fn build_message_event(
|
|
|
69
69
|
|
|
70
70
|
/// Build and sign a NIP-42 AUTH event (kind 22242) for a relay challenge.
|
|
71
71
|
/// Returns the event JSON to send back as `["AUTH", <event>]`.
|
|
72
|
+
///
|
|
73
|
+
/// `auth_tag` is an optional NIP-OA owner-attestation tag JSON
|
|
74
|
+
/// (`["auth", <owner>, <conditions>, <sig>]`) — inject it so the relay records
|
|
75
|
+
/// the agent's owner.
|
|
72
76
|
#[pyfunction]
|
|
73
|
-
|
|
77
|
+
#[pyo3(signature = (secret, challenge, relay_url, auth_tag=None))]
|
|
78
|
+
fn build_auth_event(
|
|
79
|
+
secret: &str,
|
|
80
|
+
challenge: &str,
|
|
81
|
+
relay_url: &str,
|
|
82
|
+
auth_tag: Option<&str>,
|
|
83
|
+
) -> PyResult<String> {
|
|
74
84
|
let keys = keys_from_secret(secret)?;
|
|
75
85
|
let url = RelayUrl::parse(relay_url)
|
|
76
86
|
.map_err(|e| PyValueError::new_err(format!("invalid relay url: {e}")))?;
|
|
77
|
-
let
|
|
87
|
+
let mut builder = EventBuilder::auth(challenge, url);
|
|
88
|
+
if let Some(tag_json) = auth_tag {
|
|
89
|
+
let tag = buzz_sdk::nip_oa::parse_auth_tag(tag_json)
|
|
90
|
+
.map_err(|e| PyValueError::new_err(format!("invalid auth_tag: {e}")))?;
|
|
91
|
+
builder = builder.tags([tag]);
|
|
92
|
+
}
|
|
93
|
+
let event = builder
|
|
78
94
|
.sign_with_keys(&keys)
|
|
79
95
|
.map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
|
|
80
96
|
Ok(event.as_json())
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
/// Compute a NIP-OA owner-attestation tag: the OWNER signs an attestation over
|
|
100
|
+
/// the agent's pubkey. Returns the `["auth", <owner>, <conditions>, <sig>]` tag
|
|
101
|
+
/// JSON. `owner_secret` must differ from the agent (no self-attestation).
|
|
102
|
+
#[pyfunction]
|
|
103
|
+
#[pyo3(signature = (owner_secret, agent_pubkey_hex, conditions=""))]
|
|
104
|
+
fn compute_auth_tag(
|
|
105
|
+
owner_secret: &str,
|
|
106
|
+
agent_pubkey_hex: &str,
|
|
107
|
+
conditions: &str,
|
|
108
|
+
) -> PyResult<String> {
|
|
109
|
+
let owner_keys = keys_from_secret(owner_secret)?;
|
|
110
|
+
let agent_pubkey = PublicKey::from_hex(agent_pubkey_hex)
|
|
111
|
+
.map_err(|e| PyValueError::new_err(format!("invalid agent pubkey: {e}")))?;
|
|
112
|
+
buzz_sdk::nip_oa::compute_auth_tag(&owner_keys, &agent_pubkey, conditions)
|
|
113
|
+
.map_err(|e| PyValueError::new_err(format!("compute_auth_tag: {e}")))
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
/// Sign a NIP-98 HTTP-auth event (kind 27235). Returns the
|
|
84
117
|
/// `Authorization: Nostr <base64>` header value for the Buzz HTTP bridge.
|
|
85
118
|
#[pyfunction]
|
|
@@ -140,15 +173,51 @@ fn build_profile_event(
|
|
|
140
173
|
Ok(event.as_json())
|
|
141
174
|
}
|
|
142
175
|
|
|
176
|
+
/// Build and sign a NIP-29 channel self-join event (kind 9000, role=bot).
|
|
177
|
+
/// The signer adds itself (`p` = own pubkey) to `channel_id`'s member list.
|
|
178
|
+
#[pyfunction]
|
|
179
|
+
fn build_join_channel_event(secret: &str, channel_id: &str) -> PyResult<String> {
|
|
180
|
+
let keys = keys_from_secret(secret)?;
|
|
181
|
+
let cid = Uuid::parse_str(channel_id)
|
|
182
|
+
.map_err(|e| PyValueError::new_err(format!("channel_id must be a UUID: {e}")))?;
|
|
183
|
+
let pubkey_hex = keys.public_key().to_hex();
|
|
184
|
+
// The `p` tag references the signer itself (self-join), so we must opt into
|
|
185
|
+
// self-tagging — nostr's EventBuilder strips author-matching `p` tags by
|
|
186
|
+
// default, which the relay would then reject as "missing p tag".
|
|
187
|
+
let builder = buzz_sdk::build_add_member(cid, &pubkey_hex, Some(buzz_sdk::MemberRole::Bot))
|
|
188
|
+
.map_err(|e| PyValueError::new_err(format!("build_add_member: {e}")))?
|
|
189
|
+
.allow_self_tagging();
|
|
190
|
+
let event = builder
|
|
191
|
+
.sign_with_keys(&keys)
|
|
192
|
+
.map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
|
|
193
|
+
Ok(event.as_json())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Build and sign a presence event (kind 20001). `status` is "online",
|
|
197
|
+
/// "away", or "offline". Ephemeral — publish over the WebSocket.
|
|
198
|
+
#[pyfunction]
|
|
199
|
+
#[pyo3(signature = (secret, status="online"))]
|
|
200
|
+
fn build_presence_event(secret: &str, status: &str) -> PyResult<String> {
|
|
201
|
+
let keys = keys_from_secret(secret)?;
|
|
202
|
+
let event = buzz_sdk::build_presence_update(status)
|
|
203
|
+
.map_err(|e| PyValueError::new_err(format!("build_presence: {e}")))?
|
|
204
|
+
.sign_with_keys(&keys)
|
|
205
|
+
.map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
|
|
206
|
+
Ok(event.as_json())
|
|
207
|
+
}
|
|
208
|
+
|
|
143
209
|
#[pymodule]
|
|
144
210
|
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
145
211
|
m.add_function(wrap_pyfunction!(generate_keypair, m)?)?;
|
|
146
212
|
m.add_function(wrap_pyfunction!(pubkey_from_secret, m)?)?;
|
|
147
213
|
m.add_function(wrap_pyfunction!(build_message_event, m)?)?;
|
|
148
214
|
m.add_function(wrap_pyfunction!(build_auth_event, m)?)?;
|
|
215
|
+
m.add_function(wrap_pyfunction!(compute_auth_tag, m)?)?;
|
|
149
216
|
m.add_function(wrap_pyfunction!(sign_nip98, m)?)?;
|
|
150
217
|
m.add_function(wrap_pyfunction!(verify_event, m)?)?;
|
|
151
218
|
m.add_function(wrap_pyfunction!(build_profile_event, m)?)?;
|
|
219
|
+
m.add_function(wrap_pyfunction!(build_join_channel_event, m)?)?;
|
|
220
|
+
m.add_function(wrap_pyfunction!(build_presence_event, m)?)?;
|
|
152
221
|
|
|
153
222
|
// Buzz event kinds (subset — mirrors buzz-core/src/kind.rs).
|
|
154
223
|
m.add("KIND_REACTION", 7u16)?;
|
|
@@ -157,5 +226,6 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
157
226
|
m.add("KIND_AUTH", 22242u16)?;
|
|
158
227
|
m.add("KIND_HTTP_AUTH", 27235u16)?;
|
|
159
228
|
m.add("KIND_STREAM_MESSAGE_V2", 40002u16)?;
|
|
229
|
+
m.add("KIND_ADD_MEMBER", 9000u16)?;
|
|
160
230
|
Ok(())
|
|
161
231
|
}
|
|
@@ -59,8 +59,39 @@ def test_nip98_header_shape():
|
|
|
59
59
|
|
|
60
60
|
def test_auth_event_is_kind_22242():
|
|
61
61
|
nsec, _, _ = buzzkit.generate_keypair()
|
|
62
|
-
event = json.loads(
|
|
63
|
-
buzzkit.build_auth_event(nsec, "challenge-123", "wss://relay.example")
|
|
64
|
-
)
|
|
62
|
+
event = json.loads(buzzkit.build_auth_event(nsec, "challenge-123", "wss://relay.example"))
|
|
65
63
|
assert event["kind"] == buzzkit.KIND_AUTH == 22242
|
|
66
64
|
assert ["challenge", "challenge-123"] in event["tags"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_join_channel_event_keeps_self_p_tag():
|
|
68
|
+
nsec, _, pk_hex = buzzkit.generate_keypair()
|
|
69
|
+
event = json.loads(buzzkit.build_join_channel_event(nsec, str(uuid.uuid4())))
|
|
70
|
+
assert event["kind"] == buzzkit.KIND_ADD_MEMBER == 9000
|
|
71
|
+
# The `p` tag references the signer itself; it must survive nostr's self-tag
|
|
72
|
+
# stripping (allow_self_tagging) or the relay rejects the join as "missing p tag".
|
|
73
|
+
assert ["p", pk_hex] in event["tags"]
|
|
74
|
+
assert ["role", "bot"] in event["tags"]
|
|
75
|
+
assert any(t[0] == "h" for t in event["tags"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_compute_auth_tag():
|
|
79
|
+
owner_nsec, _, owner_pk = buzzkit.generate_keypair()
|
|
80
|
+
_, _, agent_pk = buzzkit.generate_keypair()
|
|
81
|
+
tag = json.loads(buzzkit.compute_auth_tag(owner_nsec, agent_pk))
|
|
82
|
+
assert tag[0] == "auth"
|
|
83
|
+
assert tag[1] == owner_pk # owner pubkey
|
|
84
|
+
assert len(tag) == 4 # ["auth", owner, conditions, sig]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_compute_auth_tag_rejects_self():
|
|
88
|
+
nsec, _, pk_hex = buzzkit.generate_keypair()
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
buzzkit.compute_auth_tag(nsec, pk_hex) # owner == agent
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_presence_event():
|
|
94
|
+
nsec, _, _ = buzzkit.generate_keypair()
|
|
95
|
+
event = json.loads(buzzkit.build_presence_event(nsec, "online"))
|
|
96
|
+
assert event["kind"] == buzzkit.KIND_PRESENCE_UPDATE == 20001
|
|
97
|
+
assert event["content"] == "online" or ["status", "online"] in event["tags"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|