From b53504fe64a518113bf061c5057c2431fbb116bc Mon Sep 17 00:00:00 2001 From: Corry Haines Date: Mon, 26 Dec 2022 09:39:33 -0800 Subject: [PATCH] Basic OpenGraph support (#267) Creates an OpenGraph template include in base.html including the basic tags expected on all pages. Then allows any page to add additional expected tags via `context`. Currently, profiles and posts are enriched to show complete opengraph metadata, and render correctly in Discord. Note: This does not show posts in Slack like Twitter/Mastodon do. I believe this is due to Slack preferring oembed when present, which is a mastodon API endpoint we may need to create at some point. --- activities/models/post.py | 16 ++++++++++++++ activities/templatetags/opengraph.py | 23 +++++++++++++++++++++ activities/views/posts.py | 31 +++++++++++++++++----------- core/context.py | 6 ++++++ templates/_opengraph.html | 14 +++++++++++++ templates/activities/post.html | 4 ++++ templates/base.html | 3 +++ templates/identity/view.html | 4 ++++ users/models/identity.py | 13 ++++++++++++ 9 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 activities/templatetags/opengraph.py create mode 100644 templates/_opengraph.html diff --git a/activities/models/post.py b/activities/models/post.py index 7952cd5..f7e3293 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -838,6 +838,22 @@ class Post(StatorModel): raise ValueError("Actor on delete does not match object") post.delete() + ### OpenGraph API ### + + def to_opengraph_dict(self) -> dict: + return { + "og:title": f"{self.author.name} (@{self.author.handle})", + "og:type": "article", + "og:published_time": (self.published or self.created).isoformat(), + "og:modified_time": ( + self.edited or self.published or self.created + ).isoformat(), + "og:description": (self.summary or self.safe_content_local()), + "og:image:url": self.author.local_icon_url().absolute, + "og:image:height": 85, + "og:image:width": 85, + } + ### Mastodon API ### def to_mastodon_json(self, interactions=None): diff --git a/activities/templatetags/opengraph.py b/activities/templatetags/opengraph.py new file mode 100644 index 0000000..b48c1a8 --- /dev/null +++ b/activities/templatetags/opengraph.py @@ -0,0 +1,23 @@ +from django import template + +register = template.Library() + + +@register.filter +def dict_merge(base: dict, defaults: dict): + """ + Merges two input dictionaries, returning the merged result. + + `input|dict_merge:defaults` + + The defaults are overridden by any key present in the `input` dict. + """ + if not (isinstance(base, dict) or isinstance(defaults, dict)): + raise ValueError("Filter inputs must be dictionaries") + + result = {} + + result.update(defaults) + result.update(base) + + return result diff --git a/activities/views/posts.py b/activities/views/posts.py index 501c729..b4a7bf2 100644 --- a/activities/views/posts.py +++ b/activities/views/posts.py @@ -39,21 +39,28 @@ class Individual(TemplateView): # Show normal page return super().get(request) - def get_context_data(self): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + ancestors, descendants = PostService(self.post_obj).context( self.request.identity ) - return { - "identity": self.identity, - "post": self.post_obj, - "interactions": PostInteraction.get_post_interactions( - [self.post_obj] + ancestors + descendants, - self.request.identity, - ), - "link_original": True, - "ancestors": ancestors, - "descendants": descendants, - } + + context.update( + { + "identity": self.identity, + "post": self.post_obj, + "interactions": PostInteraction.get_post_interactions( + [self.post_obj] + ancestors + descendants, + self.request.identity, + ), + "link_original": True, + "ancestors": ancestors, + "descendants": descendants, + } + ) + + return context def serve_object(self): # If this not a local post, redirect to its canonical URI diff --git a/core/context.py b/core/context.py index 876c643..d94e645 100644 --- a/core/context.py +++ b/core/context.py @@ -8,4 +8,10 @@ def config_context(request): request.identity.config_identity if request.identity else None ), "top_section": request.path.strip("/").split("/")[0], + "opengraph_defaults": { + "og:site_name": Config.system.site_name, + "og:type": "website", + "og:title": Config.system.site_name, + "og:url": request.build_absolute_uri(), + }, } diff --git a/templates/_opengraph.html b/templates/_opengraph.html new file mode 100644 index 0000000..706c25c --- /dev/null +++ b/templates/_opengraph.html @@ -0,0 +1,14 @@ +{% load opengraph %} +{% with opengraph_merged=opengraph_local|dict_merge:opengraph_defaults %} + + {% for key, value in opengraph_merged.items %} + + {% if key == "og:description" %} + {# Mastodon duplicates this one tag without the og: prefix. Not sure why #} + + {% endif %} + {% endfor %} + {% block opengraph_extra %} + {% endblock %} + +{% endwith %} diff --git a/templates/activities/post.html b/templates/activities/post.html index ef1bb96..cb7e60e 100644 --- a/templates/activities/post.html +++ b/templates/activities/post.html @@ -2,6 +2,10 @@ {% block title %}Post by {{ post.author.html_name_or_handle }}{% endblock %} +{% block opengraph %} + {% include "_opengraph.html" with opengraph_local=post.to_opengraph_dict %} +{% endblock %} + {% block content %} {% for ancestor in ancestors reversed %} {% include "activities/_post.html" with post=ancestor reply=True link_original=False %} diff --git a/templates/base.html b/templates/base.html index f3df9c4..bb917bc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,6 +26,9 @@ {% if config_identity.custom_css %} {% endif %} + {% block opengraph %} + {% include "_opengraph.html" with opengraph_local=opengraph_defaults %} + {% endblock %} {% block extra_head %}{% endblock %} diff --git a/templates/identity/view.html b/templates/identity/view.html index 52520b0..39f73cc 100644 --- a/templates/identity/view.html +++ b/templates/identity/view.html @@ -2,6 +2,10 @@ {% block title %}{{ identity }}{% endblock %} +{% block opengraph %} + {% include "_opengraph.html" with opengraph_local=identity.to_opengraph_dict %} +{% endblock %} + {% block extra_head %} {% if identity.local %} diff --git a/users/models/identity.py b/users/models/identity.py index 866d542..4a536af 100644 --- a/users/models/identity.py +++ b/users/models/identity.py @@ -735,6 +735,19 @@ class Identity(StatorModel): await sync_to_async(self.save)() return True + ### OpenGraph API ### + + def to_opengraph_dict(self) -> dict: + return { + "og:title": f"{self.name} (@{self.handle})", + "og:type": "profile", + "og:description": self.summary, + "og:profile:username": self.handle, + "og:image:url": self.local_icon_url().absolute, + "og:image:height": 85, + "og:image:width": 85, + } + ### Mastodon Client API ### def to_mastodon_json(self):