2222
2323from .models import (
2424 FeedResponse ,
25+ FriendsGraph ,
2526 HandleResolution ,
2627 IdentityBundle ,
28+ NotificationItem ,
2729 OverlayState ,
2830 PeckRow ,
2931 ProfileRow ,
@@ -64,6 +66,15 @@ def put(k: str, v: Any) -> None:
6466 put ("author" , params .get ("author" ))
6567 put ("order" , params .get ("order" , "desc" ))
6668 put ("before" , params .get ("before" ))
69+ # Geo: near={"lat":..,"lng":..} (+ radius_km) → haversine; bbox=(w,s,e,n).
70+ near = params .get ("near" )
71+ if near :
72+ lat = near .get ("lat" ) if isinstance (near , dict ) else near [0 ]
73+ lng = near .get ("lng" ) if isinstance (near , dict ) else near [1 ]
74+ put ("near" , f"{ lat } ,{ lng } " )
75+ put ("radius_km" , params .get ("radius_km" ))
76+ elif params .get ("bbox" ):
77+ put ("bbox" , "," .join (str (x ) for x in params ["bbox" ]))
6778 return out
6879
6980
@@ -245,6 +256,64 @@ def get_profile(
245256 raise OverlayError (f"overlay { resp .status_code } on /v1/bio/profile" )
246257 return _interp_profile (resp .json ())
247258
259+ # social graph
260+ def get_friends (self , subject : str ) -> FriendsGraph :
261+ """GET /v1/friends/:subject — mutual-consent friendship graph for an
262+ identity ROOT pubkey. Returns an EMPTY graph on any error."""
263+ if not subject :
264+ return FriendsGraph ()
265+ try :
266+ data = self ._get_json_or_null (f"/v1/friends/{ subject } " )
267+ return FriendsGraph .from_dict (data ) if isinstance (data , dict ) else FriendsGraph ()
268+ except Exception :
269+ return FriendsGraph ()
270+
271+ def get_notifications (
272+ self , address : str , * , limit : int = 50 , offset : int = 0 , mentions : bool = True
273+ ) -> list [NotificationItem ]:
274+ """GET /v1/notifications/:address — like/reply/follow/mention/
275+ friend_request targeting a POSTING address, newest first. [] on error."""
276+ if not address :
277+ return []
278+ try :
279+ params : dict [str , str ] = {"limit" : str (limit ), "offset" : str (offset )}
280+ if not mentions :
281+ params ["mentions" ] = "0"
282+ r = self ._client .get (f"{ self .base_url } /v1/notifications/{ address } " , params = params )
283+ if r .status_code // 100 != 2 :
284+ return []
285+ data = r .json () if r .content else {}
286+ return [NotificationItem .from_dict (x ) for x in (data .get ("data" ) or []) if isinstance (x , dict )]
287+ except Exception :
288+ return []
289+
290+ def get_follows (self , address : str ) -> dict [str , Any ]:
291+ """GET /v1/follows/:address — follower/following counts + rows.
292+ Safe-empty dict on error."""
293+ empty : dict [str , Any ] = {"followers" : 0 , "following" : 0 , "data" : {"followers" : [], "following" : []}}
294+ if not address :
295+ return empty
296+ try :
297+ data = self ._get_json_or_null (f"/v1/follows/{ address } " )
298+ return data if isinstance (data , dict ) else empty
299+ except Exception :
300+ return empty
301+
302+ def get_blocks (self , address : str , * , kind : str | None = None ) -> list [dict [str , Any ]]:
303+ """GET /v1/blocks/:address — OUTGOING block/mute list (the overlay
304+ deliberately does not expose who-blocked-me). [] on error."""
305+ if not address :
306+ return []
307+ try :
308+ params = {"kind" : kind } if kind else None
309+ r = self ._client .get (f"{ self .base_url } /v1/blocks/{ address } " , params = params )
310+ if r .status_code // 100 != 2 :
311+ return []
312+ data = r .json () if r .content else {}
313+ return list (data .get ("data" ) or [])
314+ except Exception :
315+ return []
316+
248317 # feed / posts
249318 def get_feed (self , ** params : Any ) -> FeedResponse :
250319 resp = self ._client .get (f"{ self .base_url } /v1/feed" , params = _feed_query (params ))
@@ -267,8 +336,17 @@ def get_state(self) -> OverlayState:
267336 return OverlayState .from_dict (self ._get_json ("/state" ))
268337
269338 def get_topic_root (self , topic : str ) -> TopicState | None :
339+ """GET /v1/topic/:topic/root (cheap, 30s server cache; no anchor —
340+ use get_anchor/verify_root for on-chain status). Falls back to a
341+ find over /state for older overlays."""
270342 if not topic :
271343 return None
344+ try :
345+ data = self ._get_json_or_null (f"/v1/topic/{ topic } /root" )
346+ if isinstance (data , dict ) and data .get ("stateRoot" ):
347+ return TopicState .from_dict (data )
348+ except Exception :
349+ pass
272350 try :
273351 for t in self .get_state ().topics :
274352 if t .topic == topic :
@@ -395,6 +473,61 @@ async def get_profile(
395473 raise OverlayError (f"overlay { resp .status_code } on /v1/bio/profile" )
396474 return _interp_profile (resp .json ())
397475
476+ async def get_friends (self , subject : str ) -> FriendsGraph :
477+ """GET /v1/friends/:subject — mutual-consent friendship graph for an
478+ identity ROOT pubkey. Returns an EMPTY graph on any error."""
479+ if not subject :
480+ return FriendsGraph ()
481+ try :
482+ data = await self ._get_json_or_null (f"/v1/friends/{ subject } " )
483+ return FriendsGraph .from_dict (data ) if isinstance (data , dict ) else FriendsGraph ()
484+ except Exception :
485+ return FriendsGraph ()
486+
487+ async def get_notifications (
488+ self , address : str , * , limit : int = 50 , offset : int = 0 , mentions : bool = True
489+ ) -> list [NotificationItem ]:
490+ """GET /v1/notifications/:address — like/reply/follow/mention/
491+ friend_request targeting a POSTING address, newest first. [] on error."""
492+ if not address :
493+ return []
494+ try :
495+ params : dict [str , str ] = {"limit" : str (limit ), "offset" : str (offset )}
496+ if not mentions :
497+ params ["mentions" ] = "0"
498+ r = await self ._client .get (f"{ self .base_url } /v1/notifications/{ address } " , params = params )
499+ if r .status_code // 100 != 2 :
500+ return []
501+ data = r .json () if r .content else {}
502+ return [NotificationItem .from_dict (x ) for x in (data .get ("data" ) or []) if isinstance (x , dict )]
503+ except Exception :
504+ return []
505+
506+ async def get_follows (self , address : str ) -> dict [str , Any ]:
507+ """GET /v1/follows/:address — follower/following counts + rows."""
508+ empty : dict [str , Any ] = {"followers" : 0 , "following" : 0 , "data" : {"followers" : [], "following" : []}}
509+ if not address :
510+ return empty
511+ try :
512+ data = await self ._get_json_or_null (f"/v1/follows/{ address } " )
513+ return data if isinstance (data , dict ) else empty
514+ except Exception :
515+ return empty
516+
517+ async def get_blocks (self , address : str , * , kind : str | None = None ) -> list [dict [str , Any ]]:
518+ """GET /v1/blocks/:address — OUTGOING block/mute list. [] on error."""
519+ if not address :
520+ return []
521+ try :
522+ params = {"kind" : kind } if kind else None
523+ r = await self ._client .get (f"{ self .base_url } /v1/blocks/{ address } " , params = params )
524+ if r .status_code // 100 != 2 :
525+ return []
526+ data = r .json () if r .content else {}
527+ return list (data .get ("data" ) or [])
528+ except Exception :
529+ return []
530+
398531 async def get_feed (self , ** params : Any ) -> FeedResponse :
399532 resp = await self ._client .get (f"{ self .base_url } /v1/feed" , params = _feed_query (params ))
400533 if resp .status_code // 100 != 2 :
@@ -415,8 +548,16 @@ async def get_state(self) -> OverlayState:
415548 return OverlayState .from_dict (await self ._get_json ("/state" ))
416549
417550 async def get_topic_root (self , topic : str ) -> TopicState | None :
551+ """GET /v1/topic/:topic/root (cheap; no anchor — use get_anchor/
552+ verify_root). Falls back to a find over /state for older overlays."""
418553 if not topic :
419554 return None
555+ try :
556+ data = await self ._get_json_or_null (f"/v1/topic/{ topic } /root" )
557+ if isinstance (data , dict ) and data .get ("stateRoot" ):
558+ return TopicState .from_dict (data )
559+ except Exception :
560+ pass
420561 try :
421562 state = await self .get_state ()
422563 except OverlayError :
0 commit comments