Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Martynas Sklizmantas 2024-03-19 07:48:23 +01:00
commit 67660d3ee5
26 changed files with 12028 additions and 38 deletions

View file

@ -19,6 +19,13 @@ from sqlalchemy_searchable import make_searchable
from config import Config
def get_locale():
try:
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
except:
return 'en'
db = SQLAlchemy() # engine_options={'pool_size': 5, 'max_overflow': 10} # session_options={"autoflush": False}
migrate = Migrate()
login = LoginManager()
@ -27,7 +34,7 @@ login.login_message = _l('Please log in to access this page.')
mail = Mail()
bootstrap = Bootstrap5()
moment = Moment()
babel = Babel()
babel = Babel(locale_selector=get_locale)
cache = Cache()
celery = Celery(__name__, broker=Config.CELERY_BROKER_URL)
@ -130,11 +137,4 @@ def create_app(config_class=Config):
return app
def get_locale():
try:
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
except:
return 'en_US'
from app import models

View file

@ -125,24 +125,24 @@ def post_to_activity(post: Post, community: Community):
],
"object": {
"id": create_id,
"actor": f"https://{current_app.config['SERVER_NAME']}/u/{post.author.user_name}",
"actor": post.author.ap_public_url,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"object": {
"type": "Page",
"id": f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
"attributedTo": f"https://{current_app.config['SERVER_NAME']}/u/{post.author.user_name}",
"id": post.ap_id,
"attributedTo": post.author.ap_public_url,
"to": [
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
"https://www.w3.org/ns/activitystreams#Public"
],
"name": post.title,
"cc": [],
"content": post.body_html,
"content": post.body_html if post.body_html else '',
"mediaType": "text/html",
"source": {
"content": post.body,
"content": post.body if post.body else '',
"mediaType": "text/markdown"
},
"attachment": [],
@ -200,6 +200,7 @@ def instance_allowed(host: str) -> bool:
def find_actor_or_create(actor: str, create_if_not_found=True, community_only=False) -> Union[User, Community, None]:
actor_url = actor.strip()
actor = actor.strip().lower()
user = None
# actor parameter must be formatted as https://server/u/actor or https://server/c/actor
@ -244,10 +245,15 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
if create_if_not_found:
if actor.startswith('https://'):
try:
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
try:
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
except requests.exceptions.ReadTimeout:
return None
except requests.exceptions.ConnectionError:
return None
if actor_data.status_code == 200:
actor_json = actor_data.json()
actor_data.close()
@ -698,6 +704,9 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
content_type_parts = content_type.split('/')
if content_type_parts:
file_ext = '.' + content_type_parts[-1]
else:
if '?' in file_ext:
file_ext = file_ext.split('?')[0]
new_filename = gibberish(15)
@ -868,7 +877,7 @@ def refresh_instance_profile(instance_id: int):
@celery.task
def refresh_instance_profile_task(instance_id: int):
instance = Instance.query.get(instance_id)
if instance.updated_at < utcnow() - timedelta(days=7):
if instance.inbox is None or instance.updated_at < utcnow() - timedelta(days=7):
try:
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
except:
@ -930,6 +939,11 @@ def refresh_instance_profile_task(instance_id: int):
InstanceRole.instance_id == instance.id,
InstanceRole.role == 'admin').delete()
db.session.commit()
elif instance_data.status_code == 406: # Mastodon does this
instance.software = 'Mastodon'
instance.inbox = f"https://{instance.domain}/inbox"
instance.updated_at = utcnow()
db.session.commit()
# alter the effect of upvotes based on their instance. Default to 1.0

View file

@ -86,7 +86,11 @@ def register(app):
db.session.add(Settings(name='registration_open', value=json.dumps(True)))
db.session.add(Settings(name='approve_registrations', value=json.dumps(False)))
db.session.add(Settings(name='federation', value=json.dumps(True)))
banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net', 'threads.net', 'pieville.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org']
banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net',
'threads.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org',
'poa.st', 'freespeechextremist.com', 'bae.st', 'nicecrew.digital', 'detroitriotcity.com',
'pawoo.net', 'shitposter.club', 'spinster.xyz', 'catgirl.life', 'gameliberty.club',
'yggdrasil.social', 'beefyboys.win', 'brighteon.social', 'cum.salon']
for bi in banned_instances:
db.session.add(BannedInstances(domain=bi))
print("Added banned instance", bi)

View file

