mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
various small bugfixes
This commit is contained in:
parent
686ac36ac7
commit
41f6a29e33
10 changed files with 133 additions and 94 deletions
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,6 +460,9 @@ 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
|
||||
|
||||
|
|
|
@ -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,13 +233,17 @@ 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)
|
||||
if '.' + img.format.lower() in allowed_extensions:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
|
@ -261,7 +265,7 @@ def save_post(form, post: Post):
|
|||
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,7 +306,9 @@ 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)
|
||||
if '.' + img.format.lower() in allowed_extensions:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
|
@ -322,13 +328,14 @@ def save_icon_file(icon_file, directory='communities') -> File:
|
|||
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,7 +352,9 @@ 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)
|
||||
if '.' + img.format.lower() in allowed_extensions:
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
|
@ -365,6 +374,8 @@ def save_banner_file(banner_file, directory='communities') -> File:
|
|||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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) }}
|
||||
|
|
13
app/utils.py
13
app/utils.py
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue