Merge remote-tracking branch 'upstream/main'

This commit is contained in:
rra 2024-03-26 16:12:37 +01:00
commit 6d2ddb92fa
26 changed files with 213 additions and 81 deletions

View file

@ -120,9 +120,17 @@ If it does not work check the log file at logs/pyfedi.log for clues.
<div id="initialise-database-and-setup-admin-account"></div>
## Initialise database, and set up admin account
`flask init-db`
`export FLASK_APP=pyfedi.py`
`flask db upgrade`
`flask init-db`
(choose a new username, email address, and password for your PyFedi admin account)
If you see an error message "ModuleNotFoundError: No module named 'flask_babel'" then use `venv/bin/flask` instead of `flask`
for all flask commands.
<div id="run-the-app"></div>
## Run the app

View file

@ -227,6 +227,11 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
return None
if user is None:
user = Community.query.filter(Community.ap_profile_id == actor).first()
if user and user.banned:
# Try to find a non-banned copy of the community. Sometimes duplicates happen and one copy is banned.
user = Community.query.filter(Community.ap_profile_id == actor).filter(Community.banned == False).first()
if user is None: # no un-banned version of this community exists, only the banned one. So it was banned for being bad, not for being a duplicate.
return None
if user is not None:
if not user.is_local() and (user.ap_fetched_at is None or user.ap_fetched_at < utcnow() - timedelta(days=7)):
@ -564,7 +569,7 @@ def actor_json_to_model(activity_json, address, server):
ap_followers_url=activity_json['followers'],
ap_inbox_url=activity_json['endpoints']['sharedInbox'],
ap_outbox_url=activity_json['outbox'],
ap_featured_url=activity_json['featured'],
ap_featured_url=activity_json['featured'] if 'featured' in activity_json else '',
ap_moderators_url=mods_url,
ap_fetched_at=utcnow(),
ap_domain=server,
@ -892,12 +897,15 @@ def refresh_instance_profile_task(instance_id: int):
except requests.exceptions.JSONDecodeError as ex:
instance_json = {}
if 'type' in instance_json and instance_json['type'] == 'Application':
# 'name' is unreliable as the admin can change it to anything. todo: find better way
if instance_json['name'].lower() == 'kbin':
software = 'Kbin'
elif instance_json['name'].lower() == 'mbin':
software = 'Mbin'
elif instance_json['name'].lower() == 'piefed':
software = 'PieFed'
elif instance_json['name'].lower() == 'system account':
software = 'Friendica'
else:
software = 'Lemmy'
instance.inbox = instance_json['inbox']

View file

@ -627,7 +627,7 @@ def admin_users_add():
private_key, public_key = RsaKeys.generate_keypair()
user.private_key = private_key
user.public_key = public_key
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower()
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
user.roles.append(Role.query.get(form.role.data))

View file

@ -145,7 +145,7 @@ def chat_report(conversation_id):
if form.validate_on_submit():
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
type=4, reporter_id=current_user.id, suspect_conversation_id=conversation_id)
type=4, reporter_id=current_user.id, suspect_conversation_id=conversation_id, source_instance_id=1)
db.session.add(report)
# Notify site admin

View file