@ -121,7 +121,13 @@ def show_community(community: Community):
page = request.args.get('page', 1, type=int)
sort = request.args.get('sort', '' if current_user.is_anonymous else current_user.default_sort)
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
post_layout = request.args.get('layout', community.default_layout if not low_bandwidth else None)
if low_bandwidth:
post_layout = None
else:
if community.default_layout is not None:
post_layout = request.args.get('layout', community.default_layout)
else:
post_layout = request.args.get('layout', 'list')
# If nothing has changed since their last visit, return HTTP 304
current_etag = f"{community.id}{sort}{post_layout}_{hash(community.last_active)}"
@ -846,3 +852,59 @@ def community_notification(community_id: int):
db.session.commit()
return render_template('community/_notification_toggle.html', community=community)
@bp.route('/<actor>/moderate', methods=['GET'])
@login_required
def community_moderate(actor):
community = actor_to_community(actor)
if community is not None:
if community.is_moderator() or current_user.is_admin():
page = request.args.get('page', 1, type=int)
search = request.args.get('search', '')
local_remote = request.args.get('local_remote', '')
reports = Report.query.filter_by(status=0, in_community_id=community.id)
if local_remote == 'local':
reports = reports.filter_by(ap_id=None)
if local_remote == 'remote':
reports = reports.filter(Report.ap_id != None)
reports = reports.order_by(desc(Report.created_at)).paginate(page=page, per_page=1000, error_out=False)
next_url = url_for('admin.admin_reports', page=reports.next_num) if reports.has_next else None
prev_url = url_for('admin.admin_reports', page=reports.prev_num) if reports.has_prev and page != 1 else None
return render_template('community/community_moderate.html', title=_('Moderation of %(community)s', community=community.display_name()),
community=community, reports=reports, current='reports',
next_url=next_url, prev_url=prev_url,
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)
else:
abort(404)
@bp.route('/<actor>/moderate/banned', methods=['GET'])
@login_required
def community_moderate_banned(actor):
community = actor_to_community(actor)
if community is not None:
if community.is_moderator() or current_user.is_admin():
banned_people = User.query.join(CommunityBan, CommunityBan.user_id == User.id).filter(CommunityBan.community_id == community.id).all()
return render_template('community/community_moderate_banned.html',
title=_('People banned from of %(community)s', community=community.display_name()),
community=community, banned_people=banned_people, current='banned',
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)
else:
abort(404)

View file

@ -211,7 +211,7 @@ def save_post(form, post: Post):
filename = opengraph.get('og:image') or opengraph.get('og:image:url')
filename_for_extension = filename.split('?')[0] if '?' in filename else filename
unused, file_extension = os.path.splitext(filename_for_extension)
if file_extension.lower() in allowed_extensions:
if file_extension.lower() in allowed_extensions and not filename.startswith('/'):
file = url_to_thumbnail_file(filename)
if file:
file.alt_text = opengraph.get('og:title')

View file

@ -1120,11 +1120,12 @@ class Report(db.Model):
status = db.Column(db.Integer, default=0)
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
in_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
created_at = db.Column(db.DateTime, default=utcnow)
updated = db.Column(db.DateTime, default=utcnow)

View file

@ -224,7 +224,7 @@ def show_post(post_id: int):
joined_communities=joined_communities(current_user.get_id()),
inoculation=inoculation[randint(0, len(inoculation) - 1)]
)
response.headers.set('Vary', 'Accept, Cookie')
response.headers.set('Vary', 'Accept, Cookie, Accept-Language')
return response
@ -416,7 +416,7 @@ def continue_discussion(post_id, comment_id):
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()), community=post.community,
inoculation=inoculation[randint(0, len(inoculation) - 1)])
response.headers.set('Vary', 'Accept, Cookie')
response.headers.set('Vary', 'Accept, Cookie, Accept-Language')
return response
@ -795,7 +795,7 @@ def post_report(post_id: int):
if form.validate_on_submit():
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
suspect_community_id=post.community.id)
suspect_community_id=post.community.id, in_community_id=post.community.id)
db.session.add(report)
# Notify moderators

View file

@ -0,0 +1,198 @@
/*!
* baguetteBox.js
* @author feimosi
* @version 1.11.1
* @url https://github.com/feimosi/baguetteBox.js
*/
#baguetteBox-overlay {
display: none;
opacity: 0;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000000;
background-color: #222;
background-color: rgba(0, 0, 0, 0.8);
-webkit-transition: opacity .5s ease;
transition: opacity .5s ease; }
#baguetteBox-overlay.visible {
opacity: 1; }
#baguetteBox-overlay .full-image {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
text-align: center; }
#baguetteBox-overlay .full-image figure {
display: inline;
margin: 0;
height: 100%; }
#baguetteBox-overlay .full-image img {
display: inline-block;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
vertical-align: middle;
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); }
#baguetteBox-overlay .full-image figcaption {
display: block;
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: 1.8;
white-space: normal;
color: #ccc;
background-color: #000;
background-color: rgba(0, 0, 0, 0.6);
font-family: sans-serif; }
#baguetteBox-overlay .full-image:before {
content: "";
display: inline-block;
height: 50%;
width: 1px;
margin-right: -1px; }
#baguetteBox-slider {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
white-space: nowrap;
-webkit-transition: left .4s ease, -webkit-transform .4s ease;
transition: left .4s ease, -webkit-transform .4s ease;
transition: left .4s ease, transform .4s ease;
transition: left .4s ease, transform .4s ease, -webkit-transform .4s ease, -moz-transform .4s ease; }
#baguetteBox-slider.bounce-from-right {
-webkit-animation: bounceFromRight .4s ease-out;
animation: bounceFromRight .4s ease-out; }
#baguetteBox-slider.bounce-from-left {
-webkit-animation: bounceFromLeft .4s ease-out;
animation: bounceFromLeft .4s ease-out; }
@-webkit-keyframes bounceFromRight {
0% {
margin-left: 0; }
50% {
margin-left: -30px; }
100% {
margin-left: 0; } }
@keyframes bounceFromRight {
0% {
margin-left: 0; }
50% {
margin-left: -30px; }
100% {
margin-left: 0; } }
@-webkit-keyframes bounceFromLeft {
0% {
margin-left: 0; }
50% {
margin-left: 30px; }
100% {
margin-left: 0; } }
@keyframes bounceFromLeft {
0% {
margin-left: 0; }
50% {
margin-left: 30px; }
100% {
margin-left: 0; } }
.baguetteBox-button#next-button, .baguetteBox-button#previous-button {
top: 50%;
top: calc(50% - 30px);
width: 44px;
height: 60px; }
.baguetteBox-button {
position: absolute;
cursor: pointer;
outline: none;
padding: 0;
margin: 0;
border: 0;
-moz-border-radius: 15%;
border-radius: 15%;
background-color: #323232;
background-color: rgba(50, 50, 50, 0.5);
color: #ddd;
font: 1.6em sans-serif;
-webkit-transition: background-color .4s ease;
transition: background-color .4s ease; }
.baguetteBox-button:focus, .baguetteBox-button:hover {
background-color: rgba(50, 50, 50, 0.9); }
.baguetteBox-button#next-button {
right: 2%; }
.baguetteBox-button#previous-button {
left: 2%; }
.baguetteBox-button#close-button {
top: 20px;
right: 2%;
right: calc(2% + 6px);
width: 30px;
height: 30px; }
.baguetteBox-button svg {
position: absolute;
left: 0;
top: 0; }
/*
Preloader
Borrowed from http://tobiasahlin.com/spinkit/
*/
.baguetteBox-spinner {
width: 40px;
height: 40px;
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px; }
.baguetteBox-double-bounce1,
.baguetteBox-double-bounce2 {
width: 100%;
height: 100%;
-moz-border-radius: 50%;
border-radius: 50%;
background-color: #fff;
opacity: .6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: bounce 2s infinite ease-in-out;
animation: bounce 2s infinite ease-in-out; }
.baguetteBox-double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s; }
@-webkit-keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
transform: scale(0); }
50% {
-webkit-transform: scale(1);
transform: scale(1); } }
@keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
-moz-transform: scale(0);
transform: scale(0); }
50% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }

