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
c3120bc3e6
52 changed files with 1089 additions and 272 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
@ -1037,6 +1090,12 @@ def downvote_post(post, user):
|
|||
if not existing_vote:
|
||||
effect = -1.0
|
||||
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 -= current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
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,
|
||||
effect=effect)
|
||||
|
@ -1139,10 +1198,18 @@ def upvote_post(post, user):
|
|||
user.last_seen = utcnow()
|
||||
user.recalculate_attitude()
|
||||
effect = instance_weight(user.ap_domain)
|
||||
# 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 * current_app.config['SPICY_UNDER_10']
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
spicy_effect = effect * current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
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
|
||||
post.score += effect
|
||||
post.score += spicy_effect
|
||||
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
|
||||
effect=effect)
|
||||
if post.community.low_quality and effect > 0:
|
||||
|
@ -1373,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)
|
||||
|
@ -1399,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'])
|
||||
|
@ -1483,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 = ''
|
||||
|
@ -1510,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)
|
||||
|
@ -1536,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:
|
||||
|
|
|
@ -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
|
||||
|
@ -30,7 +31,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
|
|||
shorten_string, gibberish, community_membership, ap_datetime, \
|
||||
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
|
||||
joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
|
||||
community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts
|
||||
community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts, \
|
||||
blocked_users
|
||||
from feedgen.feed import FeedGenerator
|
||||
from datetime import timezone, timedelta
|
||||
|
||||
|
@ -181,10 +183,15 @@ def show_community(community: Community):
|
|||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
|
||||
if sort == '' or sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.sticky)).order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.sticky)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.sticky)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
@ -251,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,
|
||||
|
@ -652,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"/c/{community.link()}")
|
||||
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',
|
||||
|
@ -691,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,31 @@ 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':
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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()),
|
||||
|
|
|
@ -12,7 +12,7 @@ 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, 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
|
||||
|
@ -113,7 +114,7 @@ def home_page(type, sort):
|
|||
if sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
@ -148,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,
|
||||
|
|
|
@ -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,9 @@ 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, 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,6 +257,7 @@ 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,
|
||||
|
@ -303,10 +304,23 @@ def post_vote(post_id: int, vote_direction):
|
|||
if vote_direction == 'upvote':
|
||||
effect = 1
|
||||
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 += current_app.config['SPICY_UNDER_10']
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
post.score += current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
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 -= current_app.config['SPICY_UNDER_30']
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
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,
|
||||
effect=effect)
|
||||
|
@ -693,6 +707,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)
|
||||
|
||||
|
@ -918,6 +934,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',
|
||||
|
@ -956,7 +1053,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:
|
||||
|
@ -1429,7 +1526,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)
|
||||
|
|
|
@ -5,7 +5,7 @@ from sqlalchemy import desc, text, or_
|
|||
|
||||
from app import db
|
||||
from app.models import PostReply
|
||||
from app.utils import blocked_instances
|
||||
from app.utils import blocked_instances, blocked_users
|
||||
|
||||
|
||||
# replies to a post, in a tree, sorted by a variety of methods
|
||||
|
@ -17,6 +17,9 @@ def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostRe
|
|||
comments = comments.filter(or_(PostReply.instance_id.not_in(instance_ids), PostReply.instance_id == None))
|
||||
if current_user.ignore_bots:
|
||||
comments = comments.filter(PostReply.from_bot == False)
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
comments = comments.filter(PostReply.user_id.not_in(blocked_accounts))
|
||||
if sort_by == 'hot':
|
||||
comments = comments.order_by(desc(PostReply.ranking))
|
||||
elif sort_by == 'top':
|
||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy import or_
|
|||
from app.models import Post
|
||||
from app.search import bp
|
||||
from app.utils import moderating_communities, joined_communities, render_template, blocked_domains, blocked_instances, \
|
||||
communities_banned_from, recently_upvoted_posts, recently_downvoted_posts
|
||||
communities_banned_from, recently_upvoted_posts, recently_downvoted_posts, blocked_users
|
||||
|
||||
|
||||
@bp.route('/search', methods=['GET', 'POST'])
|
||||
|
@ -30,6 +30,10 @@ def run_search():
|
|||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
banned_from = communities_banned_from(current_user.id)
|
||||
if banned_from:
|
||||
posts = posts.filter(Post.community_id.not_in(banned_from))
|
||||
|
|
|
@ -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;
|
||||
|
@ -985,7 +994,8 @@ fieldset legend {
|
|||
}
|
||||
.voting_buttons_new .upvote_button, .voting_buttons_new .downvote_button {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
padding: 5px 0 5px 3px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
|
@ -1012,11 +1022,6 @@ fieldset legend {
|
|||
.voting_buttons_new .upvote_button {
|
||||
top: 1px;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.voting_buttons_new .upvote_button {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
.voting_buttons_new .upvote_button .htmx-indicator {
|
||||
left: 13px;
|
||||
top: 7px;
|
||||
|
@ -1026,14 +1031,7 @@ fieldset legend {
|
|||
}
|
||||
.voting_buttons_new .downvote_button .htmx-indicator {
|
||||
left: 12px;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.voting_buttons_new .downvote_button {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.voting_buttons_new .downvote_button .htmx-indicator {
|
||||
left: 2px;
|
||||
}
|
||||
top: 5px;
|
||||
}
|
||||
.voting_buttons_new .htmx-indicator {
|
||||
position: absolute;
|
||||
|
@ -1125,7 +1123,6 @@ fieldset legend {
|
|||
|
||||
.comment {
|
||||
clear: both;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 15px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
@ -1178,7 +1175,7 @@ fieldset legend {
|
|||
}
|
||||
.comment .comment_actions a {
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
.comment .comment_actions .hide_button {
|
||||
display: inline-block;
|
||||
|
@ -1391,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;
|
||||
|
@ -623,7 +636,8 @@ html {
|
|||
|
||||
.upvote_button, .downvote_button {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
padding: 5px 0 5px 3px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));
|
||||
|
@ -654,9 +668,7 @@ html {
|
|||
|
||||
.upvote_button {
|
||||
top: 1px;
|
||||
@include breakpoint(laptop) {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.htmx-indicator {
|
||||
left: 13px;
|
||||
top: 7px;
|
||||
|
@ -667,14 +679,8 @@ html {
|
|||
top: 1px;
|
||||
.htmx-indicator {
|
||||
left: 12px;
|
||||
top: 5px;
|
||||
}
|
||||
@include breakpoint(laptop) {
|
||||
padding-left: 5px;
|
||||
.htmx-indicator {
|
||||
left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.htmx-indicator{
|
||||
|
@ -776,7 +782,6 @@ html {
|
|||
|
||||
.comment {
|
||||
clear: both;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 15px;
|
||||
padding-top: 8px;
|
||||
|
||||
|
@ -836,7 +841,7 @@ html {
|
|||
position: relative;
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hide_button {
|
||||
|
@ -1058,3 +1063,8 @@ h1 .warning_badge {
|
|||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.responsive-video {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
}
|
|
@ -693,7 +693,7 @@ div.navbar {
|
|||
.comment_actions_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 3px;
|
||||
right: -16px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -284,7 +284,7 @@ div.navbar {
|
|||
.comment_actions_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 3px;
|
||||
right: -16px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<nav class="mb-4">
|
||||
<nav class="mb-1">
|
||||
<h2 class="visually-hidden">{{ _('Admin navigation') }}</h2>
|
||||
<a href="{{ url_for('admin.admin_home') }}">{{ _('Admin home') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
|
||||
|
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_activities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Activities') }}</h1>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>When</th>
|
||||
|
@ -62,4 +58,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,20 +4,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_activities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Activity JSON') }}</h1>
|
||||
<code><pre>{{ activity_json_data | tojson(indent=2) }}</pre></code>
|
||||
{% if current_app.debug %}
|
||||
<p><a class="btn btn-warning" href="{{ url_for('admin.activity_replay', activity_id=activity.id) }}">Re-submit this activity</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Add new user') }}</h3>
|
||||
<h1>{{ _('Add new user') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.user_name) }}
|
||||
|
@ -44,4 +39,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_approve_registrations' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Registrations') }}</h1>
|
||||
{% if registrations %}
|
||||
<p>{{ _('When registering, people are asked "%(question)s".', question=site.application_question) }} </p>
|
||||
<form method="get">
|
||||
|
@ -52,4 +48,11 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_communities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Communities') }}</h1>
|
||||
<form method="get">
|
||||
<input type="search" name="search"> <input type="submit" name="submit" value="Search">
|
||||
</form>
|
||||
|
@ -59,4 +55,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_communities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Edit %(community_name)s', community_name=community.display_name()) }}</h3>
|
||||
<h1>{{ _('Edit %(community_name)s', community_name=community.display_name()) }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_community_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.title) }}
|
||||
|
@ -59,4 +54,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,20 +4,22 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_topics' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
{% if topic %}
|
||||
<h1>{{ _('Edit %(topic_name)s', topic_name=topic.name) }}</h1>
|
||||
{% endif %}
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
{% if topic %}
|
||||
<h3>{{ _('Edit %(topic_name)s', topic_name=topic.name) }}</h3>
|
||||
{% endif %}
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h3>
|
||||
<h1>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ user.about_html|safe if user.about_html }}
|
||||
|
@ -48,4 +43,11 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,19 +4,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_federation' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Federation') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,18 +4,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_misc' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Misc settings') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -1,17 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_newsletter' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Newsletter') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_content_trash' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Most downvoted in the last 3 days') }}</h1>
|
||||
<h1>{{ _('Most downvoted posts in the last 3 days') }}</h1>
|
||||
<div class="post_list">
|
||||
{% for post in posts.items %}
|
||||
{% include 'post/_post_teaser.html' %}
|
||||
|
@ -34,4 +29,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_reports' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Reports') }}</h1>
|
||||
<form method="get">
|
||||
<input type="search" name="search" value="{{ search }}">
|
||||
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||
|
@ -66,4 +62,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_site' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Site profile') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.name) }}
|
||||
|
@ -28,4 +24,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,13 +4,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_topics' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% macro render_topic(topic, depth) %}
|
||||
<tr>
|
||||
<td nowrap="nowrap">{{ '--' * depth }} {{ topic['topic'].name }}</td>
|
||||
|
@ -32,7 +28,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p><a href="{{ url_for('admin.admin_topic_add') }}" class="btn btn-primary">{{ _('Add topic') }}</a></p>
|
||||
<h1><a href="{{ url_for('admin.admin_topic_add') }}" class="btn btn-primary" style="float: right;">{{ _('Add topic') }}</a>{{ _('Topics') }}</h1>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -45,4 +41,11 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Users') }}</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('admin.admin_users_add') }}" style="float: right;">{{ _('Add local user') }}</a>
|
||||
<form method="get">
|
||||
<input type="search" name="search" value="{{ search }}">
|
||||
|
@ -79,4 +75,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -182,7 +182,26 @@
|
|||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/donate">{{ _('Donate') }}</a></li>
|
||||
{% if user_access('change instance settings', current_user.id) %}
|
||||
<li class="nav-item"><a class="nav-link" href="/admin/">{{ _('Admin') }}</a></li>
|
||||
<li class="nav-item dropdown{% if active_parent == 'admin' %} active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/admin/" aria-haspopup="true" aria-expanded="false">{{ _('Admin') }}</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_site' }}" href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_misc' }}" href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_communities' }}" href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_topics' }}" href="{{ url_for('admin.admin_topics') }}">{{ _('Topics') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users' }}" href="{{ url_for('admin.admin_users', local_remote='local') }}">{{ _('Users') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users_trash' }}" href="{{ url_for('admin.admin_users_trash', local_remote='local') }}">{{ _('Monitoring - users') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_trash' }}" href="{{ url_for('admin.admin_content_trash') }}">{{ _('Monitoring - content') }}</a></li>
|
||||
{% if g.site.registration_mode == 'RequireApplication' %}
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_approve_registrations' }}" href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_reports' }}" href="{{ url_for('admin.admin_reports') }}">{{ _('Moderation') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_federation' }}" href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_newsletter' }}" href="{{ url_for('admin.newsletter') }}">{{ _('Newsletter') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_activities' }}" href="{{ url_for('admin.admin_activities') }}">{{ _('Activities') }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/logout">{{ _('Log out') }}</a></li>
|
||||
<li class="nav-item d-none d-md-inline-block">
|
||||
|
|
|
@ -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 %}
|
|
@ -83,6 +83,36 @@
|
|||
<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>
|
||||
{% 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 }}"
|
||||
|
|
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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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, 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
|
||||
|
@ -17,7 +17,7 @@ from app import db, celery, cache
|
|||
from app.topic.forms import ChooseTopicsForm
|
||||
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
||||
community_membership, blocked_domains, validation_required, mimetype_from_url, blocked_instances, \
|
||||
communities_banned_from
|
||||
communities_banned_from, blocked_users
|
||||
|
||||
|
||||
@bp.route('/topic/<path:topic_path>', methods=['GET'])
|
||||
|
@ -63,12 +63,18 @@ def show_topic(topic_path):
|
|||
posts = posts.filter(Post.nsfw == False)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
|
||||
# filter blocked domains and instances
|
||||
domains_ids = blocked_domains(current_user.id)
|
||||
if domains_ids:
|
||||
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
|
||||
banned_from = communities_banned_from(current_user.id)
|
||||
if banned_from:
|
||||
posts = posts.filter(Post.community_id.not_in(banned_from))
|
||||
|
@ -77,7 +83,7 @@ def show_topic(topic_path):
|
|||
if sort == '' or sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
@ -111,7 +117,8 @@ 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)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from app.user.utils import purge_user_then_delete
|
|||
from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \
|
||||
is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership, user_filters_home, \
|
||||
user_filters_posts, user_filters_replies, moderating_communities, joined_communities, theme_list, blocked_instances, \
|
||||
allowlist_html, recently_upvoted_posts, recently_downvoted_posts
|
||||
allowlist_html, recently_upvoted_posts, recently_downvoted_posts, blocked_users
|
||||
from sqlalchemy import desc, or_, text
|
||||
import os
|
||||
|
||||
|
@ -294,6 +294,7 @@ def block_profile(actor):
|
|||
# federate block
|
||||
|
||||
flash(f'{actor} has been blocked.')
|
||||
cache.delete_memoized(blocked_users, current_user.id)
|
||||
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
@ -322,6 +323,7 @@ def unblock_profile(actor):
|
|||
# federate unblock
|
||||
|
||||
flash(f'{actor} has been unblocked.')
|
||||
cache.delete_memoized(blocked_users, current_user.id)
|
||||
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
|
61
app/utils.py
61
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,10 +27,12 @@ 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, \
|
||||
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan, Topic
|
||||
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan, Topic, UserBlock
|
||||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
|
@ -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 == '':
|
||||
|
@ -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):
|
||||
|
@ -335,6 +348,12 @@ def blocked_instances(user_id) -> List[int]:
|
|||
return [block.instance_id for block in blocks]
|
||||
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def blocked_users(user_id) -> List[int]:
|
||||
blocks = UserBlock.query.filter_by(blocker_id=user_id)
|
||||
return [block.blocked_id for block in blocks]
|
||||
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def blocked_phrases() -> List[str]:
|
||||
site = Site.query.get(1)
|
||||
|
@ -872,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')) or 1.0
|
||||
SPICY_UNDER_30 = float(os.environ.get('SPICY_UNDER_30')) or 1.0
|
||||
SPICY_UNDER_60 = float(os.environ.get('SPICY_UNDER_60')) or 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