@ -76,15 +76,13 @@ def register(app):
db.configure_mappers()
db.create_all()
private_key, public_key = RsaKeys.generate_keypair()
db.session.add(Site(name="PieFed", description='', public_key=public_key, private_key=private_key))
db.session.add(Site(name="PieFed", description='Explore Anything, Discuss Everything.', public_key=public_key, private_key=private_key))
db.session.add(Instance(domain=app.config['SERVER_NAME'], software='PieFed')) # Instance 1 is always the local instance
db.session.add(Settings(name='allow_nsfw', value=json.dumps(False)))
db.session.add(Settings(name='allow_nsfl', value=json.dumps(False)))
db.session.add(Settings(name='allow_dislike', value=json.dumps(True)))
db.session.add(Settings(name='allow_local_image_posts', value=json.dumps(True)))
db.session.add(Settings(name='allow_remote_image_posts', value=json.dumps(True)))
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', 'noauthority.social', 'pieville.net', 'links.hackliberty.org',
@ -95,21 +93,6 @@ def register(app):
db.session.add(BannedInstances(domain=bi))
print("Added banned instance", bi)
print("Populating DB with instances and interests")
print("See interests.txt")
interests = file_get_contents('interests.txt')
db.session.add(Interest(name='🕊 Chilling', communities=parse_communities(interests, 'chilling')))
db.session.add(Interest(name='💭 Interesting stuff', communities=parse_communities(interests, 'interesting stuff')))
db.session.add(Interest(name='📰 News & Politics', communities=parse_communities(interests, 'news & politics')))
db.session.add(Interest(name='🎮 Gaming', communities=parse_communities(interests, 'gaming')))
db.session.add(Interest(name='🤓 Linux', communities=parse_communities(interests, 'linux')))
db.session.add(Interest(name='♻️ Environment', communities=parse_communities(interests, 'environment')))
db.session.add(Interest(name='🏳‍🌈 LGBTQ+', communities=parse_communities(interests, 'lgbtq')))
db.session.add(Interest(name='🛠 Programming', communities=parse_communities(interests, 'programming')))
db.session.add(Interest(name='🖥️ Tech', communities=parse_communities(interests, 'tech')))
db.session.add(Interest(name='🤗 Mental Health', communities=parse_communities(interests, 'mental health')))
db.session.add(Interest(name='💊 Health', communities=parse_communities(interests, 'health')))
# Load initial domain block list
block_list = retrieve_block_list()
if block_list:

View file

@ -136,9 +136,10 @@ class CreatePostForm(FlaskForm):
current_user.reputation -= 1
db.session.commit()
return False
community = Community.query.get(self.communities.data)
if community.is_local() and g.site.allow_local_image_posts is False:
self.communities.errors.append(_('Images cannot be posted to local communities.'))
if self.communities:
community = Community.query.get(self.communities.data)
if community.is_local() and g.site.allow_local_image_posts is False:
self.communities.errors.append(_('Images cannot be posted to local communities.'))
elif self.post_type.data == 'poll':
self.discussion_title.errors.append(_('Poll not implemented yet.'))
return False

View file

