agentstr 0.1.11__py3-none-any.whl → 0.1.12__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- agentstr/__init__.py +20 -13
- agentstr/buyer.py +291 -0
- agentstr/buyer.pyi +31 -0
- agentstr/{marketplace.py → merchant.py} +126 -323
- agentstr/merchant.pyi +37 -0
- agentstr/models.py +381 -0
- agentstr/models.pyi +103 -0
- agentstr/nostr.py +389 -53
- agentstr/nostr.pyi +82 -0
- agentstr/py.typed +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.12.dist-info}/METADATA +36 -53
- agentstr-0.1.12.dist-info/RECORD +15 -0
- agentstr-0.1.11.dist-info/RECORD +0 -8
- {agentstr-0.1.11.dist-info → agentstr-0.1.12.dist-info}/LICENSE +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.12.dist-info}/WHEEL +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.12.dist-info}/top_level.txt +0 -0
agentstr/nostr.py
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
+
import json
|
1
2
|
import logging
|
2
|
-
|
3
|
+
import traceback
|
4
|
+
from datetime import timedelta
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
7
|
+
|
8
|
+
from agentstr.models import MerchantProduct, MerchantStall, NostrProfile
|
3
9
|
|
4
10
|
try:
|
5
11
|
import asyncio
|
@@ -10,11 +16,13 @@ except ImportError:
|
|
10
16
|
|
11
17
|
try:
|
12
18
|
from nostr_sdk import (
|
19
|
+
Alphabet,
|
13
20
|
Client,
|
14
21
|
Coordinate,
|
15
|
-
Event,
|
16
22
|
EventBuilder,
|
17
23
|
EventId,
|
24
|
+
Events,
|
25
|
+
Filter,
|
18
26
|
Keys,
|
19
27
|
Kind,
|
20
28
|
Metadata,
|
@@ -23,8 +31,11 @@ try:
|
|
23
31
|
PublicKey,
|
24
32
|
ShippingCost,
|
25
33
|
ShippingMethod,
|
34
|
+
SingleLetterTag,
|
26
35
|
StallData,
|
27
36
|
Tag,
|
37
|
+
TagKind,
|
38
|
+
TagStandard,
|
28
39
|
Timestamp,
|
29
40
|
)
|
30
41
|
|
@@ -44,8 +55,6 @@ class NostrClient:
|
|
44
55
|
"""
|
45
56
|
|
46
57
|
logger = logging.getLogger("NostrClient")
|
47
|
-
ERROR: str = "ERROR"
|
48
|
-
SUCCESS: str = "SUCCESS"
|
49
58
|
|
50
59
|
def __init__(
|
51
60
|
self,
|
@@ -74,6 +83,7 @@ class NostrClient:
|
|
74
83
|
self.keys = Keys.parse(nsec)
|
75
84
|
self.nostr_signer = NostrSigner.keys(self.keys)
|
76
85
|
self.client = Client(self.nostr_signer)
|
86
|
+
self.connected = False
|
77
87
|
|
78
88
|
def delete_event(self, event_id: EventId, reason: Optional[str] = None) -> EventId:
|
79
89
|
"""
|
@@ -98,7 +108,7 @@ class NostrClient:
|
|
98
108
|
Publish generic Nostr event to the relay
|
99
109
|
|
100
110
|
Returns:
|
101
|
-
EventId: event id
|
111
|
+
EventId: event id published
|
102
112
|
|
103
113
|
Raises:
|
104
114
|
RuntimeError: if the product can't be published
|
@@ -113,7 +123,7 @@ class NostrClient:
|
|
113
123
|
text: text to be published as kind 1 event
|
114
124
|
|
115
125
|
Returns:
|
116
|
-
EventId: EventId if successful
|
126
|
+
EventId: EventId if successful
|
117
127
|
|
118
128
|
Raises:
|
119
129
|
RuntimeError: if the product can't be published
|
@@ -121,7 +131,7 @@ class NostrClient:
|
|
121
131
|
# Run the async publishing function synchronously
|
122
132
|
return asyncio.run(self._async_publish_note(text))
|
123
133
|
|
124
|
-
def publish_product(self, product:
|
134
|
+
def publish_product(self, product: MerchantProduct) -> EventId:
|
125
135
|
"""
|
126
136
|
Create or update a NIP-15 Marketplace product with event kind 30018
|
127
137
|
|
@@ -129,13 +139,16 @@ class NostrClient:
|
|
129
139
|
product: product to be published
|
130
140
|
|
131
141
|
Returns:
|
132
|
-
EventId: event id
|
142
|
+
EventId: event id of the publication event
|
133
143
|
|
134
144
|
Raises:
|
135
145
|
RuntimeError: if the product can't be published
|
136
146
|
"""
|
137
147
|
# Run the async publishing function synchronously
|
138
|
-
|
148
|
+
try:
|
149
|
+
return asyncio.run(self._async_publish_product(product))
|
150
|
+
except Exception as e:
|
151
|
+
raise RuntimeError(f"Failed to publish product: {e}")
|
139
152
|
|
140
153
|
def publish_profile(self, name: str, about: str, picture: str) -> EventId:
|
141
154
|
"""
|
@@ -147,7 +160,7 @@ class NostrClient:
|
|
147
160
|
picture: url to a png file with a picture for the profile
|
148
161
|
|
149
162
|
Returns:
|
150
|
-
EventId: event id if successful
|
163
|
+
EventId: event id if successful
|
151
164
|
|
152
165
|
Raises:
|
153
166
|
RuntimeError: if the profile can't be published
|
@@ -155,20 +168,166 @@ class NostrClient:
|
|
155
168
|
# Run the async publishing function synchronously
|
156
169
|
return asyncio.run(self._async_publish_profile(name, about, picture))
|
157
170
|
|
158
|
-
def publish_stall(self, stall:
|
171
|
+
def publish_stall(self, stall: MerchantStall) -> EventId:
|
159
172
|
"""Publish a stall to nostr
|
160
173
|
|
161
174
|
Args:
|
162
175
|
stall: stall to be published
|
163
176
|
|
164
177
|
Returns:
|
165
|
-
EventId:
|
178
|
+
EventId: Id of the publication event
|
179
|
+
|
180
|
+
Raises:
|
181
|
+
RuntimeError: if the stall can't be published
|
166
182
|
"""
|
167
183
|
try:
|
168
184
|
return asyncio.run(self._async_publish_stall(stall))
|
169
185
|
except Exception as e:
|
170
|
-
|
171
|
-
|
186
|
+
raise RuntimeError(f"Failed to publish stall: {e}")
|
187
|
+
|
188
|
+
def retrieve_products_from_seller(self, seller: PublicKey) -> List[MerchantProduct]:
|
189
|
+
"""
|
190
|
+
Retrieve all products from a given seller.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
seller: PublicKey of the seller
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
List[MerchantProduct]: list of products from the seller
|
197
|
+
"""
|
198
|
+
products = []
|
199
|
+
try:
|
200
|
+
events = asyncio.run(self._async_retrieve_products_from_seller(seller))
|
201
|
+
events_list = events.to_vec()
|
202
|
+
for event in events_list:
|
203
|
+
content = json.loads(event.content())
|
204
|
+
product_data = ProductData(
|
205
|
+
id=content.get("id"),
|
206
|
+
stall_id=content.get("stall_id"),
|
207
|
+
name=content.get("name"),
|
208
|
+
description=content.get("description"),
|
209
|
+
images=content.get("images", []),
|
210
|
+
currency=content.get("currency"),
|
211
|
+
price=content.get("price"),
|
212
|
+
quantity=content.get("quantity"),
|
213
|
+
specs=content.get("specs", {}),
|
214
|
+
shipping=content.get("shipping", []),
|
215
|
+
categories=content.get("categories", []),
|
216
|
+
)
|
217
|
+
products.append(MerchantProduct.from_product_data(product_data))
|
218
|
+
return products
|
219
|
+
except Exception as e:
|
220
|
+
raise RuntimeError(f"Failed to retrieve products: {e}")
|
221
|
+
|
222
|
+
def retrieve_profile(self, public_key: PublicKey) -> NostrProfile:
|
223
|
+
"""
|
224
|
+
Retrieve a Nostr profile from the relay.
|
225
|
+
|
226
|
+
Args:
|
227
|
+
public_key: bech32 encoded public key of the profile to retrieve
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
NostrProfile: profile of the author
|
231
|
+
|
232
|
+
Raises:
|
233
|
+
RuntimeError: if the profile can't be retrieved
|
234
|
+
"""
|
235
|
+
try:
|
236
|
+
return asyncio.run(self._async_retrieve_profile(public_key))
|
237
|
+
except Exception as e:
|
238
|
+
raise RuntimeError(f"Failed to retrieve profile: {e}")
|
239
|
+
|
240
|
+
def retrieve_sellers(self) -> set[NostrProfile]:
|
241
|
+
"""
|
242
|
+
Retrieve all sellers from the relay.
|
243
|
+
Sellers are npubs who have published a stall.
|
244
|
+
Return set may be empty if metadata can't be retrieved for any author.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
set[NostrProfile]: set of seller profiles (skips authors with missing metadata)
|
248
|
+
"""
|
249
|
+
|
250
|
+
sellers: set[NostrProfile] = set()
|
251
|
+
|
252
|
+
# First we retrieve all stalls from the relay
|
253
|
+
|
254
|
+
try:
|
255
|
+
events = asyncio.run(self._async_retrieve_all_stalls())
|
256
|
+
except Exception as e:
|
257
|
+
raise RuntimeError(f"Failed to retrieve stalls: {e}")
|
258
|
+
|
259
|
+
# Now we search for unique npubs from the list of stalls
|
260
|
+
|
261
|
+
events_list = events.to_vec()
|
262
|
+
authors: Dict[PublicKey, NostrProfile] = {}
|
263
|
+
|
264
|
+
for event in events_list:
|
265
|
+
if event.kind() == Kind(30017):
|
266
|
+
# Is this event the first time we see this author?
|
267
|
+
if event.author() not in authors:
|
268
|
+
# First time we see this author. Let's add the profile to the dictionary
|
269
|
+
try:
|
270
|
+
profile = asyncio.run(
|
271
|
+
self._async_retrieve_profile(event.author())
|
272
|
+
)
|
273
|
+
# Add the profile to the dictionary, associating it with the author's PublicKey
|
274
|
+
authors[event.author()] = profile
|
275
|
+
except Exception as e:
|
276
|
+
# print(
|
277
|
+
# f"Failed to retrieve profile for {event.author().to_bech32()}: {e}"
|
278
|
+
# )
|
279
|
+
continue
|
280
|
+
|
281
|
+
# Now we add locations from the event locations to the profile
|
282
|
+
|
283
|
+
for tag in event.tags().to_vec():
|
284
|
+
standardized_tag = tag.as_standardized()
|
285
|
+
if isinstance(standardized_tag, TagStandard.GEOHASH):
|
286
|
+
string_repr = str(standardized_tag)
|
287
|
+
extracted_geohash = string_repr.split("=")[1].rstrip(
|
288
|
+
")"
|
289
|
+
) # Splitting and removing the closing parenthesis
|
290
|
+
# print(
|
291
|
+
# f"Adding location {extracted_geohash} to profile {authors[event.author()].get_name()}"
|
292
|
+
# )
|
293
|
+
profile = authors[event.author()]
|
294
|
+
profile.add_location(extracted_geohash)
|
295
|
+
authors[event.author()] = profile
|
296
|
+
# print(
|
297
|
+
# f"New locations for {authors[event.author()].get_name()}: {authors[event.author()].get_locations()}"
|
298
|
+
# )
|
299
|
+
# else:
|
300
|
+
# print(f"Unknown tag: {standardized_tag}")
|
301
|
+
|
302
|
+
# once we're done iterating over the events, we return the set of profiles
|
303
|
+
return set(authors.values())
|
304
|
+
|
305
|
+
def retrieve_stalls_from_seller(self, seller: PublicKey) -> List[StallData]:
|
306
|
+
"""
|
307
|
+
Retrieve all stalls from a given seller.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
seller: PublicKey of the seller
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
List[StallData]: list of stalls from the seller
|
314
|
+
"""
|
315
|
+
stalls = []
|
316
|
+
try:
|
317
|
+
events = asyncio.run(self._async_retrieve_stalls_from_seller(seller))
|
318
|
+
events_list = events.to_vec()
|
319
|
+
for event in events_list:
|
320
|
+
try:
|
321
|
+
# Parse the content field instead of the whole event
|
322
|
+
content = event.content()
|
323
|
+
stall = StallData.from_json(content)
|
324
|
+
stalls.append(stall)
|
325
|
+
except Exception as e:
|
326
|
+
self.logger.warning(f"Failed to parse stall data: {e}")
|
327
|
+
continue
|
328
|
+
return stalls
|
329
|
+
except Exception as e:
|
330
|
+
raise RuntimeError(f"Failed to retrieve stalls: {e}")
|
172
331
|
|
173
332
|
@classmethod
|
174
333
|
def set_logging_level(cls, logging_level: int) -> None:
|
@@ -186,23 +345,26 @@ class NostrClient:
|
|
186
345
|
# --*-- async functions for internal use only. Developers should use synchronous functions above
|
187
346
|
# ----------------------------------------------------------------------------------------------
|
188
347
|
|
189
|
-
async def _async_connect(self) ->
|
348
|
+
async def _async_connect(self) -> None:
|
190
349
|
"""Asynchronous function to add relay to the NostrClient instance and connect to it.
|
350
|
+
TBD: refactor to not return anything if successful and raise an exception if not
|
191
351
|
|
192
|
-
|
193
|
-
|
352
|
+
Raises:
|
353
|
+
RuntimeError: if the relay can't be connected to
|
194
354
|
"""
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
355
|
+
|
356
|
+
if not self.connected:
|
357
|
+
try:
|
358
|
+
await self.client.add_relay(self.relay)
|
359
|
+
NostrClient.logger.info(f"Relay {self.relay} succesfully added.")
|
360
|
+
await self.client.connect()
|
361
|
+
await asyncio.sleep(2) # give time for slower connections
|
362
|
+
NostrClient.logger.info("Connected to relay.")
|
363
|
+
self.connected = True
|
364
|
+
except Exception as e:
|
365
|
+
raise RuntimeError(
|
366
|
+
f"Unable to connect to relay {self.relay}. Exception: {e}."
|
367
|
+
)
|
206
368
|
|
207
369
|
async def _async_publish_event(self, event_builder: EventBuilder) -> EventId:
|
208
370
|
"""
|
@@ -214,27 +376,35 @@ class NostrClient:
|
|
214
376
|
Raises:
|
215
377
|
RuntimeError: if the event can't be published
|
216
378
|
"""
|
217
|
-
|
379
|
+
try:
|
380
|
+
await self._async_connect()
|
218
381
|
|
219
|
-
|
220
|
-
|
382
|
+
# Add debug logging
|
383
|
+
NostrClient.logger.debug(f"Attempting to publish event: {event_builder}")
|
384
|
+
NostrClient.logger.debug(
|
385
|
+
f"Using keys: {self.keys.public_key().to_bech32()}"
|
386
|
+
)
|
221
387
|
|
222
|
-
|
388
|
+
# Wait for connection and try to publish
|
223
389
|
output = await self.client.send_event_builder(event_builder)
|
224
|
-
|
225
|
-
|
226
|
-
|
390
|
+
|
391
|
+
# More detailed error handling
|
392
|
+
if not output:
|
393
|
+
raise RuntimeError("No output received from send_event_builder")
|
394
|
+
if len(output.success) == 0:
|
395
|
+
raise RuntimeError(
|
396
|
+
f"Event rejected by relay. Reason: {output.message if hasattr(output, 'message') else 'unknown'}"
|
227
397
|
)
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
except Exception as e:
|
232
|
-
NostrClient.logger.error(
|
233
|
-
f"NostrClient instance not properly initialized. Exception: {e}."
|
234
|
-
)
|
235
|
-
raise RuntimeError(
|
236
|
-
f"NostrClient instance not properly initialized. Exception: {e}."
|
398
|
+
|
399
|
+
NostrClient.logger.info(
|
400
|
+
f"Event published with event id: {output.id.to_bech32()}"
|
237
401
|
)
|
402
|
+
return output.id
|
403
|
+
|
404
|
+
except Exception as e:
|
405
|
+
NostrClient.logger.error(f"Failed to publish event: {str(e)}")
|
406
|
+
NostrClient.logger.debug("Event details:", exc_info=True)
|
407
|
+
raise RuntimeError(f"Unable to publish event: {str(e)}")
|
238
408
|
|
239
409
|
async def _async_publish_note(self, text: str) -> EventId:
|
240
410
|
"""
|
@@ -244,7 +414,7 @@ class NostrClient:
|
|
244
414
|
text: text to be published as kind 1 event
|
245
415
|
|
246
416
|
Returns:
|
247
|
-
EventId: event id if successful
|
417
|
+
EventId: event id if successful
|
248
418
|
|
249
419
|
Raises:
|
250
420
|
RuntimeError: if the event can't be published
|
@@ -252,7 +422,7 @@ class NostrClient:
|
|
252
422
|
event_builder = EventBuilder.text_note(text)
|
253
423
|
return await self._async_publish_event(event_builder)
|
254
424
|
|
255
|
-
async def _async_publish_product(self, product:
|
425
|
+
async def _async_publish_product(self, product: MerchantProduct) -> EventId:
|
256
426
|
"""
|
257
427
|
Asynchronous function to create or update a NIP-15 Marketplace product with event kind 30018
|
258
428
|
|
@@ -260,7 +430,7 @@ class NostrClient:
|
|
260
430
|
product: product to publish
|
261
431
|
|
262
432
|
Returns:
|
263
|
-
EventId: event id if successful
|
433
|
+
EventId: event id if successful
|
264
434
|
|
265
435
|
Raises:
|
266
436
|
RuntimeError: if the product can't be published
|
@@ -271,7 +441,7 @@ class NostrClient:
|
|
271
441
|
|
272
442
|
# EventBuilder.product_data() has a bug with tag handling.
|
273
443
|
# We use the function to create the content field and discard the eventbuilder
|
274
|
-
bad_event_builder = EventBuilder.product_data(product)
|
444
|
+
bad_event_builder = EventBuilder.product_data(product.to_product_data())
|
275
445
|
|
276
446
|
# create an event from bad_event_builder to extract the content - not broadcasted
|
277
447
|
bad_event = await self.client.sign_event_builder(bad_event_builder)
|
@@ -296,7 +466,7 @@ class NostrClient:
|
|
296
466
|
picture: url to a png file with a picture for the profile
|
297
467
|
|
298
468
|
Returns:
|
299
|
-
EventId: event id if successful
|
469
|
+
EventId: event id if successful
|
300
470
|
|
301
471
|
Raises:
|
302
472
|
RuntimeError: if the profile can't be published
|
@@ -308,7 +478,7 @@ class NostrClient:
|
|
308
478
|
event_builder = EventBuilder.metadata(metadata_content)
|
309
479
|
return await self._async_publish_event(event_builder)
|
310
480
|
|
311
|
-
async def _async_publish_stall(self, stall:
|
481
|
+
async def _async_publish_stall(self, stall: MerchantStall) -> EventId:
|
312
482
|
"""
|
313
483
|
Asynchronous function to create or update a NIP-15 Marketplace stall with event kind 30017
|
314
484
|
|
@@ -316,12 +486,178 @@ class NostrClient:
|
|
316
486
|
stall: stall to be published
|
317
487
|
|
318
488
|
Returns:
|
319
|
-
EventId:
|
489
|
+
EventId: Id of the publication event
|
320
490
|
|
321
491
|
Raises:
|
322
492
|
RuntimeError: if the profile can't be published
|
323
493
|
"""
|
324
494
|
|
325
|
-
|
326
|
-
|
495
|
+
# good_event_builder = EventBuilder(Kind(30018), content).tags(
|
496
|
+
# [Tag.identifier(product.id), Tag.coordinate(coordinate_tag)]
|
497
|
+
# )
|
498
|
+
|
499
|
+
self.logger.info(f" Merchant Stall: {stall}")
|
500
|
+
event_builder = EventBuilder.stall_data(stall.to_stall_data()).tags(
|
501
|
+
[
|
502
|
+
Tag.custom(
|
503
|
+
TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.G)),
|
504
|
+
[stall.geohash],
|
505
|
+
),
|
506
|
+
]
|
507
|
+
)
|
327
508
|
return await self._async_publish_event(event_builder)
|
509
|
+
|
510
|
+
async def _async_retrieve_all_stalls(self) -> Events:
|
511
|
+
"""
|
512
|
+
Asynchronous function to retreive all stalls from a relay
|
513
|
+
This function is used internally to find Merchants.
|
514
|
+
|
515
|
+
Returns:
|
516
|
+
Events: events containing all stalls.
|
517
|
+
|
518
|
+
Raises:
|
519
|
+
RuntimeError: if the stalls can't be retrieved
|
520
|
+
"""
|
521
|
+
try:
|
522
|
+
await self._async_connect()
|
523
|
+
except Exception as e:
|
524
|
+
raise RuntimeError("Unable to connect to the relay")
|
525
|
+
|
526
|
+
try:
|
527
|
+
filter = Filter().kind(Kind(30017))
|
528
|
+
events = await self.client.fetch_events_from(
|
529
|
+
urls=[self.relay], filter=filter, timeout=timedelta(seconds=2)
|
530
|
+
)
|
531
|
+
return events
|
532
|
+
except Exception as e:
|
533
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}")
|
534
|
+
|
535
|
+
async def _async_retrieve_products_from_seller(self, seller: PublicKey) -> Events:
|
536
|
+
"""
|
537
|
+
Asynchronous function to retrieve the products for a given author
|
538
|
+
|
539
|
+
Args:
|
540
|
+
seller: PublicKey of the seller to retrieve the products for
|
541
|
+
|
542
|
+
Returns:
|
543
|
+
Events: list of events containing the products of the seller
|
544
|
+
|
545
|
+
Raises:
|
546
|
+
RuntimeError: if the products can't be retrieved
|
547
|
+
"""
|
548
|
+
try:
|
549
|
+
await self._async_connect()
|
550
|
+
except Exception as e:
|
551
|
+
raise RuntimeError("Unable to connect to the relay")
|
552
|
+
|
553
|
+
try:
|
554
|
+
# print(f"Retrieving products from seller: {seller}")
|
555
|
+
filter = Filter().kind(Kind(30018)).authors([seller])
|
556
|
+
events = await self.client.fetch_events_from(
|
557
|
+
urls=[self.relay], filter=filter, timeout=timedelta(seconds=2)
|
558
|
+
)
|
559
|
+
return events
|
560
|
+
except Exception as e:
|
561
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}")
|
562
|
+
|
563
|
+
async def _async_retrieve_profile(self, author: PublicKey) -> NostrProfile:
|
564
|
+
"""
|
565
|
+
Asynchronous function to retrieve the profile for a given author
|
566
|
+
|
567
|
+
Args:
|
568
|
+
author: PublicKey of the author to retrieve the profile for
|
569
|
+
|
570
|
+
Returns:
|
571
|
+
NostrProfile: profile of the author
|
572
|
+
|
573
|
+
Raises:
|
574
|
+
RuntimeError: if the profile can't be retrieved
|
575
|
+
"""
|
576
|
+
try:
|
577
|
+
await self._async_connect()
|
578
|
+
except Exception as e:
|
579
|
+
raise RuntimeError("Unable to connect to the relay")
|
580
|
+
|
581
|
+
try:
|
582
|
+
metadata = await self.client.fetch_metadata(
|
583
|
+
public_key=author, timeout=timedelta(seconds=2)
|
584
|
+
)
|
585
|
+
return NostrProfile.from_metadata(metadata, author)
|
586
|
+
except Exception as e:
|
587
|
+
raise RuntimeError(f"Unable to retrieve metadata: {e}")
|
588
|
+
|
589
|
+
async def _async_retrieve_stalls_from_seller(self, seller: PublicKey) -> Events:
|
590
|
+
"""
|
591
|
+
Asynchronous function to retrieve the stall for a given author
|
592
|
+
|
593
|
+
Args:
|
594
|
+
seller: PublicKey of the seller to retrieve the stall for
|
595
|
+
|
596
|
+
Returns:
|
597
|
+
Events: list of events containing the stalls of the seller
|
598
|
+
|
599
|
+
Raises:
|
600
|
+
RuntimeError: if the stall can't be retrieved
|
601
|
+
"""
|
602
|
+
try:
|
603
|
+
await self._async_connect()
|
604
|
+
except Exception as e:
|
605
|
+
raise RuntimeError("Unable to connect to the relay")
|
606
|
+
|
607
|
+
try:
|
608
|
+
filter = Filter().kind(Kind(30017)).authors([seller])
|
609
|
+
events = await self.client.fetch_events_from(
|
610
|
+
urls=[self.relay], filter=filter, timeout=timedelta(seconds=2)
|
611
|
+
)
|
612
|
+
return events
|
613
|
+
except Exception as e:
|
614
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}")
|
615
|
+
|
616
|
+
|
617
|
+
def generate_and_save_keys(env_var: str, env_path: Path) -> Keys:
|
618
|
+
"""Generate new nostr keys and save the private key to .env file.
|
619
|
+
|
620
|
+
Args:
|
621
|
+
env_var: Name of the environment variable to store the key
|
622
|
+
env_path: Path to the .env file. If None, looks for .env in current directory
|
623
|
+
|
624
|
+
Returns:
|
625
|
+
The generated Keys object
|
626
|
+
"""
|
627
|
+
# Generate new keys
|
628
|
+
keys = Keys.generate()
|
629
|
+
nsec = keys.secret_key().to_bech32()
|
630
|
+
|
631
|
+
# Determine .env path
|
632
|
+
if env_path is None:
|
633
|
+
env_path = Path.cwd() / ".env"
|
634
|
+
|
635
|
+
# Read existing .env content
|
636
|
+
env_content = ""
|
637
|
+
if env_path.exists():
|
638
|
+
with open(env_path, "r") as f:
|
639
|
+
env_content = f.read()
|
640
|
+
|
641
|
+
# Check if the env var already exists
|
642
|
+
lines = env_content.splitlines()
|
643
|
+
new_lines = []
|
644
|
+
var_found = False
|
645
|
+
|
646
|
+
for line in lines:
|
647
|
+
if line.startswith(f"{env_var}="):
|
648
|
+
new_lines.append(f"{env_var}={nsec}")
|
649
|
+
var_found = True
|
650
|
+
else:
|
651
|
+
new_lines.append(line)
|
652
|
+
|
653
|
+
# If var wasn't found, add it
|
654
|
+
if not var_found:
|
655
|
+
new_lines.append(f"{env_var}={nsec}")
|
656
|
+
|
657
|
+
# Write back to .env
|
658
|
+
with open(env_path, "w") as f:
|
659
|
+
f.write("\n".join(new_lines))
|
660
|
+
if new_lines: # Add final newline if there's content
|
661
|
+
f.write("\n")
|
662
|
+
|
663
|
+
return keys
|
agentstr/nostr.pyi
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
from logging import Logger
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import ClassVar, List, Optional
|
4
|
+
|
5
|
+
from nostr_sdk import ( # type: ignore
|
6
|
+
Client,
|
7
|
+
Event,
|
8
|
+
EventBuilder,
|
9
|
+
EventId,
|
10
|
+
Events,
|
11
|
+
Keys,
|
12
|
+
Kind,
|
13
|
+
Metadata,
|
14
|
+
NostrSigner,
|
15
|
+
ProductData,
|
16
|
+
PublicKey,
|
17
|
+
ShippingCost,
|
18
|
+
ShippingMethod,
|
19
|
+
StallData,
|
20
|
+
Tag,
|
21
|
+
Timestamp,
|
22
|
+
)
|
23
|
+
|
24
|
+
from agentstr.models import MerchantProduct, MerchantStall, NostrProfile
|
25
|
+
|
26
|
+
# Re-export all needed types
|
27
|
+
__all__ = [
|
28
|
+
"Event",
|
29
|
+
"EventBuilder",
|
30
|
+
"Events",
|
31
|
+
"EventId",
|
32
|
+
"Keys",
|
33
|
+
"Kind",
|
34
|
+
"Metadata",
|
35
|
+
"ProductData",
|
36
|
+
"PublicKey",
|
37
|
+
"ShippingCost",
|
38
|
+
"ShippingMethod",
|
39
|
+
"StallData",
|
40
|
+
"Timestamp",
|
41
|
+
]
|
42
|
+
|
43
|
+
class NostrClient:
|
44
|
+
logger: ClassVar[Logger]
|
45
|
+
relay: str
|
46
|
+
keys: Keys
|
47
|
+
nostr_signer: NostrSigner
|
48
|
+
client: Client
|
49
|
+
|
50
|
+
def __init__(self, relay: str, nsec: str) -> None: ...
|
51
|
+
def delete_event(
|
52
|
+
self, event_id: EventId, reason: Optional[str] = None
|
53
|
+
) -> EventId: ...
|
54
|
+
def publish_event(self, event_builder: EventBuilder) -> EventId: ...
|
55
|
+
def publish_note(self, text: str) -> EventId: ...
|
56
|
+
def publish_product(self, product: MerchantProduct) -> EventId: ...
|
57
|
+
def publish_profile(self, name: str, about: str, picture: str) -> EventId: ...
|
58
|
+
def publish_stall(self, stall: MerchantStall) -> EventId: ...
|
59
|
+
def retrieve_products_from_seller(
|
60
|
+
self, seller: PublicKey
|
61
|
+
) -> List[MerchantProduct]: ...
|
62
|
+
def retrieve_profile(self, public_key: PublicKey) -> NostrProfile: ...
|
63
|
+
def retrieve_stalls_from_seller(self, seller: PublicKey) -> List[StallData]: ...
|
64
|
+
def retrieve_sellers(self) -> set[NostrProfile]: ...
|
65
|
+
@classmethod
|
66
|
+
def set_logging_level(cls, logging_level: int) -> None: ...
|
67
|
+
async def _async_connect(self) -> None: ...
|
68
|
+
async def _async_publish_event(self, event_builder: EventBuilder) -> EventId: ...
|
69
|
+
async def _async_publish_note(self, text: str) -> EventId: ...
|
70
|
+
async def _async_publish_product(self, product: MerchantProduct) -> EventId: ...
|
71
|
+
async def _async_publish_profile(
|
72
|
+
self, name: str, about: str, picture: str
|
73
|
+
) -> EventId: ...
|
74
|
+
async def _async_publish_stall(self, stall: MerchantStall) -> EventId: ...
|
75
|
+
async def _async_retrieve_all_stalls(self) -> Events: ...
|
76
|
+
async def _async_retrieve_products_from_seller(
|
77
|
+
self, seller: PublicKey
|
78
|
+
) -> Events: ...
|
79
|
+
async def _async_retrieve_profile(self, author: PublicKey) -> NostrProfile: ...
|
80
|
+
async def _async_retrieve_stalls_from_seller(self, seller: PublicKey) -> Events: ...
|
81
|
+
|
82
|
+
def generate_and_save_keys(env_var: str, env_path: Optional[Path] = None) -> Keys: ...
|
agentstr/py.typed
ADDED
File without changes
|