diff --git a/app/community/routes.py b/app/community/routes.py index 6c189a9e..0d36b40a 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -1,5 +1,6 @@ from datetime import date, datetime, timedelta +import requests from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort from flask_login import login_user, logout_user, current_user, login_required from flask_babel import _ @@ -10,7 +11,7 @@ from app import db, constants from app.activitypub.signature import RsaKeys, HttpSignature from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies, \ - get_comment_branch, post_reply_count, ensure_directory_exists + get_comment_branch, post_reply_count, ensure_directory_exists, opengraph_parse, url_to_thumbnail_file from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, PostReply, \ PostReplyVote, PostVote, File @@ -211,6 +212,28 @@ def add_post(actor): domain = domain_from_url(form.link_url.data) domain.post_count += 1 post.domain = domain + valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'} + unused, file_extension = os.path.splitext(form.link_url.data) # do not use _ here instead of 'unused' + # this url is a link to an image - generate a thumbnail of it + if file_extension in valid_extensions: + file = url_to_thumbnail_file(form.link_url.data) + if file: + post.image = file + db.session.add(file) + else: + # check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag + opengraph = opengraph_parse(form.link_url.data) + if opengraph and opengraph.get('og:image', '') != '': + filename = opengraph.get('og:image') + valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'} + unused, file_extension = os.path.splitext(filename) + if file_extension.lower() in valid_extensions: + file = url_to_thumbnail_file(filename) + if file: + file.alt_text = opengraph.get('og:title') + post.image = file + db.session.add(file) + elif form.type.data == 'image': allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic'] post.title = form.image_title.data @@ -304,7 +327,8 @@ def show_post(post_id: int): return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator, canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH, - description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE) + description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE, + POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE) @bp.route('/post//', methods=['GET', 'POST']) diff --git a/app/community/util.py b/app/community/util.py index d94f1545..327226b5 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -1,11 +1,15 @@ from datetime import datetime from typing import List -from app import db +import requests +from PIL import Image, ImageOps + +from app import db, cache from app.models import Community, File, BannedInstances, PostReply -from app.utils import get_request +from app.utils import get_request, gibberish from sqlalchemy import desc, text import os +from opengraph_parse import parse_page def search_for_community(address: str): @@ -143,3 +147,32 @@ def ensure_directory_exists(directory): if not os.path.isdir(rebuild_directory): os.mkdir(rebuild_directory) rebuild_directory += '/' + + +@cache.memoize(timeout=50) +def opengraph_parse(url): + try: + return parse_page(url) + except Exception as ex: + return None + + +def url_to_thumbnail_file(filename) -> File: + unused, file_extension = os.path.splitext(filename) + response = requests.get(filename, timeout=5) + if response.status_code == 200: + new_filename = gibberish(15) + directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4] + ensure_directory_exists(directory) + final_place = os.path.join(directory, new_filename + file_extension) + with open(final_place, 'wb') as f: + f.write(response.content) + with Image.open(final_place) as img: + img = ImageOps.exif_transpose(img) + img.thumbnail((150, 150)) + img.save(final_place) + thumbnail_width = img.width + thumbnail_height = img.height + return File(file_name=new_filename + file_extension, thumbnail_width=thumbnail_width, + thumbnail_height=thumbnail_height, thumbnail_path=final_place, + source_url=filename) diff --git a/app/models.py b/app/models.py index 2ef3a8a7..1d0b2a9f 100644 --- a/app/models.py +++ b/app/models.py @@ -375,6 +375,12 @@ class Post(db.Model): db.session.execute(text('DELETE FROM post_reply WHERE post_id = :post_id'), {'post_id': self.id}) db.session.execute(text('DELETE FROM post_vote WHERE post_id = :post_id'), {'post_id': self.id}) + def youtube_embed(self): + if self.url: + vpos = self.url.find('v=') + if vpos != -1: + return self.url[vpos + 2:vpos + 13] + class PostReply(db.Model): query_class = FullTextSearchQuery diff --git a/app/static/scss/_mixins.scss b/app/static/scss/_mixins.scss index 2662a5ce..5238a148 100644 --- a/app/static/scss/_mixins.scss +++ b/app/static/scss/_mixins.scss @@ -20,5 +20,5 @@ } .pl-0 { - padding-left: 0; + padding-left: 0!important; } \ No newline at end of file diff --git a/app/static/structure.css b/app/static/structure.css index 9514039d..a22f34bb 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -1,7 +1,7 @@ /* This file contains SCSS used for creating the general structure of pages. Selectors should be things like body, h1, nav, etc which are used site-wide */ .pl-0 { - padding-left: 0; + padding-left: 0 !important; } /* for more info about the feather font used for icons see https://at-ui.github.io/feather-font/ */ @@ -303,7 +303,7 @@ fieldset legend { } #breadcrumb_nav { - display: none; + font-size: 87%; } @media (min-width: 992px) { @@ -370,6 +370,26 @@ fieldset legend { .post_list .post_teaser .meta_row a, .post_list .post_teaser .main_row a, .post_list .post_teaser .utilities_row a { text-decoration: none; } +.post_list .post_teaser .thumbnail { + padding-left: 0; + padding-right: 0; +} +.post_list .post_teaser .thumbnail img { + position: absolute; + right: 70px; + height: 70px; + margin-top: -47px; +} + +.url_thumbnail { + float: right; + margin-top: 6px; + margin-right: 6px; +} + +.post_image img { + max-width: 100%; +} .comments > .comment { margin-left: 0; diff --git a/app/static/structure.scss b/app/static/structure.scss index 00499498..5af70785 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -52,7 +52,7 @@ nav, etc which are used site-wide */ } #breadcrumb_nav { - display: none; + font-size: 87%; } @include breakpoint(tablet) { @@ -131,12 +131,35 @@ nav, etc which are used site-wide */ } } + .thumbnail { + padding-left: 0; + padding-right: 0; + img { + position: absolute; + right: 70px; + height: 70px; + margin-top: -47px; + } + } + border-bottom: solid 2px $light-grey; padding-top: 8px; padding-bottom: 8px; } } +.url_thumbnail { + float: right; + margin-top: 6px; + margin-right: 6px; +} + +.post_image { + img { + max-width: 100%; + } +} + .comments > .comment { margin-left: 0; border-top: solid 1px $grey; diff --git a/app/static/styles.css b/app/static/styles.css index 3229fc03..9c4a2467 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -1,6 +1,6 @@ /* */ .pl-0 { - padding-left: 0; + padding-left: 0 !important; } /* for more info about the feather font used for icons see https://at-ui.github.io/feather-font/ */ diff --git a/app/templates/community/_post_full.html b/app/templates/community/_post_full.html index b1849f75..3a64f428 100644 --- a/app/templates/community/_post_full.html +++ b/app/templates/community/_post_full.html @@ -39,22 +39,37 @@
{% include "community/_post_voting_buttons.html" %}
-

