various small bugfixes

This commit is contained in:
rimu 2024-01-25 20:16:08 +13:00
parent 686ac36ac7
commit 41f6a29e33
10 changed files with 133 additions and 94 deletions

View file

@ -408,7 +408,10 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
# Announce is new content and votes that happened on a remote server.
if request_json['type'] == 'Announce':
if request_json['object']['type'] == 'Create':
if isinstance(request_json['object'], str):
activity_log.activity_json = json.dumps(request_json)
activity_log.exception_message = 'invalid json?'
elif request_json['object']['type'] == 'Create':
activity_log.activity_type = request_json['object']['type']
user_ap_id = request_json['object']['object']['attributedTo']
try:

View file

@ -536,6 +536,7 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
# Load image data into Pillow
Image.MAX_IMAGE_PIXELS = 89478485
image = Image.open(BytesIO(source_image))
image = ImageOps.exif_transpose(image)
img_width = image.width
@ -563,11 +564,11 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
# Alert regarding fascist meme content
try:
image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'))
image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'), timeout=30)
except FileNotFoundError as e:
image_text = ''
if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
post = Post.query.filter(image_id=file.id).first()
post = Post.query.filter_by(image_id=file.id).first()
notification = Notification(title='Review this',
user_id=1,
author_id=post.user_id,

View file

@ -1,9 +1,15 @@
from flask import request
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
from flask_babel import _, lazy_gettext as _l
from app import db
from app.utils import domain_from_url, MultiCheckboxField
from PIL import Image, ImageOps
from io import BytesIO
import pytesseract
class AddLocalCommunity(FlaskForm):
@ -36,7 +42,7 @@ class SearchRemoteCommunity(FlaskForm):
class CreatePostForm(FlaskForm):
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
post_type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
discussion_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
discussion_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)}, render_kw={'placeholder': 'Text (optional)'})
link_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
@ -56,14 +62,14 @@ class CreatePostForm(FlaskForm):
def validate(self, extra_validators=None) -> bool:
if not super().validate():
return False
if self.type.data is None or self.type.data == '':
self.type.data = 'discussion'
if self.post_type.data is None or self.post_type.data == '':
self.post_type.data = 'discussion'
if self.type.data == 'discussion':
if self.post_type.data == 'discussion':
if self.discussion_title.data == '':
self.discussion_title.errors.append(_('Title is required.'))
return False
elif self.type.data == 'link':
elif self.post_type.data == 'link':
if self.link_title.data == '':
self.link_title.errors.append(_('Title is required.'))
return False
@ -74,14 +80,27 @@ class CreatePostForm(FlaskForm):
if domain and domain.banned:
self.link_url.errors.append(_(f"Links to %s are not allowed.".format(domain.name)))
return False
elif self.type.data == 'image':
elif self.post_type.data == 'image':
if self.image_title.data == '':
self.image_title.errors.append(_('Title is required.'))
return False
if self.image_file.data == '':
self.image_file.errors.append(_('File is required.'))
return False
elif self.type.data == 'poll':
uploaded_file = request.files['image_file']
if uploaded_file and uploaded_file.filename != '':
Image.MAX_IMAGE_PIXELS = 89478485
# Do not allow fascist meme content
try:
image_text = pytesseract.image_to_string(Image.open(BytesIO(uploaded_file.read())).convert('L'))
except FileNotFoundError as e:
image_text = ''
if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
self.image_file.errors.append(f"This image is an invalid file type.") # deliberately misleading error message
current_user.reputation -= 1
db.session.commit()
return False
elif self.post_type.data == 'poll':
self.discussion_title.errors.append(_('Poll not implemented yet.'))
return False

View file

@ -1,3 +1,4 @@
from io import BytesIO
from random import randint
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, g, json
@ -19,7 +20,7 @@ from app.models import User, Community, CommunityMember, CommunityJoinRequest, C
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog
from app.community import bp
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
shorten_string, gibberish, community_membership, ap_datetime, \
request_etag_matches, return_304, instance_banned, can_create, can_upvote, can_downvote, user_filters_posts, \
joined_communities, moderating_communities
from feedgen.feed import FeedGenerator
@ -459,13 +460,16 @@ def add_post(actor):
return redirect(f"/c/{community.link()}")
else:
# when request.form has some data in it, it means form validation failed. Set the post_type so the correct tab is shown. See setupPostTypeTabs() in scripts.js
if request.form.get('post_type', None):
form.post_type.data = request.form.get('post_type')
form.communities.data = community.id
form.notify_author.data = True
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
images_disabled=images_disabled, markdown_editor=True, low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1',
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities = joined_communities(current_user.id),
joined_communities=joined_communities(current_user.id),
inoculation=inoculation[randint(0, len(inoculation) - 1)]
)

