diff --git a/.isort.cfg b/.isort.cfg index e59d6737ecc09e8863549d5b0ff2cc19f454f693..e2b23eaf0e6609d4132474671b8f150283257d65 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -3,4 +3,4 @@ line_length = 88 multi_line_output = 3 include_trailing_comma = true -known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevents,markdown,modelcluster,pirates,pytest,pytz,requests,requests_cache,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,yaml +known_third_party = PyPDF2,arrow,attr,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevents,markdown,modelcluster,pirates,pytest,pytz,requests,requests_cache,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,yaml diff --git a/shared/models.py b/shared/models.py index 6ee08e0e8dfc3bc4b5c2cfae05157188d597d0a3..bb652dfd77351d0fc132dc7b3e12daf87e73138e 100644 --- a/shared/models.py +++ b/shared/models.py @@ -1,7 +1,13 @@ import logging +from time import time +from attr import dataclass +from django.core.files.temp import NamedTemporaryFile +from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.forms import ValidationError from django.utils import timezone +from requests import request from wagtail.admin.panels import FieldPanel, MultiFieldPanel, PublishingPanel from wagtail.fields import StreamField from wagtail.models import Page @@ -9,12 +15,104 @@ from wagtail.models import Page from shared.blocks import ( DEFAULT_CONTENT_BLOCKS, FooterLinksBlock, + ImageCreatorMixin, MenuItemBlock, MenuParentBlock, ) logger = logging.getLogger(__name__) +FIVE_MINUTES_IN_SECONDS = 300 + + +@dataclass +class MastodonImage: + """ + Partial type definition for mastodon API image link + """ + + url: str + + +@dataclass +class MastodonToot: + """ + Partial type definition for mastodon API toot + """ + + id: str + content: str + published: str + attachment: list[MastodonImage] + + +@dataclass +class MastodonToots: + """ + Partial type definition for mastodon API toots + """ + + orderedItems: list[MastodonToot] + + +@dataclass +class MastodonUser: + """ + Partial type definition for mastodon API user + """ + + icon: MastodonImage + summary: str + preferredUsername: str + + +class Mastodon(ImageCreatorMixin, models.Model): + url = models.URLField() + toots = models.JSONField(encoder=DjangoJSONEncoder, null=True) + summary = models.TextField(null=True, blank=True) + user_image = models.ImageField(null=True, blank=True, upload_to="mastodon_users/") + timestamp = models.FloatField() + + def download_toots(self): + """ + Downloads toots from mastodon feed + """ + # https://mastodon.pirati.cz/users/ivanbartos/outbox?page=true + full_url = self.url + "/outbox?page=true" + return request("GET", full_url).json() + + def download_user(self): + """ + Downloads user page from mastodon feed + """ + # https://mastodon.pirati.cz/users/ivanbartos.json + full_url = self.url + ".json" + return request("GET", full_url).json() + + def parse_url(self, now: float): + toots: MastodonToots = self.download_toots() + user: MastodonUser = self.download_user() + + # Taken from: https://gist.github.com/anderser/2172888 + temporary_stored_image = NamedTemporaryFile(delete=True) + temporary_stored_image.write(user.icon.url) + temporary_stored_image.flush() + self.user_image.save(f"{user.preferredUsername}.jpg", temporary_stored_image) + + self.toots = toots.orderedItems + self.summary = user.summary + self.timestamp = now + + def refresh_toots(self): + now = time() + if self.timestamp != None and now - self.timestamp < FIVE_MINUTES_IN_SECONDS: + return + try: + self.parse_url(now) + self.save() + except: + logger.error("Mastodon refresh failed for %s", self.url, exc_info=True) + class MastodonFeedMixin(models.Model): mastodon_feed = models.URLField( @@ -22,6 +120,37 @@ class MastodonFeedMixin(models.Model): blank=True, help_text="Zadejte mastodon feed url v tomto formátu: https://mastodon.pirati.cz/users/ivanbartos", ) + mastodon = models.ForeignKey( + Mastodon, null=True, blank=True, on_delete=models.PROTECT + ) + + def clean(self): + super().clean() + try: + mastodon = ( + self.mastodon + if self.mastodon is not None + else Mastodon.objects.create(url=self.calendar_url) + ) + mastodon.parse_url(time()) + except: + raise ValidationError("Update mastodonu se nepovedl") + + def save(self, *args, **kwargs): + if self.mastodon_feed: + if self.mastodon: + if self.mastodon.url != self.mastodon_feed: + self.mastodon.url = self.mastodon_feed + self.mastodon.save() + else: + self.mastodon = Mastodon.objects.create(url=self.mastodon_feed) + + self.mastodon.refresh_toots() + + elif self.mastodon: + self.mastodon = None + + super().save(*args, **kwargs) class Meta: abstract = True diff --git a/shared/templates/shared/mastodon_feed_snippet.html b/shared/templates/shared/mastodon_feed_snippet.html index 24a34a74c53ce3f4edad7c367bf042bdfc576a12..c135a0c80e3c1f853c9ea3af952cc6ab7f3390e3 100644 --- a/shared/templates/shared/mastodon_feed_snippet.html +++ b/shared/templates/shared/mastodon_feed_snippet.html @@ -2,24 +2,12 @@ {% if is_link %} <a href="{{ page.mastodon_feed }}" class="social-icon ">{% include "shared/mastodon_icon_snippet.html" with size=link_size invert=True %}</a> {% else %} - <iframe - allowfullscreen - sandbox="allow-top-navigation allow-scripts allow-popups allow-popups-to-escape-sandbox" - width="400" - height="400" - src="https://mastofeed.com/apiv2/feed?userurl={{ page.mastodon_feed | urlencode:'' }}&theme=dark&size=100&header=true&replies=false&boosts=false"> - </iframe> + {% include "shared/mastodon_feed_toots_snippet.html" with mastodon_feed=page.mastodon_feed %} {% endif %} {% elif page.root_page.mastodon_feed %} {% if is_link %} <a href="{{ page.root_page.mastodon_feed }}" class="social-icon ">{% include "shared/mastodon_icon_snippet.html" with size=link_size invert=True %}</a> {% else %} - <iframe - allowfullscreen - sandbox="allow-top-navigation allow-scripts allow-popups allow-popups-to-escape-sandbox" - width="400" - height="400" - src="https://mastofeed.com/apiv2/feed?userurl={{ page.root_page.mastodon_feed | urlencode:'' }}&theme=dark&size=100&header=true&replies=false&boosts=false"> - </iframe> + {% include "shared/mastodon_feed_toots_snippet.html" with mastodon_feed=page.root_page.mastodon_feed %} {% endif %} {% endif %} diff --git a/shared/templates/shared/mastodon_feed_toots_snippet.html b/shared/templates/shared/mastodon_feed_toots_snippet.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391