{{ post.title }}

- {% if post.url %} -

{{ post.url|shorten_url }} - External link -

+ {% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %} +
+ {{ post.image.alt_text }} +
{% endif %} +

{{ post.title }}

submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }} {% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}

- {% if post.image_id %} + {% if post.type == POST_TYPE_LINK %} +

{{ post.url|shorten_url }} + External link +

+ {% if 'youtube.com' in post.url %} +
+ {% endif %} + {% elif post.type == POST_TYPE_IMAGE %}
- {{ post.image.alt_text }} + {{ post.image.alt_text }}
+ {% else %} + {% if post.image_id and not (post.url and 'youtube.com' in post.url) %} + {{ post.image.alt_text }} + {% endif %} {% endif %} + + {% endif %} diff --git a/app/templates/community/_post_teaser.html b/app/templates/community/_post_teaser.html index 7ed094cc..067c2cef 100644 --- a/app/templates/community/_post_teaser.html +++ b/app/templates/community/_post_teaser.html @@ -2,7 +2,7 @@
-
+

{{ post.title }} {% if post.type == POST_TYPE_IMAGE %} {% endif %} @@ -14,13 +14,14 @@ {% endif %}

{{ render_username(post.author) }} ยท {{ moment(post.posted_at).fromNow() }} -
{% if post.image_id %} -
+
{{ post.image.alt_text }} + height="50" />
{% endif %} +
+
diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 23ddcad9..01f93172 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -34,7 +34,7 @@ - + @@ -44,8 +44,8 @@ {% for community in communities %} - - + + diff --git a/app/utils.py b/app/utils.py index 5bde7189..6e68a39a 100644 --- a/app/utils.py +++ b/app/utils.py @@ -161,7 +161,7 @@ def markdown_to_text(markdown_text) -> str: def domain_from_url(url: str, create=True) -> Domain: - parsed_url = urlparse(url) + parsed_url = urlparse(url.lower().replace('www.', '')) domain = Domain.query.filter_by(name=parsed_url.hostname.lower()).first() if create and domain is None: domain = Domain(name=parsed_url.hostname.lower()) diff --git a/requirements.txt b/requirements.txt index 5b3ffe58..19f533b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ beautifulsoup4==4.12.2 flask-caching==2.0.2 Pillow pillow-heif +opengraph-parse=0.0.6
{{ _('Name') }}{{ _('Community') }} {{ _('Posts') }} {{ _('Comments') }} {{ _('Active') }}
{{ community.display_name() }}{{ community.display_name() }} {{ community.post_count }} {{ community.post_reply_count }} {{ moment(community.last_active).fromNow(refresh=True) }}