buzzkit 0.1.0__tar.gz → 0.1.1__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.
@@ -168,7 +168,7 @@ dependencies = [
168
168
 
169
169
  [[package]]
170
170
  name = "buzzkit"
171
- version = "0.1.0"
171
+ version = "0.1.1"
172
172
  dependencies = [
173
173
  "base64",
174
174
  "buzz-core",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "buzzkit"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  description = "PyO3 bindings over Block's Buzz zero-I/O crates (buzz-core / buzz-sdk)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buzzkit
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,6 @@
1
1
  """Redeem a relay invite so the agent becomes a community member.
2
2
 
3
- BUZZ_RELAY_URL=wss://... python examples/claim_invite.py <invite_url_or_code>
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
  """List channels (kind 39000 metadata) visible to the agent.
2
2
 
3
- BUZZ_RELAY_URL=wss://... python examples/list_channels.py
3
+ BUZZ_RELAY_URL=wss://... python examples/list_channels.py
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  """Offline proof — no relay. keygen -> sign a Buzz message -> verify -> tamper check.
2
2
 
3
- python examples/offline_sign.py
3
+ python examples/offline_sign.py
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
- BUZZ_RELAY_URL=wss://... python examples/send_message.py <channel-uuid> ["message"]
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
- BUZZ_RELAY_URL=wss://... python examples/set_profile.py ["Display Name"]
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
- BUZZ_RELAY_URL=wss://... python examples/subscribe.py <channel-uuid>
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.0"
7
+ version = "0.1.1"
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,6 +18,7 @@ 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,
21
23
  build_profile_event,
22
24
  generate_keypair,
@@ -26,9 +28,10 @@ from ._native import (
26
28
  )
27
29
  from .client import BuzzClient
28
30
 
29
- __version__ = "0.1.0"
31
+ __version__ = "0.1.1"
30
32
 
31
33
  __all__ = [
34
+ "KIND_ADD_MEMBER",
32
35
  "KIND_AUTH",
33
36
  "KIND_HTTP_AUTH",
34
37
  "KIND_PRESENCE_UPDATE",
@@ -37,6 +40,7 @@ __all__ = [
37
40
  "KIND_STREAM_MESSAGE_V2",
38
41
  "BuzzClient",
39
42
  "build_auth_event",
43
+ "build_join_channel_event",
40
44
  "build_message_event",
41
45
  "build_profile_event",
42
46
  "generate_keypair",
@@ -21,6 +21,9 @@ 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 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
+
24
27
  def build_auth_event(secret: str, challenge: str, relay_url: str) -> str:
25
28
  """Build + sign a NIP-42 AUTH event (kind 22242); returns event JSON."""
26
29
 
@@ -36,3 +39,4 @@ KIND_PRESENCE_UPDATE: int
36
39
  KIND_AUTH: int
37
40
  KIND_HTTP_AUTH: int
38
41
  KIND_STREAM_MESSAGE_V2: int
42
+ KIND_ADD_MEMBER: int
@@ -108,6 +108,17 @@ 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
+
111
122
  async def query(self, filters: list[dict]) -> list[dict]:
112
123
  """Run a NIP-01 REQ over the HTTP bridge; returns a list of events."""
113
124
  url = f"{self._http}/query"
@@ -140,6 +140,26 @@ fn build_profile_event(
140
140
  Ok(event.as_json())
141
141
  }
142
142
 
143
+ /// Build and sign a NIP-29 channel self-join event (kind 9000, role=bot).
144
+ /// The signer adds itself (`p` = own pubkey) to `channel_id`'s member list.
145
+ #[pyfunction]
146
+ fn build_join_channel_event(secret: &str, channel_id: &str) -> PyResult<String> {
147
+ let keys = keys_from_secret(secret)?;
148
+ let cid = Uuid::parse_str(channel_id)
149
+ .map_err(|e| PyValueError::new_err(format!("channel_id must be a UUID: {e}")))?;
150
+ let pubkey_hex = keys.public_key().to_hex();
151
+ // The `p` tag references the signer itself (self-join), so we must opt into
152
+ // self-tagging — nostr's EventBuilder strips author-matching `p` tags by
153
+ // default, which the relay would then reject as "missing p tag".
154
+ let builder = buzz_sdk::build_add_member(cid, &pubkey_hex, Some(buzz_sdk::MemberRole::Bot))
155
+ .map_err(|e| PyValueError::new_err(format!("build_add_member: {e}")))?
156
+ .allow_self_tagging();
157
+ let event = builder
158
+ .sign_with_keys(&keys)
159
+ .map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
160
+ Ok(event.as_json())
161
+ }
162
+
143
163
  #[pymodule]
144
164
  fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
145
165
  m.add_function(wrap_pyfunction!(generate_keypair, m)?)?;
@@ -149,6 +169,7 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
149
169
  m.add_function(wrap_pyfunction!(sign_nip98, m)?)?;
150
170
  m.add_function(wrap_pyfunction!(verify_event, m)?)?;
151
171
  m.add_function(wrap_pyfunction!(build_profile_event, m)?)?;
172
+ m.add_function(wrap_pyfunction!(build_join_channel_event, m)?)?;
152
173
 
153
174
  // Buzz event kinds (subset — mirrors buzz-core/src/kind.rs).
154
175
  m.add("KIND_REACTION", 7u16)?;
@@ -157,5 +178,6 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
157
178
  m.add("KIND_AUTH", 22242u16)?;
158
179
  m.add("KIND_HTTP_AUTH", 27235u16)?;
159
180
  m.add("KIND_STREAM_MESSAGE_V2", 40002u16)?;
181
+ m.add("KIND_ADD_MEMBER", 9000u16)?;
160
182
  Ok(())
161
183
  }
@@ -59,8 +59,17 @@ 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"])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes