mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
6ecc14be3f
44 changed files with 990 additions and 215 deletions
|
@ -79,7 +79,7 @@ sudo apt install tesseract-ocr
|
|||
|
||||
* Clone PyFedi
|
||||
|
||||
```basg
|
||||
```bash
|
||||
git clone https://codeberg.org/rimu/pyfedi.git
|
||||
```
|
||||
|
||||
|
@ -214,6 +214,13 @@ Once you have ngrok working, edit the `.env` file and change the `SERVER_NAME` v
|
|||
|
||||
## Running PieFed in production
|
||||
|
||||
Running PieFed in production relies on several additional packages that need to be installed.
|
||||
|
||||
```bash
|
||||
source venv/bin/activate #if not already in virtual environment
|
||||
pip3 install gunicorn celery
|
||||
```
|
||||
|
||||
Copy `celery_worker.default.py` to `celery_worker.py`. Edit `DATABASE_URL` and `SERVER_NAME` to have the same values as in `.env`.
|
||||
|
||||
Edit `gunicorn.conf.py` and change `worker_tmp_dir` if needed.
|
||||
|
|
|
@ -458,7 +458,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
|
||||
# Notify recipient
|
||||
notify = Notification(title=shorten_string('New message from ' + sender.display_name()),
|
||||
url=f'/chat/{existing_conversation.id}', user_id=recipient.id,
|
||||
url=f'/chat/{existing_conversation.id}#message_{new_message}', user_id=recipient.id,
|
||||
author_id=sender.id)
|
||||
db.session.add(notify)
|
||||
recipient.unread_notifications += 1
|
||||
|
@ -672,30 +672,16 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
ocp.cross_posts.remove(post.id)
|
||||
delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id)
|
||||
activity_log.result = 'success'
|
||||
elif request_json['object']['type'] == 'Page': # Editing a post
|
||||
post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
|
||||
if post:
|
||||
try:
|
||||
update_post_from_activity(post, request_json)
|
||||
except KeyError:
|
||||
activity_log.result = 'exception'
|
||||
elif request_json['object']['type'] == 'Page': # Sent for Mastodon's benefit
|
||||
activity_log.result = 'ignored'
|
||||
activity_log.exception_message = 'Intended for Mastodon'
|
||||
db.session.add(activity_log)
|
||||
db.session.commit()
|
||||
return
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
activity_log.exception_message = 'Post not found'
|
||||
elif request_json['object']['type'] == 'Note': # Editing a reply
|
||||
reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first()
|
||||
if reply:
|
||||
try:
|
||||
update_post_reply_from_activity(reply, request_json)
|
||||
except KeyError:
|
||||
activity_log.result = 'exception'
|
||||
elif request_json['object']['type'] == 'Note': # Never sent?
|
||||
activity_log.result = 'ignored'
|
||||
activity_log.exception_message = 'Intended for Mastodon'
|
||||
db.session.add(activity_log)
|
||||
db.session.commit()
|
||||
return
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
activity_log.exception_message = 'PostReply not found'
|
||||
elif request_json['object']['type'] == 'Update': # Editing a post or comment
|
||||
if request_json['object']['object']['type'] == 'Page':
|
||||
post = Post.query.filter_by(ap_id=request_json['object']['object']['id']).first()
|
||||
|
|
|
@ -12,7 +12,8 @@ from flask_babel import _
|
|||
from sqlalchemy import text, func
|
||||
from app import db, cache, constants, celery
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
|
||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation
|
||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation, \
|
||||
Language
|
||||
import time
|
||||
import base64
|
||||
import requests
|
||||
|
@ -27,7 +28,7 @@ import pytesseract
|
|||
from app.utils import get_request, allowlist_html, get_setting, ap_datetime, markdown_to_html, \
|
||||
is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking, \
|
||||
shorten_string, reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, remove_tracking_from_link, \
|
||||
blocked_phrases, microblog_content_to_title
|
||||
blocked_phrases, microblog_content_to_title, generate_image_from_video_url, is_video_url
|
||||
|
||||
|
||||
def public_key():
|
||||
|
@ -171,7 +172,7 @@ def post_to_activity(post: Post, community: Community):
|
|||
activity_data["object"]["object"]["updated"] = ap_datetime(post.edited_at)
|
||||
if post.language is not None:
|
||||
activity_data["object"]["object"]["language"] = {"identifier": post.language}
|
||||
if post.type == POST_TYPE_LINK and post.url is not None:
|
||||
if (post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO) and post.url is not None:
|
||||
activity_data["object"]["object"]["attachment"] = [{"href": post.url, "type": "Link"}]
|
||||
if post.image_id is not None:
|
||||
activity_data["object"]["object"]["image"] = {"url": post.image.view_url(), "type": "Image"}
|
||||
|
@ -208,7 +209,7 @@ def post_to_page(post: Post, community: Community):
|
|||
activity_data["updated"] = ap_datetime(post.edited_at)
|
||||
if post.language is not None:
|
||||
activity_data["language"] = {"identifier": post.language}
|
||||
if post.type == POST_TYPE_LINK and post.url is not None:
|
||||
if (post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO) and post.url is not None:
|
||||
activity_data["attachment"] = [{"href": post.url, "type": "Link"}]
|
||||
if post.image_id is not None:
|
||||
activity_data["image"] = {"url": post.image.view_url(), "type": "Image"}
|
||||
|
@ -342,6 +343,16 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
|
|||
return None
|
||||
|
||||
|
||||
def find_language_or_create(code: str, name: str) -> Language:
|
||||
existing_language = Language.query.filter(Language.code == code).first()
|
||||
if existing_language:
|
||||
return existing_language
|
||||
else:
|
||||
new_language = Language(code=code, name=name)
|
||||
db.session.add(new_language)
|
||||
return new_language
|
||||
|
||||
|
||||
def extract_domain_and_actor(url_string: str):
|
||||
# Parse the URL
|
||||
parsed_url = urlparse(url_string)
|
||||
|
@ -637,6 +648,9 @@ def actor_json_to_model(activity_json, address, server):
|
|||
image = File(source_url=activity_json['image']['url'])
|
||||
community.image = image
|
||||
db.session.add(image)
|
||||
if 'language' in activity_json and isinstance(activity_json['language'], list):
|
||||
for ap_language in activity_json['language']:
|
||||
community.languages.append(find_language_or_create(ap_language['identifier'], ap_language['name']))
|
||||
db.session.add(community)
|
||||
db.session.commit()
|
||||
if community.icon_id:
|
||||
|
@ -738,6 +752,45 @@ def make_image_sizes(file_id, thumbnail_width=50, medium_width=120, directory='p
|
|||
def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
|
||||
file = File.query.get(file_id)
|
||||
if file and file.source_url:
|
||||
# Videos
|
||||
if file.source_url.endswith('.mp4') or file.source_url.endswith('.webm'):
|
||||
new_filename = gibberish(15)
|
||||
|
||||
# set up the storage directory
|
||||
directory = f'app/static/media/{directory}/' + new_filename[0:2] + '/' + new_filename[2:4]
|
||||
ensure_directory_exists(directory)
|
||||
|
||||
# file path and names to store the resized images on disk
|
||||
final_place = os.path.join(directory, new_filename + '.jpg')
|
||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||
|
||||
generate_image_from_video_url(file.source_url, final_place)
|
||||
|
||||
image = Image.open(final_place)
|
||||
img_width = image.width
|
||||
|
||||
# Resize the image to medium
|
||||
if medium_width:
|
||||
if img_width > medium_width:
|
||||
image.thumbnail((medium_width, medium_width))
|
||||
image.save(final_place)
|
||||
file.file_path = final_place
|
||||
file.width = image.width
|
||||
file.height = image.height
|
||||
|
||||
# Resize the image to a thumbnail (webp)
|
||||
if thumbnail_width:
|
||||
if img_width > thumbnail_width:
|
||||
image.thumbnail((thumbnail_width, thumbnail_width))
|
||||
image.save(final_place_thumbnail, format="WebP", quality=93)
|
||||
file.thumbnail_path = final_place_thumbnail
|
||||
file.thumbnail_width = image.width
|
||||
file.thumbnail_height = image.height
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Images
|
||||
else:
|
||||
try:
|
||||
source_image_response = get_request(file.source_url)
|
||||
except:
|
||||
|
@ -1039,9 +1092,9 @@ def downvote_post(post, user):
|
|||
post.down_votes += 1
|
||||
# Make 'hot' sort more spicy by amplifying the effect of early downvotes
|
||||
if post.up_votes + post.down_votes <= 30:
|
||||
post.score -= 5.0
|
||||
post.score -= current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score -= 2.0
|
||||
post.score -= current_app.config['SPICY_UNDER_60']
|
||||
else:
|
||||
post.score -= 1.0
|
||||
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
|
||||
|
@ -1148,11 +1201,11 @@ def upvote_post(post, user):
|
|||
# Make 'hot' sort more spicy by amplifying the effect of early upvotes
|
||||
spicy_effect = effect
|
||||
if post.up_votes + post.down_votes <= 10:
|
||||
spicy_effect = effect * 10
|
||||
spicy_effect = effect * current_app.config['SPICY_UNDER_10']
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
spicy_effect = effect * 5
|
||||
spicy_effect = effect * current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
spicy_effect = effect * 2
|
||||
spicy_effect = effect * current_app.config['SPICY_UNDER_60']
|
||||
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
|
||||
if not existing_vote:
|
||||
post.up_votes += 1
|
||||
|
@ -1387,6 +1440,11 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
image = File(source_url=post.url)
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
elif is_video_url(post.url):
|
||||
post.type = POST_TYPE_VIDEO
|
||||
image = File(source_url=post.url)
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
else:
|
||||
post.type = POST_TYPE_LINK
|
||||
post.url = remove_tracking_from_link(post.url)
|
||||
|
@ -1413,6 +1471,9 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
else:
|
||||
post = None
|
||||
activity_log.exception_message = domain.name + ' is blocked by admin'
|
||||
if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict):
|
||||
language = find_language_or_create(request_json['object']['language']['identifier'], request_json['object']['language']['name'])
|
||||
post.language_id = language.id
|
||||
if post is not None:
|
||||
if 'image' in request_json['object'] and post.image is None:
|
||||
image = File(source_url=request_json['object']['image']['url'])
|
||||
|
@ -1497,6 +1558,11 @@ def update_post_from_activity(post: Post, request_json: dict):
|
|||
name += ' ' + microblog_content_to_title(post.body_html)
|
||||
nsfl_in_title = '[NSFL]' in name.upper() or '(NSFL)' in name.upper()
|
||||
post.title = name
|
||||
# Language
|
||||
if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict):
|
||||
language = find_language_or_create(request_json['object']['language']['identifier'], request_json['object']['language']['name'])
|
||||
post.language_id = language.id
|
||||
# Links
|
||||
old_url = post.url
|
||||
old_image_id = post.image_id
|
||||
post.url = ''
|
||||
|
@ -1524,6 +1590,11 @@ def update_post_from_activity(post: Post, request_json: dict):
|
|||
image = File(source_url=post.url)
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
elif is_video_url(post.url):
|
||||
post.type == POST_TYPE_VIDEO
|
||||
image = File(source_url=post.url)
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
else:
|
||||
post.type = POST_TYPE_LINK
|
||||
post.url = remove_tracking_from_link(post.url)
|
||||
|
@ -1550,6 +1621,7 @@ def update_post_from_activity(post: Post, request_json: dict):
|
|||
else:
|
||||
post.url = old_url # don't change if url changed from non-banned domain to banned domain
|
||||
|
||||
# Posts which link to the same url as other posts
|
||||
new_cross_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > utcnow() - timedelta(days=6)).all()
|
||||
for ncp in new_cross_posts:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileRequired, FileAllowed
|
||||
from sqlalchemy import func
|
||||
from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField, SelectField, \
|
||||
from wtforms import StringField, PasswordField, SubmitField, EmailField, HiddenField, BooleanField, TextAreaField, SelectField, \
|
||||
FileField, IntegerField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
@ -17,6 +17,7 @@ class SiteProfileForm(FlaskForm):
|
|||
])
|
||||
sidebar = TextAreaField(_l('Sidebar'))
|
||||
legal_information = TextAreaField(_l('Legal information'))
|
||||
contact_email = EmailField(_l('General instance contact email address'), validators=[Email(), DataRequired(), Length(min=5, max=255)])
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ def admin_site():
|
|||
site.sidebar = form.sidebar.data
|
||||
site.legal_information = form.legal_information.data
|
||||
site.updated = utcnow()
|
||||
site.contact_email = form.contact_email.data
|
||||
if site.id is None:
|
||||
db.session.add(site)
|
||||
db.session.commit()
|
||||
|
|
|
@ -14,21 +14,20 @@ def send_message(message: str, conversation_id: int) -> ChatMessage:
|
|||
reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id,
|
||||
body=message, body_html=allowlist_html(markdown_to_html(message)))
|
||||
conversation.updated_at = utcnow()
|
||||
db.session.add(reply)
|
||||
db.session.commit()
|
||||
for recipient in conversation.members:
|
||||
if recipient.id != current_user.id:
|
||||
if recipient.is_local():
|
||||
# Notify local recipient
|
||||
notify = Notification(title=shorten_string('New message from ' + current_user.display_name()),
|
||||
url='/chat/' + str(conversation_id),
|
||||
url=f'/chat/{conversation_id}#message_{reply.id}',
|
||||
user_id=recipient.id,
|
||||
author_id=current_user.id)
|
||||
db.session.add(notify)
|
||||
recipient.unread_notifications += 1
|
||||
db.session.add(reply)
|
||||
db.session.commit()
|
||||
else:
|
||||
db.session.add(reply)
|
||||
db.session.commit()
|
||||
# Federate reply
|
||||
reply_json = {
|
||||
"actor": current_user.profile_id(),
|
||||
|
|
|
@ -116,6 +116,26 @@ class CreateLinkForm(FlaskForm):
|
|||
return True
|
||||
|
||||
|
||||
class CreateVideoForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
video_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
video_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
video_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
||||
render_kw={'placeholder': 'https://...'})
|
||||
sticky = BooleanField(_l('Sticky'))
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('Gore/gross'))
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
def validate(self, extra_validators=None) -> bool:
|
||||
domain = domain_from_url(self.video_url.data, create=False)
|
||||
if domain and domain.banned:
|
||||
self.video_url.errors.append(_("Videos from %(domain)s are not allowed.", domain=domain.name))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class CreateImageForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
image_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
|
|
|
@ -10,17 +10,18 @@ from sqlalchemy import or_, desc, text
|
|||
|
||||
from app import db, constants, cache
|
||||
from app.activitypub.signature import RsaKeys, post_request
|
||||
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes
|
||||
from app.activitypub.util import default_context, notify_about_post, make_image_sizes
|
||||
from app.chat.util import send_message
|
||||
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, ReportCommunityForm, \
|
||||
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \
|
||||
ReportCommunityForm, \
|
||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
||||
EscalateReportForm, ResolveReportForm
|
||||
EscalateReportForm, ResolveReportForm, CreateVideoForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||
delete_post_from_community, delete_post_reply_from_community
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR, REPORT_STATE_NEW, REPORT_STATE_ESCALATED, REPORT_STATE_RESOLVED, \
|
||||
REPORT_STATE_DISCARDED
|
||||
REPORT_STATE_DISCARDED, POST_TYPE_VIDEO
|
||||
from app.inoculation import inoculation
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply
|
||||
|
@ -257,7 +258,7 @@ def show_community(community: Community):
|
|||
|
||||
return render_template('community/community.html', community=community, title=community.title, breadcrumbs=breadcrumbs,
|
||||
is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description,
|
||||
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING,
|
||||
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_VIDEO=POST_TYPE_VIDEO, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING,
|
||||
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities,
|
||||
next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth,
|
||||
|
@ -499,7 +500,7 @@ def add_discussion_post(actor):
|
|||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
return redirect(f"/post/{post.id}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
@ -572,7 +573,7 @@ def add_image_post(actor):
|
|||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
return redirect(f"/post/{post.id}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
@ -645,7 +646,7 @@ def add_link_post(actor):
|
|||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
return redirect(f"/post/{post.id}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
@ -658,6 +659,79 @@ def add_link_post(actor):
|
|||
)
|
||||
|
||||
|
||||
@bp.route('/<actor>/submit_video', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def add_video_post(actor):
|
||||
if current_user.banned:
|
||||
return show_ban_message()
|
||||
community = actor_to_community(actor)
|
||||
|
||||
form = CreateVideoForm()
|
||||
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if not(community.is_moderator() or community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
|
||||
if form.validate_on_submit():
|
||||
community = Community.query.get_or_404(form.communities.data)
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post, 'video')
|
||||
community.post_count += 1
|
||||
community.last_active = g.site.last_active = utcnow()
|
||||
db.session.commit()
|
||||
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
|
||||
db.session.commit()
|
||||
if post.image_id and post.image.file_path is None:
|
||||
make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view
|
||||
|
||||
# Update list of cross posts
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=6)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
else:
|
||||
op.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [op.id]
|
||||
else:
|
||||
post.cross_posts.append(op.id)
|
||||
db.session.commit()
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/post/{post.id}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
||||
return render_template('community/add_video_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.id),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
def federate_post(community, post):
|
||||
page = {
|
||||
'type': 'Page',
|
||||
|
@ -697,7 +771,7 @@ def federate_post(community, post):
|
|||
"object": page,
|
||||
'@context': default_context()
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO:
|
||||
page['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
|
|
|
@ -11,7 +11,7 @@ from pillow_heif import register_heif_opener
|
|||
from app import db, cache, celery
|
||||
from app.activitypub.signature import post_request
|
||||
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, default_context
|
||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO
|
||||
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
||||
Instance, Notification, User, ActivityPubLog
|
||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
||||
|
@ -112,7 +112,8 @@ def retrieve_mods_and_backfill(community_id: int):
|
|||
post.ranking = post_ranking(post.score, post.posted_at)
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=3), Post.posted_at < post.posted_at + timedelta(days=3)).all()
|
||||
Post.posted_at > post.posted_at - timedelta(days=3),
|
||||
Post.posted_at < post.posted_at + timedelta(days=3)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
|
@ -223,6 +224,11 @@ def save_post(form, post: Post, type: str):
|
|||
remove_old_file(post.image_id)
|
||||
post.image_id = None
|
||||
|
||||
if post.url.endswith('.mp4') or post.url.endswith('.webm'):
|
||||
file = File(source_url=form.link_url.data) # make_image_sizes() will take care of turning this into a still image
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
else:
|
||||
unused, file_extension = os.path.splitext(form.link_url.data)
|
||||
# this url is a link to an image - turn it into a image post
|
||||
if file_extension.lower() in allowed_extensions:
|
||||
|
@ -303,11 +309,45 @@ def save_post(form, post: Post, type: str):
|
|||
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
elif type == 'video':
|
||||
form.video_url.data = form.video_url.data.strip()
|
||||
post.title = form.video_title.data
|
||||
post.body = form.video_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
url_changed = post.id is None or form.video_url.data != post.url
|
||||
post.url = remove_tracking_from_link(form.video_url.data.strip())
|
||||
post.type = POST_TYPE_VIDEO
|
||||
domain = domain_from_url(form.video_url.data)
|
||||
domain.post_count += 1
|
||||
post.domain = domain
|
||||
|
||||
if url_changed:
|
||||
if post.image_id:
|
||||
remove_old_file(post.image_id)
|
||||
post.image_id = None
|
||||
if form.video_url.data.endswith('.mp4') or form.video_url.data.endswith('.webm'):
|
||||
file = File(source_url=form.video_url.data) # make_image_sizes() will take care of turning this into a still image
|
||||
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.video_url.data)
|
||||
if opengraph and (opengraph.get('og:image', '') != '' or opengraph.get('og:image:url', '') != ''):
|
||||
filename = opengraph.get('og:image') or opengraph.get('og:image:url')
|
||||
filename_for_extension = filename.split('?')[0] if '?' in filename else filename
|
||||
unused, file_extension = os.path.splitext(filename_for_extension)
|
||||
if file_extension.lower() in allowed_extensions and not filename.startswith('/'):
|
||||
file = url_to_thumbnail_file(filename)
|
||||
if file:
|
||||
file.alt_text = shorten_string(opengraph.get('og:title'), 295)
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif type == 'poll':
|
||||
...
|
||||
else:
|
||||
raise Exception('invalid post type')
|
||||
|
||||
if post.id is None:
|
||||
if current_user.reputation > 100:
|
||||
post.up_votes = 1
|
||||
|
|
|
@ -44,6 +44,7 @@ def show_domain(domain_id):
|
|||
prev_url = url_for('domain.show_domain', domain_id=domain_id, page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||
return render_template('domain/domain.html', domain=domain, title=domain.name, posts=posts,
|
||||
POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE, POST_TYPE_LINK=constants.POST_TYPE_LINK,
|
||||
POST_TYPE_VIDEO=constants.POST_TYPE_VIDEO,
|
||||
next_url=next_url, prev_url=prev_url,
|
||||
content_filters=content_filters,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
|
|
|
@ -10,9 +10,9 @@ from sqlalchemy.sql.operators import or_, and_
|
|||
|
||||
from app import db, cache
|
||||
from app.activitypub.util import default_context, make_image_sizes_async, refresh_user_profile, find_actor_or_create, \
|
||||
refresh_community_profile_task
|
||||
refresh_community_profile_task, users_total, active_month, local_posts, local_communities, local_comments
|
||||
from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER, POST_TYPE_IMAGE, POST_TYPE_LINK, \
|
||||
SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR
|
||||
SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_VIDEO
|
||||
from app.email import send_email, send_welcome_email
|
||||
from app.inoculation import inoculation
|
||||
from app.main import bp
|
||||
|
@ -25,7 +25,8 @@ from sqlalchemy_searchable import search
|
|||
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
||||
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
|
||||
joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \
|
||||
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts
|
||||
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts, \
|
||||
generate_image_from_video_url
|
||||
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
|
||||
InstanceRole, Notification
|
||||
from PIL import Image
|
||||
|
@ -48,7 +49,6 @@ def index(sort=None):
|
|||
def popular(sort=None):
|
||||
return home_page('popular', sort)
|
||||
|
||||
|
||||
@bp.route('/all', methods=['GET'])
|
||||
@bp.route('/all/<sort>', methods=['GET'])
|
||||
def all_posts(sort=None):
|
||||
|
@ -149,7 +149,7 @@ def home_page(type, sort):
|
|||
recently_downvoted = []
|
||||
|
||||
return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True,
|
||||
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_VIDEO=POST_TYPE_VIDEO,
|
||||
low_bandwidth=low_bandwidth, recently_upvoted=recently_upvoted,
|
||||
recently_downvoted=recently_downvoted,
|
||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
|
@ -241,19 +241,17 @@ def donate():
|
|||
@bp.route('/about')
|
||||
def about_page():
|
||||
|
||||
users = User.query.filter_by(ap_id=None, deleted=False, banned=False).all()
|
||||
user_amount = len(users)
|
||||
# Todo, figure out how to filter the user list with the list of user_role user_id == 4
|
||||
#admins = users.filter()
|
||||
# Todo, figure out how to filter the user list with the list of user_role user_id == 4
|
||||
#staff = users.filter()
|
||||
user_amount = users_total()
|
||||
MAU = active_month()
|
||||
posts_amount = local_posts()
|
||||
|
||||
domains_amount = len(Domain.query.filter_by(banned=False).all())
|
||||
community_amount = len(Community.query.all())
|
||||
admins = db.session.execute(text('SELECT user_name, email FROM "user" WHERE "id" IN (SELECT "user_id" FROM "user_role" WHERE "role_id" = 4) ORDER BY id')).all()
|
||||
staff = db.session.execute(text('SELECT user_name FROM "user" WHERE "id" IN (SELECT "user_id" FROM "user_role" WHERE "role_id" = 2) ORDER BY id')).all()
|
||||
domains_amount = db.session.execute(text('SELECT COUNT(id) as c FROM "domain" WHERE "banned" IS false')).scalar()
|
||||
community_amount = local_communities()
|
||||
instance = Instance.query.filter_by(id=1).first()
|
||||
|
||||
|
||||
return render_template('about.html', user_amount=user_amount, domains_amount=domains_amount, community_amount=community_amount, instance=instance)#, admins=admins)
|
||||
return render_template('about.html', user_amount=user_amount, mau=MAU, posts_amount=posts_amount, domains_amount=domains_amount, community_amount=community_amount, instance=instance, admins=admins, staff=staff)
|
||||
|
||||
|
||||
@bp.route('/privacy')
|
||||
|
@ -303,6 +301,7 @@ def list_files(directory):
|
|||
|
||||
@bp.route('/test')
|
||||
def test():
|
||||
x = find_actor_or_create('https://lemmy.ml/u/const_void')
|
||||
md = "::: spoiler I'm all for ya having fun and your right to hurt yourself.\n\nI am a former racer, commuter, and professional Buyer for a chain of bike shops. I'm also disabled from the crash involving the 6th and 7th cars that have hit me in the last 170k+ miles of riding. I only barely survived what I simplify as a \"broken neck and back.\" Cars making U-turns are what will get you if you ride long enough, \n\nespecially commuting. It will look like just another person turning in front of you, you'll compensate like usual, and before your brain can even register what is really happening, what was your normal escape route will close and you're going to crash really hard. It is the only kind of crash that your intuition is useless against.\n:::"
|
||||
|
||||
return markdown_to_html(md)
|
||||
|
|
|
@ -169,6 +169,28 @@ class ChatMessage(db.Model):
|
|||
sender = db.relationship('User', foreign_keys=[sender_id])
|
||||
|
||||
|
||||
class Tag(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(256))
|
||||
|
||||
|
||||
class Language(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
code = db.Column(db.String(5), index=True)
|
||||
name = db.Column(db.String(50))
|
||||
|
||||
|
||||
community_language = db.Table('community_language', db.Column('community_id', db.Integer, db.ForeignKey('community.id')),
|
||||
db.Column('language_id', db.Integer, db.ForeignKey('language.id')),
|
||||
db.PrimaryKeyConstraint('community_id', 'language_id')
|
||||
)
|
||||
|
||||
post_tag = db.Table('post_tag', db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
|
||||
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
|
||||
db.PrimaryKeyConstraint('post_id', 'tag_id')
|
||||
)
|
||||
|
||||
|
||||
class File(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
file_path = db.Column(db.String(255))
|
||||
|
@ -365,6 +387,7 @@ class Community(db.Model):
|
|||
replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
||||
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
|
||||
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
||||
languages = db.relationship('Language', lazy='dynamic', secondary=community_language, backref=db.backref('communities', lazy='dynamic'))
|
||||
|
||||
@cache.memoize(timeout=500)
|
||||
def icon_image(self, size='default') -> str:
|
||||
|
@ -838,9 +861,11 @@ class User(UserMixin, db.Model):
|
|||
post.delete_dependencies()
|
||||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
post_replies = PostReply.query.filter_by(user_id=self.id).all()
|
||||
for reply in post_replies:
|
||||
reply.body = reply.body_html = ''
|
||||
reply.delete_dependencies()
|
||||
db.session.delete(reply)
|
||||
db.session.commit()
|
||||
|
||||
def mention_tag(self):
|
||||
|
@ -893,7 +918,9 @@ class Post(db.Model):
|
|||
language = db.Column(db.String(10))
|
||||
edited_at = db.Column(db.DateTime)
|
||||
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
|
||||
language_id = db.Column(db.Integer, index=True)
|
||||
cross_posts = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
|
||||
tags = db.relationship('Tag', lazy='dynamic', secondary=post_tag, backref=db.backref('posts', lazy='dynamic'))
|
||||
|
||||
ap_id = db.Column(db.String(255), index=True)
|
||||
ap_create_id = db.Column(db.String(100))
|
||||
|
@ -1018,6 +1045,10 @@ class PostReply(db.Model):
|
|||
return parent.author.profile_id()
|
||||
|
||||
def delete_dependencies(self):
|
||||
for child_reply in self.child_replies():
|
||||
child_reply.delete_dependencies()
|
||||
db.session.delete(child_reply)
|
||||
|
||||
db.session.query(Report).filter(Report.suspect_post_reply_id == self.id).delete()
|
||||
db.session.execute(text('DELETE FROM post_reply_vote WHERE post_reply_id = :post_reply_id'),
|
||||
{'post_reply_id': self.id})
|
||||
|
@ -1025,6 +1056,9 @@ class PostReply(db.Model):
|
|||
file = File.query.get(self.image_id)
|
||||
file.delete_from_disk()
|
||||
|
||||
def child_replies(self):
|
||||
return PostReply.query.filter_by(parent_id=self.id).all()
|
||||
|
||||
def has_replies(self):
|
||||
reply = PostReply.query.filter_by(parent_id=self.id).first()
|
||||
return reply is not None
|
||||
|
|
|
@ -13,9 +13,10 @@ from app.activitypub.util import default_context
|
|||
from app.community.util import save_post, send_to_remote_instance
|
||||
from app.inoculation import inoculation
|
||||
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
|
||||
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm
|
||||
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm
|
||||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||
from app.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, POST_TYPE_IMAGE, \
|
||||
POST_TYPE_ARTICLE, POST_TYPE_VIDEO
|
||||
from app.models import Post, PostReply, \
|
||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
|
||||
Topic, User, Instance
|
||||
|
@ -257,11 +258,13 @@ def show_post(post_id: int):
|
|||
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,
|
||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
||||
POST_TYPE_VIDEO=constants.POST_TYPE_VIDEO, autoplay=request.args.get('autoplay', False),
|
||||
noindex=not post.author.indexable,
|
||||
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
|
||||
recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies,
|
||||
etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1', SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
|
@ -305,20 +308,20 @@ def post_vote(post_id: int, vote_direction):
|
|||
post.up_votes += 1
|
||||
# Make 'hot' sort more spicy by amplifying the effect of early upvotes
|
||||
if post.up_votes + post.down_votes <= 10:
|
||||
post.score += 10
|
||||
post.score += current_app.config['SPICY_UNDER_10']
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
post.score += 5
|
||||
post.score += current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score += 2
|
||||
post.score += current_app.config['SPICY_UNDER_60']
|
||||
else:
|
||||
post.score += 1
|
||||
else:
|
||||
effect = -1
|
||||
post.down_votes += 1
|
||||
if post.up_votes + post.down_votes <= 30:
|
||||
post.score -= 5
|
||||
post.score -= current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score -= 2
|
||||
post.score -= current_app.config['SPICY_UNDER_60']
|
||||
else:
|
||||
post.score -= 1
|
||||
vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=post.author.id,
|
||||
|
@ -489,6 +492,7 @@ def continue_discussion(post_id, comment_id):
|
|||
is_moderator=is_moderator, comment=comment, replies=replies, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||
response.headers.set('Vary', 'Accept, Cookie, Accept-Language')
|
||||
return response
|
||||
|
@ -674,7 +678,8 @@ def add_reply(post_id: int, comment_id: int):
|
|||
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
||||
is_moderator=is_moderator, form=form, comment=in_reply_to, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()), mods=mod_list,
|
||||
joined_communities = joined_communities(current_user.id),
|
||||
joined_communities = joined_communities(current_user.id), community=post.community,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||
|
||||
|
||||
|
@ -706,6 +711,8 @@ def post_edit(post_id: int):
|
|||
return redirect(url_for('post.post_edit_link_post', post_id=post_id))
|
||||
elif post.type == POST_TYPE_IMAGE:
|
||||
return redirect(url_for('post.post_edit_image_post', post_id=post_id))
|
||||
elif post.type == POST_TYPE_VIDEO:
|
||||
return redirect(url_for('post.post_edit_video_post', post_id=post_id))
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
@ -931,6 +938,87 @@ def post_edit_link_post(post_id: int):
|
|||
abort(401)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/edit_video', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_edit_video_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreateVideoForm()
|
||||
del form.communities
|
||||
|
||||
mods = post.community.moderators()
|
||||
if post.community.private_mods:
|
||||
mod_list = []
|
||||
else:
|
||||
mod_user_ids = [mod.user_id for mod in mods]
|
||||
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
||||
|
||||
if post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if post.community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if post.community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
|
||||
old_url = post.url
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post, 'video')
|
||||
post.community.last_active = utcnow()
|
||||
post.edited_at = utcnow()
|
||||
db.session.commit()
|
||||
|
||||
if post.url != old_url:
|
||||
if post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
|
||||
new_cross_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.edited_at - timedelta(days=6)).all()
|
||||
for ncp in new_cross_posts:
|
||||
if ncp.cross_posts is None:
|
||||
ncp.cross_posts = [post.id]
|
||||
else:
|
||||
ncp.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [ncp.id]
|
||||
else:
|
||||
post.cross_posts.append(ncp.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
post.flush_cache()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
# federate edit
|
||||
|
||||
if not post.community.local_only:
|
||||
federate_post_update(post)
|
||||
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
form.video_title.data = post.title
|
||||
form.video_body.data = post.body
|
||||
form.video_url.data = post.url
|
||||
form.notify_author.data = post.notify_author
|
||||
form.nsfw.data = post.nsfw
|
||||
form.nsfl.data = post.nsfl
|
||||
form.sticky.data = post.sticky
|
||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
return render_template('post/post_edit_video.html', title=_('Edit post'), form=form, post=post,
|
||||
markdown_editor=current_user.markdown_editor, mods=mod_list,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
def federate_post_update(post):
|
||||
page_json = {
|
||||
'type': 'Page',
|
||||
|
@ -969,7 +1057,7 @@ def federate_post_update(post):
|
|||
],
|
||||
'object': page_json,
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO:
|
||||
page_json['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
|
@ -1430,7 +1518,8 @@ def post_reply_edit(post_id: int, comment_id: int):
|
|||
form.notify_author.data = post_reply.notify_author
|
||||
return render_template('post/post_reply_edit.html', title=_('Edit comment'), form=form, post=post, post_reply=post_reply,
|
||||
comment=comment, markdown_editor=current_user.markdown_editor, moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||
else:
|
||||
abort(401)
|
||||
|
@ -1442,7 +1531,7 @@ def post_reply_delete(post_id: int, comment_id: int):
|
|||
post = Post.query.get_or_404(post_id)
|
||||
post_reply = PostReply.query.get_or_404(comment_id)
|
||||
community = post.community
|
||||
if post_reply.user_id == current_user.id or community.is_moderator():
|
||||
if post_reply.user_id == current_user.id or community.is_moderator() or current_user.is_admin():
|
||||
if post_reply.has_replies():
|
||||
post_reply.body = 'Deleted by author' if post_reply.author.id == current_user.id else 'Deleted by moderator'
|
||||
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||
|
|
|
@ -707,6 +707,10 @@ fieldset legend {
|
|||
border-radius: 2px;
|
||||
top: 0;
|
||||
}
|
||||
.post_list .post_teaser .thumbnail .fe-video {
|
||||
left: 121px;
|
||||
top: 0;
|
||||
}
|
||||
.post_list .post_teaser .thumbnail img {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
|
@ -932,6 +936,11 @@ fieldset legend {
|
|||
border-top: solid 1px #bbb;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.comments > .comment .comment_body hr {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
opacity: 0.1;
|
||||
}
|
||||
.comments > .comment:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
|
@ -1379,4 +1388,9 @@ h1 .warning_badge {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.responsive-video {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=structure.css.map */
|
||||
|
|
|
@ -308,6 +308,11 @@ html {
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.fe-video {
|
||||
left: 121px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
|
@ -552,6 +557,14 @@ html {
|
|||
border-top: solid 1px $grey;
|
||||
margin-right: 8px;
|
||||
|
||||
.comment_body {
|
||||
hr {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
|
@ -1050,3 +1063,8 @@ h1 .warning_badge {
|
|||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-video {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
|
@ -8,16 +8,18 @@
|
|||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('About %(site_name)s', site_name=g.site.name) }}</h1>
|
||||
|
||||
<p> {{g.site.name}} is a <a href="https://join.piefed.social/">pyfedi</a> instance created on {{instance.created_at}}. It is home to <a href="/people">{{user_amount}} users</a>, <a href="/communities/local"> {{community_amount}} communities</a> who discussed <a href="/domains">{{domains_amount}} domains</a>. This instance is administerred and staffed by $PLACEHOLDER_ADMINS and $PLACEHOLDER_STAFF.</p>
|
||||
<p> {{g.site.name}} is a <a href="https://join.piefed.social/">pyfedi</a> instance created on {{instance.created_at.strftime('%d-%m-%Y')}}. It is home to <a href="/people">{{user_amount}} users</a> (of which {{mau}} active in the last month). In the <a href="/communities/local"> {{community_amount}} communities</a> we discussed <a href="/domains">{{domains_amount}} domains</a> and made {{posts_amount}} posts.</p>
|
||||
<h2> Team </h2>
|
||||
<p>This instance is administerred by {% for admin in admins %}<a href="/u/{{ admin.user_name }}">{{ admin.user_name }}</a>{{ ", " if not loop.last }}{% endfor %}.</p>
|
||||
<p>It is moderated by {% for s in staff %}<a href="/u/{{ s.user_name }}">{{ s.user_name }}</a>{{ ", " if not loop.last }}{% endfor %}.</p>
|
||||
<h2>Contact</h2>
|
||||
<p>Placeholder Admin email</p>
|
||||
<p>{{g.site.contact_email | safe }}</p>
|
||||
<h2> About Us </h2>
|
||||
<p> {{g.site.description | safe }} </p>
|
||||
<p> {{g.site.sidebar}} </p>
|
||||
<p> {{g.site.sidebar | safe }} </p>
|
||||
{% if g.site.legal_information %}
|
||||
<h2> Legal Information </h2>
|
||||
<p> {{g.site.legal_information}} </p>
|
||||
<p> {{g.site.legal_information | safe }} </p>
|
||||
<p> <a href="/privacy"> Our Privacy Policy </a> </p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
<p class="small field_hint">HTML is allowed in this field.</p>
|
||||
{{ render_field(form.legal_information) }}
|
||||
<p class="small field_hint">HTML is allowed in this field.</p>
|
||||
{{ render_field(form.contact_email) }}
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
{% extends 'base.html' %}
|
||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
|
|
|
@ -270,7 +270,7 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js', changed=getmtime('js/markdown/downarea.js')) }}"></script>
|
||||
{% endif %}
|
||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
||||
<script src="{{ url_for('static', filename='themes/' + theme() + '/scripts.js') }}" />
|
||||
<script src="{{ url_for('static', filename='themes/' + theme() + '/scripts.js') }}"></script>
|
||||
{% endif %}
|
||||
{% block end_scripts %}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{% if community %}
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="mobile_create_post d-md-none mt-1">
|
||||
<a class="btn btn-primary" href="/community/{{ community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="btn-group mt-1 mb-2">
|
||||
<a href="?sort=hot&layout={{ post_layout }}" aria-label="{{ _('Sort by hot') }}" class="btn {{ 'btn-primary' if sort == '' or sort == 'hot' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||
{{ _('Hot') }}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
|
|
98
app/templates/community/add_video_post.html
Normal file
98
app/templates/community/add_video_post.html
Normal file
|
@ -0,0 +1,98 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
|
||||
{{ render_field(form.video_title) }}
|
||||
{{ render_field(form.video_url) }}
|
||||
<p class="small field_hint">{{ _('Provide a URL ending with .mp4 or .webm.') }}</p>
|
||||
{{ render_field(form.video_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#video_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('video_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -104,9 +104,11 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/submit" rel="nofollow">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-6">
|
||||
{% if current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER] %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe" rel="nofollow">{{ _('Leave') }}</a>
|
||||
|
|
|
@ -83,6 +83,40 @@
|
|||
<span class="fe fe-external"></span></a></p>
|
||||
{% if post.url.endswith('.mp3') %}
|
||||
<p><audio controls preload="{{ 'none' if low_bandwidth else 'metadata' }}" src="{{ post.url }}"></audio></p>
|
||||
{% elif post.url.endswith('.mp4') or post.url.endswith('.webm') %}
|
||||
<p>
|
||||
<video class="responsive-video" controls preload="{{ 'metadata' if low_bandwidth else 'auto' }}">
|
||||
{% if post.url.endswith('.mp4') %}
|
||||
<source src="{{ post.url }}" media="video/mp4" />
|
||||
{% elif post.url.endswith('.webm') %}
|
||||
<source src="{{ post.url }}" media="video/webm" />
|
||||
{% endif %}
|
||||
</video></p>
|
||||
{% elif post.url.startswith('https://streamable.com') %}
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="{{ post.url.replace('streamable.com/', 'streamable.com/e/') }}" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% elif post.url.startswith('https://www.redgifs.com/watch/') %}
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="{{ post.url.replace('redgifs.com/watch/', 'redgifs.com/ifr/') }}" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% endif %}
|
||||
{% if 'youtube.com' in post.url %}
|
||||
<p><a href="https://piped.video/watch?v={{ post.youtube_embed() }}">{{ _('Watch on piped.video') }} <span class="fe fe-external"></span></a></p>
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="https://www.youtube.com/embed/{{ post.youtube_embed() }}?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% endif %}
|
||||
{% elif post.type == POST_TYPE_VIDEO %}
|
||||
<p><a href="{{ post.url }}" rel="nofollow ugc" target="_blank" class="post_link" aria-label="Go to post url">{{ post.url|shorten_url }}
|
||||
<span class="fe fe-external"></span></a></p>
|
||||
{% if post.url.endswith('.mp4') or post.url.endswith('.webm') %}
|
||||
<p>
|
||||
<video class="responsive-video" controls preload="{{ 'none' if low_bandwidth else 'auto' }}" {{ 'autoplay muted' if autoplay }}>
|
||||
{% if post.url.endswith('.mp4') %}
|
||||
<source src="{{ post.url }}" media="video/mp4" />
|
||||
{% elif post.url.endswith('.webm') %}
|
||||
<source src="{{ post.url }}" media="video/webm" />
|
||||
{% endif %}
|
||||
</video></p>
|
||||
{% elif post.url.startswith('https://streamable.com') %}
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="{{ post.url.replace('streamable.com/', 'streamable.com/e/') }}" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% elif post.url.startswith('https://www.redgifs.com/watch/') %}
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="{{ post.url.replace('redgifs.com/watch/', 'redgifs.com/ifr/') }}" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% endif %}
|
||||
{% if 'youtube.com' in post.url %}
|
||||
<p><a href="https://piped.video/watch?v={{ post.youtube_embed() }}">{{ _('Watch on piped.video') }} <span class="fe fe-external"></span></a></p>
|
||||
|
|
|
@ -16,15 +16,18 @@
|
|||
{% if post.image_id %}
|
||||
<div class="thumbnail{{ ' lbw' if low_bandwidth }}" aria-hidden="true">
|
||||
{% if low_bandwidth %}
|
||||
{% if post.type == POST_TYPE_LINK %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('Read article') }}"><span class="fe fe-external"></span></a>
|
||||
{% if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('Follow link') }}"><span class="fe fe-external"></span></a>
|
||||
{% elif post.type == POST_TYPE_IMAGE %}
|
||||
<a href="{{ post.image.view_url() }}" rel="nofollow ugc" aria-label="{{ _('View image') }}" target="_blank"><span class="fe fe-magnify"></span></a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}" aria-label="{{ _('Read post') }}"><span class="fe fe-reply"></span></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if post.type == POST_TYPE_LINK %}
|
||||
{% if post.type == POST_TYPE_VIDEO %}
|
||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id, sort='new' if sort == 'active' else None, autoplay='true') }}" rel="nofollow ugc" aria-label="{{ _('Read article') }}"><span class="fe fe-video"></span><img src="{{ post.image.thumbnail_url() }}"
|
||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" class="{{ 'blur' if (post.nsfw and not post.community.nsfw) or (post.nsfl and not post.community.nsfl) }}" /></a>
|
||||
{% elif post.type == POST_TYPE_LINK %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('Read article') }}"><span class="fe fe-external"></span><img src="{{ post.image.thumbnail_url() }}"
|
||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" class="{{ 'blur' if (post.nsfw and not post.community.nsfw) or (post.nsfl and not post.community.nsfl) }}" /></a>
|
||||
{% elif post.type == POST_TYPE_IMAGE %}
|
||||
|
@ -39,7 +42,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
||||
{% if (post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO) and post.domain_id %}
|
||||
<div class="thumbnail{{ ' lbw' if low_bandwidth }} missing_thumbnail" aria-hidden="true">
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('Read article') }}"><span class="fe fe-external"></span></a>
|
||||
</div>
|
||||
|
@ -47,8 +50,8 @@
|
|||
{% endif %}
|
||||
<h3>{% if post.sticky %}<span class="fe fe-sticky-left"></span>{% endif %}<a href="{{ url_for('activitypub.post_ap', post_id=post.id, sort='new' if sort == 'active' else None) }}" class="post_teaser_title_a">{{ post.title }}</a>
|
||||
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image" aria-hidden="true"> </span>{% endif %}
|
||||
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
||||
{% if post.url and 'youtube.com' in post.url %}
|
||||
{% if (post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO) and post.domain_id %}
|
||||
{% if post.url and (post.type == POST_TYPE_VIDEO or 'youtube.com' in post.url) %}
|
||||
<span class="fe fe-video" aria-hidden="true"></span>
|
||||
{% elif post.url.endswith('.mp3') %}
|
||||
<span class="fe fe-audio" aria-hidden="true"></span>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% set thumbnail = post.image.view_url() %}
|
||||
{% endif %}
|
||||
<div class="masonry_thumb" title="{{ post.title }}">
|
||||
{% if post.type == POST_TYPE_LINK %}
|
||||
{% if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO %}
|
||||
{% if post.image.medium_url() %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.medium_url() }}"
|
||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" title="{{ post.title }}"
|
||||
|
|
|
@ -50,9 +50,11 @@
|
|||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe" rel="nofollow">{{ _('Join') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- <form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{% else %}
|
||||
{% if comment['comment'].author.avatar_id and comment['comment'].score > -10 and not low_bandwidth %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
||||
<img src="{{ comment['comment'].author.avatar_image() }}" alt="Avatar" loading="lazy" /></a>
|
||||
<img src="{{ comment['comment'].author.avatar_thumbnail() }}" alt="Avatar" loading="lazy" /></a>
|
||||
{% endif %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}">
|
||||
{{ comment['comment'].author.display_name() }}</a>
|
||||
|
@ -101,9 +101,11 @@
|
|||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe" rel="nofollow">{{ _('Join') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- <form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
{% else %}
|
||||
{% if comment['comment'].author.avatar_id and comment['comment'].score > -10 and not low_bandwidth %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.ap_id }}">
|
||||
<img src="{{ comment['comment'].author.avatar_image() }}" alt="" loading="lazy" /></a>
|
||||
<img src="{{ comment['comment'].author.avatar_thumbnail() }}" alt="" loading="lazy" /></a>
|
||||
{% endif %}
|
||||
<a href="/u/{{ comment['comment'].author.link() }}" title="{{ comment['comment'].author.link() }}" aria-label="{{ _('Author') }}">
|
||||
{{ comment['comment'].author.display_name() }}</a>
|
||||
|
@ -169,9 +169,11 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-6">
|
||||
{% if current_user.is_authenticated and community_membership(current_user, post.community) >= SUBSCRIPTION_MEMBER %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe" rel="nofollow">{{ _('Leave') }}</a>
|
||||
|
|
75
app/templates/post/post_edit_video.html
Normal file
75
app/templates/post/post_edit_video.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.video_title) }}
|
||||
{{ render_field(form.video_url) }}
|
||||
{{ render_field(form.video_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#video_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.link_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('video_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description_html|safe if post.community.description_html else '' }}</p>
|
||||
<p>{{ post.community.rules_html|safe if post.community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.link() }}">{{ mod.display_name() }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -41,14 +41,16 @@
|
|||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.is_authenticated and community_membership(current_user, post.community) %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe" rel="nofollow">{{ _('Unsubscribe') }}</a>
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe" rel="nofollow">{{ _('Leave') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe" rel="nofollow">{{ _('Subscribe') }}</a>
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe" rel="nofollow">{{ _('Join') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and current_user.is_authenticated and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<div class="col-6">
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- <form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() %}
|
||||
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin() %}
|
||||
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
|
||||
{{ _('Edit') }}</a></li>
|
||||
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
|
||||
|
|
|
@ -13,10 +13,17 @@
|
|||
<tbody>
|
||||
{% for community in communities %}
|
||||
{% if not community.user_is_banned(current_user) %}
|
||||
{% if not community.restricted_to_mods or (community.restricted_to_mods and community_membership(current_user, community) in [SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER]) %}
|
||||
<tr>
|
||||
<th class="pl-2"><a class="choose_topic_for_post" data-id="{{ community.id }}" href="/community/{{ community.link() }}/submit" rel="nofollow" aria-label="{{ _('Post in %(name)s', name=community.display_name()) }}"><img src="{{ community.icon_image('tiny') }}" class="community_icon rounded-circle" loading="lazy" alt="" />
|
||||
{{ community.display_name() }}</a></th>
|
||||
{{ community.display_name() }}</a></th><th></th>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th class="pl-2"><a class="choose_topic" data-id="{{ community.id }}" href="/c/{{ community.link() }}" rel="nofollow" aria-label="{{ _('View %(name)s', name=community.display_name()) }}"><img src="{{ community.icon_image('tiny') }}" class="community_icon rounded-circle" loading="lazy" alt="" />
|
||||
{{ community.display_name() }}</a></th><th>(Posts are restricted to Mods)</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -26,7 +26,17 @@
|
|||
{% elif user.avatar_image() != '' %}
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
{% if low_bandwidth %}
|
||||
<img class="community_icon_big rounded-circle" src="{{ user.avatar_image() }}" alt="{{ _('Profile pic') }}" />
|
||||
{% else %}
|
||||
{% if user.avatar.source_url and user.avatar.source_url.endswith('.mp4') %}
|
||||
<video autoplay disablepictureinpicture loop muted class="community_icon_big rounded-circle">
|
||||
<source src="{{ user.avatar.source_url }}" type="video/mp4">
|
||||
</video>
|
||||
{% else %}
|
||||
<img class="community_icon_big rounded-circle" src="{{ user.avatar_image() }}" alt="{{ _('Profile pic') }}" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<h1 class="mt-3">{{ user.display_name() if user.is_local() else user.display_name() + ', ' + user.ap_id }}</h1>
|
||||
|
|
|
@ -9,7 +9,7 @@ from flask_babel import _
|
|||
from sqlalchemy import text, desc, or_
|
||||
|
||||
from app.activitypub.signature import post_request
|
||||
from app.constants import SUBSCRIPTION_NONMEMBER, POST_TYPE_IMAGE, POST_TYPE_LINK
|
||||
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_IMAGE, POST_TYPE_LINK, POST_TYPE_VIDEO
|
||||
from app.inoculation import inoculation
|
||||
from app.models import Topic, Community, Post, utcnow, CommunityMember, CommunityJoinRequest, User
|
||||
from app.topic import bp
|
||||
|
@ -117,7 +117,10 @@ def show_topic(topic_path):
|
|||
show_post_community=True, moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)],
|
||||
POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_IMAGE=POST_TYPE_IMAGE)
|
||||
POST_TYPE_LINK=POST_TYPE_LINK, POST_TYPE_IMAGE=POST_TYPE_IMAGE,
|
||||
POST_TYPE_VIDEO=POST_TYPE_VIDEO,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
@ -200,8 +203,7 @@ def topic_create_post(topic_name):
|
|||
community = Community.query.get_or_404(int(request.form.get('community_id')))
|
||||
return redirect(url_for('community.join_then_add', actor=community.link()))
|
||||
return render_template('topic/topic_create_post.html', communities=communities, topic=topic,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()))
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR)
|
||||
|
||||
|
||||
def topics_for_form():
|
||||
|
|
55
app/utils.py
55
app/utils.py
|
@ -4,6 +4,7 @@ import bisect
|
|||
import hashlib
|
||||
import mimetypes
|
||||
import random
|
||||
import tempfile
|
||||
import urllib
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, date
|
||||
|
@ -14,7 +15,7 @@ import math
|
|||
from urllib.parse import urlparse, parse_qs, urlencode
|
||||
from functools import wraps
|
||||
import flask
|
||||
from bs4 import BeautifulSoup, NavigableString, MarkupResemblesLocatorWarning
|
||||
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||
import requests
|
||||
|
@ -26,6 +27,8 @@ from wtforms.fields import SelectField, SelectMultipleField
|
|||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||
from app import db, cache
|
||||
import re
|
||||
from moviepy.editor import VideoFileClip
|
||||
from PIL import Image
|
||||
|
||||
from app.email import send_welcome_email
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
|
||||
|
@ -165,6 +168,13 @@ def is_image_url(url):
|
|||
return any(path.endswith(extension) for extension in common_image_extensions)
|
||||
|
||||
|
||||
def is_video_url(url):
|
||||
parsed_url = urlparse(url)
|
||||
path = parsed_url.path.lower()
|
||||
common_video_extensions = ['.mp4', '.webm']
|
||||
return any(path.endswith(extension) for extension in common_video_extensions)
|
||||
|
||||
|
||||
# sanitise HTML using an allow list
|
||||
def allowlist_html(html: str) -> str:
|
||||
if html is None or html == '':
|
||||
|
@ -221,7 +231,7 @@ def markdown_to_html(markdown_text) -> str:
|
|||
if markdown_text:
|
||||
raw_html = markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'strike': True})
|
||||
# replace lemmy spoiler tokens with appropriate html tags instead. (until possibly added as extra to markdown2)
|
||||
re_spoiler = re.compile(r':{3} spoiler\s+?(\S.+?)(?:\n|</p>)(.+?)(?:\n|<p>):{3}', re.S)
|
||||
re_spoiler = re.compile(r':{3}\s*?spoiler\s+?(\S.+?)(?:\n|</p>)(.+?)(?:\n|<p>):{3}', re.S)
|
||||
raw_html = re_spoiler.sub(r'<details><summary>\1</summary><p>\2</p></details>', raw_html)
|
||||
return allowlist_html(raw_html)
|
||||
else:
|
||||
|
@ -284,10 +294,13 @@ def domain_from_url(url: str, create=True) -> Domain:
|
|||
|
||||
|
||||
def shorten_string(input_str, max_length=50):
|
||||
if input_str:
|
||||
if len(input_str) <= max_length:
|
||||
return input_str
|
||||
else:
|
||||
return input_str[:max_length - 3] + '…'
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def shorten_url(input: str, max_length=20):
|
||||
|
@ -878,6 +891,44 @@ def in_sorted_list(arr, target):
|
|||
return index < len(arr) and arr[index] == target
|
||||
|
||||
|
||||
# Makes a still image from a video url, without downloading the whole video file
|
||||
def generate_image_from_video_url(video_url, output_path, length=2):
|
||||
|
||||
response = requests.get(video_url, stream=True)
|
||||
content_type = response.headers.get('Content-Type')
|
||||
if content_type:
|
||||
if 'video/mp4' in content_type:
|
||||
temp_file_extension = '.mp4'
|
||||
elif 'video/webm' in content_type:
|
||||
temp_file_extension = '.webm'
|
||||
else:
|
||||
raise ValueError("Unsupported video format")
|
||||
else:
|
||||
raise ValueError("Content-Type not found in response headers")
|
||||
|
||||
# Generate a random temporary file name
|
||||
temp_file_name = gibberish(15) + temp_file_extension
|
||||
temp_file_path = os.path.join(tempfile.gettempdir(), temp_file_name)
|
||||
|
||||
# Write the downloaded data to a temporary file
|
||||
with open(temp_file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=4096):
|
||||
f.write(chunk)
|
||||
if os.path.getsize(temp_file_path) >= length * 1024 * 1024:
|
||||
break
|
||||
|
||||
# Generate thumbnail from the temporary file
|
||||
clip = VideoFileClip(temp_file_path)
|
||||
thumbnail = clip.get_frame(0)
|
||||
clip.close()
|
||||
|
||||
# Save the image
|
||||
thumbnail_image = Image.fromarray(thumbnail)
|
||||
thumbnail_image.save(output_path)
|
||||
|
||||
os.remove(temp_file_path)
|
||||
|
||||
|
||||
@cache.memoize(timeout=600)
|
||||
def recently_upvoted_posts(user_id) -> List[int]:
|
||||
post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'),
|
||||
|
|
|
@ -49,3 +49,7 @@ class Config(object):
|
|||
|
||||
CLOUDFLARE_API_TOKEN = os.environ.get('CLOUDFLARE_API_TOKEN') or ''
|
||||
CLOUDFLARE_ZONE_ID = os.environ.get('CLOUDFLARE_ZONE_ID') or ''
|
||||
|
||||
SPICY_UNDER_10 = float(os.environ.get('SPICY_UNDER_10', 1.0))
|
||||
SPICY_UNDER_30 = float(os.environ.get('SPICY_UNDER_30', 1.0))
|
||||
SPICY_UNDER_60 = float(os.environ.get('SPICY_UNDER_60', 1.0))
|
||||
|
|
|
@ -34,9 +34,10 @@ time of things.
|
|||
# Coding Standards / Guidelines
|
||||
|
||||
**[PEP 8](https://peps.python.org/pep-0008/)** covers the basics. PyCharm encourages this by default -
|
||||
VS Code coders are encouraged to try the free community edition of PyCharm but it is by no means required.
|
||||
VS Code coders may like to try the free community edition of PyCharm but it is by no means required.
|
||||
|
||||
Use PEP 8 conventions for line length, naming, indentation. Use descriptive commit messages.
|
||||
Use PEP 8 conventions for naming, indentation. Use descriptive commit messages. Try to limit lines of code
|
||||
to a length of roughly 120 characters.
|
||||
|
||||
Database model classes are singular. As in "Car", not "Cars".
|
||||
|
||||
|
|
42
migrations/versions/980966fba5f4_post_one_language.py
Normal file
42
migrations/versions/980966fba5f4_post_one_language.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
"""post one language
|
||||
|
||||
Revision ID: 980966fba5f4
|
||||
Revises: fd2af23f4b1f
|
||||
Create Date: 2024-04-16 21:23:34.642869
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '980966fba5f4'
|
||||
down_revision = 'fd2af23f4b1f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('post_language')
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('language_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_post_language_id'), ['language_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_post_language_id'))
|
||||
batch_op.drop_column('language_id')
|
||||
|
||||
op.create_table('post_language',
|
||||
sa.Column('post_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
||||
sa.Column('language_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(['language_id'], ['language.id'], name='post_language_language_id_fkey'),
|
||||
sa.ForeignKeyConstraint(['post_id'], ['post.id'], name='post_language_post_id_fkey'),
|
||||
sa.PrimaryKeyConstraint('post_id', 'language_id', name='post_language_pkey')
|
||||
)
|
||||
# ### end Alembic commands ###
|
69
migrations/versions/fd2af23f4b1f_tags_and_languages.py
Normal file
69
migrations/versions/fd2af23f4b1f_tags_and_languages.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
"""tags and languages
|
||||
|
||||
Revision ID: fd2af23f4b1f
|
||||
Revises: 91a931afd6d9
|
||||
Create Date: 2024-04-16 21:15:07.225254
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fd2af23f4b1f'
|
||||
down_revision = '91a931afd6d9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('language',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('code', sa.String(length=5), nullable=True),
|
||||
sa.Column('name', sa.String(length=50), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('language', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_language_code'), ['code'], unique=False)
|
||||
|
||||
op.create_table('tag',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=256), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('community_language',
|
||||
sa.Column('community_id', sa.Integer(), nullable=False),
|
||||
sa.Column('language_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['community_id'], ['community.id'], ),
|
||||
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ),
|
||||
sa.PrimaryKeyConstraint('community_id', 'language_id')
|
||||
)
|
||||
op.create_table('post_language',
|
||||
sa.Column('post_id', sa.Integer(), nullable=False),
|
||||
sa.Column('language_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['language_id'], ['language.id'], ),
|
||||
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ),
|
||||
sa.PrimaryKeyConstraint('post_id', 'language_id')
|
||||
)
|
||||
op.create_table('post_tag',
|
||||
sa.Column('post_id', sa.Integer(), nullable=False),
|
||||
sa.Column('tag_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ),
|
||||
sa.PrimaryKeyConstraint('post_id', 'tag_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('post_tag')
|
||||
op.drop_table('post_language')
|
||||
op.drop_table('community_language')
|
||||
op.drop_table('tag')
|
||||
with op.batch_alter_table('language', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_language_code'))
|
||||
|
||||
op.drop_table('language')
|
||||
# ### end Alembic commands ###
|
|
@ -8,7 +8,7 @@ from flask_login import current_user
|
|||
from app import create_app, db, cli
|
||||
import os, click
|
||||
from flask import session, g, json, request, current_app
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE, POST_TYPE_VIDEO
|
||||
from app.models import Site
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
|
||||
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \
|
||||
|
@ -22,7 +22,8 @@ cli.register(app)
|
|||
def app_context_processor():
|
||||
def getmtime(filename):
|
||||
return os.path.getmtime('app/static/' + filename)
|
||||
return dict(getmtime=getmtime, post_type_link=POST_TYPE_LINK, post_type_image=POST_TYPE_IMAGE, post_type_article=POST_TYPE_ARTICLE)
|
||||
return dict(getmtime=getmtime, post_type_link=POST_TYPE_LINK, post_type_image=POST_TYPE_IMAGE,
|
||||
post_type_article=POST_TYPE_ARTICLE, post_type_video=POST_TYPE_VIDEO)
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
|
|
|
@ -32,3 +32,4 @@ Werkzeug==2.3.3
|
|||
pytesseract==0.3.10
|
||||
sentry-sdk==1.40.6
|
||||
python-slugify==8.0.4
|
||||
moviepy==1.0.3
|
||||
|
|
Loading…
Add table
Reference in a new issue