View file

@ -0,0 +1,793 @@
/*!
* baguetteBox.js
* @author feimosi
* @version 1.11.1
* @url https://github.com/feimosi/baguetteBox.js
*/
/* global define, module */
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.baguetteBox = factory();
}
}(this, function () {
'use strict';
// SVG shapes used on the buttons
var leftArrow = '<svg width="44" height="60">' +
'<polyline points="30 10 10 30 30 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
'</svg>',
rightArrow = '<svg width="44" height="60">' +
'<polyline points="14 10 34 30 14 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
'</svg>',
closeX = '<svg width="30" height="30">' +
'<g stroke="rgb(160,160,160)" stroke-width="4">' +
'<line x1="5" y1="5" x2="25" y2="25"/>' +
'<line x1="5" y1="25" x2="25" y2="5"/>' +
'</g></svg>';
// Global options and their defaults
var options = {},
defaults = {
captions: true,
buttons: 'auto',
fullScreen: false,
noScrollbars: false,
bodyClass: 'baguetteBox-open',
titleTag: false,
async: false,
preload: 2,
animation: 'slideIn',
afterShow: null,
afterHide: null,
onChange: null,
overlayBackgroundColor: 'rgba(0,0,0,.8)'
};
// Object containing information about features compatibility
var supports = {};
// DOM Elements references
var overlay, slider, previousButton, nextButton, closeButton;
// An array with all images in the current gallery
var currentGallery = [];
// Current image index inside the slider
var currentIndex = 0;
// Visibility of the overlay
var isOverlayVisible = false;
// Touch event start position (for slide gesture)
var touch = {};
// If set to true ignore touch events because animation was already fired
var touchFlag = false;
// Regex pattern to match image files
var regex = /.+\.(gif|jpe?g|png|webp)/i;
// Object of all used galleries
var data = {};
// Array containing temporary images DOM elements
var imagesElements = [];
// The last focused element before opening the overlay
var documentLastFocus = null;
var overlayClickHandler = function(event) {
// Close the overlay when user clicks directly on the background
if (event.target.id.indexOf('baguette-img') !== -1) {
hideOverlay();
}
};
var previousButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
showPreviousImage();
};
var nextButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
showNextImage();
};
var closeButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
hideOverlay();
};
var touchstartHandler = function(event) {
touch.count++;
if (touch.count > 1) {
touch.multitouch = true;
}
// Save x and y axis position
touch.startX = event.changedTouches[0].pageX;
touch.startY = event.changedTouches[0].pageY;
};
var touchmoveHandler = function(event) {
// If action was already triggered or multitouch return
if (touchFlag || touch.multitouch) {
return;
}
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
var touchEvent = event.touches[0] || event.changedTouches[0];
// Move at least 40 pixels to trigger the action
if (touchEvent.pageX - touch.startX > 40) {
touchFlag = true;
showPreviousImage();
} else if (touchEvent.pageX - touch.startX < -40) {
touchFlag = true;
showNextImage();
// Move 100 pixels up to close the overlay
} else if (touch.startY - touchEvent.pageY > 100) {
hideOverlay();
}
};
var touchendHandler = function() {
touch.count--;
if (touch.count <= 0) {
touch.multitouch = false;
}
touchFlag = false;
};
var contextmenuHandler = function() {
touchendHandler();
};
var trapFocusInsideOverlay = function(event) {
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
event.stopPropagation();
initFocus();
}
};
// forEach polyfill for IE8
// http://stackoverflow.com/a/14827443/1077846
/* eslint-disable */
if (![].forEach) {
Array.prototype.forEach = function(callback, thisArg) {
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
// filter polyfill for IE8
// https://gist.github.com/eliperelman/1031656
if (![].filter) {
Array.prototype.filter = function(a, b, c, d, e) {
c = this;
d = [];
for (e = 0; e < c.length; e++)
a.call(b, c[e], e, c) && d.push(c[e]);
return d;
};
}
/* eslint-enable */
// Script entry point
function run(selector, userOptions) {
// Fill supports object
supports.transforms = testTransformsSupport();
supports.svg = testSvgSupport();
supports.passiveEvents = testPassiveEventsSupport();
buildOverlay();
removeFromCache(selector);
return bindImageClickListeners(selector, userOptions);
}
function bindImageClickListeners(selector, userOptions) {
// For each gallery bind a click event to every image inside it
var galleryNodeList = document.querySelectorAll(selector);
var selectorData = {
galleries: [],
nodeList: galleryNodeList
};
data[selector] = selectorData;
[].forEach.call(galleryNodeList, function(galleryElement) {
if (userOptions && userOptions.filter) {
regex = userOptions.filter;
}
// Get nodes from gallery elements or single-element galleries
var tagsNodeList = [];
if (galleryElement.tagName === 'A') {
tagsNodeList = [galleryElement];
} else {
tagsNodeList = galleryElement.getElementsByTagName('a');
}
// Filter 'a' elements from those not linking to images
tagsNodeList = [].filter.call(tagsNodeList, function(element) {
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
return regex.test(element.href);
}
});
if (tagsNodeList.length === 0) {
return;
}
var gallery = [];
[].forEach.call(tagsNodeList, function(imageElement, imageIndex) {
var imageElementClickHandler = function(event) {
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
prepareOverlay(gallery, userOptions);
showOverlay(imageIndex);
};
var imageItem = {
eventHandler: imageElementClickHandler,
imageElement: imageElement
};
bind(imageElement, 'click', imageElementClickHandler);
gallery.push(imageItem);
});
selectorData.galleries.push(gallery);
});
return selectorData.galleries;
}
function clearCachedData() {
for (var selector in data) {
if (data.hasOwnProperty(selector)) {
removeFromCache(selector);
}
}
}
function removeFromCache(selector) {
if (!data.hasOwnProperty(selector)) {
return;
}
var galleries = data[selector].galleries;
[].forEach.call(galleries, function(gallery) {
[].forEach.call(gallery, function(imageItem) {
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
});
if (currentGallery === gallery) {
currentGallery = [];
}
});
delete data[selector];
}
function buildOverlay() {
overlay = getByID('baguetteBox-overlay');
// Check if the overlay already exists
if (overlay) {
slider = getByID('baguetteBox-slider');
previousButton = getByID('previous-button');
nextButton = getByID('next-button');
closeButton = getByID('close-button');
return;
}
// Create overlay element
overlay = create('div');
overlay.setAttribute('role', 'dialog');
overlay.id = 'baguetteBox-overlay';
document.getElementsByTagName('body')[0].appendChild(overlay);
// Create gallery slider element
slider = create('div');
slider.id = 'baguetteBox-slider';
overlay.appendChild(slider);
// Create all necessary buttons
previousButton = create('button');
previousButton.setAttribute('type', 'button');
previousButton.id = 'previous-button';
previousButton.setAttribute('aria-label', 'Previous');
previousButton.innerHTML = supports.svg ? leftArrow : '&lt;';
overlay.appendChild(previousButton);
nextButton = create('button');
nextButton.setAttribute('type', 'button');
nextButton.id = 'next-button';
nextButton.setAttribute('aria-label', 'Next');
nextButton.innerHTML = supports.svg ? rightArrow : '&gt;';
overlay.appendChild(nextButton);
closeButton = create('button');
closeButton.setAttribute('type', 'button');
closeButton.id = 'close-button';
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = supports.svg ? closeX : '&times;';
overlay.appendChild(closeButton);
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
bindEvents();
}
function keyDownHandler(event) {
switch (event.keyCode) {
case 37: // Left arrow
showPreviousImage();
break;
case 39: // Right arrow
showNextImage();
break;
case 27: // Esc
hideOverlay();
break;
case 36: // Home
showFirstImage(event);
break;
case 35: // End
showLastImage(event);
break;
}
}
function bindEvents() {
var passiveEvent = supports.passiveEvents ? { passive: false } : null;
var nonPassiveEvent = supports.passiveEvents ? { passive: true } : null;
bind(overlay, 'click', overlayClickHandler);
bind(previousButton, 'click', previousButtonClickHandler);
bind(nextButton, 'click', nextButtonClickHandler);
bind(closeButton, 'click', closeButtonClickHandler);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
bind(overlay, 'touchend', touchendHandler);
bind(document, 'focus', trapFocusInsideOverlay, true);
}
function unbindEvents() {
var passiveEvent = supports.passiveEvents ? { passive: false } : null;
var nonPassiveEvent = supports.passiveEvents ? { passive: true } : null;
unbind(overlay, 'click', overlayClickHandler);
unbind(previousButton, 'click', previousButtonClickHandler);
unbind(nextButton, 'click', nextButtonClickHandler);
unbind(closeButton, 'click', closeButtonClickHandler);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
}
function prepareOverlay(gallery, userOptions) {
// If the same gallery is being opened prevent from loading it once again
if (currentGallery === gallery) {
return;
}
currentGallery = gallery;
// Update gallery specific options
setOptions(userOptions);
// Empty slider of previous contents (more effective than .innerHTML = "")
while (slider.firstChild) {
slider.removeChild(slider.firstChild);
}
imagesElements.length = 0;
var imagesFiguresIds = [];
var imagesCaptionsIds = [];
// Prepare and append images containers and populate figure and captions IDs arrays
for (var i = 0, fullImage; i < gallery.length; i++) {
fullImage = create('div');
fullImage.className = 'full-image';
fullImage.id = 'baguette-img-' + i;
imagesElements.push(fullImage);
imagesFiguresIds.push('baguetteBox-figure-' + i);
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
slider.appendChild(imagesElements[i]);
}
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
}
function setOptions(newOptions) {
if (!newOptions) {
newOptions = {};
}
// Fill options object
for (var item in defaults) {
options[item] = defaults[item];
if (typeof newOptions[item] !== 'undefined') {
options[item] = newOptions[item];
}
}
/* Apply new options */
// Change transition for proper animation
slider.style.transition = slider.style.webkitTransition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
options.animation === 'slideIn' ? '' : 'none');
// Hide buttons if necessary
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
options.buttons = false;
}
// Set buttons style to hide or display them
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
// Set overlay color
try {
overlay.style.backgroundColor = options.overlayBackgroundColor;
} catch (e) {
// Silence the error and continue
}
}
function showOverlay(chosenImageIndex) {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'hidden';
document.body.style.overflowY = 'scroll';
}
if (overlay.style.display === 'block') {
return;
}
bind(document, 'keydown', keyDownHandler);
currentIndex = chosenImageIndex;
touch = {
count: 0,
startX: null,
startY: null
};
loadImage(currentIndex, function() {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
overlay.style.display = 'block';
if (options.fullScreen) {
enterFullScreen();
}
// Fade in overlay
setTimeout(function() {
overlay.className = 'visible';
if (options.bodyClass && document.body.classList) {
document.body.classList.add(options.bodyClass);
}
if (options.afterShow) {
options.afterShow();
}
}, 50);
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
documentLastFocus = document.activeElement;
initFocus();
isOverlayVisible = true;
}
function initFocus() {
if (options.buttons) {
previousButton.focus();
} else {
closeButton.focus();
}
}
function enterFullScreen() {
if (overlay.requestFullscreen) {
overlay.requestFullscreen();
} else if (overlay.webkitRequestFullscreen) {
overlay.webkitRequestFullscreen();
} else if (overlay.mozRequestFullScreen) {
overlay.mozRequestFullScreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function hideOverlay() {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'auto';
document.body.style.overflowY = 'auto';
}
if (overlay.style.display === 'none') {
return;
}
unbind(document, 'keydown', keyDownHandler);
// Fade out and hide the overlay
overlay.className = '';
setTimeout(function() {
overlay.style.display = 'none';
if (document.fullscreen) {
exitFullscreen();
}
if (options.bodyClass && document.body.classList) {
document.body.classList.remove(options.bodyClass);
}
if (options.afterHide) {
options.afterHide();
}
documentLastFocus && documentLastFocus.focus();
isOverlayVisible = false;
}, 500);
}
function loadImage(index, callback) {
var imageContainer = imagesElements[index];
var galleryItem = currentGallery[index];
// Return if the index exceeds prepared images in the overlay
// or if the current gallery has been changed / closed
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
return;
}
// If image is already loaded run callback and return
if (imageContainer.getElementsByTagName('img')[0]) {
if (callback) {
callback();
}
return;
}
// Get element reference, optional caption and source path
var imageElement = galleryItem.imageElement;
var thumbnailElement = imageElement.getElementsByTagName('img')[0];
var imageCaption = typeof options.captions === 'function' ?
options.captions.call(currentGallery, imageElement) :
imageElement.getAttribute('data-caption') || imageElement.title;
var imageSrc = getImageSrc(imageElement);
// Prepare figure element
var figure = create('figure');
figure.id = 'baguetteBox-figure-' + index;
figure.innerHTML = '<div class="baguetteBox-spinner">' +
'<div class="baguetteBox-double-bounce1"></div>' +
'<div class="baguetteBox-double-bounce2"></div>' +
'</div>';
// Insert caption if available
if (options.captions && imageCaption) {
var figcaption = create('figcaption');
figcaption.id = 'baguetteBox-figcaption-' + index;
figcaption.innerHTML = imageCaption;
figure.appendChild(figcaption);
}
imageContainer.appendChild(figure);
// Prepare gallery img element
var image = create('img');
image.onload = function() {
// Remove loader element
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
figure.removeChild(spinner);
if (!options.async && callback) {
callback();
}
};
image.setAttribute('src', imageSrc);
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption) {
image.title = imageCaption;
}
figure.appendChild(image);
// Run callback
if (options.async && callback) {
callback();
}
}
// Get image source location, mostly used for responsive images
function getImageSrc(image) {
// Set default image path from href
var result = image.href;
// If dataset is supported find the most suitable image
if (image.dataset) {
var srcs = [];
// Get all possible image versions depending on the resolution
for (var item in image.dataset) {
if (item.substring(0, 3) === 'at-' && !isNaN(item.substring(3))) {
srcs[item.replace('at-', '')] = image.dataset[item];
}
}
// Sort resolutions ascending
var keys = Object.keys(srcs).sort(function(a, b) {
return parseInt(a, 10) < parseInt(b, 10) ? -1 : 1;
});
// Get real screen resolution
var width = window.innerWidth * window.devicePixelRatio;
// Find the first image bigger than or equal to the current width
var i = 0;
while (i < keys.length - 1 && keys[i] < width) {
i++;
}
result = srcs[keys[i]] || result;
}
return result;
}
// Return false at the right end of the gallery
function showNextImage() {
return show(currentIndex + 1);
}
// Return false at the left end of the gallery
function showPreviousImage() {
return show(currentIndex - 1);
}
// Return false at the left end of the gallery
function showFirstImage(event) {
if (event) {
event.preventDefault();
}
return show(0);
}
// Return false at the right end of the gallery
function showLastImage(event) {
if (event) {
event.preventDefault();
}
return show(currentGallery.length - 1);
}
/**
* Move the gallery to a specific index
* @param `index` {number} - the position of the image
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
* @return {boolean} - true on success or false if the index is invalid
*/
function show(index, gallery) {
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
prepareOverlay(gallery, options);
showOverlay(index);
return true;
}
if (index < 0) {
if (options.animation) {
bounceAnimation('left');
}
return false;
}
if (index >= imagesElements.length) {
if (options.animation) {
bounceAnimation('right');
}
return false;
}
currentIndex = index;
loadImage(currentIndex, function() {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
return true;
}
/**
* Triggers the bounce animation
* @param {('left'|'right')} direction - Direction of the movement
*/
function bounceAnimation(direction) {
slider.className = 'bounce-from-' + direction;
setTimeout(function() {
slider.className = '';
}, 400);
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
setTimeout(function() {
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
slider.style.opacity = 1;
}, 400);
} else {
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
}
}
// CSS 3D Transforms test
function testTransformsSupport() {
var div = create('div');
return typeof div.style.perspective !== 'undefined' || typeof div.style.webkitPerspective !== 'undefined';
}
// Inline SVG test
function testSvgSupport() {
var div = create('div');
div.innerHTML = '<svg/>';
return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
}
// Borrowed from https://github.com/seiyria/bootstrap-slider/pull/680/files
/* eslint-disable getter-return */
function testPassiveEventsSupport() {
var passiveEvents = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
passiveEvents = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) { /* Silence the error and continue */ }
return passiveEvents;
}
/* eslint-enable getter-return */
function preloadNext(index) {
if (index - currentIndex >= options.preload) {
return;
}
loadImage(index + 1, function() {
preloadNext(index + 1);
});
}
function preloadPrev(index) {
if (currentIndex - index >= options.preload) {
return;
}
loadImage(index - 1, function() {
preloadPrev(index - 1);
});
}
function bind(element, event, callback, options) {
if (element.addEventListener) {
element.addEventListener(event, callback, options);
} else {
// IE8 fallback
element.attachEvent('on' + event, function(event) {
// `event` and `event.target` are not provided in IE8
event = event || window.event;
event.target = event.target || event.srcElement;
callback(event);
});
}
}
function unbind(element, event, callback, options) {
if (element.removeEventListener) {
element.removeEventListener(event, callback, options);
} else {
// IE8 fallback
element.detachEvent('on' + event, callback);
}
}
function getByID(id) {
return document.getElementById(id);
}
function create(element) {
return document.createElement(element);
}
function destroyPlugin() {
unbindEvents();
clearCachedData();
unbind(document, 'keydown', keyDownHandler);
document.getElementsByTagName('body')[0].removeChild(document.getElementById('baguetteBox-overlay'));
data = {};
currentGallery = [];
currentIndex = 0;
}
return {
run: run,
show: show,
showNext: showNextImage,
showPrevious: showPreviousImage,
hide: hideOverlay,
destroy: destroyPlugin
};
}));

