#!/usr/bin/env python3
# Libervia plugin to handle events
# Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from dataclasses import dataclass
import json
from textwrap import dedent
from typing import Callable, Dict, List, Optional, Union
from urllib import parse
import fnmatch
from lxml import etree
import treq
from twisted.internet import defer
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.exceptions import ConflictError
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.tools.common import data_format
from libervia.backend.tools.common.async_utils import async_lru
log = getLogger(__name__)
PLUGIN_INFO = {
C.PI_NAME: "Preview",
C.PI_IMPORT_NAME: "Preview",
C.PI_TYPE: C.PLUG_TYPE_MISC,
C.PI_PROTOCOLS: ["Open Graph", "oEmbed"],
C.PI_DEPENDENCIES: ["TEXT_SYNTAXES"],
C.PI_MAIN: "Preview",
C.PI_HANDLER: "no",
C.PI_DESCRIPTION: dedent(
_(
"""\
Retrieves and provides a preview of URLs using various protocols. Initially, it
uses the Open Graph protocol for most web pages. Specialized handlers are
implemented for YouTube using the oEmbed protocol.
"""
)
),
}
OG_TAGS = [
"title",
"type",
"image",
"url",
"audio",
"description",
"determiner",
"locale",
"locale:alternate",
"site_name",
"video",
]
class PreviewFetchError(Exception):
pass
@dataclass
class Protocol:
name: str
callback: Callable
priority: int
class Preview:
protocols: Dict[str, Protocol] = {}
domain_protocols: Dict[str, str] = {}
def __init__(self, host):
log.info(_("Preview plugin initialization"))
self.host = host
# generic protocols
self.register("open_graph", self.fetch_open_graph_data, priority=100)
self.register("oembed", self.fetch_generic_oembed_data, priority=50)
self.register("generic", self.fetch_generic_data, priority=0)
# domain specific protocols
self.register("oembed-youtube", self.fetch_youtube_oembed_data, priority=-100)
self.register_domain_protocol(
["www.youtube.com", "youtu.be", "m.youtube.com"], "oembed-youtube"
)
self.register("wikipedia", self.fetch_wikipedia_data, priority=-80)
self.register_domain_protocol(["*.wikipedia.org"], "wikipedia")
self.register("invidious", self.fetch_invidious_data, priority=-90)
self.register_domain_protocol(
["yewtu.be", "www.yewtu.be", "invidious.fdn.fr"],
"invidious"
)
# bridge methods
host.bridge.add_method(
"url_preview_get",
".plugin",
in_sign="sss",
out_sign="s",
method=self._url_preview_get,
async_=True,
)
# API
def _url_preview_get(self, url: str, options: str, profile_key: str) -> defer.Deferred:
client = self.host.get_client(profile_key)
d = defer.ensureDeferred(
self.get_preview_data(client, url, data_format.deserialise(options))
)
d.addCallback(data_format.serialise)
return d
@async_lru()
async def get_preview_data(
self, client: SatXMPPEntity, url: str, options: dict
) -> Optional[dict]:
"""Fetch preview data from a url using registered protocols
@param url: The url to fetch the preview data from
@param options: Additional options that may be used while fetching preview data
@return: A dictionary containing the preview data or None if no data could be
fetched
"""
parsed_url = parse.urlparse(url)
domain = parsed_url.netloc
preview_data: Optional[dict] = None
matched_protocol = None
for registered_domain, registered_protocol in self.domain_protocols.items():
if fnmatch.fnmatch(domain, registered_domain):
matched_protocol = registered_protocol
break
if matched_protocol is not None:
callback = self.protocols[matched_protocol].callback
preview_data = await callback(client, url, options)
else:
for name, protocol in sorted(
self.protocols.items(), key=lambda item: item[1].priority, reverse=True
):
try:
preview_data = await protocol.callback(client, url, options)
except Exception as e:
log.warning(f"Can't run protocol {name} for {url}: {e}")
else:
if preview_data is not None:
matched_protocol = protocol.name
break
if preview_data is not None:
preview_data["protocol"] = matched_protocol
# we don't clean html for youtube as we need Javascript to make it work, and
# for invidious as we generate it ourself
if "html" in preview_data:
if matched_protocol in ("oembed-youtube", "invidious"):
# this flag indicate that we know the source of HTML and we should be
# able to trust it. This will add `allow-scripts` and
# `allow-same-origin` in the preview