Mercurial > libervia-backend
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 |