diff --git a/fxsharing/shares/tasks.py b/fxsharing/shares/tasks.py index 6954ff6..0ef9006 100644 --- a/fxsharing/shares/tasks.py +++ b/fxsharing/shares/tasks.py @@ -1,5 +1,4 @@ import functools -import mimetypes from urllib.parse import urljoin, urlparse from django.conf import settings @@ -18,16 +17,6 @@ logger = get_task_logger(__name__) FAVICON_MAX_BYTES = 1 * 1024 * 1024 # 1 MB cap -CONTENT_TYPE_TO_EXT = { - "image/png": ".png", - "image/jpeg": ".jpg", - "image/jpg": ".jpg", - "image/gif": ".gif", - "image/webp": ".webp", - "image/svg+xml": ".svg", - "image/x-icon": ".ico", - "image/vnd.microsoft.icon": ".ico", -} @functools.cache @@ -76,14 +65,8 @@ def download_and_store_favicon(favicon_url, link_url, headers): ) return None - ext = ( - CONTENT_TYPE_TO_EXT.get(content_type) - or mimetypes.guess_extension(content_type) - or ".ico" - ) - hostname = urlparse(link_url).hostname - object_name = f"favicons/{hostname}{ext}" + object_name = f"favicons/{hostname}" client = _get_gcs_client() bucket = client.bucket(bucket_name) diff --git a/fxsharing/shares/urls.py b/fxsharing/shares/urls.py index e45ae8e..1d12189 100644 --- a/fxsharing/shares/urls.py +++ b/fxsharing/shares/urls.py @@ -10,6 +10,7 @@ path("event", views.record_client_event, name="record_client_event"), path("report/", views.report_share, name="report_share"), path("auth-complete", views.auth_complete, name="auth_complete"), + path("get_favicon_url", views.get_favicon_url, name="get_favicon_url"), ] if settings.DEBUG: diff --git a/fxsharing/shares/views.py b/fxsharing/shares/views.py index 4819512..7416646 100644 --- a/fxsharing/shares/views.py +++ b/fxsharing/shares/views.py @@ -3,6 +3,7 @@ import json import logging from datetime import timedelta +from urllib.parse import urlparse from django.conf import settings from django.contrib import messages @@ -36,7 +37,12 @@ from .cinder_schema import decision_created_schema from .models import Link, Share, ShareStatus from .share_schema import share_schema -from .tasks import process_new_share, purge_cdn_cache, submit_share_to_cinder +from .tasks import ( + _get_gcs_client, + process_new_share, + purge_cdn_cache, + submit_share_to_cinder, +) log = logging.getLogger(__name__) @@ -96,6 +102,31 @@ def view_share(request, shortcode): return response +def get_favicon_url(request): + bucket_name = settings.GCS_IMAGE_BUCKET + if not bucket_name: + return None + + favicons = {} + + urls = set([urlparse(url).hostname for url in request.GET.getlist("url")]) + + for hostname in urls: + object_name = f"favicons/{hostname}" + + client = _get_gcs_client() + bucket = client.bucket(bucket_name) + blob = bucket.blob(object_name) + + favicon_url = None + if blob.exists(): + favicon_url = f"https://storage.googleapis.com/{bucket_name}/{object_name}" + favicons[hostname] = favicon_url + + # returns a dict with hostname as key and the favicon url as the value + return JsonResponse(favicons) + + SHARE_EXPIRY_DAYS = 7 diff --git a/static/components/moz-share.mjs b/static/components/moz-share.mjs index a609c85..eec9d59 100644 --- a/static/components/moz-share.mjs +++ b/static/components/moz-share.mjs @@ -429,8 +429,13 @@ class MozShare extends MozLitElement { static queries = { reportDialog: "#report-dialog", reportForm: "#report-dialog form", + mozLinks: { all: "moz-link" }, }; + static MAX_RETRIES = 10; + static BASE_DELAY = 200; + static MAX_DELAY = 20000; + connectedCallback() { super.connectedCallback(); this.init(); @@ -462,6 +467,8 @@ class MozShare extends MozLitElement { } catch (e) { console.error(e); } + + this.setupPollingForFavicons(); } /** @@ -484,6 +491,56 @@ class MozShare extends MozLitElement { return links; } + get linksWithoutFavicons() { + return this.flatLinks.filter((l) => !l.favicon_url); + } + + async setupPollingForFavicons() { + const shouldPollForFavicon = this.linksWithoutFavicons.length > 0; + + if (shouldPollForFavicon) { + let attempt = 0; + + while (attempt < MozShare.MAX_RETRIES) { + try { + return await this.pollForFavicon(); + } catch { + // Use exponential backoff for requesting the favicons + const delay = Math.min( + MozShare.MAX_DELAY, + MozLink.BASE_DELAY * Math.pow(2, attempt), + ); + const jitter = Math.random() * delay; + + await new Promise((resolve) => setTimeout(resolve, delay + jitter)); + + attempt += 1; + } + } + } + } + + async pollForFavicon() { + let params = new URLSearchParams(); + this.linksWithoutFavicons.forEach((l) => params.append("url", l.url)); + + let response = await fetch(`/get_favicon_url?${params.toString()}`); + + let favicons = await response.json(); + if (favicons) { + for (let [hostname, favicon_url] of Object.entries(favicons)) { + let matchedHosts = this.linksWithoutFavicons.filter( + (l) => new URL(l.url).hostname === hostname, + ); + matchedHosts.forEach((l) => (l.favicon_url = favicon_url)); + } + this.mozLinks.forEach((ml) => ml.requestUpdate()); + return; + } + + throw new Error("Failed to get favicon on this attempt"); + } + copyLink() { navigator.clipboard.writeText(location.href); recordEvent("copy_link", {});