@ -9,7 +9,7 @@ from sqlalchemy import or_, desc
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
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes
from app.chat.util import send_message
from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm
@ -49,7 +49,8 @@ def add_local():
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
public_key=public_key, description_html=markdown_to_html(form.description.data),
rules_html=markdown_to_html(form.rules.data), local_only=form.local_only.data,
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data.lower(),
ap_public_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
ap_followers_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data + '/followers',
ap_domain=current_app.config['SERVER_NAME'],
subscriptions_count=1, instance_id=1, low_quality='memes' in form.url.data)
@ -102,9 +103,9 @@ def add_remote():
flash(_('Community not found.'), 'warning')
else:
flash(_('Community not found. If you are searching for a nsfw community it is blocked by this instance.'), 'warning')
if new_community.banned:
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
else:
if new_community.banned:
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
return render_template('community/add_remote.html',
title=_('Add remote community'), form=form, new_community=new_community,
@ -465,6 +466,8 @@ def add_post(actor):
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
notify_about_post(post)
@ -575,7 +578,7 @@ def community_report(community_id: int):
form = ReportCommunityForm()
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_community_id=community.id)
type=1, reporter_id=current_user.id, suspect_community_id=community.id, source_instance_id=1)
db.session.add(report)
# Notify admin
@ -921,13 +924,13 @@ def community_moderate(actor):
reports = Report.query.filter_by(status=0, in_community_id=community.id)
if local_remote == 'local':
reports = reports.filter_by(ap_id=None)
reports = reports.filter(Report.source_instance_id == 1)
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)
reports = reports.filter(Report.source_instance_id != 1)
reports = reports.filter(Report.status >= 0).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
next_url = url_for('community.community_moderate', page=reports.next_num) if reports.has_next else None
prev_url = url_for('community.community_moderate', 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',
@ -960,4 +963,4 @@ def community_moderate_banned(actor):
else:
abort(401)
else:
abort(404)
abort(404)

View file

@ -96,7 +96,7 @@ def retrieve_mods_and_backfill(community_id: int):
if outbox_request.status_code == 200:
outbox_data = outbox_request.json()
outbox_request.close()
if outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data:
if 'type' in outbox_data and outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data:
activities_processed = 0
for activity in outbox_data['orderedItems']:
user = find_actor_or_create(activity['object']['actor'])
@ -210,13 +210,13 @@ def save_post(form, post: Post):
remove_old_file(post.image_id)
post.image_id = None
unused, file_extension = os.path.splitext(form.link_url.data) # do not use _ here instead of 'unused'
# this url is a link to an image - generate a thumbnail of it
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:
file = url_to_thumbnail_file(form.link_url.data)
if file:
post.image = file
db.session.add(file)
file = File(source_url=form.link_url.data)
post.image = file
db.session.add(file)
post.type = POST_TYPE_IMAGE
else:
# check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag
opengraph = opengraph_parse(form.link_url.data)
@ -255,6 +255,7 @@ def save_post(form, post: Post):
# save the file
final_place = os.path.join(directory, new_filename + file_ext)
final_place_medium = os.path.join(directory, new_filename + '_medium.webp')
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
uploaded_file.seek(0)
uploaded_file.save(final_place)
@ -270,18 +271,20 @@ def save_post(form, post: Post):
img = ImageOps.exif_transpose(img)
img_width = img.width
img_height = img.height
if img.width > 2000 or img.height > 2000:
img.thumbnail((2000, 2000))
img.save(final_place)
img.thumbnail((2000, 2000))
img.save(final_place)
if img.width > 512 or img.height > 512:
img.thumbnail((512, 512))
img.save(final_place_medium, format="WebP", quality=93)
img_width = img.width
img_height = img.height
# save a second, smaller, version as a thumbnail
img.thumbnail((256, 256))
img.thumbnail((150, 150))
img.save(final_place_thumbnail, format="WebP", quality=93)
thumbnail_width = img.width
thumbnail_height = img.height
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=alt_text,
file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text,
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))

View file

@ -16,4 +16,10 @@ SUBSCRIPTION_NONMEMBER = 0
SUBSCRIPTION_PENDING = -1
SUBSCRIPTION_BANNED = -2
THREAD_CUTOFF_DEPTH = 4
THREAD_CUTOFF_DEPTH = 4
REPORT_STATE_NEW = 0
REPORT_STATE_ESCALATED = 1
REPORT_STATE_APPEALED = 2
REPORT_STATE_RESOLVED = 3
REPORT_STATE_DISCARDED = -1

View file

@ -343,8 +343,8 @@ def activitypub_application():
'@context': default_context(),
'type': 'Application',
'id': f"https://{current_app.config['SERVER_NAME']}/",
'name': g.site.name,
'summary': g.site.description,
'name': 'PieFed',
'summary': g.site.name + ' - ' + g.site.description,
'published': ap_datetime(g.site.created_at),
'updated': ap_datetime(g.site.updated),
'inbox': f"https://{current_app.config['SERVER_NAME']}/site_inbox",

View file

