comparison sat/plugins/plugin_comp_ap_gateway/http_server.py @ 4037:524856bd7b19

massive refactoring to switch from camelCase to snake_case: historically, Libervia (SàT before) was using camelCase as allowed by PEP8 when using a pre-PEP8 code, to use the same coding style as in Twisted. However, snake_case is more readable and it's better to follow PEP8 best practices, so it has been decided to move on full snake_case. Because Libervia has a huge codebase, this ended with a ugly mix of camelCase and snake_case. To fix that, this patch does a big refactoring by renaming every function and method (including bridge) that are not coming from Twisted or Wokkel, to use fully snake_case. This is a massive change, and may result in some bugs.
author Goffi <goffi@goffi.org>
date Sat, 08 Apr 2023 13:54:42 +0200
parents 78b5f356900c
children
comparison
equal deleted inserted replaced
4036:c4464d7ae97b 4037:524856bd7b19
64 def __init__(self, ap_gateway): 64 def __init__(self, ap_gateway):
65 self.apg = ap_gateway 65 self.apg = ap_gateway
66 self._seen_digest = deque(maxlen=50) 66 self._seen_digest = deque(maxlen=50)
67 super().__init__() 67 super().__init__()
68 68
69 def responseCode( 69 def response_code(
70 self, 70 self,
71 request: "HTTPRequest", 71 request: "HTTPRequest",
72 http_code: int, 72 http_code: int,
73 msg: Optional[str] = None 73 msg: Optional[str] = None
74 ) -> None: 74 ) -> None:
75 """Log and set HTTP return code and associated message""" 75 """Log and set HTTP return code and associated message"""
76 if msg is not None: 76 if msg is not None:
77 log.warning(msg) 77 log.warning(msg)
78 request.setResponseCode(http_code, None if msg is None else msg.encode()) 78 request.setResponseCode(http_code, None if msg is None else msg.encode())
79 79
80 def _onRequestError(self, failure_: failure.Failure, request: "HTTPRequest") -> None: 80 def _on_request_error(self, failure_: failure.Failure, request: "HTTPRequest") -> None:
81 exc = failure_.value 81 exc = failure_.value
82 if isinstance(exc, exceptions.NotFound): 82 if isinstance(exc, exceptions.NotFound):
83 self.responseCode( 83 self.response_code(
84 request, 84 request,
85 http.NOT_FOUND, 85 http.NOT_FOUND,
86 str(exc) 86 str(exc)
87 ) 87 )
88 else: 88 else:
89 log.exception(f"Internal error: {failure_.value}") 89 log.exception(f"Internal error: {failure_.value}")
90 self.responseCode( 90 self.response_code(
91 request, 91 request,
92 http.INTERNAL_SERVER_ERROR, 92 http.INTERNAL_SERVER_ERROR,
93 f"internal error: {failure_.value}" 93 f"internal error: {failure_.value}"
94 ) 94 )
95 request.finish() 95 request.finish()
105 if not resource.startswith("acct:") or not account: 105 if not resource.startswith("acct:") or not account:
106 return web_resource.ErrorPage( 106 return web_resource.ErrorPage(
107 http.BAD_REQUEST, "Bad Request" , "Invalid webfinger resource" 107 http.BAD_REQUEST, "Bad Request" , "Invalid webfinger resource"
108 ).render(request) 108 ).render(request)
109 109
110 actor_url = self.apg.buildAPURL(TYPE_ACTOR, account) 110 actor_url = self.apg.build_apurl(TYPE_ACTOR, account)
111 111
112 resp = { 112 resp = {
113 "aliases": [actor_url], 113 "aliases": [actor_url],
114 "subject": resource, 114 "subject": resource,
115 "links": [ 115 "links": [
122 } 122 }
123 request.setHeader("content-type", CONTENT_TYPE_AP) 123 request.setHeader("content-type", CONTENT_TYPE_AP)
124 request.write(json.dumps(resp).encode()) 124 request.write(json.dumps(resp).encode())
125 request.finish() 125 request.finish()
126 126
127 async def handleUndoActivity( 127 async def handle_undo_activity(
128 self, 128 self,
129 request: "HTTPRequest", 129 request: "HTTPRequest",
130 data: dict, 130 data: dict,
131 account_jid: jid.JID, 131 account_jid: jid.JID,
132 node: Optional[str], 132 node: Optional[str],
134 ap_url: str, 134 ap_url: str,
135 signing_actor: str 135 signing_actor: str
136 ) -> None: 136 ) -> None:
137 if node is None: 137 if node is None:
138 node = self.apg._m.namespace 138 node = self.apg._m.namespace
139 client = await self.apg.getVirtualClient(signing_actor) 139 client = await self.apg.get_virtual_client(signing_actor)
140 object_ = data.get("object") 140 object_ = data.get("object")
141 if isinstance(object_, str): 141 if isinstance(object_, str):
142 # we check first if it's not a cached object 142 # we check first if it's not a cached object
143 ap_cache_key = f"{ST_AP_CACHE}{object_}" 143 ap_cache_key = f"{ST_AP_CACHE}{object_}"
144 value = await self.apg.client._ap_storage.get(ap_cache_key) 144 value = await self.apg.client._ap_storage.get(ap_cache_key)
147 if value is not None: 147 if value is not None:
148 objects = [value] 148 objects = [value]
149 # because we'll undo the activity, we can remove it from cache 149 # because we'll undo the activity, we can remove it from cache
150 await self.apg.client._ap_storage.remove(ap_cache_key) 150 await self.apg.client._ap_storage.remove(ap_cache_key)
151 else: 151 else:
152 objects = await self.apg.apGetList(data, "object") 152 objects = await self.apg.ap_get_list(data, "object")
153 for obj in objects: 153 for obj in objects:
154 type_ = obj.get("type") 154 type_ = obj.get("type")
155 actor = await self.apg.apGetSenderActor(obj) 155 actor = await self.apg.ap_get_sender_actor(obj)
156 if actor != signing_actor: 156 if actor != signing_actor:
157 log.warning(f"ignoring object not attributed to signing actor: {data}") 157 log.warning(f"ignoring object not attributed to signing actor: {data}")
158 continue 158 continue
159 159
160 if type_ == "Follow": 160 if type_ == "Follow":
161 try: 161 try:
162 target_account = obj["object"] 162 target_account = obj["object"]
163 except KeyError: 163 except KeyError:
164 log.warning(f'ignoring invalid object, missing "object" key: {data}') 164 log.warning(f'ignoring invalid object, missing "object" key: {data}')
165 continue 165 continue
166 if not self.apg.isLocalURL(target_account): 166 if not self.apg.is_local_url(target_account):
167 log.warning(f"ignoring unfollow request to non local actor: {data}") 167 log.warning(f"ignoring unfollow request to non local actor: {data}")
168 continue 168 continue
169 await self.apg._p.unsubscribe( 169 await self.apg._p.unsubscribe(
170 client, 170 client,
171 account_jid, 171 account_jid,
173 sender=client.jid, 173 sender=client.jid,
174 ) 174 )
175 elif type_ == "Announce": 175 elif type_ == "Announce":
176 # we can use directly the Announce object, as only the "id" field is 176 # we can use directly the Announce object, as only the "id" field is
177 # needed 177 # needed
178 await self.apg.newAPDeleteItem(client, None, node, obj) 178 await self.apg.new_ap_delete_item(client, None, node, obj)
179 elif type_ == TYPE_LIKE: 179 elif type_ == TYPE_LIKE:
180 await self.handleAttachmentItem(client, obj, {"noticed": False}) 180 await self.handle_attachment_item(client, obj, {"noticed": False})
181 elif type_ == TYPE_REACTION: 181 elif type_ == TYPE_REACTION:
182 await self.handleAttachmentItem(client, obj, { 182 await self.handle_attachment_item(client, obj, {
183 "reactions": {"operation": "update", "remove": [obj["content"]]} 183 "reactions": {"operation": "update", "remove": [obj["content"]]}
184 }) 184 })
185 else: 185 else:
186 log.warning(f"Unmanaged undo type: {type_!r}") 186 log.warning(f"Unmanaged undo type: {type_!r}")
187 187
188 async def handleFollowActivity( 188 async def handle_follow_activity(
189 self, 189 self,
190 request: "HTTPRequest", 190 request: "HTTPRequest",
191 data: dict, 191 data: dict,
192 account_jid: jid.JID, 192 account_jid: jid.JID,
193 node: Optional[str], 193 node: Optional[str],
195 ap_url: str, 195 ap_url: str,
196 signing_actor: str 196 signing_actor: str
197 ) -> None: 197 ) -> None:
198 if node is None: 198 if node is None:
199 node = self.apg._m.namespace 199 node = self.apg._m.namespace
200 client = await self.apg.getVirtualClient(signing_actor) 200 client = await self.apg.get_virtual_client(signing_actor)
201 try: 201 try:
202 subscription = await self.apg._p.subscribe( 202 subscription = await self.apg._p.subscribe(
203 client, 203 client,
204 account_jid, 204 account_jid,
205 node, 205 node,
206 # subscriptions from AP are always public 206 # subscriptions from AP are always public
207 options=self.apg._pps.setPublicOpt() 207 options=self.apg._pps.set_public_opt()
208 ) 208 )
209 except pubsub.SubscriptionPending: 209 except pubsub.SubscriptionPending:
210 log.info(f"subscription to node {node!r} of {account_jid} is pending") 210 log.info(f"subscription to node {node!r} of {account_jid} is pending")
211 # TODO: manage SubscriptionUnconfigured 211 # TODO: manage SubscriptionUnconfigured
212 else: 212 else:
213 if subscription.state != "subscribed": 213 if subscription.state != "subscribed":
214 # other states should raise an Exception 214 # other states should raise an Exception
215 raise exceptions.InternalError('"subscribed" state was expected') 215 raise exceptions.InternalError('"subscribed" state was expected')
216 inbox = await self.apg.getAPInboxFromId(signing_actor, use_shared=False) 216 inbox = await self.apg.get_ap_inbox_from_id(signing_actor, use_shared=False)
217 actor_id = self.apg.buildAPURL(TYPE_ACTOR, ap_account) 217 actor_id = self.apg.build_apurl(TYPE_ACTOR, ap_account)
218 accept_data = self.apg.create_activity( 218 accept_data = self.apg.create_activity(
219 "Accept", actor_id, object_=data 219 "Accept", actor_id, object_=data
220 ) 220 )
221 await self.apg.signAndPost(inbox, actor_id, accept_data) 221 await self.apg.sign_and_post(inbox, actor_id, accept_data)
222 await self.apg._c.synchronise(client, account_jid, node, resync=False) 222 await self.apg._c.synchronise(client, account_jid, node, resync=False)
223 223
224 async def handleAcceptActivity( 224 async def handle_accept_activity(
225 self, 225 self,
226 request: "HTTPRequest", 226 request: "HTTPRequest",
227 data: dict, 227 data: dict,
228 account_jid: jid.JID, 228 account_jid: jid.JID,
229 node: Optional[str], 229 node: Optional[str],
231 ap_url: str, 231 ap_url: str,
232 signing_actor: str 232 signing_actor: str
233 ) -> None: 233 ) -> None:
234 if node is None: 234 if node is None:
235 node = self.apg._m.namespace 235 node = self.apg._m.namespace
236 client = await self.apg.getVirtualClient(signing_actor) 236 client = await self.apg.get_virtual_client(signing_actor)
237 objects = await self.apg.apGetList(data, "object") 237 objects = await self.apg.ap_get_list(data, "object")
238 for obj in objects: 238 for obj in objects:
239 type_ = obj.get("type") 239 type_ = obj.get("type")
240 if type_ == "Follow": 240 if type_ == "Follow":
241 follow_node = await self.apg.host.memory.storage.getPubsubNode( 241 follow_node = await self.apg.host.memory.storage.get_pubsub_node(
242 client, client.jid, node, with_subscriptions=True 242 client, client.jid, node, with_subscriptions=True
243 ) 243 )
244 if follow_node is None: 244 if follow_node is None:
245 log.warning( 245 log.warning(
246 f"Received a follow accept on an unknown node: {node!r} at " 246 f"Received a follow accept on an unknown node: {node!r} at "
268 f"Unhandled subscription state {sub.state!r}" 268 f"Unhandled subscription state {sub.state!r}"
269 ) 269 )
270 else: 270 else:
271 log.warning(f"Unmanaged accept type: {type_!r}") 271 log.warning(f"Unmanaged accept type: {type_!r}")
272 272
273 async def handleDeleteActivity( 273 async def handle_delete_activity(
274 self, 274 self,
275 request: "HTTPRequest", 275 request: "HTTPRequest",
276 data: dict, 276 data: dict,
277 account_jid: Optional[jid.JID], 277 account_jid: Optional[jid.JID],
278 node: Optional[str], 278 node: Optional[str],
280 ap_url: str, 280 ap_url: str,
281 signing_actor: str 281 signing_actor: str
282 ): 282 ):
283 if node is None: 283 if node is None:
284 node = self.apg._m.namespace 284 node = self.apg._m.namespace
285 client = await self.apg.getVirtualClient(signing_actor) 285 client = await self.apg.get_virtual_client(signing_actor)
286 objects = await self.apg.apGetList(data, "object") 286 objects = await self.apg.ap_get_list(data, "object")
287 for obj in objects: 287 for obj in objects:
288 await self.apg.newAPDeleteItem(client, account_jid, node, obj) 288 await self.apg.new_ap_delete_item(client, account_jid, node, obj)
289 289
290 async def handleNewAPItems( 290 async def handle_new_ap_items(
291 self, 291 self,
292 request: "HTTPRequest", 292 request: "HTTPRequest",
293 data: dict, 293 data: dict,
294 account_jid: Optional[jid.JID], 294 account_jid: Optional[jid.JID],
295 node: Optional[str], 295 node: Optional[str],
296 signing_actor: str, 296 signing_actor: str,
297 repeated: bool = False, 297 repeated: bool = False,
298 ): 298 ):
299 """Helper method to handle workflow for new AP items 299 """Helper method to handle workflow for new AP items
300 300
301 accept globally the same parameter as for handleCreateActivity 301 accept globally the same parameter as for handle_create_activity
302 @param repeated: if True, the item is an item republished from somewhere else 302 @param repeated: if True, the item is an item republished from somewhere else
303 """ 303 """
304 if "_repeated" in data: 304 if "_repeated" in data:
305 log.error( 305 log.error(
306 '"_repeated" field already present in given AP item, this should not ' 306 '"_repeated" field already present in given AP item, this should not '
307 f"happen. Ignoring object from {signing_actor}\n{data}" 307 f"happen. Ignoring object from {signing_actor}\n{data}"
308 ) 308 )
309 raise exceptions.DataError("unexpected field in item") 309 raise exceptions.DataError("unexpected field in item")
310 client = await self.apg.getVirtualClient(signing_actor) 310 client = await self.apg.get_virtual_client(signing_actor)
311 objects = await self.apg.apGetList(data, "object") 311 objects = await self.apg.ap_get_list(data, "object")
312 for obj in objects: 312 for obj in objects:
313 if node is None: 313 if node is None:
314 if obj.get("type") == TYPE_EVENT: 314 if obj.get("type") == TYPE_EVENT:
315 node = self.apg._events.namespace 315 node = self.apg._events.namespace
316 else: 316 else:
317 node = self.apg._m.namespace 317 node = self.apg._m.namespace
318 sender = await self.apg.apGetSenderActor(obj) 318 sender = await self.apg.ap_get_sender_actor(obj)
319 if repeated: 319 if repeated:
320 # we don't check sender when item is repeated, as it should be different 320 # we don't check sender when item is repeated, as it should be different
321 # from post author in this case 321 # from post author in this case
322 sender_jid = await self.apg.getJIDFromId(sender) 322 sender_jid = await self.apg.get_jid_from_id(sender)
323 repeater_jid = await self.apg.getJIDFromId(signing_actor) 323 repeater_jid = await self.apg.get_jid_from_id(signing_actor)
324 repeated_item_id = obj["id"] 324 repeated_item_id = obj["id"]
325 if self.apg.isLocalURL(repeated_item_id): 325 if self.apg.is_local_url(repeated_item_id):
326 # the repeated object is from XMPP, we need to parse the URL to find 326 # the repeated object is from XMPP, we need to parse the URL to find
327 # the right ID 327 # the right ID
328 url_type, url_args = self.apg.parseAPURL(repeated_item_id) 328 url_type, url_args = self.apg.parse_apurl(repeated_item_id)
329 if url_type != "item": 329 if url_type != "item":
330 raise exceptions.DataError( 330 raise exceptions.DataError(
331 "local URI is not an item: {repeated_id}" 331 "local URI is not an item: {repeated_id}"
332 ) 332 )
333 try: 333 try:
337 except (RuntimeError, ValueError): 337 except (RuntimeError, ValueError):
338 raise exceptions.DataError( 338 raise exceptions.DataError(
339 "local URI is invalid: {repeated_id}" 339 "local URI is invalid: {repeated_id}"
340 ) 340 )
341 else: 341 else:
342 url_jid, url_node = await self.apg.getJIDAndNode(url_account) 342 url_jid, url_node = await self.apg.get_jid_and_node(url_account)
343 if ((url_jid != sender_jid 343 if ((url_jid != sender_jid
344 or url_node and url_node != self.apg._m.namespace)): 344 or url_node and url_node != self.apg._m.namespace)):
345 raise exceptions.DataError( 345 raise exceptions.DataError(
346 "announced ID doesn't match sender ({sender}): " 346 "announced ID doesn't match sender ({sender}): "
347 f"[repeated_item_id]" 347 f"[repeated_item_id]"
350 repeated_item_id = url_item_id 350 repeated_item_id = url_item_id
351 351
352 obj["_repeated"] = { 352 obj["_repeated"] = {
353 "by": repeater_jid.full(), 353 "by": repeater_jid.full(),
354 "at": data.get("published"), 354 "at": data.get("published"),
355 "uri": uri.buildXMPPUri( 355 "uri": uri.build_xmpp_uri(
356 "pubsub", 356 "pubsub",
357 path=sender_jid.full(), 357 path=sender_jid.full(),
358 node=self.apg._m.namespace, 358 node=self.apg._m.namespace,
359 item=repeated_item_id 359 item=repeated_item_id
360 ) 360 )
367 log.warning( 367 log.warning(
368 "Ignoring object not attributed to signing actor: {obj}" 368 "Ignoring object not attributed to signing actor: {obj}"
369 ) 369 )
370 continue 370 continue
371 371
372 await self.apg.newAPItem(client, account_jid, node, obj) 372 await self.apg.new_ap_item(client, account_jid, node, obj)
373 373
374 async def handleCreateActivity( 374 async def handle_create_activity(
375 self, 375 self,
376 request: "HTTPRequest", 376 request: "HTTPRequest",
377 data: dict, 377 data: dict,
378 account_jid: Optional[jid.JID], 378 account_jid: Optional[jid.JID],
379 node: Optional[str], 379 node: Optional[str],
380 ap_account: Optional[str], 380 ap_account: Optional[str],
381 ap_url: str, 381 ap_url: str,
382 signing_actor: str 382 signing_actor: str
383 ): 383 ):
384 await self.handleNewAPItems(request, data, account_jid, node, signing_actor) 384 await self.handle_new_ap_items(request, data, account_jid, node, signing_actor)
385 385
386 async def handleUpdateActivity( 386 async def handle_update_activity(
387 self, 387 self,
388 request: "HTTPRequest", 388 request: "HTTPRequest",
389 data: dict, 389 data: dict,
390 account_jid: Optional[jid.JID], 390 account_jid: Optional[jid.JID],
391 node: Optional[str], 391 node: Optional[str],
393 ap_url: str, 393 ap_url: str,
394 signing_actor: str 394 signing_actor: str
395 ): 395 ):
396 # Update is the same as create: the item ID stays the same, thus the item will be 396 # Update is the same as create: the item ID stays the same, thus the item will be
397 # overwritten 397 # overwritten
398 await self.handleNewAPItems(request, data, account_jid, node, signing_actor) 398 await self.handle_new_ap_items(request, data, account_jid, node, signing_actor)
399 399
400 async def handleAnnounceActivity( 400 async def handle_announce_activity(
401 self, 401 self,
402 request: "HTTPRequest", 402 request: "HTTPRequest",
403 data: dict, 403 data: dict,
404 account_jid: Optional[jid.JID], 404 account_jid: Optional[jid.JID],
405 node: Optional[str], 405 node: Optional[str],
406 ap_account: Optional[str], 406 ap_account: Optional[str],
407 ap_url: str, 407 ap_url: str,
408 signing_actor: str 408 signing_actor: str
409 ): 409 ):
410 # we create a new item 410 # we create a new item
411 await self.handleNewAPItems( 411 await self.handle_new_ap_items(
412 request, 412 request,
413 data, 413 data,
414 account_jid, 414 account_jid,
415 node, 415 node,
416 signing_actor, 416 signing_actor,
417 repeated=True 417 repeated=True
418 ) 418 )
419 419
420 async def handleAttachmentItem( 420 async def handle_attachment_item(
421 self, 421 self,
422 client: SatXMPPEntity, 422 client: SatXMPPEntity,
423 data: dict, 423 data: dict,
424 attachment_data: dict 424 attachment_data: dict
425 ) -> None: 425 ) -> None:
445 # a while. 445 # a while.
446 # TODO: add a way to flush old cached AP items. 446 # TODO: add a way to flush old cached AP items.
447 await client._ap_storage.aset(f"{ST_AP_CACHE}{data['id']}", data) 447 await client._ap_storage.aset(f"{ST_AP_CACHE}{data['id']}", data)
448 448
449 for target_id in target_ids: 449 for target_id in target_ids:
450 if not self.apg.isLocalURL(target_id): 450 if not self.apg.is_local_url(target_id):
451 log.debug(f"ignoring non local target ID: {target_id}") 451 log.debug(f"ignoring non local target ID: {target_id}")
452 continue 452 continue
453 url_type, url_args = self.apg.parseAPURL(target_id) 453 url_type, url_args = self.apg.parse_apurl(target_id)
454 if url_type != TYPE_ITEM: 454 if url_type != TYPE_ITEM:
455 log.warning(f"unexpected local URL for attachment on item {target_id}") 455 log.warning(f"unexpected local URL for attachment on item {target_id}")
456 continue 456 continue
457 try: 457 try:
458 account, item_id = url_args 458 account, item_id = url_args
459 except ValueError: 459 except ValueError:
460 raise ValueError(f"invalid URL: {target_id}") 460 raise ValueError(f"invalid URL: {target_id}")
461 author_jid, item_node = await self.apg.getJIDAndNode(account) 461 author_jid, item_node = await self.apg.get_jid_and_node(account)
462 if item_node is None: 462 if item_node is None:
463 item_node = self.apg._m.namespace 463 item_node = self.apg._m.namespace
464 attachment_node = self.apg._pa.getAttachmentNodeName( 464 attachment_node = self.apg._pa.get_attachment_node_name(
465 author_jid, item_node, item_id 465 author_jid, item_node, item_id
466 ) 466 )
467 cached_node = await self.apg.host.memory.storage.getPubsubNode( 467 cached_node = await self.apg.host.memory.storage.get_pubsub_node(
468 client, 468 client,
469 author_jid, 469 author_jid,
470 attachment_node, 470 attachment_node,
471 with_subscriptions=True, 471 with_subscriptions=True,
472 create=True 472 create=True
473 ) 473 )
474 found_items, __ = await self.apg.host.memory.storage.getItems( 474 found_items, __ = await self.apg.host.memory.storage.get_items(
475 cached_node, item_ids=[client.jid.userhost()] 475 cached_node, item_ids=[client.jid.userhost()]
476 ) 476 )
477 if not found_items: 477 if not found_items:
478 old_item_elt = None 478 old_item_elt = None
479 else: 479 else:
485 {"extra": attachment_data}, 485 {"extra": attachment_data},
486 old_item_elt, 486 old_item_elt,
487 None 487 None
488 ) 488 )
489 # we reparse the element, as there can be other attachments 489 # we reparse the element, as there can be other attachments
490 attachments_data = self.apg._pa.items2attachmentData(client, [item_elt]) 490 attachments_data = self.apg._pa.items_2_attachment_data(client, [item_elt])
491 # and we update the cache 491 # and we update the cache
492 await self.apg.host.memory.storage.cachePubsubItems( 492 await self.apg.host.memory.storage.cache_pubsub_items(
493 client, 493 client,
494 cached_node, 494 cached_node,
495 [item_elt], 495 [item_elt],
496 attachments_data or [{}] 496 attachments_data or [{}]
497 ) 497 )
498 498
499 if self.apg.isVirtualJID(author_jid): 499 if self.apg.is_virtual_jid(author_jid):
500 # the attachment is on t a virtual pubsub service (linking to an AP item), 500 # the attachment is on t a virtual pubsub service (linking to an AP item),
501 # we notify all subscribers 501 # we notify all subscribers
502 for subscription in cached_node.subscriptions: 502 for subscription in cached_node.subscriptions:
503 if subscription.state != SubscriptionState.SUBSCRIBED: 503 if subscription.state != SubscriptionState.SUBSCRIBED:
504 continue 504 continue
507 attachment_node, 507 attachment_node,
508 [(subscription.subscriber, None, [item_elt])] 508 [(subscription.subscriber, None, [item_elt])]
509 ) 509 )
510 else: 510 else:
511 # the attachment is on an XMPP item, we publish it to the attachment node 511 # the attachment is on an XMPP item, we publish it to the attachment node
512 await self.apg._p.sendItems( 512 await self.apg._p.send_items(
513 client, author_jid, attachment_node, [item_elt] 513 client, author_jid, attachment_node, [item_elt]
514 ) 514 )
515 515
516 async def handleLikeActivity( 516 async def handle_like_activity(
517 self, 517 self,
518 request: "HTTPRequest", 518 request: "HTTPRequest",
519 data: dict, 519 data: dict,
520 account_jid: Optional[jid.JID], 520 account_jid: Optional[jid.JID],
521 node: Optional[str], 521 node: Optional[str],
522 ap_account: Optional[str], 522 ap_account: Optional[str],
523 ap_url: str, 523 ap_url: str,
524 signing_actor: str 524 signing_actor: str
525 ) -> None: 525 ) -> None:
526 client = await self.apg.getVirtualClient(signing_actor) 526 client = await self.apg.get_virtual_client(signing_actor)
527 await self.handleAttachmentItem(client, data, {"noticed": True}) 527 await self.handle_attachment_item(client, data, {"noticed": True})
528 528
529 async def handleEmojireactActivity( 529 async def handle_emojireact_activity(
530 self, 530 self,
531 request: "HTTPRequest", 531 request: "HTTPRequest",
532 data: dict, 532 data: dict,
533 account_jid: Optional[jid.JID], 533 account_jid: Optional[jid.JID],
534 node: Optional[str], 534 node: Optional[str],
535 ap_account: Optional[str], 535 ap_account: Optional[str],
536 ap_url: str, 536 ap_url: str,
537 signing_actor: str 537 signing_actor: str
538 ) -> None: 538 ) -> None:
539 client = await self.apg.getVirtualClient(signing_actor) 539 client = await self.apg.get_virtual_client(signing_actor)
540 await self.handleAttachmentItem(client, data, { 540 await self.handle_attachment_item(client, data, {
541 "reactions": {"operation": "update", "add": [data["content"]]} 541 "reactions": {"operation": "update", "add": [data["content"]]}
542 }) 542 })
543 543
544 async def handleJoinActivity( 544 async def handle_join_activity(
545 self, 545 self,
546 request: "HTTPRequest", 546 request: "HTTPRequest",
547 data: dict, 547 data: dict,
548 account_jid: Optional[jid.JID], 548 account_jid: Optional[jid.JID],
549 node: Optional[str], 549 node: Optional[str],
550 ap_account: Optional[str], 550 ap_account: Optional[str],
551 ap_url: str, 551 ap_url: str,
552 signing_actor: str 552 signing_actor: str
553 ) -> None: 553 ) -> None:
554 client = await self.apg.getVirtualClient(signing_actor) 554 client = await self.apg.get_virtual_client(signing_actor)
555 await self.handleAttachmentItem(client, data, {"rsvp": {"attending": "yes"}}) 555 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "yes"}})
556 556
557 async def handleLeaveActivity( 557 async def handle_leave_activity(
558 self, 558 self,
559 request: "HTTPRequest", 559 request: "HTTPRequest",
560 data: dict, 560 data: dict,
561 account_jid: Optional[jid.JID], 561 account_jid: Optional[jid.JID],
562 node: Optional[str], 562 node: Optional[str],
563 ap_account: Optional[str], 563 ap_account: Optional[str],
564 ap_url: str, 564 ap_url: str,
565 signing_actor: str 565 signing_actor: str
566 ) -> None: 566 ) -> None:
567 client = await self.apg.getVirtualClient(signing_actor) 567 client = await self.apg.get_virtual_client(signing_actor)
568 await self.handleAttachmentItem(client, data, {"rsvp": {"attending": "no"}}) 568 await self.handle_attachment_item(client, data, {"rsvp": {"attending": "no"}})
569 569
570 async def APActorRequest( 570 async def ap_actor_request(
571 self, 571 self,
572 request: "HTTPRequest", 572 request: "HTTPRequest",
573 data: Optional[dict], 573 data: Optional[dict],
574 account_jid: jid.JID, 574 account_jid: jid.JID,
575 node: Optional[str], 575 node: Optional[str],
576 ap_account: str, 576 ap_account: str,
577 ap_url: str, 577 ap_url: str,
578 signing_actor: Optional[str] 578 signing_actor: Optional[str]
579 ) -> dict: 579 ) -> dict:
580 inbox = self.apg.buildAPURL(TYPE_INBOX, ap_account) 580 inbox = self.apg.build_apurl(TYPE_INBOX, ap_account)
581 shared_inbox = self.apg.buildAPURL(TYPE_SHARED_INBOX) 581 shared_inbox = self.apg.build_apurl(TYPE_SHARED_INBOX)
582 outbox = self.apg.buildAPURL(TYPE_OUTBOX, ap_account) 582 outbox = self.apg.build_apurl(TYPE_OUTBOX, ap_account)
583 followers = self.apg.buildAPURL(TYPE_FOLLOWERS, ap_account) 583 followers = self.apg.build_apurl(TYPE_FOLLOWERS, ap_account)
584 following = self.apg.buildAPURL(TYPE_FOLLOWING, ap_account) 584 following = self.apg.build_apurl(TYPE_FOLLOWING, ap_account)
585 585
586 # we have to use AP account as preferredUsername because it is used to retrieve 586 # we have to use AP account as preferredUsername because it is used to retrieve
587 # actor handle (see https://socialhub.activitypub.rocks/t/how-to-retrieve-user-server-tld-handle-from-actors-url/2196) 587 # actor handle (see https://socialhub.activitypub.rocks/t/how-to-retrieve-user-server-tld-handle-from-actors-url/2196)
588 preferred_username = ap_account.split("@", 1)[0] 588 preferred_username = ap_account.split("@", 1)[0]
589 589
590 identity_data = await self.apg._i.getIdentity(self.apg.client, account_jid) 590 identity_data = await self.apg._i.get_identity(self.apg.client, account_jid)
591 if node and node.startswith(self.apg._events.namespace): 591 if node and node.startswith(self.apg._events.namespace):
592 events = outbox 592 events = outbox
593 else: 593 else:
594 events_account = await self.apg.getAPAccountFromJidAndNode( 594 events_account = await self.apg.get_ap_account_from_jid_and_node(
595 account_jid, self.apg._events.namespace 595 account_jid, self.apg._events.namespace
596 ) 596 )
597 events = self.apg.buildAPURL(TYPE_OUTBOX, events_account) 597 events = self.apg.build_apurl(TYPE_OUTBOX, events_account)
598 598
599 actor_data = { 599 actor_data = {
600 "@context": [ 600 "@context": [
601 "https://www.w3.org/ns/activitystreams", 601 "https://www.w3.org/ns/activitystreams",
602 "https://w3id.org/security/v1" 602 "https://w3id.org/security/v1"
634 filename = avatar_data["filename"] 634 filename = avatar_data["filename"]
635 media_type = avatar_data["media_type"] 635 media_type = avatar_data["media_type"]
636 except KeyError: 636 except KeyError:
637 log.error(f"incomplete avatar data: {identity_data!r}") 637 log.error(f"incomplete avatar data: {identity_data!r}")
638 else: 638 else:
639 avatar_url = self.apg.buildAPURL("avatar", filename) 639 avatar_url = self.apg.build_apurl("avatar", filename)
640 actor_data["icon"] = { 640 actor_data["icon"] = {
641 "type": "Image", 641 "type": "Image",
642 "url": avatar_url, 642 "url": avatar_url,
643 "mediaType": media_type 643 "mediaType": media_type
644 } 644 }
645 645
646 return actor_data 646 return actor_data
647 647
648 def getCanonicalURL(self, request: "HTTPRequest") -> str: 648 def get_canonical_url(self, request: "HTTPRequest") -> str:
649 return parse.urljoin( 649 return parse.urljoin(
650 f"https://{self.apg.public_url}", 650 f"https://{self.apg.public_url}",
651 request.path.decode().rstrip("/") 651 request.path.decode().rstrip("/")
652 # we unescape "@" for the same reason as in [APActorRequest] 652 # we unescape "@" for the same reason as in [ap_actor_request]
653 ).replace("%40", "@") 653 ).replace("%40", "@")
654 654
655 def queryData2RSMRequest( 655 def query_data_2_rsm_request(
656 self, 656 self,
657 query_data: Dict[str, List[str]] 657 query_data: Dict[str, List[str]]
658 ) -> rsm.RSMRequest: 658 ) -> rsm.RSMRequest:
659 """Get RSM kwargs to use with RSMRequest from query data""" 659 """Get RSM kwargs to use with RSMRequest from query data"""
660 page = query_data.get("page") 660 page = query_data.get("page")
671 pass 671 pass
672 else: 672 else:
673 return rsm.RSMRequest(**kwargs) 673 return rsm.RSMRequest(**kwargs)
674 raise ValueError(f"Invalid query data: {query_data!r}") 674 raise ValueError(f"Invalid query data: {query_data!r}")
675 675
676 async def APOutboxPageRequest( 676 async def ap_outbox_page_request(
677 self, 677 self,
678 request: "HTTPRequest", 678 request: "HTTPRequest",
679 data: Optional[dict], 679 data: Optional[dict],
680 account_jid: jid.JID, 680 account_jid: jid.JID,
681 node: Optional[str], 681 node: Optional[str],
688 # we only keep useful keys, and sort to have consistent URL which can 688 # we only keep useful keys, and sort to have consistent URL which can
689 # be used as ID 689 # be used as ID
690 url_keys = sorted(set(query_data) & {"page", "index", "before", "after"}) 690 url_keys = sorted(set(query_data) & {"page", "index", "before", "after"})
691 query_data = {k: query_data[k] for k in url_keys} 691 query_data = {k: query_data[k] for k in url_keys}
692 try: 692 try:
693 items, metadata = await self.apg._p.getItems( 693 items, metadata = await self.apg._p.get_items(
694 client=self.apg.client, 694 client=self.apg.client,
695 service=account_jid, 695 service=account_jid,
696 node=node, 696 node=node,
697 rsm_request=self.queryData2RSMRequest(query_data), 697 rsm_request=self.query_data_2_rsm_request(query_data),
698 extra = {C.KEY_USE_CACHE: False} 698 extra = {C.KEY_USE_CACHE: False}
699 ) 699 )
700 except error.StanzaError as e: 700 except error.StanzaError as e:
701 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}") 701 log.warning(f"Can't get data from pubsub node {node} at {account_jid}: {e}")
702 return {} 702 return {}
703 703
704 base_url = self.getCanonicalURL(request) 704 base_url = self.get_canonical_url(request)
705 url = f"{base_url}?{parse.urlencode(query_data, True)}" 705 url = f"{base_url}?{parse.urlencode(query_data, True)}"
706 if node and node.startswith(self.apg._events.namespace): 706 if node and node.startswith(self.apg._events.namespace):
707 ordered_items = [ 707 ordered_items = [
708 await self.apg.ap_events.event_data_2_ap_item( 708 await self.apg.ap_events.event_data_2_ap_item(
709 self.apg._events.event_elt_2_event_data(item), 709 self.apg._events.event_elt_2_event_data(item),
751 first = None 751 first = None
752 ret_data["next"] = f"{base_url}?{parse.urlencode({'before': first})}" 752 ret_data["next"] = f"{base_url}?{parse.urlencode({'before': first})}"
753 753
754 return ret_data 754 return ret_data
755 755
756 async def APOutboxRequest( 756 async def ap_outbox_request(
757 self, 757 self,
758 request: "HTTPRequest", 758 request: "HTTPRequest",
759 data: Optional[dict], 759 data: Optional[dict],
760 account_jid: jid.JID, 760 account_jid: jid.JID,
761 node: Optional[str], 761 node: Optional[str],
767 node = self.apg._m.namespace 767 node = self.apg._m.namespace
768 768
769 parsed_url = parse.urlparse(request.uri.decode()) 769 parsed_url = parse.urlparse(request.uri.decode())
770 query_data = parse.parse_qs(parsed_url.query) 770 query_data = parse.parse_qs(parsed_url.query)
771 if query_data: 771 if query_data:
772 return await self.APOutboxPageRequest( 772 return await self.ap_outbox_page_request(
773 request, data, account_jid, node, ap_account, ap_url, query_data 773 request, data, account_jid, node, ap_account, ap_url, query_data
774 ) 774 )
775 775
776 # XXX: we can't use disco#info here because this request won't work on a bare jid 776 # XXX: we can't use disco#info here because this request won't work on a bare jid
777 # due to security considerations of XEP-0030 (we don't have presence 777 # due to security considerations of XEP-0030 (we don't have presence
778 # subscription). 778 # subscription).
779 # The current workaround is to do a request as if RSM was available, and actually 779 # The current workaround is to do a request as if RSM was available, and actually
780 # check its availability according to result. 780 # check its availability according to result.
781 try: 781 try:
782 __, metadata = await self.apg._p.getItems( 782 __, metadata = await self.apg._p.get_items(
783 client=self.apg.client, 783 client=self.apg.client,
784 service=account_jid, 784 service=account_jid,
785 node=node, 785 node=node,
786 max_items=0, 786 max_items=0,
787 rsm_request=rsm.RSMRequest(max_=0), 787 rsm_request=rsm.RSMRequest(max_=0),
797 f"No RSM metadata found when requesting pubsub node {node} at " 797 f"No RSM metadata found when requesting pubsub node {node} at "
798 f"{account_jid}, defaulting to items_count=20" 798 f"{account_jid}, defaulting to items_count=20"
799 ) 799 )
800 items_count = 20 800 items_count = 20
801 801
802 url = self.getCanonicalURL(request) 802 url = self.get_canonical_url(request)
803 url_first_page = f"{url}?{parse.urlencode({'page': 'first'})}" 803 url_first_page = f"{url}?{parse.urlencode({'page': 'first'})}"
804 url_last_page = f"{url}?{parse.urlencode({'page': 'last'})}" 804 url_last_page = f"{url}?{parse.urlencode({'page': 'last'})}"
805 return { 805 return {
806 "@context": ["https://www.w3.org/ns/activitystreams"], 806 "@context": ["https://www.w3.org/ns/activitystreams"],
807 "id": url, 807 "id": url,
809 "type": "OrderedCollection", 809 "type": "OrderedCollection",
810 "first": url_first_page, 810 "first": url_first_page,
811 "last": url_last_page, 811 "last": url_last_page,
812 } 812 }
813 813
814 async def APInboxRequest( 814 async def ap_inbox_request(
815 self, 815 self,
816 request: "HTTPRequest", 816 request: "HTTPRequest",
817 data: Optional[dict], 817 data: Optional[dict],
818 account_jid: Optional[jid.JID], 818 account_jid: Optional[jid.JID],
819 node: Optional[str], 819 node: Optional[str],
822 signing_actor: Optional[str] 822 signing_actor: Optional[str]
823 ) -> None: 823 ) -> None:
824 assert data is not None 824 assert data is not None
825 if signing_actor is None: 825 if signing_actor is None:
826 raise exceptions.InternalError("signing_actor must be set for inbox requests") 826 raise exceptions.InternalError("signing_actor must be set for inbox requests")
827 await self.checkSigningActor(data, signing_actor) 827 await self.check_signing_actor(data, signing_actor)
828 activity_type = (data.get("type") or "").lower() 828 activity_type = (data.get("type") or "").lower()
829 if not activity_type in ACTIVITY_TYPES_LOWER: 829 if not activity_type in ACTIVITY_TYPES_LOWER:
830 return self.responseCode( 830 return self.response_code(
831 request, 831 request,
832 http.UNSUPPORTED_MEDIA_TYPE, 832 http.UNSUPPORTED_MEDIA_TYPE,
833 f"request is not an activity, ignoring" 833 f"request is not an activity, ignoring"
834 ) 834 )
835 835
836 if account_jid is None and activity_type not in ACTIVIY_NO_ACCOUNT_ALLOWED: 836 if account_jid is None and activity_type not in ACTIVIY_NO_ACCOUNT_ALLOWED:
837 return self.responseCode( 837 return self.response_code(
838 request, 838 request,
839 http.UNSUPPORTED_MEDIA_TYPE, 839 http.UNSUPPORTED_MEDIA_TYPE,
840 f"{activity_type.title()!r} activity must target an account" 840 f"{activity_type.title()!r} activity must target an account"
841 ) 841 )
842 842
843 try: 843 try:
844 method = getattr(self, f"handle{activity_type.title()}Activity") 844 method = getattr(self, f"handle_{activity_type}_activity")
845 except AttributeError: 845 except AttributeError:
846 return self.responseCode( 846 return self.response_code(
847 request, 847 request,
848 http.UNSUPPORTED_MEDIA_TYPE, 848 http.UNSUPPORTED_MEDIA_TYPE,
849 f"{activity_type.title()} activity is not yet supported" 849 f"{activity_type.title()} activity is not yet supported"
850 ) 850 )
851 else: 851 else:
852 await method( 852 await method(
853 request, data, account_jid, node, ap_account, ap_url, signing_actor 853 request, data, account_jid, node, ap_account, ap_url, signing_actor
854 ) 854 )
855 855
856 async def APFollowersRequest( 856 async def ap_followers_request(
857 self, 857 self,
858 request: "HTTPRequest", 858 request: "HTTPRequest",
859 data: Optional[dict], 859 data: Optional[dict],
860 account_jid: jid.JID, 860 account_jid: jid.JID,
861 node: Optional[str], 861 node: Optional[str],
864 signing_actor: Optional[str] 864 signing_actor: Optional[str]
865 ) -> dict: 865 ) -> dict:
866 if node is None: 866 if node is None:
867 node = self.apg._m.namespace 867 node = self.apg._m.namespace
868 client = self.apg.client 868 client = self.apg.client
869 subscribers = await self.apg._pps.getPublicNodeSubscriptions( 869 subscribers = await self.apg._pps.get_public_node_subscriptions(
870 client, account_jid, node 870 client, account_jid, node
871 ) 871 )
872 followers = [] 872 followers = []
873 for subscriber in subscribers.keys(): 873 for subscriber in subscribers.keys():
874 if self.apg.isVirtualJID(subscriber): 874 if self.apg.is_virtual_jid(subscriber):
875 # the subscriber is an AP user subscribed with this gateway 875 # the subscriber is an AP user subscribed with this gateway
876 ap_account = self.apg._e.unescape(subscriber.user) 876 ap_account = self.apg._e.unescape(subscriber.user)
877 else: 877 else:
878 # regular XMPP user 878 # regular XMPP user
879 ap_account = await self.apg.getAPAccountFromJidAndNode(subscriber, node) 879 ap_account = await self.apg.get_ap_account_from_jid_and_node(subscriber, node)
880 followers.append(ap_account) 880 followers.append(ap_account)
881 881
882 url = self.getCanonicalURL(request) 882 url = self.get_canonical_url(request)
883 return { 883 return {
884 "@context": ["https://www.w3.org/ns/activitystreams"], 884 "@context": ["https://www.w3.org/ns/activitystreams"],
885 "type": "OrderedCollection", 885 "type": "OrderedCollection",
886 "id": url, 886 "id": url,
887 "totalItems": len(subscribers), 887 "totalItems": len(subscribers),
890 "id": url, 890 "id": url,
891 "orderedItems": followers 891 "orderedItems": followers
892 } 892 }
893 } 893 }
894 894
895 async def APFollowingRequest( 895 async def ap_following_request(
896 self, 896 self,
897 request: "HTTPRequest", 897 request: "HTTPRequest",
898 data: Optional[dict], 898 data: Optional[dict],
899 account_jid: jid.JID, 899 account_jid: jid.JID,
900 node: Optional[str], 900 node: Optional[str],
907 client, account_jid, node 907 client, account_jid, node
908 ) 908 )
909 following = [] 909 following = []
910 for sub_dict in subscriptions: 910 for sub_dict in subscriptions:
911 service = jid.JID(sub_dict["service"]) 911 service = jid.JID(sub_dict["service"])
912 if self.apg.isVirtualJID(service): 912 if self.apg.is_virtual_jid(service):
913 # the subscription is to an AP actor with this gateway 913 # the subscription is to an AP actor with this gateway
914 ap_account = self.apg._e.unescape(service.user) 914 ap_account = self.apg._e.unescape(service.user)
915 else: 915 else:
916 # regular XMPP user 916 # regular XMPP user
917 ap_account = await self.apg.getAPAccountFromJidAndNode( 917 ap_account = await self.apg.get_ap_account_from_jid_and_node(
918 service, sub_dict["node"] 918 service, sub_dict["node"]
919 ) 919 )
920 following.append(ap_account) 920 following.append(ap_account)
921 921
922 url = self.getCanonicalURL(request) 922 url = self.get_canonical_url(request)
923 return { 923 return {
924 "@context": ["https://www.w3.org/ns/activitystreams"], 924 "@context": ["https://www.w3.org/ns/activitystreams"],
925 "type": "OrderedCollection", 925 "type": "OrderedCollection",
926 "id": url, 926 "id": url,
927 "totalItems": len(subscriptions), 927 "totalItems": len(subscriptions),
951 for k,v in request.getAllHeaders().items() 951 for k,v in request.getAllHeaders().items()
952 ) 952 )
953 to_log.append(f" headers:\n{headers}") 953 to_log.append(f" headers:\n{headers}")
954 return to_log 954 return to_log
955 955
956 async def APRequest( 956 async def ap_request(
957 self, 957 self,
958 request: "HTTPRequest", 958 request: "HTTPRequest",
959 data: Optional[dict] = None, 959 data: Optional[dict] = None,
960 signing_actor: Optional[str] = None 960 signing_actor: Optional[str] = None
961 ) -> None: 961 ) -> None:
965 path = request.path.decode() 965 path = request.path.decode()
966 ap_url = parse.urljoin( 966 ap_url = parse.urljoin(
967 f"https://{self.apg.public_url}", 967 f"https://{self.apg.public_url}",
968 path 968 path
969 ) 969 )
970 request_type, extra_args = self.apg.parseAPURL(ap_url) 970 request_type, extra_args = self.apg.parse_apurl(ap_url)
971 if ((MEDIA_TYPE_AP not in (request.getHeader("accept") or "") 971 if ((MEDIA_TYPE_AP not in (request.getHeader("accept") or "")
972 and request_type in self.apg.html_redirect)): 972 and request_type in self.apg.html_redirect)):
973 # this is not a AP request, and we have a redirections for it 973 # this is not a AP request, and we have a redirections for it
974 kw = {} 974 kw = {}
975 if extra_args: 975 if extra_args:
976 kw["jid"], kw["node"] = await self.apg.getJIDAndNode(extra_args[0]) 976 kw["jid"], kw["node"] = await self.apg.get_jid_and_node(extra_args[0])
977 kw["jid_user"] = kw["jid"].user 977 kw["jid_user"] = kw["jid"].user
978 if kw["node"] is None: 978 if kw["node"] is None:
979 kw["node"] = self.apg._m.namespace 979 kw["node"] = self.apg._m.namespace
980 if len(extra_args) > 1: 980 if len(extra_args) > 1:
981 kw["item"] = extra_args[1] 981 kw["item"] = extra_args[1]
1005 return 1005 return
1006 1006
1007 if len(extra_args) == 0: 1007 if len(extra_args) == 0:
1008 if request_type != "shared_inbox": 1008 if request_type != "shared_inbox":
1009 raise exceptions.DataError(f"Invalid request type: {request_type!r}") 1009 raise exceptions.DataError(f"Invalid request type: {request_type!r}")
1010 ret_data = await self.APInboxRequest( 1010 ret_data = await self.ap_inbox_request(
1011 request, data, None, None, None, ap_url, signing_actor 1011 request, data, None, None, None, ap_url, signing_actor
1012 ) 1012 )
1013 elif request_type == "avatar": 1013 elif request_type == "avatar":
1014 if len(extra_args) != 1: 1014 if len(extra_args) != 1:
1015 raise exceptions.DataError("avatar argument expected in URL") 1015 raise exceptions.DataError("avatar argument expected in URL")
1016 avatar_filename = extra_args[0] 1016 avatar_filename = extra_args[0]
1017 avatar_path = self.apg.host.common_cache.getPath(avatar_filename) 1017 avatar_path = self.apg.host.common_cache.getPath(avatar_filename)
1018 return static.File(str(avatar_path)).render(request) 1018 return static.File(str(avatar_path)).render(request)
1019 elif request_type == "item": 1019 elif request_type == "item":
1020 ret_data = await self.apg.apGetLocalObject(ap_url) 1020 ret_data = await self.apg.ap_get_local_object(ap_url)
1021 if "@context" not in ret_data: 1021 if "@context" not in ret_data:
1022 ret_data["@context"] = [NS_AP] 1022 ret_data["@context"] = [NS_AP]
1023 else: 1023 else:
1024 if len(extra_args) > 1: 1024 if len(extra_args) > 1:
1025 log.warning(f"unexpected extra arguments: {extra_args!r}") 1025 log.warning(f"unexpected extra arguments: {extra_args!r}")
1026 ap_account = extra_args[0] 1026 ap_account = extra_args[0]
1027 account_jid, node = await self.apg.getJIDAndNode(ap_account) 1027 account_jid, node = await self.apg.get_jid_and_node(ap_account)
1028 if request_type not in AP_REQUEST_TYPES.get( 1028 if request_type not in AP_REQUEST_TYPES.get(
1029 request.method.decode().upper(), [] 1029 request.method.decode().upper(), []
1030 ): 1030 ):
1031 raise exceptions.DataError(f"Invalid request type: {request_type!r}") 1031 raise exceptions.DataError(f"Invalid request type: {request_type!r}")
1032 method = getattr(self, f"AP{request_type.title()}Request") 1032 method = getattr(self, f"AP{request_type.title()}Request")
1044 to_log.append(f"{pformat(ret_data)}") 1044 to_log.append(f"{pformat(ret_data)}")
1045 to_log.append("---") 1045 to_log.append("---")
1046 log.info("\n".join(to_log)) 1046 log.info("\n".join(to_log))
1047 request.finish() 1047 request.finish()
1048 1048
1049 async def APPostRequest(self, request: "HTTPRequest") -> None: 1049 async def ap_post_request(self, request: "HTTPRequest") -> None:
1050 try: 1050 try:
1051 data = json.load(request.content) 1051 data = json.load(request.content)
1052 if not isinstance(data, dict): 1052 if not isinstance(data, dict):
1053 log.warning(f"JSON data should be an object (uri={request.uri.decode()})") 1053 log.warning(f"JSON data should be an object (uri={request.uri.decode()})")
1054 self.responseCode( 1054 self.response_code(
1055 request, 1055 request,
1056 http.BAD_REQUEST, 1056 http.BAD_REQUEST,
1057 f"invalid body, was expecting a JSON object" 1057 f"invalid body, was expecting a JSON object"
1058 ) 1058 )
1059 request.finish() 1059 request.finish()
1060 return 1060 return
1061 except (json.JSONDecodeError, ValueError) as e: 1061 except (json.JSONDecodeError, ValueError) as e:
1062 self.responseCode( 1062 self.response_code(
1063 request, 1063 request,
1064 http.BAD_REQUEST, 1064 http.BAD_REQUEST,
1065 f"invalid json in inbox request: {e}" 1065 f"invalid json in inbox request: {e}"
1066 ) 1066 )
1067 request.finish() 1067 request.finish()
1079 return 1079 return
1080 except KeyError: 1080 except KeyError:
1081 pass 1081 pass
1082 1082
1083 try: 1083 try:
1084 signing_actor = await self.checkSignature(request) 1084 signing_actor = await self.check_signature(request)
1085 except exceptions.EncryptionError as e: 1085 except exceptions.EncryptionError as e:
1086 if self.apg.verbose: 1086 if self.apg.verbose:
1087 to_log = self._get_to_log(request) 1087 to_log = self._get_to_log(request)
1088 to_log.append(f" body: {request.content.read()!r}") 1088 to_log.append(f" body: {request.content.read()!r}")
1089 request.content.seek(0) 1089 request.content.seek(0)
1090 log.info("\n".join(to_log)) 1090 log.info("\n".join(to_log))
1091 self.responseCode( 1091 self.response_code(
1092 request, 1092 request,
1093 http.FORBIDDEN, 1093 http.FORBIDDEN,
1094 f"invalid signature: {e}" 1094 f"invalid signature: {e}"
1095 ) 1095 )
1096 request.finish() 1096 request.finish()
1097 return 1097 return
1098 except Exception as e: 1098 except Exception as e:
1099 self.responseCode( 1099 self.response_code(
1100 request, 1100 request,
1101 http.INTERNAL_SERVER_ERROR, 1101 http.INTERNAL_SERVER_ERROR,
1102 f"Can't check signature: {e}" 1102 f"Can't check signature: {e}"
1103 ) 1103 )
1104 request.finish() 1104 request.finish()
1113 return 1113 return
1114 self._seen_digest.append(digest) 1114 self._seen_digest.append(digest)
1115 1115
1116 # default response code, may be changed, e.g. in case of exception 1116 # default response code, may be changed, e.g. in case of exception
1117 try: 1117 try:
1118 return await self.APRequest(request, data, signing_actor) 1118 return await self.ap_request(request, data, signing_actor)
1119 except Exception as e: 1119 except Exception as e:
1120 self._onRequestError(failure.Failure(e), request) 1120 self._on_request_error(failure.Failure(e), request)
1121 1121
1122 async def checkSigningActor(self, data: dict, signing_actor: str) -> None: 1122 async def check_signing_actor(self, data: dict, signing_actor: str) -> None:
1123 """That that signing actor correspond to actor declared in data 1123 """That that signing actor correspond to actor declared in data
1124 1124
1125 @param data: request payload 1125 @param data: request payload
1126 @param signing_actor: actor ID of the signing entity, as returned by 1126 @param signing_actor: actor ID of the signing entity, as returned by
1127 checkSignature 1127 check_signature
1128 @raise exceptions.NotFound: no actor found in data 1128 @raise exceptions.NotFound: no actor found in data
1129 @raise exceptions.EncryptionError: signing actor doesn't match actor in data 1129 @raise exceptions.EncryptionError: signing actor doesn't match actor in data
1130 """ 1130 """
1131 actor = await self.apg.apGetSenderActor(data) 1131 actor = await self.apg.ap_get_sender_actor(data)
1132 1132
1133 if signing_actor != actor: 1133 if signing_actor != actor:
1134 raise exceptions.EncryptionError( 1134 raise exceptions.EncryptionError(
1135 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})" 1135 f"signing actor ({signing_actor}) doesn't match actor in data ({actor})"
1136 ) 1136 )
1137 1137
1138 async def checkSignature(self, request: "HTTPRequest") -> str: 1138 async def check_signature(self, request: "HTTPRequest") -> str:
1139 """Check and validate HTTP signature 1139 """Check and validate HTTP signature
1140 1140
1141 @return: id of the signing actor 1141 @return: id of the signing actor
1142 1142
1143 @raise exceptions.EncryptionError: signature is not present or doesn't match 1143 @raise exceptions.EncryptionError: signature is not present or doesn't match
1240 given_digest = hashes["sha-256"] 1240 given_digest = hashes["sha-256"]
1241 except KeyError: 1241 except KeyError:
1242 raise exceptions.EncryptionError( 1242 raise exceptions.EncryptionError(
1243 "Only SHA-256 algorithm is currently supported for digest" 1243 "Only SHA-256 algorithm is currently supported for digest"
1244 ) 1244 )
1245 __, computed_digest = self.apg.getDigest(body) 1245 __, computed_digest = self.apg.get_digest(body)
1246 if given_digest != computed_digest: 1246 if given_digest != computed_digest:
1247 raise exceptions.EncryptionError( 1247 raise exceptions.EncryptionError(
1248 f"SHA-256 given and computed digest differ:\n" 1248 f"SHA-256 given and computed digest differ:\n"
1249 f"given: {given_digest!r}\ncomputed: {computed_digest!r}" 1249 f"given: {given_digest!r}\ncomputed: {computed_digest!r}"
1250 ) 1250 )
1273 1273
1274 if created > limit_ts: 1274 if created > limit_ts:
1275 raise exceptions.EncryptionError("Signature has expired") 1275 raise exceptions.EncryptionError("Signature has expired")
1276 1276
1277 try: 1277 try:
1278 return await self.apg.checkSignature( 1278 return await self.apg.check_signature(
1279 sign_data["signature"], 1279 sign_data["signature"],
1280 key_id, 1280 key_id,
1281 headers 1281 headers
1282 ) 1282 )
1283 except exceptions.EncryptionError: 1283 except exceptions.EncryptionError:
1285 headers["(request-target)"] = f"{method} {parse.unquote(url)}" 1285 headers["(request-target)"] = f"{method} {parse.unquote(url)}"
1286 log.debug( 1286 log.debug(
1287 "Using workaround for (request-target) encoding bug in signature, " 1287 "Using workaround for (request-target) encoding bug in signature, "
1288 "see https://github.com/mastodon/mastodon/issues/18871" 1288 "see https://github.com/mastodon/mastodon/issues/18871"
1289 ) 1289 )
1290 return await self.apg.checkSignature( 1290 return await self.apg.check_signature(
1291 sign_data["signature"], 1291 sign_data["signature"],
1292 key_id, 1292 key_id,
1293 headers 1293 headers
1294 ) 1294 )
1295 1295
1301 path = request.path.decode().lstrip("/") 1301 path = request.path.decode().lstrip("/")
1302 if path.startswith(".well-known/webfinger"): 1302 if path.startswith(".well-known/webfinger"):
1303 defer.ensureDeferred(self.webfinger(request)) 1303 defer.ensureDeferred(self.webfinger(request))
1304 return server.NOT_DONE_YET 1304 return server.NOT_DONE_YET
1305 elif path.startswith(self.apg.ap_path): 1305 elif path.startswith(self.apg.ap_path):
1306 d = defer.ensureDeferred(self.APRequest(request)) 1306 d = defer.ensureDeferred(self.ap_request(request))
1307 d.addErrback(self._onRequestError, request) 1307 d.addErrback(self._on_request_error, request)
1308 return server.NOT_DONE_YET 1308 return server.NOT_DONE_YET
1309 1309
1310 return web_resource.NoResource().render(request) 1310 return web_resource.NoResource().render(request)
1311 1311
1312 def render_POST(self, request): 1312 def render_POST(self, request):
1313 path = request.path.decode().lstrip("/") 1313 path = request.path.decode().lstrip("/")
1314 if not path.startswith(self.apg.ap_path): 1314 if not path.startswith(self.apg.ap_path):
1315 return web_resource.NoResource().render(request) 1315 return web_resource.NoResource().render(request)
1316 defer.ensureDeferred(self.APPostRequest(request)) 1316 defer.ensureDeferred(self.ap_post_request(request))
1317 return server.NOT_DONE_YET 1317 return server.NOT_DONE_YET
1318 1318
1319 1319
1320 class HTTPRequest(server.Request): 1320 class HTTPRequest(server.Request):
1321 pass 1321 pass