diff --git a/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py b/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py index 66a38f1c..e04da06b 100644 --- a/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py +++ b/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py @@ -61,12 +61,20 @@ from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger from libervia.backend.memory import persistent from libervia.backend.memory.sqla_mapping import History, SubscriptionState -from libervia.backend.plugins.plugin_xep_0277 import XEP_0277 +from libervia.backend.plugins.plugin_xep_0277 import ( + XEP_0277, + MbData, + Comment, + Attachment, + Repeated, + MbExtra, +) from libervia.backend.tools import utils from libervia.backend.tools.common import data_format, tls, uri from libervia.backend.tools.common.async_utils import async_lru from .ad_hoc import APAdHocService + # We import PLUGIN_INFO as it's needed for the plugin, and we put it in .constants because # it's necessary to create the ComponentSettings subclass. from .constants import PLUGIN_INFO @@ -524,7 +532,7 @@ class APGateway: return ap_item["object"] else: # this is a blog item - mb_data = await self._m.item_2_mb_data( + mb_data = await MbData.from_element( self.client, found_item, author_jid, node ) ap_item = await self.mb_data_2_ap_item(self.client, mb_data) @@ -1132,8 +1140,8 @@ class APGateway: ) else: # blog item - mb_data = await self._m.item_2_mb_data(client, item, service, node) - author_jid = jid.JID(mb_data["author_jid"]) + mb_data = await MbData.from_element(client, item, service, node) + author_jid = mb_data.author_jid if subscribe_extra_nodes and not self.is_virtual_jid(author_jid): # we subscribe automatically to comment nodes if any recipient_jid = self.get_local_jid_from_account(ap_account) @@ -1151,11 +1159,11 @@ class APGateway: ) try: await self._pa.subscribe( - recipient_client, service, node, mb_data["id"] + recipient_client, service, node, mb_data.id ) except exceptions.NotFound: log.debug( - f"no attachment node found for item {mb_data['id']!r} on " + f"no attachment node found for item {mb_data.id!r} on " f"{node!r} at {service}" ) ap_item = await self.mb_data_2_ap_item(client, mb_data, is_new=is_new) @@ -1645,21 +1653,20 @@ class APGateway: async def ap_item_2_mb_data_and_elt( self, requestor_actor_id: str, ap_item: dict - ) -> tuple[dict, domish.Element]: + ) -> tuple[MbData, domish.Element]: """Convert AP item to parsed microblog data and corresponding item element @param requestor_actor_id: ID of the actor requesting the conversion. @param ap_item: AP item to convert. @return: microblog and correspondign element. """ + assert self.client is not None mb_data = await self.ap_item_2_mb_data(requestor_actor_id, ap_item) - item_elt = await self._m.mb_data_2_entry_elt( - self.client, mb_data, mb_data["id"], None, self._m.namespace - ) - if "repeated" in mb_data["extra"]: - item_elt["publisher"] = mb_data["extra"]["repeated"]["by"] + item_elt = await self._m.mb_data_2_entry_elt(self.client, mb_data) + if mb_data.extra.repeated: + item_elt["publisher"] = mb_data.extra.repeated.by.full() else: - item_elt["publisher"] = mb_data["author_jid"] + item_elt["publisher"] = mb_data.author_jid.full() return mb_data, item_elt async def ap_item_2_mb_elt( @@ -1756,7 +1763,7 @@ class APGateway: last_level_item = parents[parent_limit] return (self._m.get_comments_node(last_level_item["id"]), None) - async def ap_item_2_mb_data(self, requestor_actor_id: str, ap_item: dict) -> dict: + async def ap_item_2_mb_data(self, requestor_actor_id: str, ap_item: dict) -> MbData: """Convert AP activity or object to microblog data @param actor_id: ID of the actor doing the request. @@ -1779,30 +1786,32 @@ class APGateway: if not item_id: log.warning(f'No "id" found in AP item: {ap_object!r}') raise exceptions.DataError - mb_data = {"id": item_id, "extra": {}} + + # Create MbData instance with required fields + mb_data = MbData(id=item_id, service=None, extra=MbExtra()) # content try: language, content_xhtml = ap_object["contentMap"].popitem() except (KeyError, AttributeError): try: - mb_data["content_xhtml"] = ap_object["content"] + mb_data.content_xhtml = ap_object["content"] except KeyError: log.warning(f"no content found:\n{ap_object!r}") raise exceptions.DataError else: - mb_data["language"] = language - mb_data["content_xhtml"] = content_xhtml + mb_data.language = language + mb_data.content_xhtml = content_xhtml - mb_data["content"] = await self._t.convert( - mb_data["content_xhtml"], + mb_data.content = await self._t.convert( + mb_data.content_xhtml, self._t.SYNTAX_XHTML, self._t.SYNTAX_TEXT, False, ) + # attachments if "attachment" in ap_object: - attachments = mb_data["extra"][C.KEY_ATTACHMENTS] = [] for ap_attachment in ap_object["attachment"]: try: url = ap_attachment["url"] @@ -1815,17 +1824,20 @@ class APGateway: if not url.startswith("http"): log.warning(f"non HTTP URL in attachment, ignoring: {ap_attachment}") continue - attachment = {"url": url} - for ap_key, key in ( - ("mediaType", "media_type"), - # XXX: as weird as it seems, "name" is actually used for description - # in AP world - ("name", "desc"), - ): - value = ap_attachment.get(ap_key) - if value: - attachment[key] = value - attachments.append(attachment) + + attachment = Attachment( + sources=[{"url": url}], + ) + + if media_type := ap_attachment.get("mediaType"): + attachment.media_type = media_type + + # XXX: as weird as it seems, "name" is actually used for description + # in AP world + if desc := ap_attachment.get("name"): + attachment.desc = desc + + mb_data.extra.attachments.append(attachment) # author if is_activity: @@ -1840,10 +1852,10 @@ class APGateway: log.warning("multiple actors are not managed") account = authors[0] - author_jid = self.get_local_jid_from_account(account).full() + author_jid = self.get_local_jid_from_account(account) - mb_data["author"] = account.split("@", 1)[0] - mb_data["author_jid"] = author_jid + mb_data.author = account.split("@", 1)[0] + mb_data.author_jid = author_jid # published/updated for field in ("published", "updated"): @@ -1852,15 +1864,18 @@ class APGateway: value = ap_object.get("published") if value: try: - mb_data[field] = calendar.timegm( + timestamp = calendar.timegm( dateutil.parser.parse(str(value)).utctimetuple() ) + setattr(mb_data, field, timestamp) except dateutil.parser.ParserError as e: log.warning(f"Can't parse {field!r} field: {e}") # repeat if "_repeated" in ap_item: - mb_data["extra"]["repeated"] = ap_item["_repeated"] + mb_data.extra.repeated = Repeated( + by=jid.JID(ap_item["_repeated"]["by"]), uri=ap_item["_repeated"]["uri"] + ) # comments in_reply_to = ap_object.get("inReplyTo") @@ -1868,12 +1883,14 @@ class APGateway: requestor_actor_id, item_id, in_reply_to ) if comments_node is not None: - comments_data = { - "service": author_jid, - "node": comments_node, - "uri": uri.build_xmpp_uri("pubsub", path=author_jid, node=comments_node), - } - mb_data["comments"] = [comments_data] + comments_data = Comment( + service=author_jid, + node=comments_node, + uri=uri.build_xmpp_uri( + "pubsub", path=author_jid.full(), node=comments_node + ), + ) + mb_data.comments = [comments_data] return mb_data @@ -1910,7 +1927,7 @@ class APGateway: # we found several cached item with given ID, we check if there is one # corresponding to this author try: - author = jid.JID(mb_data["author_jid"]).userhostJID() + author = mb_data.author_jid.userhostJID() cached_item = next( i for i in found_items @@ -1930,17 +1947,19 @@ class APGateway: return self.build_apurl(TYPE_ITEM, parent_ap_account, parent_item) - async def repeated_mb_2_ap_item(self, mb_data: dict) -> dict: + async def repeated_mb_2_ap_item(self, mb_data: MbData) -> dict: """Convert repeated blog item to suitable AP Announce activity @param mb_data: microblog metadata of an item repeating an other blog post @return: Announce activity linking to the repeated item """ - repeated = mb_data["extra"]["repeated"] - repeater = jid.JID(repeated["by"]) + assert mb_data.id is not None + repeated = mb_data.extra.repeated + assert repeated is not None + repeater = repeated.by repeater_account = await self.get_ap_account_from_jid_and_node(repeater, None) repeater_id = self.build_apurl(TYPE_ACTOR, repeater_account) - repeated_uri = repeated["uri"] + repeated_uri = repeated.uri if not repeated_uri.startswith("xmpp:"): log.warning( @@ -1957,7 +1976,7 @@ class APGateway: raise NotImplementedError rep_service = jid.JID(parsed_url["path"]) rep_item = parsed_url["item"] - activity_id = self.build_apurl("item", repeater.userhost(), mb_data["id"]) + activity_id = self.build_apurl("item", repeater.userhost(), mb_data.id) if self.is_virtual_jid(rep_service): # it's an AP actor linked through this gateway @@ -1990,7 +2009,7 @@ class APGateway: async def mb_data_2_ap_item( self, client: SatXMPPEntity, - mb_data: dict, + mb_data: MbData, public: bool = True, is_new: bool = True, ) -> dict: @@ -2011,36 +2030,31 @@ class APGateway: be. @return: Activity item """ - extra = mb_data.get("extra", {}) - if "repeated" in extra: + if mb_data.extra.repeated: return await self.repeated_mb_2_ap_item(mb_data) - if not mb_data.get("id"): - mb_data["id"] = shortuuid.uuid() - if not mb_data.get("author_jid"): - mb_data["author_jid"] = client.jid.userhost() - ap_account = await self.get_ap_account_from_jid_and_node( - jid.JID(mb_data["author_jid"]), None - ) + if not mb_data.id: + mb_data.id = shortuuid.uuid() + if not mb_data.author_jid: + mb_data.author_jid = client.jid.userhostJID() + ap_account = await self.get_ap_account_from_jid_and_node(mb_data.author_jid, None) url_actor = self.build_apurl(TYPE_ACTOR, ap_account) - url_item = self.build_apurl(TYPE_ITEM, ap_account, mb_data["id"]) + url_item = self.build_apurl(TYPE_ITEM, ap_account, mb_data.id) ap_object = { "id": url_item, "type": "Note", - "published": utils.xmpp_date(mb_data.get("published")), + "published": utils.xmpp_date(mb_data.published), "attributedTo": url_actor, - "content": mb_data.get("content_xhtml") or mb_data["content"], + "content": mb_data.content_xhtml or mb_data.content, } - language = mb_data.get("language") - if language: - ap_object["contentMap"] = {language: ap_object["content"]} + if mb_data.language: + ap_object["contentMap"] = {mb_data.language: ap_object["content"]} - attachments = extra.get(C.KEY_ATTACHMENTS) - if attachments: + if mb_data.extra.attachments: ap_attachments = ap_object["attachment"] = [] - for attachment in attachments: + for attachment in mb_data.extra.attachments: try: - url = next(s["url"] for s in attachment["sources"] if "url" in s) + url = next(s["url"] for s in attachment.sources if "url" in s) except (StopIteration, KeyError): log.warning(f"Ignoring attachment without URL: {attachment}") continue @@ -2050,7 +2064,7 @@ class APGateway: # XXX: yes "name", cf. [ap_item_2_mb_data] ("desc", "name"), ): - value = attachment.get(key) + value = getattr(attachment, key, None) if value: ap_attachment[ap_key] = value ap_attachments.append(ap_attachment) @@ -2080,8 +2094,8 @@ class APGateway: } ) try: - node = mb_data["node"] - service = jid.JID(mb_data["service"]) + node = mb_data.node + service = mb_data.service except KeyError: # node and service must always be specified when this method is used raise exceptions.InternalError("node or service is missing in mb_data") @@ -2092,8 +2106,8 @@ class APGateway: except MissingLocalPartError: # we have no local part, this is probably a comment try: - service = jid.JID(mb_data["author_jid"]) - if not mb_data["author_jid_verified"]: + service = mb_data.author_jid + if not mb_data.author_jid_verified: raise KeyError except KeyError: raise exceptions.DataError( @@ -2195,10 +2209,10 @@ class APGateway: ) else: try: - mb_data = await self._m.item_2_mb_data( + mb_data = await MbData.from_element( self.client, items[0].data, jid_, node ) - if "repeated" in mb_data["extra"]: + if mb_data.extra.repeated: # we are deleting a repeated item, we must translate this to an # "Undo" of the "Announce" activity instead of a "Delete" one announce = await self.repeated_mb_2_ap_item(mb_data) @@ -2238,6 +2252,7 @@ class APGateway: this method handle the conversion to AP items and posting """ + assert self.client is not None if client != self.client: return mess_data if mess_data["type"] not in ("chat", "normal"): @@ -2268,18 +2283,19 @@ class APGateway: log.warning(f"ignoring empty message: {mess_data}") return mess_data - mb_data = { - "content": message, - } + mb_data = MbData( + content = message, + service = None + ) if language: - mb_data["language"] = language + mb_data.language = language origin_id = mess_data["extra"].get("origin_id") if origin_id: # we need to use origin ID when present to be able to retract the message - mb_data["id"] = origin_id + mb_data.id = origin_id attachments = mess_data["extra"].get(C.KEY_ATTACHMENTS) if attachments: - mb_data["extra"] = {C.KEY_ATTACHMENTS: attachments} + mb_data.extra = MbExtra(attachments=attachments) client = self.client.get_virtual_client(mess_data["from"]) ap_item = await self.mb_data_2_ap_item(client, mb_data, public=False) @@ -2402,7 +2418,7 @@ class APGateway: cached_item = cached_items[0] - mb_data = await self._m.item_2_mb_data( + mb_data = await MbData.from_element( client, cached_item.data, pubsub_service, pubsub_node ) ap_item = await self.mb_data_2_ap_item(client, mb_data) @@ -2465,13 +2481,15 @@ class APGateway: f"{parent_item_node!r})\n{pformat(ap_item)}" ) return - parent_item_parsed = await self._m.item_2_mb_data( + parent_item_parsed = await MbData.from_element( client, parent_item_elt, parent_item_service, parent_item_node ) try: - comment_service = jid.JID(parent_item_parsed["comments"][0]["service"]) - comment_node = parent_item_parsed["comments"][0]["node"] - except (KeyError, IndexError): + comment_service = parent_item_parsed.comments[0].service + comment_node = parent_item_parsed.comments[0].node + if comment_node is None or comment_service is None: + raise IndexError + except IndexError: # we don't have a comment node set for this item from libervia.backend.tools.xml_tools import pp_elt @@ -2612,8 +2630,8 @@ class APGateway: if destinee is not None: targets_jids.add(destinee) mb_data = await self.ap_item_2_mb_data(requestor_actor_id, item) - extra = {"origin_id": mb_data["id"]} - attachments = mb_data["extra"].get(C.KEY_ATTACHMENTS) + extra: dict[str, Any] = {"origin_id": mb_data.id} + attachments = mb_data.extra.attachments if attachments: extra[C.KEY_ATTACHMENTS] = attachments @@ -2622,8 +2640,8 @@ class APGateway: defer_l.append( client.sendMessage( target_jid, - {"": mb_data.get("content", "")}, - mb_data.get("title"), + {"": mb_data.content or ""}, + mb_data.title, extra=extra, ) ) diff --git a/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py b/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py index 349eb940..f5cf158e 100644 --- a/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py +++ b/libervia/backend/plugins/plugin_comp_ap_gateway/http_server.py @@ -16,60 +16,61 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import time -import html -from typing import TYPE_CHECKING, Optional, Dict, List, Any -import json -from urllib import parse from collections import deque +import html +import json +import time +from typing import Any, Dict, List, Optional, TYPE_CHECKING import unicodedata +from urllib import parse +from twisted.internet import defer +from twisted.python import failure from twisted.web import http, resource as web_resource, server from twisted.web import static from twisted.web import util as web_util -from twisted.python import failure -from twisted.internet import defer -from twisted.words.protocols.jabber import jid, error +from twisted.words.protocols.jabber import error, jid from wokkel import pubsub, rsm from libervia.backend.core import exceptions from libervia.backend.core.constants import Const as C -from libervia.backend.core.i18n import _ from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger -from libervia.backend.tools.common import date_utils, uri from libervia.backend.memory.sqla_mapping import SubscriptionState +from libervia.backend.plugins.plugin_xep_0277 import MbData +from libervia.backend.tools.common import date_utils, uri + +from .constants import ( + ACTIVITY_TYPES_LOWER, + ACTIVIY_NO_ACCOUNT_ALLOWED, + AP_REQUEST_TYPES, + CONTENT_TYPE_AP, + CONTENT_TYPE_WEBFINGER, + HS2019, + MEDIA_TYPE_AP, + MEDIA_TYPE_AP_ALT, + NS_AP, + PAGE_SIZE, + SIGN_EXP, + SIGN_HEADERS, + ST_AP_CACHE, + TYPE_ACTOR, + TYPE_EVENT, + TYPE_FOLLOWERS, + TYPE_FOLLOWING, + TYPE_INBOX, + TYPE_ITEM, + TYPE_LIKE, + TYPE_OUTBOX, + TYPE_REACTION, + TYPE_SHARED_INBOX, +) +from .regex import RE_SIG_PARAM if TYPE_CHECKING: from . import APGateway -from .constants import ( - NS_AP, - MEDIA_TYPE_AP, - MEDIA_TYPE_AP_ALT, - CONTENT_TYPE_WEBFINGER, - CONTENT_TYPE_AP, - TYPE_ACTOR, - TYPE_INBOX, - TYPE_SHARED_INBOX, - TYPE_OUTBOX, - TYPE_EVENT, - AP_REQUEST_TYPES, - PAGE_SIZE, - ACTIVITY_TYPES_LOWER, - ACTIVIY_NO_ACCOUNT_ALLOWED, - SIGN_HEADERS, - HS2019, - SIGN_EXP, - TYPE_FOLLOWERS, - TYPE_FOLLOWING, - TYPE_ITEM, - TYPE_LIKE, - TYPE_REACTION, - ST_AP_CACHE, -) -from .regex import RE_SIG_PARAM - log = getLogger(__name__) @@ -730,9 +731,7 @@ class HTTPAPGServer(web_resource.Resource): ordered_items = [ await self.apg.mb_data_2_ap_item( self.apg.client, - await self.apg._m.item_2_mb_data( - self.apg.client, item, account_jid, node - ), + await MbData.from_element(self.apg.client, item, account_jid, node), ) for item in reversed(items) ] diff --git a/libervia/backend/plugins/plugin_xep_0277.py b/libervia/backend/plugins/plugin_xep_0277.py index a6f1ad8a..509e4c5c 100644 --- a/libervia/backend/plugins/plugin_xep_0277.py +++ b/libervia/backend/plugins/plugin_xep_0277.py @@ -940,24 +940,19 @@ class XEP_0277: async def mb_data_2_entry_elt( self, client: SatXMPPEntity, - mb_data: dict, + mb_data: MbData, ) -> pubsub.Item: - """Convert a data dict to en entry usable to create an item + """Convert microblog data to an entry usable to create an item. - @param mb_data: data dict as given by bridge method. - @param item_id: id of the item to use - @param service: pubsub service where the item is sent - Needed to construct Atom id - @param node: pubsub node where the item is sent - Needed to construct Atom id + @param mb_data: Microblog Data @return: deferred which fire domish.Element """ entry_elt = domish.Element((NS_ATOM, "entry")) - extra = mb_data.get("extra", {}) + extra = mb_data.extra ## language ## - if "language" in mb_data: - entry_elt[(C.NS_XML, "lang")] = mb_data["language"].strip() + if mb_data.language: + entry_elt[(C.NS_XML, "lang")] = mb_data.language.strip() ## content and title ## text_syntaxes = cast(TextSyntaxes, self.host.plugins["TEXT_SYNTAXES"]) @@ -965,16 +960,17 @@ class XEP_0277: for elem_name in ("title", "content"): for type_ in ["", "_rich", "_xhtml"]: attr = f"{elem_name}{type_}" - if attr in mb_data: + value = getattr(mb_data, attr) + if value is not None: elem = entry_elt.addElement(elem_name) if type_: if type_ == "_rich": # convert input from current syntax to XHTML xml_content = await text_syntaxes.convert( - mb_data[attr], + value, text_syntaxes.get_current_syntax(client.profile), "XHTML", ) - if f"{elem_name}_xhtml" in mb_data: + if getattr(mb_data, f"{elem_name}_xhtml") is not None: raise failure.Failure( exceptions.DataError( _( @@ -983,7 +979,7 @@ class XEP_0277: ) ) else: - xml_content = mb_data[attr] + xml_content = value div_elt = xml_tools.ElementParser()( xml_content, namespace=C.NS_XHTML @@ -999,7 +995,7 @@ class XEP_0277: div_elt = wrap_div_elt elem.addChild(div_elt) elem["type"] = "xhtml" - if elem_name not in mb_data: + if getattr(mb_data, elem_name) is None: # there is raw text content, which is mandatory # so we create one from xhtml content elem_txt = entry_elt.addElement(elem_name) @@ -1015,7 +1011,7 @@ class XEP_0277: elem_txt["type"] = "text" else: # raw text only needs to be escaped to get HTML-safe sequence - elem.addContent(mb_data[attr]) + elem.addContent(value) elem["type"] = "text" try: @@ -1032,138 +1028,120 @@ class XEP_0277: elem.name = "title" ## attachments ## - attachments = extra.get(C.KEY_ATTACHMENTS) - if attachments: - for attachment in attachments: - try: - url = attachment["url"] - except KeyError: - try: - url = next(s["url"] for s in attachment["sources"] if "url" in s) - except (StopIteration, KeyError): - log.warning( - f'"url" missing in attachment, ignoring: {attachment}' - ) - continue + for attachment in extra.attachments: + try: + url = attachment.sources[0]["url"] + except (IndexError, KeyError): + log.warning(f'"url" missing in attachment, ignoring: {attachment}') + continue - if not url.startswith("http"): - log.warning(f"non HTTP URL in attachment, ignoring: {attachment}") - continue - link_elt = entry_elt.addElement("link") - # XXX: "uri" is set in self._manage_comments if not already existing - link_elt["href"] = url - if attachment.get("external", False): - # this is a link to an external data such as a website - link_elt["rel"] = "related" - else: - # this is an attached file - link_elt["rel"] = "enclosure" - for key, attr in ( - ("media_type", "type"), - ("desc", "title"), - ("size", "lenght"), - ): - value = attachment.get(key) - if value: - link_elt[attr] = str(value) + if not url.startswith("http"): + log.warning(f"non HTTP URL in attachment, ignoring: {attachment}") + continue + link_elt = entry_elt.addElement("link") + # XXX: "uri" is set in self._manage_comments if not already existing + link_elt["href"] = url + if attachment.external: + # this is a link to an external data such as a website + link_elt["rel"] = "related" + else: + # this is an attached file + link_elt["rel"] = "enclosure" + for key, attr in ( + ("media_type", "type"), + ("desc", "title"), + ("size", "lenght"), + ): + value = getattr(attachment, key, None) + if value: + link_elt[attr] = str(value) ## alternate links ## - alt_links = extra.get("alt_links") - if alt_links: - for link_data in alt_links: - url_template = link_data["url"] - url = url_template.format( - service=quote(service.full(), safe=""), - node=quote(node, safe=""), - item=quote(item_id, safe=""), - ) + for alt_link in extra.alt_links: + url_template = alt_link.url + url = url_template.format( + service=quote(mb_data.service.full(), safe=""), + node=quote(mb_data.node, safe=""), + item=quote(mb_data.id, safe=""), + ) - link_elt = entry_elt.addElement("link") - link_elt["href"] = url - link_elt["rel"] = "alternate" + link_elt = entry_elt.addElement("link") + link_elt["href"] = url + link_elt["rel"] = "alternate" - media_type = link_data.get("media_type") - if not media_type: - parsed_url = urlparse(url) - if parsed_url.scheme in ["http", "https"]: - media_type = "text/html" - else: - media_type = guess_type(url)[0] or "application/octet-stream" + media_type = alt_link.media_type + if not media_type: + parsed_url = urlparse(url) + if parsed_url.scheme in ["http", "https"]: + media_type = "text/html" + else: + media_type = guess_type(url)[0] or "application/octet-stream" - link_elt["type"] = media_type + link_elt["type"] = media_type ## author ## author_elt = entry_elt.addElement("author") - try: - author_name = mb_data["author"] - except KeyError: - # FIXME: must use better name - author_name = client.jid.user + # Use the author name from MbData, fallback to client's user if not set + author_name = mb_data.author or client.jid.user author_elt.addElement("name", content=author_name) - try: - author_jid_s = mb_data["author_jid"] - except KeyError: - author_jid_s = client.jid.userhost() + # Use the author JID from MbData, fallback to client's JID if not set + author_jid_s = mb_data.author_jid or client.jid.userhost() author_elt.addElement("uri", content="xmpp:{}".format(author_jid_s)) - try: - author_jid_s = mb_data["author_email"] - except KeyError: - pass - ## published/updated time ## current_time = time.time() entry_elt.addElement( "updated", - content=utils.xmpp_date(float(mb_data.get("updated", current_time))), + content=utils.xmpp_date(float(mb_data.updated or current_time)), ) entry_elt.addElement( "published", - content=utils.xmpp_date(float(mb_data.get("published", current_time))), + content=utils.xmpp_date(float(mb_data.published or current_time)), ) ## categories ## - for tag in mb_data.get("tags", []): + for tag in mb_data.tags: category_elt = entry_elt.addElement("category") category_elt["term"] = tag ## id ## - entry_id = mb_data.get( - "id", - xmpp_uri.build_xmpp_uri( - "pubsub", - path=service.full() if service is not None else client.jid.userhost(), - node=node, - item=item_id, + entry_id = mb_data.id or xmpp_uri.build_xmpp_uri( + "pubsub", + path=( + mb_data.service.full() + if mb_data.service is not None + else client.jid.userhost() ), + node=mb_data.node, + item=mb_data.id, ) entry_elt.addElement("id", content=entry_id) # ## comments ## - for comments_data in mb_data.get("comments", []): + for comment in mb_data.comments: link_elt = entry_elt.addElement("link") # XXX: "uri" is set in self._manage_comments if not already existing try: - link_elt["href"] = comments_data["uri"] + link_elt["href"] = comment.uri except KeyError: - log.warning(f"missing URI in comments data: {comments_data}") + log.warning(f"missing URI in comments data: {comment}") entry_elt.children.remove(link_elt) else: link_elt["rel"] = "replies" link_elt["title"] = "comments" - if "repeated" in extra: + if extra.repeated: try: - repeated = extra["repeated"] + repeated = extra.repeated link_elt = entry_elt.addElement("link") link_elt["rel"] = "via" - link_elt["href"] = repeated["uri"] + link_elt["href"] = repeated.uri except KeyError as e: - log.warning(f"invalid repeated element({e}): {extra['repeated']}") + log.warning(f"invalid repeated element({e}): {extra.repeated}") ## final item building ## - item_elt = pubsub.Item(id=item_id, payload=entry_elt) + item_elt = pubsub.Item(id=mb_data.id, payload=entry_elt) ## the trigger ## # if other plugins have things to add or change