View file

@ -12,8 +12,37 @@ document.addEventListener("DOMContentLoaded", function () {
setupTopicChooser();
setupConversationChooser();
setupMarkdownEditorEnabler();
setupLightboxGallery();
});
function setupLightboxGallery() {
// Check if there are elements with either "post_list_masonry_wide" or "post_list_masonry" class
var widePosts = document.querySelectorAll('.post_list_masonry_wide');
var regularPosts = document.querySelectorAll('.post_list_masonry');
// Enable lightbox on masonry images
if (widePosts.length > 0) {
baguetteBox.run('.post_list_masonry_wide', {
fullScreen: false,
titleTag: true,
preload: 5,
captions: function(element) {
return element.getElementsByTagName('img')[0].title;
}
});
}
if (regularPosts.length > 0) {
baguetteBox.run('.post_list_masonry', {
fullScreen: false,
titleTag: true,
preload: 5,
captions: function(element) {
return element.getElementsByTagName('img')[0].title;
}
});
}
}
// fires after all resources have loaded, including stylesheets and js files
window.addEventListener("load", function () {

View file

@ -43,6 +43,7 @@
<link href="{{ url_for('static', filename='themes/high_contrast/styles.css') }}" type="text/css" rel="alternate stylesheet" title="High contrast" />
{% if not low_bandwidth %}
<link href="{{ url_for('static', filename='js/markdown/downarea.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ url_for('static', filename='js/lightbox/baguetteBox.css') }}" type="text/css" rel="stylesheet" />
{% endif %}
{% if theme() and file_exists('app/templates/themes/' + theme() + '/styles.css') %}
<link href="{{ url_for('static', filename='themes/' + theme() + '/styles.css') }}" type="text/css" rel="stylesheet" />
@ -234,10 +235,14 @@
{% endblock %}
{% if not low_bandwidth %}
{{ str(bootstrap.load_js()).replace('<script ', '<script nonce="' + session['nonce'] + '" ')|safe }}
<script src="{{ url_for('static', filename='js/lightbox/baguetteBox.js') }}" nonce="{{ session['nonce'] }}"></script>
{% endif %}
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
{% if not low_bandwidth %}
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
<!-- -->
{% endif %}
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
{% endif %}
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}

View file

@ -0,0 +1,14 @@
<div class="btn-group mt-1 mb-2">
<a href="/community/{{ community.link() }}/moderate" aria-label="{{ _('Sort by hot') }}" class="btn {{ 'btn-primary' if current == '' or current == 'reports' else 'btn-outline-secondary' }}" rel="nofollow noindex">
{{ _('Reports') }}
</a>
<a href="/community/{{ community.link() }}/moderate/banned" class="btn {{ 'btn-primary' if current == 'banned' else 'btn-outline-secondary' }}" rel="nofollow noindex">
{{ _('Banned people') }}
</a>
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }}" rel="nofollow noindex">
{{ _('Appeals') }}
</a>
<a href="/community/{{ community.link() }}/moderate/modlog" class="btn {{ 'btn-primary' if current == 'modlog' else 'btn-outline-secondary' }}" rel="nofollow noindex">
{{ _('Mod log') }}
</a>
</div>

