comparison libervia/backend/plugins/plugin_comp_ap_gateway/pubsub_service.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 49019947cc76
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
35 from libervia.backend.tools import image 35 from libervia.backend.tools import image
36 from libervia.backend.tools.utils import ensure_deferred 36 from libervia.backend.tools.utils import ensure_deferred
37 from libervia.backend.tools.web import download_file 37 from libervia.backend.tools.web import download_file
38 from libervia.backend.memory.sqla_mapping import PubsubSub, SubscriptionState 38 from libervia.backend.memory.sqla_mapping import PubsubSub, SubscriptionState
39 39
40 from .constants import ( 40 from .constants import TYPE_ACTOR, ST_AVATAR, MAX_AVATAR_SIZE
41 TYPE_ACTOR,
42 ST_AVATAR,
43 MAX_AVATAR_SIZE
44 )
45 41
46 42
47 log = getLogger(__name__) 43 log = getLogger(__name__)
48 44
49 # all nodes have the same config 45 # all nodes have the same config
50 NODE_CONFIG = [ 46 NODE_CONFIG = [
51 {"var": "pubsub#persist_items", "type": "boolean", "value": True}, 47 {"var": "pubsub#persist_items", "type": "boolean", "value": True},
52 {"var": "pubsub#max_items", "value": "max"}, 48 {"var": "pubsub#max_items", "value": "max"},
53 {"var": "pubsub#access_model", "type": "list-single", "value": "open"}, 49 {"var": "pubsub#access_model", "type": "list-single", "value": "open"},
54 {"var": "pubsub#publish_model", "type": "list-single", "value": "open"}, 50 {"var": "pubsub#publish_model", "type": "list-single", "value": "open"},
55
56 ] 51 ]
57 52
58 NODE_CONFIG_VALUES = {c["var"]: c["value"] for c in NODE_CONFIG} 53 NODE_CONFIG_VALUES = {c["var"]: c["value"] for c in NODE_CONFIG}
59 NODE_OPTIONS = {c["var"]: {} for c in NODE_CONFIG} 54 NODE_OPTIONS = {c["var"]: {} for c in NODE_CONFIG}
60 for c in NODE_CONFIG: 55 for c in NODE_CONFIG:
61 NODE_OPTIONS[c["var"]].update({k:v for k,v in c.items() if k not in ("var", "value")}) 56 NODE_OPTIONS[c["var"]].update(
57 {k: v for k, v in c.items() if k not in ("var", "value")}
58 )
62 59
63 60
64 class APPubsubService(rsm.PubSubService): 61 class APPubsubService(rsm.PubSubService):
65 """Pubsub service for XMPP requests""" 62 """Pubsub service for XMPP requests"""
66 63
86 @return: requestor actor ID, recipient actor ID and recipient inbox 83 @return: requestor actor ID, recipient actor ID and recipient inbox
87 @raise error.StanzaError: "item-not-found" is raised if not user part is specified 84 @raise error.StanzaError: "item-not-found" is raised if not user part is specified
88 in requestor 85 in requestor
89 """ 86 """
90 if not recipient.user: 87 if not recipient.user:
91 raise error.StanzaError( 88 raise error.StanzaError("item-not-found", text="No user part specified")
92 "item-not-found",
93 text="No user part specified"
94 )
95 requestor_actor_id = self.apg.build_apurl(TYPE_ACTOR, requestor.userhost()) 89 requestor_actor_id = self.apg.build_apurl(TYPE_ACTOR, requestor.userhost())
96 recipient_account = self.apg._e.unescape(recipient.user) 90 recipient_account = self.apg._e.unescape(recipient.user)
97 recipient_actor_id = await self.apg.get_ap_actor_id_from_account(recipient_account) 91 recipient_actor_id = await self.apg.get_ap_actor_id_from_account(
92 recipient_account
93 )
98 inbox = await self.apg.get_ap_inbox_from_id(recipient_actor_id, use_shared=False) 94 inbox = await self.apg.get_ap_inbox_from_id(recipient_actor_id, use_shared=False)
99 return requestor_actor_id, recipient_actor_id, inbox 95 return requestor_actor_id, recipient_actor_id, inbox
100
101 96
102 @ensure_deferred 97 @ensure_deferred
103 async def publish(self, requestor, service, nodeIdentifier, items): 98 async def publish(self, requestor, service, nodeIdentifier, items):
104 if self.apg.local_only and not self.apg.is_local(requestor): 99 if self.apg.local_only and not self.apg.is_local(requestor):
105 raise error.StanzaError( 100 raise error.StanzaError(
106 "forbidden", 101 "forbidden", "Only local users can publish on this gateway."
107 "Only local users can publish on this gateway."
108 ) 102 )
109 if not service.user: 103 if not service.user:
110 raise error.StanzaError( 104 raise error.StanzaError(
111 "bad-request", 105 "bad-request",
112 "You must specify an ActivityPub actor account in JID user part." 106 "You must specify an ActivityPub actor account in JID user part.",
113 ) 107 )
114 ap_account = self.apg._e.unescape(service.user) 108 ap_account = self.apg._e.unescape(service.user)
115 if ap_account.count("@") != 1: 109 if ap_account.count("@") != 1:
116 raise error.StanzaError( 110 raise error.StanzaError(
117 "bad-request", 111 "bad-request", f"{ap_account!r} is not a valid ActivityPub actor account."
118 f"{ap_account!r} is not a valid ActivityPub actor account."
119 ) 112 )
120 113
121 client = self.apg.client.get_virtual_client(requestor) 114 client = self.apg.client.get_virtual_client(requestor)
122 if self.apg._pa.is_attachment_node(nodeIdentifier): 115 if self.apg._pa.is_attachment_node(nodeIdentifier):
123 await self.apg.convert_and_post_attachments( 116 await self.apg.convert_and_post_attachments(
128 client, ap_account, service, nodeIdentifier, items 121 client, ap_account, service, nodeIdentifier, items
129 ) 122 )
130 cached_node = await self.host.memory.storage.get_pubsub_node( 123 cached_node = await self.host.memory.storage.get_pubsub_node(
131 client, service, nodeIdentifier, with_subscriptions=True, create=True 124 client, service, nodeIdentifier, with_subscriptions=True, create=True
132 ) 125 )
133 await self.host.memory.storage.cache_pubsub_items( 126 await self.host.memory.storage.cache_pubsub_items(client, cached_node, items)
134 client,
135 cached_node,
136 items
137 )
138 for subscription in cached_node.subscriptions: 127 for subscription in cached_node.subscriptions:
139 if subscription.state != SubscriptionState.SUBSCRIBED: 128 if subscription.state != SubscriptionState.SUBSCRIBED:
140 continue 129 continue
141 self.notifyPublish( 130 self.notifyPublish(
142 service, 131 service, nodeIdentifier, [(subscription.subscriber, None, items)]
143 nodeIdentifier, 132 )
144 [(subscription.subscriber, None, items)] 133
145 ) 134 async def ap_following_2_elt(
146 135 self, requestor_actor_id: str, ap_item: dict
147 async def ap_following_2_elt(self, requestor_actor_id: str, ap_item: dict) -> domish.Element: 136 ) -> domish.Element:
148 """Convert actor ID from following collection to XMPP item 137 """Convert actor ID from following collection to XMPP item
149 138
150 @param requestor_actor_id: ID of the actor doing the request. 139 @param requestor_actor_id: ID of the actor doing the request.
151 @param ap_item: AP item from which actor ID must be extracted. 140 @param ap_item: AP item from which actor ID must be extracted.
152 """ 141 """
157 ) 146 )
158 item_elt = pubsub.Item(id=actor_id, payload=subscription_elt) 147 item_elt = pubsub.Item(id=actor_id, payload=subscription_elt)
159 return item_elt 148 return item_elt
160 149
161 async def ap_follower_2_elt( 150 async def ap_follower_2_elt(
162 self, 151 self, requestor_actor_id: str, ap_item: dict
163 requestor_actor_id: str,
164 ap_item: dict
165 ) -> domish.Element: 152 ) -> domish.Element:
166 """Convert actor ID from followers collection to XMPP item 153 """Convert actor ID from followers collection to XMPP item
167 154
168 @param requestor_actor_id: ID of the actor doing the request. 155 @param requestor_actor_id: ID of the actor doing the request.
169 @param ap_item: AP item from which actor ID must be extracted. 156 @param ap_item: AP item from which actor ID must be extracted.
173 subscriber_elt = self.apg._pps.build_subscriber_elt(actor_jid) 160 subscriber_elt = self.apg._pps.build_subscriber_elt(actor_jid)
174 item_elt = pubsub.Item(id=actor_id, payload=subscriber_elt) 161 item_elt = pubsub.Item(id=actor_id, payload=subscriber_elt)
175 return item_elt 162 return item_elt
176 163
177 async def generate_v_card( 164 async def generate_v_card(
178 self, 165 self, requestor_actor_id: str, ap_account: str
179 requestor_actor_id: str,
180 ap_account: str
181 ) -> domish.Element: 166 ) -> domish.Element:
182 """Generate vCard4 (XEP-0292) item element from ap_account's metadata 167 """Generate vCard4 (XEP-0292) item element from ap_account's metadata
183 168
184 @param requestor_actor_id: ID of the actor doing the request. 169 @param requestor_actor_id: ID of the actor doing the request.
185 @param ap_account: AP account from where the vcard must be retrieved. 170 @param ap_account: AP account from where the vcard must be retrieved.
186 @return: <item> with the <vcard> element 171 @return: <item> with the <vcard> element
187 """ 172 """
188 actor_data = await self.apg.get_ap_actor_data_from_account( 173 actor_data = await self.apg.get_ap_actor_data_from_account(
189 requestor_actor_id, 174 requestor_actor_id, ap_account
190 ap_account
191 ) 175 )
192 identity_data = {} 176 identity_data = {}
193 177
194 summary = actor_data.get("summary") 178 summary = actor_data.get("summary")
195 # summary is HTML, we have to convert it to text 179 # summary is HTML, we have to convert it to text
210 item_elt.addChild(vcard_elt) 194 item_elt.addChild(vcard_elt)
211 item_elt["id"] = self.apg._p.ID_SINGLETON 195 item_elt["id"] = self.apg._p.ID_SINGLETON
212 return item_elt 196 return item_elt
213 197
214 async def get_avatar_data( 198 async def get_avatar_data(
215 self, 199 self, client: SatXMPPEntity, requestor_actor_id: str, ap_account: str
216 client: SatXMPPEntity,
217 requestor_actor_id: str,
218 ap_account: str
219 ) -> dict[str, Any]: 200 ) -> dict[str, Any]:
220 """Retrieve actor's avatar if any, cache it and file actor_data 201 """Retrieve actor's avatar if any, cache it and file actor_data
221 202
222 @param client: client to use for the request. 203 @param client: client to use for the request.
223 @param requestor_actor_id: ID of the actor doing the request. 204 @param requestor_actor_id: ID of the actor doing the request.
257 dest_path = Path(dir_name, filename) 238 dest_path = Path(dir_name, filename)
258 await download_file(url, dest_path, max_size=MAX_AVATAR_SIZE) 239 await download_file(url, dest_path, max_size=MAX_AVATAR_SIZE)
259 avatar_data = { 240 avatar_data = {
260 "path": dest_path, 241 "path": dest_path,
261 "filename": filename, 242 "filename": filename,
262 'media_type': image.guess_type(dest_path), 243 "media_type": image.guess_type(dest_path),
263 } 244 }
264 245
265 await self.apg._i.cache_avatar( 246 await self.apg._i.cache_avatar(self.apg.IMPORT_NAME, avatar_data)
266 self.apg.IMPORT_NAME,
267 avatar_data
268 )
269 else: 247 else:
270 avatar_data = { 248 avatar_data = {
271 "cache_uid": cache["uid"], 249 "cache_uid": cache["uid"],
272 "path": cache["path"], 250 "path": cache["path"],
273 "media_type": cache["mime_type"] 251 "media_type": cache["mime_type"],
274 } 252 }
275 253
276 return avatar_data 254 return avatar_data
277 255
278 async def generate_avatar_metadata( 256 async def generate_avatar_metadata(
279 self, 257 self, client: SatXMPPEntity, requestor_actor_id: str, ap_account: str
280 client: SatXMPPEntity,
281 requestor_actor_id: str,
282 ap_account: str
283 ) -> domish.Element: 258 ) -> domish.Element:
284 """Generate the metadata element for user avatar 259 """Generate the metadata element for user avatar
285 260
286 @param requestor_actor_id: ID of the actor doing the request. 261 @param requestor_actor_id: ID of the actor doing the request.
287 @raise StanzaError("item-not-found"): no avatar is present in actor data (in 262 @raise StanzaError("item-not-found"): no avatar is present in actor data (in
306 @param requestor_actor_id: ID of the actor doing the request. 281 @param requestor_actor_id: ID of the actor doing the request.
307 @raise StanzaError("item-not-found"): no avatar cached with requested ID 282 @raise StanzaError("item-not-found"): no avatar cached with requested ID
308 """ 283 """
309 if not itemIdentifiers: 284 if not itemIdentifiers:
310 avatar_data = await self.get_avatar_data( 285 avatar_data = await self.get_avatar_data(
311 client, 286 client, requestor_actor_id, ap_account
312 requestor_actor_id,
313 ap_account
314 ) 287 )
315 if "base64" not in avatar_data: 288 if "base64" not in avatar_data:
316 await threads.deferToThread( 289 await threads.deferToThread(
317 self._blocking_b_6_4_encode_avatar, 290 self._blocking_b_6_4_encode_avatar, avatar_data
318 avatar_data
319 ) 291 )
320 else: 292 else:
321 if len(itemIdentifiers) > 1: 293 if len(itemIdentifiers) > 1:
322 # only a single item ID is supported 294 # only a single item ID is supported
323 raise error.StanzaError("item-not-found") 295 raise error.StanzaError("item-not-found")
325 # just to be sure that that we don't have an empty string 297 # just to be sure that that we don't have an empty string
326 assert item_id 298 assert item_id
327 cache_data = self.apg.host.common_cache.get_metadata(item_id) 299 cache_data = self.apg.host.common_cache.get_metadata(item_id)
328 if cache_data is None: 300 if cache_data is None:
329 raise error.StanzaError("item-not-found") 301 raise error.StanzaError("item-not-found")
330 avatar_data = { 302 avatar_data = {"cache_uid": item_id, "path": cache_data["path"]}
331 "cache_uid": item_id,
332 "path": cache_data["path"]
333 }
334 await threads.deferToThread(self._blocking_b_6_4_encode_avatar, avatar_data) 303 await threads.deferToThread(self._blocking_b_6_4_encode_avatar, avatar_data)
335 304
336 return self.apg._a.build_item_data_elt(avatar_data) 305 return self.apg._a.build_item_data_elt(avatar_data)
337 306
338 @ensure_deferred 307 @ensure_deferred
341 requestor: jid.JID, 310 requestor: jid.JID,
342 service: jid.JID, 311 service: jid.JID,
343 node: str, 312 node: str,
344 maxItems: Optional[int], 313 maxItems: Optional[int],
345 itemIdentifiers: Optional[List[str]], 314 itemIdentifiers: Optional[List[str]],
346 rsm_req: Optional[rsm.RSMRequest] 315 rsm_req: Optional[rsm.RSMRequest],
347 ) -> Tuple[List[domish.Element], Optional[rsm.RSMResponse]]: 316 ) -> Tuple[List[domish.Element], Optional[rsm.RSMResponse]]:
348 if not service.user: 317 if not service.user:
349 return [], None 318 return [], None
350 ap_account = self.host.plugins["XEP-0106"].unescape(service.user) 319 ap_account = self.host.plugins["XEP-0106"].unescape(service.user)
351 if ap_account.count("@") != 1: 320 if ap_account.count("@") != 1:
352 log.warning(f"Invalid AP account used by {requestor}: {ap_account!r}") 321 log.warning(f"Invalid AP account used by {requestor}: {ap_account!r}")
353 return [], None 322 return [], None
354 323
355 requestor_actor_id = self.apg.build_apurl( 324 requestor_actor_id = self.apg.build_apurl(
356 TYPE_ACTOR, 325 TYPE_ACTOR, await self.apg.get_ap_account_from_jid_and_node(service, node)
357 await self.apg.get_ap_account_from_jid_and_node(service, node)
358 ) 326 )
359 327
360 # cached_node may be pre-filled with some nodes (e.g. attachments nodes), 328 # cached_node may be pre-filled with some nodes (e.g. attachments nodes),
361 # otherwise it is filled when suitable 329 # otherwise it is filled when suitable
362 cached_node = None 330 cached_node = None
377 # vCard4 request 345 # vCard4 request
378 item_elt = await self.generate_v_card(requestor_actor_id, ap_account) 346 item_elt = await self.generate_v_card(requestor_actor_id, ap_account)
379 return [item_elt], None 347 return [item_elt], None
380 elif node == self.apg._a.namespace_metadata: 348 elif node == self.apg._a.namespace_metadata:
381 item_elt = await self.generate_avatar_metadata( 349 item_elt = await self.generate_avatar_metadata(
382 self.apg.client, 350 self.apg.client, requestor_actor_id, ap_account
383 requestor_actor_id,
384 ap_account
385 ) 351 )
386 return [item_elt], None 352 return [item_elt], None
387 elif node == self.apg._a.namespace_data: 353 elif node == self.apg._a.namespace_data:
388 item_elt = await self.generate_avatar_data( 354 item_elt = await self.generate_avatar_data(
389 self.apg.client, 355 self.apg.client, requestor_actor_id, ap_account, itemIdentifiers
390 requestor_actor_id,
391 ap_account,
392 itemIdentifiers
393 ) 356 )
394 return [item_elt], None 357 return [item_elt], None
395 elif self.apg._pa.is_attachment_node(node): 358 elif self.apg._pa.is_attachment_node(node):
396 use_cache = True 359 use_cache = True
397 # we check cache here because we emit an item-not-found error if the node is 360 # we check cache here because we emit an item-not-found error if the node is
408 parser = self.apg.ap_events.ap_item_2_event_elt 371 parser = self.apg.ap_events.ap_item_2_event_elt
409 else: 372 else:
410 raise error.StanzaError( 373 raise error.StanzaError(
411 "feature-not-implemented", 374 "feature-not-implemented",
412 text=f"AP Gateway {C.APP_VERSION} only supports " 375 text=f"AP Gateway {C.APP_VERSION} only supports "
413 f"{self.apg._m.namespace} node for now" 376 f"{self.apg._m.namespace} node for now",
414 ) 377 )
415 collection_name = "outbox" 378 collection_name = "outbox"
416 use_cache = True 379 use_cache = True
417 380
418 if use_cache: 381 if use_cache:
442 return items, None 405 return items, None
443 else: 406 else:
444 if rsm_req is None: 407 if rsm_req is None:
445 if maxItems is None: 408 if maxItems is None:
446 maxItems = 20 409 maxItems = 20
447 kwargs.update({ 410 kwargs.update(
448 "max_items": maxItems, 411 {
449 "chronological_pagination": False, 412 "max_items": maxItems,
450 }) 413 "chronological_pagination": False,
414 }
415 )
451 else: 416 else:
452 if len( 417 if (
453 [v for v in (rsm_req.after, rsm_req.before, rsm_req.index) 418 len(
454 if v is not None] 419 [
455 ) > 1: 420 v
421 for v in (rsm_req.after, rsm_req.before, rsm_req.index)
422 if v is not None
423 ]
424 )
425 > 1
426 ):
456 raise error.StanzaError( 427 raise error.StanzaError(
457 "bad-request", 428 "bad-request",
458 text="You can't use after, before and index at the same time" 429 text="You can't use after, before and index at the same time",
459 ) 430 )
460 kwargs.update({"max_items": rsm_req.max}) 431 kwargs.update({"max_items": rsm_req.max})
461 if rsm_req.after is not None: 432 if rsm_req.after is not None:
462 kwargs["after_id"] = rsm_req.after 433 kwargs["after_id"] = rsm_req.after
463 elif rsm_req.before is not None: 434 elif rsm_req.before is not None:
474 if self.apg._m.is_comment_node(node): 445 if self.apg._m.is_comment_node(node):
475 parent_item = self.apg._m.get_parent_item(node) 446 parent_item = self.apg._m.get_parent_item(node)
476 try: 447 try:
477 parent_data = await self.apg.ap_get(parent_item, requestor_actor_id) 448 parent_data = await self.apg.ap_get(parent_item, requestor_actor_id)
478 collection = await self.apg.ap_get_object( 449 collection = await self.apg.ap_get_object(
479 requestor_actor_id, 450 requestor_actor_id, parent_data.get("object", {}), "replies"
480 parent_data.get("object", {}),
481 "replies"
482 ) 451 )
483 except Exception as e: 452 except Exception as e:
484 raise error.StanzaError( 453 raise error.StanzaError("item-not-found", text=str(e))
485 "item-not-found",
486 text=str(e)
487 )
488 else: 454 else:
489 actor_data = await self.apg.get_ap_actor_data_from_account( 455 actor_data = await self.apg.get_ap_actor_data_from_account(
490 requestor_actor_id, 456 requestor_actor_id, ap_account
491 ap_account 457 )
492 ) 458 collection = await self.apg.ap_get_object(
493 collection = await self.apg.ap_get_object(requestor_actor_id, actor_data, collection_name) 459 requestor_actor_id, actor_data, collection_name
460 )
494 if not collection: 461 if not collection:
495 raise error.StanzaError( 462 raise error.StanzaError(
496 "item-not-found", 463 "item-not-found",
497 text=f"No collection found for node {node!r} (account: {ap_account})" 464 text=f"No collection found for node {node!r} (account: {ap_account})",
498 ) 465 )
499 466
500 kwargs["parser"] = parser 467 kwargs["parser"] = parser
501 return await self.apg.get_ap_items(requestor_actor_id, collection, **kwargs) 468 return await self.apg.get_ap_items(requestor_actor_id, collection, **kwargs)
502 469
526 ) 493 )
527 subscription = None 494 subscription = None
528 else: 495 else:
529 try: 496 try:
530 subscription = next( 497 subscription = next(
531 s for s in node.subscriptions 498 s
499 for s in node.subscriptions
532 if s.subscriber == requestor.userhostJID() 500 if s.subscriber == requestor.userhostJID()
533 ) 501 )
534 except StopIteration: 502 except StopIteration:
535 subscription = None 503 subscription = None
536 504
537 if subscription is None: 505 if subscription is None:
538 subscription = PubsubSub( 506 subscription = PubsubSub(subscriber=requestor.userhostJID(), state=sub_state)
539 subscriber=requestor.userhostJID(),
540 state=sub_state
541 )
542 node.subscriptions.append(subscription) 507 node.subscriptions.append(subscription)
543 await self.host.memory.storage.add(node) 508 await self.host.memory.storage.add(node)
544 else: 509 else:
545 if subscription.state is None: 510 if subscription.state is None:
546 subscription.state = sub_state 511 subscription.state = sub_state
584 requestor, service 549 requestor, service
585 ) 550 )
586 data = self.apg.create_activity( 551 data = self.apg.create_activity(
587 "Undo", 552 "Undo",
588 req_actor_id, 553 req_actor_id,
589 self.apg.create_activity( 554 self.apg.create_activity("Follow", req_actor_id, recip_actor_id),
590 "Follow",
591 req_actor_id,
592 recip_actor_id
593 )
594 ) 555 )
595 556
596 resp = await self.apg.sign_and_post(inbox, req_actor_id, data) 557 resp = await self.apg.sign_and_post(inbox, req_actor_id, data)
597 if resp.code >= 300: 558 if resp.code >= 300:
598 text = await resp.text() 559 text = await resp.text()
600 561
601 def getConfigurationOptions(self): 562 def getConfigurationOptions(self):
602 return NODE_OPTIONS 563 return NODE_OPTIONS
603 564
604 def getConfiguration( 565 def getConfiguration(
605 self, 566 self, requestor: jid.JID, service: jid.JID, nodeIdentifier: str
606 requestor: jid.JID,
607 service: jid.JID,
608 nodeIdentifier: str
609 ) -> defer.Deferred: 567 ) -> defer.Deferred:
610 return defer.succeed(NODE_CONFIG_VALUES) 568 return defer.succeed(NODE_CONFIG_VALUES)
611 569
612 def getNodeInfo( 570 def getNodeInfo(
613 self, 571 self,
614 requestor: jid.JID, 572 requestor: jid.JID,
615 service: jid.JID, 573 service: jid.JID,
616 nodeIdentifier: str, 574 nodeIdentifier: str,
617 pep: bool = False, 575 pep: bool = False,
618 recipient: Optional[jid.JID] = None 576 recipient: Optional[jid.JID] = None,
619 ) -> Optional[dict]: 577 ) -> Optional[dict]:
620 if not nodeIdentifier: 578 if not nodeIdentifier:
621 return None 579 return None
622 info = { 580 info = {"type": "leaf", "meta-data": NODE_CONFIG}
623 "type": "leaf",
624 "meta-data": NODE_CONFIG
625 }
626 return info 581 return info