View file

@ -14,7 +14,7 @@ from app.activitypub.util import find_actor_or_create, actor_json_to_model, post
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
Instance, Notification, User
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image, allowlist_html, \
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string
from sqlalchemy import desc, text
import os
@ -152,6 +152,7 @@ def url_to_thumbnail_file(filename) -> File:
with open(final_place, 'wb') as f:
f.write(response.content)
response.close()
Image.MAX_IMAGE_PIXELS = 89478485
with Image.open(final_place) as img:
img = ImageOps.exif_transpose(img)
img.thumbnail((150, 150))
@ -167,12 +168,12 @@ def save_post(form, post: Post):
post.nsfw = form.nsfw.data
post.nsfl = form.nsfl.data
post.notify_author = form.notify_author.data
if form.type.data == '' or form.type.data == 'discussion':
if form.post_type.data == '' or form.post_type.data == 'discussion':
post.title = form.discussion_title.data
post.body = form.discussion_body.data
post.body_html = markdown_to_html(post.body)
post.type = POST_TYPE_ARTICLE
elif form.type.data == 'link':
elif form.post_type.data == 'link':
post.title = form.link_title.data
post.body = form.link_body.data
post.body_html = markdown_to_html(post.body)
@ -187,10 +188,10 @@ def save_post(form, post: Post):
if post.image_id:
remove_old_file(post.image_id)
post.image_id = None
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.lower() in valid_extensions:
if file_extension.lower() in allowed_extensions:
file = url_to_thumbnail_file(form.link_url.data)
if file:
post.image = file
@ -200,16 +201,15 @@ def save_post(form, post: Post):
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:
if file_extension.lower() in allowed_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':
elif form.post_type.data == 'image':
post.title = form.image_title.data
post.body = form.image_body.data
post.body_html = markdown_to_html(post.body)
@ -222,7 +222,7 @@ def save_post(form, post: Post):
# check if this is an allowed type of file
file_ext = os.path.splitext(uploaded_file.filename)[1]
if file_ext.lower() not in allowed_extensions or file_ext.lower() != validate_image(uploaded_file.stream):
if file_ext.lower() not in allowed_extensions:
abort(400)
new_filename = gibberish(15)
@ -233,35 +233,39 @@ def save_post(form, post: Post):
# save the file
final_place = os.path.join(directory, new_filename + file_ext)
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
uploaded_file.seek(0)
uploaded_file.save(final_place)
if file_ext.lower() == '.heic':
register_heif_opener()
#Image.MAX_IMAGE_PIXELS = 89478485
# resize if necessary
img = Image.open(final_place)
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
if img.width > 2000 or img.height > 2000:
img.thumbnail((2000, 2000))
img.save(final_place)
if '.' + img.format.lower() in allowed_extensions:
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((256, 256))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
if img.width > 2000 or img.height > 2000:
img.thumbnail((2000, 2000))
img.save(final_place)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((256, 256))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=form.image_title.data,
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))
post.image = file
db.session.add(file)
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=form.image_title.data,
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))
post.image = file
db.session.add(file)
elif form.type.data == 'poll':
elif form.post_type.data == 'poll':
...
else:
raise Exception('invalid post type')
@ -285,7 +289,7 @@ def remove_old_file(file_id):
def save_icon_file(icon_file, directory='communities') -> File:
# check if this is an allowed type of file
file_ext = os.path.splitext(icon_file.filename)[1]
if file_ext.lower() not in allowed_extensions or file_ext.lower() != validate_image(icon_file.stream):
if file_ext.lower() not in allowed_extensions:
abort(400)
new_filename = gibberish(15)
@ -302,33 +306,36 @@ def save_icon_file(icon_file, directory='communities') -> File:
register_heif_opener()
# resize if necessary
Image.MAX_IMAGE_PIXELS = 89478485
img = Image.open(final_place)
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
if img.width > 250 or img.height > 250:
img.thumbnail((250, 250))
img.save(final_place)
if '.' + img.format.lower() in allowed_extensions:
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((40, 40))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
if img.width > 250 or img.height > 250:
img.thumbnail((250, 250))
img.save(final_place)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((40, 40))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=f'{directory} icon',
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail)
db.session.add(file)
return file
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=f'{directory} icon',
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail)
db.session.add(file)
return file
else:
abort(400)
def save_banner_file(banner_file, directory='communities') -> File:
# check if this is an allowed type of file
file_ext = os.path.splitext(banner_file.filename)[1]
if file_ext.lower() not in allowed_extensions or file_ext.lower() != validate_image(
banner_file.stream):
if file_ext.lower() not in allowed_extensions:
abort(400)
new_filename = gibberish(15)
@ -345,26 +352,30 @@ def save_banner_file(banner_file, directory='communities') -> File:
register_heif_opener()
# resize if necessary
Image.MAX_IMAGE_PIXELS = 89478485
img = Image.open(final_place)
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
if img.width > 1600 or img.height > 600:
img.thumbnail((1600, 600))
img.save(final_place)
if '.' + img.format.lower() in allowed_extensions:
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
if img.width > 1600 or img.height > 600:
img.thumbnail((1600, 600))
img.save(final_place)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((700, 500))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((700, 500))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=f'{directory} banner',
width=img_width, height=img_height, thumbnail_width=thumbnail_width, thumbnail_height=thumbnail_height)
db.session.add(file)
return file
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=f'{directory} banner',
width=img_width, height=img_height, thumbnail_width=thumbnail_width, thumbnail_height=thumbnail_height)
db.session.add(file)
return file
else:
abort(400)
# NB this always signs POSTs as the community so is only suitable for Announce activities