View file

@ -175,7 +175,9 @@
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
{% if is_moderator or is_owner or is_admin %}
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
{% endif %}
{% if is_owner or is_admin %}
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
{% endif %}

View file

@ -0,0 +1,84 @@
{% 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">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
</ol>
</nav>
<div class="row">
<div class="col-12 col-md-10">
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
</div>
<div class="col-12 col-md-2 text-right">
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
</div>
</div>
{% include "community/_community_moderation_nav.html" %}
<h2>{{ _('Reports') }}</h2>
{% if reports.items %}
<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>
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
<input type="submit" name="submit" value="Search" class="btn btn-primary">
</form>
<table class="table table-striped">
<tr>
<th>Local/Remote</th>
<th>Reasons</th>
<th>Description</th>
<th>Type</th>
<th>Created</th>
<th>Actions</th>
</tr>
{% for report in reports.items %}
<tr>
<td>{{ 'Local' if report.is_local() else 'Remote' }}</td>
<td>{{ report.reasons }}</td>
<td>{{ report.description }}</td>
<td>{{ report.type_text() }}</td>
<td>{{ moment(report.created_at).fromNow() }}</td>
<td>
{% if report.suspect_conversation_id %}
<a href="/chat/{{ report.suspect_conversation_id }}#message">View</a>
{% elif report.suspect_post_reply_id %}
<a href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a>
{% elif report.suspect_post_id %}
<a href="/post/{{ report.suspect_post_id }}">View</a>
{% elif report.suspect_user_id %}
<a href="/user/{{ report.suspect_user_id }}">View</a>
{% elif report.suspect_community_id %}
<a href="/user/{{ report.suspect_community_id }}">View</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<nav aria-label="Pagination" class="mt-4" role="navigation">
{% if prev_url %}
<a href="{{ prev_url }}" class="btn btn-primary">
<span aria-hidden="true">&larr;</span> {{ _('Previous page') }}
</a>
{% endif %}
{% if next_url %}
<a href="{{ next_url }}" class="btn btn-primary">
{{ _('Next page') }} <span aria-hidden="true">&rarr;</span>
</a>
{% endif %}
</nav>
{% else %}
<p>{{ _('No reports yet') }}</p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,65 @@
{% 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">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
</ol>
</nav>
<div class="row">
<div class="col-12 col-md-10">
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
</div>
<div class="col-12 col-md-2 text-right">
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
</div>
</div>
{% include "community/_community_moderation_nav.html" %}
<h2>{{ _('Banned people') }}</h2>
{% if banned_people %}
<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>
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
<input type="submit" name="submit" value="Search" class="btn btn-primary">
</form>
<table class="table table-striped mt-1">
<tr>
<th>Name</th>
<th>Local/Remote</th>
<th>Reports</th>
<th>IP</th>
<th>Actions</th>
</tr>
{% for user in banned_people %}
<tr>
<td><img src="{{ user.avatar_thumbnail() }}" class="community_icon rounded-circle" loading="lazy" />
{{ user.display_name() }}</td>
<td>{{ 'Local' if user.is_local() else 'Remote' }}</td>
<td>{{ user.reports if user.reports > 0 }} </td>
<td>{{ user.ip_address if user.ip_address }} </td>
<td>{% if user.is_local() %}
<a href="/u/{{ user.link() }}">View local</a>
{% else %}
<a href="{{ user.ap_profile_id }}">View remote</a>
{% endif %}
| <a href="#" class="confirm_first">{{ _('Un ban') }}</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>{{ _('No banned people yet') }}</p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -14,20 +14,23 @@
{% if post.type == POST_TYPE_LINK %}
{% 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 '' }}" loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" title="{{ post.title }}"
loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
{% elif post.image.source_url %}
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.source_url }}"
alt="{{ post.title }}" loading="lazy" /></a>
alt="{{ post.title }}" title="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
{% else %}
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.url }}"
alt="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
alt="{{ post.title }}" title="{{ post.title }}"
loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
{% endif %}
{% elif post.type == POST_TYPE_IMAGE %}
<a href="{{ post.image.view_url() }}" rel="nofollow ugc" target="_blank"><img src="{{ post.image.medium_url() }}"
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" title="{{ post.title }}"
loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
{% else %}
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}"
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" /></a>
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
{% endif %}
</div>
<div class="masonry_info">
@ -49,8 +52,9 @@
{% else %}
{% if post.url and (post.url.endswith('.jpg') or post.url.endswith('.webp') or post.url.endswith('.png') or post.url.endswith('.gif') or post.url.endswith('.avif') or post.url.endswith('.jpeg')) %}
<div class="masonry_thumb" title="{{ post.title }}">
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('See image') }}"><img src="{{ post.url }}"
alt="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.url }}"
alt="{{ post.title }}" title="{{ post.title }}"
loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
</div>
<div class="masonry_info">
<div class="row">

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,9 @@ import math
from urllib.parse import urlparse, parse_qs, urlencode
from functools import wraps
import flask
from bs4 import BeautifulSoup, NavigableString
from bs4 import BeautifulSoup, NavigableString, MarkupResemblesLocatorWarning
import warnings
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
import requests
import os
from flask import current_app, json, redirect, url_for, request, make_response, Response, g
@ -57,7 +59,7 @@ def return_304(etag, content_type=None):
resp = make_response('', 304)
resp.headers.add_header('ETag', request.headers['If-None-Match'])
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
resp.headers.add_header('Vary', 'Accept, Cookie')
resp.headers.add_header('Vary', 'Accept, Cookie, Accept-Language')
if content_type:
resp.headers.set('Content-Type', content_type)
return resp
@ -91,6 +93,9 @@ def get_request(uri, params=None, headers=None) -> requests.Response:
except requests.exceptions.ReadTimeout as read_timeout:
current_app.logger.info(f"{uri} {read_timeout}")
raise requests.exceptions.ReadTimeout from read_timeout
except requests.exceptions.ConnectionError as connection_error:
current_app.logger.info(f"{uri} {connection_error}")
raise requests.exceptions.ConnectionError from connection_error
return response
@ -194,7 +199,7 @@ def allowlist_html(html: str) -> str:
else:
# Filter and sanitize attributes
for attr in list(tag.attrs):
if attr not in ['href', 'src', 'alt']:
if attr not in ['href', 'src', 'alt', 'class']:
del tag[attr]
# Add nofollow and target=_blank to anchors
if tag.name == 'a':
@ -246,7 +251,7 @@ def html_to_markdown_worker(element, indent_level=0):
def markdown_to_html(markdown_text) -> str:
if markdown_text:
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True}))
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'spoiler': True}))
else:
return ''