@ -440,6 +440,10 @@ class Community(db.Model):
retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
return retval.lower()
def public_url(self):
result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
return result
def is_local(self):
return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME'])
@ -466,6 +470,14 @@ class Community(db.Model):
instances = instances.filter(Instance.id != 1, Instance.gone_forever == False)
return instances.all()
def has_followers_from_domain(self, domain: str) -> bool:
instances = Instance.query.join(User, User.instance_id == Instance.id).join(CommunityMember, CommunityMember.user_id == User.id)
instances = instances.filter(CommunityMember.community_id == self.id, CommunityMember.is_banned == False)
for instance in instances:
if instance.domain == domain:
return True
return False
def delete_dependencies(self):
for post in self.posts:
post.delete_dependencies()
@ -750,6 +762,10 @@ class User(UserMixin, db.Model):
result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
return result
def public_url(self):
result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
return result
def created_recently(self):
return self.created and self.created > utcnow() - timedelta(days=7)
@ -803,6 +819,12 @@ class User(UserMixin, db.Model):
reply.body = reply.body_html = ''
db.session.commit()
def mention_tag(self):
if self.ap_domain is None:
return '@' + self.user_name + '@' + current_app.config['SERVER_NAME']
else:
return '@' + self.user_name + '@' + self.ap_domain
class ActivityLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
@ -1171,7 +1193,7 @@ class Report(db.Model):
id = db.Column(db.Integer, primary_key=True)
reasons = db.Column(db.String(256))
description = db.Column(db.String(256))
status = db.Column(db.Integer, default=0)
status = db.Column(db.Integer, default=0) # 0 = new, 1 = escalated to admin, 2 = being appealed, 3 = resolved, 4 = discarded
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('community.id'))
@ -1180,6 +1202,7 @@ class Report(db.Model):
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'))
source_instance_id = db.Column(db.Integer, db.ForeignKey('instance.id')) # the instance of the reporter. mostly used to distinguish between local (instance 1) and remote reports
created_at = db.Column(db.DateTime, default=utcnow)
updated = db.Column(db.DateTime, default=utcnow)
@ -1192,7 +1215,7 @@ class Report(db.Model):
return types[self.type]
def is_local(self):
return True
return self.source_instance == 1
class IpBan(db.Model):

View file

@ -118,12 +118,12 @@ def show_post(post_id: int):
reply_json = {
'type': 'Note',
'id': reply.profile_id(),
'attributedTo': current_user.profile_id(),
'attributedTo': current_user.public_url(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
community.profile_id(),
community.public_url(), post.author.public_url()
],
'content': reply.body_html,
'inReplyTo': post.profile_id(),
@ -134,20 +134,30 @@ def show_post(post_id: int):
},
'published': ap_datetime(utcnow()),
'distinguished': False,
'audience': community.profile_id()
'audience': community.public_url(),
'tag': [{
'href': post.author.public_url(),
'name': post.author.mention_tag(),
'type': 'Mention'
}]
}
create_json = {
'type': 'Create',
'actor': current_user.profile_id(),
'audience': community.profile_id(),
'actor': current_user.public_url(),
'audience': community.public_url(),
'to': [
'https://www.w3.org/ns/activitystreams#Public'
],
'cc': [
community.ap_profile_id
community.public_url(), post.author.public_url()
],
'object': reply_json,
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
'tag': [{
'href': post.author.public_url(),
'name': post.author.mention_tag(),
'type': 'Mention'
}]
}
if not community.is_local(): # this is a remote community, send it to the instance that hosts it
success = post_request(community.ap_inbox_url, create_json, current_user.private_key,
@ -161,7 +171,7 @@ def show_post(post_id: int):
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"actor": community.ap_profile_id,
"actor": community.public_url(),
"cc": [
community.ap_followers_url
],
@ -173,6 +183,17 @@ def show_post(post_id: int):
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
send_to_remote_instance(instance.id, community.id, announce)
# send copy of Note to post author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
if not post.author.is_local() and post.author.ap_domain != community.ap_domain:
if not community.is_local() or (community.is_local and not community.has_followers_from_domain(post.author.ap_domain)):
success = post_request(post.author.ap_inbox_url, create_json, current_user.private_key,
current_user.ap_profile_id + '#main-key')
if not success:
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
personal_inbox = post.author.public_url() + '/inbox'
post_request(personal_inbox, create_json, current_user.private_key,
current_user.ap_profile_id + '#main-key')
return redirect(url_for('activitypub.post_ap', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
else:
replies = post_replies(post.id, sort)
@ -827,7 +848,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, in_community_id=post.community.id)
suspect_community_id=post.community.id, in_community_id=post.community.id, source_instance_id=1)
db.session.add(report)
# Notify moderators
@ -931,7 +952,8 @@ def post_reply_report(post_id: int, comment_id: int):
if form.validate_on_submit():
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
type=2, reporter_id=current_user.id, suspect_post_id=post.id, suspect_community_id=post.community.id,
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id)
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id, in_community_id=post.community.id,
source_instance_id=1)
db.session.add(report)
# Notify moderators