View file

@ -19,7 +19,7 @@ from app.models import Post, PostReply, \
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community
from app.post import bp
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \
shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \
reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities
@ -643,16 +643,16 @@ def post_edit(post_id: int):
return redirect(url_for('activitypub.post_ap', post_id=post.id))
else:
if post.type == constants.POST_TYPE_ARTICLE:
form.type.data = 'discussion'
form.post_type.data = 'discussion'
form.discussion_title.data = post.title
form.discussion_body.data = post.body
elif post.type == constants.POST_TYPE_LINK:
form.type.data = 'link'
form.post_type.data = 'link'
form.link_title.data = post.title
form.link_body.data = post.body
form.link_url.data = post.url
elif post.type == constants.POST_TYPE_IMAGE:
form.type.data = 'image'
form.post_type.data = 'image'
form.image_title.data = post.title
form.image_body.data = post.body
form.notify_author.data = post.notify_author

View file

@ -234,27 +234,37 @@ function setupPostTypeTabs() {
const tabEl = document.querySelector('#discussion-tab')
if(tabEl) {
tabEl.addEventListener('show.bs.tab', event => {
document.getElementById('type').value = 'discussion';
document.getElementById('post_type ').value = 'discussion';
});
}
const tabE2 = document.querySelector('#link-tab')
if(tabE2) {
tabE2.addEventListener('show.bs.tab', event => {
document.getElementById('type').value = 'link';
document.getElementById('post_type').value = 'link';
});
}
const tabE3 = document.querySelector('#image-tab')
if(tabE3) {
tabE3.addEventListener('show.bs.tab', event => {
document.getElementById('type').value = 'image';
document.getElementById('post_type').value = 'image';
});
}
const tabE4 = document.querySelector('#poll-tab')
if(tabE4) {
tabE4.addEventListener('show.bs.tab', event => {
document.getElementById('type').value = 'poll';
document.getElementById('post_type').value = 'poll';
});
}
// Check if there is a hidden field with the name 'type'. This is set if server-side validation of the form fails
var typeField = document.getElementById('post_type');
if (typeField && typeField.tagName === 'INPUT' && typeField.type === 'hidden') {
var typeVal = typeField.value;
if(typeVal) {
const tab = document.getElementById(typeVal + '-tab');
if(tab)
tab.click();
}
}
}

View file

@ -75,7 +75,7 @@
Poll
</div>
</div>
{{ render_field(form.type) }}
{{ render_field(form.post_type) }}
<div class="row mt-4">
<div class="col-md-3">
{{ render_field(form.notify_author) }}

View file

@ -95,7 +95,7 @@
Poll
</div>
</div>
{{ render_field(form.type) }}
{{ render_field(form.post_type) }}
<div class="row mt-4">
<div class="col-md-3">
{{ render_field(form.notify_author) }}

View file

@ -312,15 +312,6 @@ def ensure_directory_exists(directory):
rebuild_directory += '/'
def validate_image(stream):
header = stream.read(512)
stream.seek(0)
format = imghdr.what(None, header)
if not format:
return None
return '.' + (format if format != 'jpeg' else 'jpg')
def validation_required(func):
@wraps(func)
def decorated_view(*args, **kwargs):
@ -609,9 +600,9 @@ def _confidence(ups, downs):
def confidence(ups, downs) -> float:
if ups is None:
if ups is None or ups < 0:
ups = 0
if downs is None:
if downs is None or downs < 0:
downs = 0
if ups + downs == 0:
return 0.0