View file

@ -1,3 +1,2 @@
[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
[jinja2: app/templates/**.html]

View file

@ -22,7 +22,7 @@ class Config(object):
RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY")
MODE = os.environ.get('MODE') or 'development'
LANGUAGES = ['en']
LANGUAGES = ['de', 'en']
FULL_AP_CONTEXT = bool(int(os.environ.get('FULL_AP_CONTEXT', 0)))
CACHE_TYPE = os.environ.get('CACHE_TYPE') or 'FileSystemCache'
CACHE_REDIS_URL = os.environ.get('CACHE_REDIS_URL') or 'redis://localhost:6379/1'

View file

@ -4,6 +4,8 @@ date > updated.txt
sudo systemctl stop celery.service
git pull
source venv/bin/activate
export FLASK_APP=pyfedi.py
flask db upgrade
pybabel compile -d app/translations
sudo systemctl start celery.service
sudo systemctl restart pyfedi.service

View file

@ -16,3 +16,18 @@ python profile_app.py
instead of
flask run
translations:
See https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiii-i18n-and-l10n-2018
to add a new language which can be worked on:
pybabel init -i messages.pot -d app/translations -l <language code>
after changes to the files in app/translations/* are mode, they need to be compiled
pybabel compile -d app/translations

View file

@ -0,0 +1,38 @@
"""report in community
Revision ID: 81175e11c083
Revises: e72aa356e4d0
Create Date: 2024-03-18 20:37:43.216482
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '81175e11c083'
down_revision = 'e72aa356e4d0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('report', schema=None) as batch_op:
batch_op.add_column(sa.Column('in_community_id', sa.Integer(), nullable=True))
batch_op.drop_constraint('report_suspect_community_id_fkey', type_='foreignkey')
batch_op.create_foreign_key(None, 'community', ['suspect_community_id'], ['id'])
batch_op.create_foreign_key(None, 'community', ['in_community_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('report', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.create_foreign_key('report_suspect_community_id_fkey', 'user', ['suspect_community_id'], ['id'])
batch_op.drop_column('in_community_id')
# ### end Alembic commands ###