View file

@ -582,7 +582,11 @@ var DownArea = (function () {
if (self.textarea.selectionStart != self.textarea.selectionEnd) {
end = self.textarea.value.substr(self.textarea.selectionEnd);
var range = self.textarea.value.slice(self.textarea.selectionStart, self.textarea.selectionEnd);
blockquote = "".concat(blockquote).concat(range.trim());
var lines = range.trim().split('\n');
var modifiedLines = lines.map(function (line) {
return "> " + line.trim();
});
blockquote = modifiedLines.join('\n') + '\n';
}
if (start.length && start[start.length - 1] != '\n') {
blockquote = "\n".concat(blockquote);

View file

@ -246,7 +246,7 @@
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
<!-- -->
{% endif %}
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js', changed=getmtime('js/markdown/downarea.js')) }}"></script>
{% endif %}
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
<script src="{{ url_for('static', filename='themes/' + theme() + '/scripts.js') }}" />

View file

@ -57,8 +57,6 @@
<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>

View file

@ -56,7 +56,9 @@
</li>
{% endfor %}
</ul>
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a>
<a class="btn btn-primary" href="/topics">{{ _('Browse topics') }}</a>
</p>
</div>
</div>

View file

@ -105,4 +105,5 @@
{% else %}
<p>{{ _('There are no communities yet.') }}</p>
{% endif %}
<p><a href="/topics" class="btn btn-primary">{{ _('Browse topics') }}</a></p>
{% endblock %}

View file

@ -82,8 +82,15 @@
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</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 %}
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
{% endif %}
</div>
</div>
{% endif %}

View file

@ -133,8 +133,15 @@
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</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 %}
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
{% endif %}
</div>
</div>
{% endif %}

View file

@ -227,8 +227,15 @@
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</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 %}
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
{% endif %}
</div>
</div>
{% endif %}

View file

@ -78,8 +78,15 @@
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</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 %}
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
{% endif %}
</div>
</div>
{% endif %}

View file

@ -54,7 +54,9 @@
</li>
{% endfor %}
</ul>
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a>
<a class="btn btn-primary" href="/topics">{{ _('Browse topics') }}</a>
</p>
</div>
</div>

View file

@ -34,6 +34,12 @@ def show_people():
joined_communities=joined_communities(current_user.get_id()), title=_('People'))
@bp.route('/user/<int:user_id>', methods=['GET'])
def show_profile_by_id(user_id):
user = User.query.get_or_404(user_id)
return show_profile(user)
def show_profile(user):
if (user.deleted or user.banned) and current_user.is_anonymous:
abort(404)
@ -332,7 +338,7 @@ def report_profile(actor):
if user and not user.banned:
if form.validate_on_submit():
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
type=0, reporter_id=current_user.id, suspect_user_id=user.id)
type=0, reporter_id=current_user.id, suspect_user_id=user.id, source_instance_id=1)
db.session.add(report)
# Notify site admin

View file

@ -670,7 +670,7 @@ def finalize_user_setup(user, application_required=False):
private_key, public_key = RsaKeys.generate_keypair()
user.private_key = private_key
user.public_key = public_key
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower()
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
db.session.commit()

View file

@ -0,0 +1,34 @@
"""report source
Revision ID: 04697ae91fac
Revises: 2b028a70bd7a
Create Date: 2024-03-26 22:13:16.749010
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '04697ae91fac'
down_revision = '2b028a70bd7a'
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('source_instance_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_report_source_instance_id', 'instance', ['source_instance_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('fk_report_source_instance_id', type_='foreignkey')
batch_op.drop_column('source_instance_id')
# ### end Alembic commands ###

View file

@ -1,3 +1,4 @@
urllib3==1.26.11
Flask==2.3.2
python-dotenv==1.0.0
flask-wtf==1.1.1
@ -30,4 +31,3 @@ redis==5.0.1
Werkzeug==2.3.3
pytesseract==0.3.10
sentry-sdk==1.40.6
urllib3==1.26.1