Merge pull request 'config/mail-config' (#1) from config/mail-config into main

Reviewed-on: https://codeberg.org/saint/pyfedi/pulls/1
This commit is contained in:
saint 2024-02-25 09:01:04 +00:00
commit 1fcefd6628
17 changed files with 229 additions and 45 deletions

4
.gitignore vendored
View file

@ -162,3 +162,7 @@ cython_debug/
app/static/*.css.map
/app/static/media/
celery_worker.py
# sensitive data
.env.docker
.gunicorn.conf.py

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
# syntax=docker/dockerfile:1.4
FROM --platform=$BUILDPLATFORM python:3-alpine AS builder
RUN apk update
RUN apk add pkgconfig
RUN apk add --virtual build-deps gcc python3-dev musl-dev
WORKDIR /app
COPY . /app
RUN pip3 install -r requirements.txt
RUN pip3 install gunicorn
RUN chmod u+x ./entrypoint.sh
RUN chmod u+x ./entrypoint_celery.sh
RUN adduser -D python
RUN chown -R python:python /app
USER python
ENTRYPOINT ["./entrypoint.sh"]

View file

@ -97,7 +97,7 @@ def create_app(config_class=Config):
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='errors@rimu.geek.nz',
fromaddr=(app.config['MAIL_FROM']),
toaddrs=app.config['ADMINS'], subject='PieFed error',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)

View file

@ -21,7 +21,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
update_post_from_activity, undo_vote, undo_downvote
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest
import werkzeug.exceptions
@ -116,6 +116,24 @@ def nodeinfo2():
return jsonify(nodeinfo_data)
@bp.route('/api/v1/instance/domain_blocks')
@cache.cached(timeout=600)
def domain_blocks():
use_allowlist = get_setting('use_allowlist', False)
if use_allowlist:
return jsonify([])
else:
retval = []
for domain in BannedInstances.query.all():
retval.append({
'domain': domain.domain,
'digest': sha256_digest(domain.domain),
'severity': 'suspend',
'comment': domain.reason if domain.reason else ''
})
return jsonify(retval)
@bp.route('/api/v3/site')
#@cache.cached(timeout=600)
def lemmy_site():
@ -759,18 +777,33 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
else:
ap_id = request_json['object']['id'] # kbin
post = Post.query.filter_by(ap_id=ap_id).first()
# Delete post
if post:
post.delete_dependencies()
post.community.post_count -= 1
db.session.delete(post)
db.session.commit()
activity_log.result = 'success'
else:
# Delete PostReply
reply = PostReply.query.filter_by(ap_id=ap_id).first()
if reply:
reply.body_html = '<p><em>deleted</em></p>'
reply.body = 'deleted'
reply.post.reply_count -= 1
db.session.commit()
activity_log.result = 'success'
db.session.commit()
activity_log.result = 'success'
else:
# Delete User
user = find_actor_or_create(ap_id, create_if_not_found=False)
if user:
user.deleted = True
user.delete_dependencies()
db.session.commit()
activity_log.result = 'success'
else:
activity_log.exception_message = 'Delete: cannot find ' + ap_id
elif request_json['type'] == 'Like': # Upvote
activity_log.activity_type = request_json['type']
user_ap_id = request_json['actor']

View file

@ -196,7 +196,7 @@ def instance_allowed(host: str) -> bool:
return instance is not None
def find_actor_or_create(actor: str) -> Union[User, Community, None]:
def find_actor_or_create(actor: str, create_if_not_found=True) -> Union[User, Community, None]:
actor = actor.strip().lower()
user = None
# actor parameter must be formatted as https://server/u/actor or https://server/c/actor
@ -238,42 +238,43 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
refresh_instance_profile(user.instance_id)
return user
else: # User does not exist in the DB, it's going to need to be created from it's remote home instance
if actor.startswith('https://'):
try:
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
if actor_data.status_code == 200:
actor_json = actor_data.json()
actor_data.close()
return actor_json_to_model(actor_json, address, server)
else:
# retrieve user details via webfinger, etc
try:
webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address}@{server}"})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address}@{server}"})
if webfinger_data.status_code == 200:
webfinger_json = webfinger_data.json()
webfinger_data.close()
for links in webfinger_json['links']:
if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile
type = links['type'] if 'type' in links else 'application/activity+json'
# retrieve the activitypub profile
try:
actor_data = get_request(links['href'], headers={'Accept': type})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
actor_data = get_request(links['href'], headers={'Accept': type})
# to see the structure of the json contained in actor_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json
if actor_data.status_code == 200:
actor_json = actor_data.json()
actor_data.close()
return actor_json_to_model(actor_json, address, server)
if create_if_not_found:
if actor.startswith('https://'):
try:
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
if actor_data.status_code == 200:
actor_json = actor_data.json()
actor_data.close()
return actor_json_to_model(actor_json, address, server)
else:
# retrieve user details via webfinger, etc
try:
webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address}@{server}"})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address}@{server}"})
if webfinger_data.status_code == 200:
webfinger_json = webfinger_data.json()
webfinger_data.close()
for links in webfinger_json['links']:
if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile
type = links['type'] if 'type' in links else 'application/activity+json'
# retrieve the activitypub profile
try:
actor_data = get_request(links['href'], headers={'Accept': type})
except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10))
actor_data = get_request(links['href'], headers={'Accept': type})
# to see the structure of the json contained in actor_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json
if actor_data.status_code == 200:
actor_json = actor_data.json()
actor_data.close()
return actor_json_to_model(actor_json, address, server)
return None
@ -732,6 +733,8 @@ def refresh_instance_profile_task(instance_id: int):
software = 'Kbin'
elif instance_json['name'].lower() == 'mbin':
software = 'Mbin'
elif instance_json['name'].lower() == 'piefed':
software = 'PieFed'
else:
software = 'Lemmy'
instance.inbox = instance_json['inbox']

View file

@ -291,6 +291,15 @@ h1 {
}
}
.fe-audio {
position: relative;
top: 1px;
left: 0;
&:before {
content: "\e9fc";
}
}
.fe-poll {
position: relative;
top: 2px;

View file

@ -314,6 +314,15 @@ h1 .fe-bell, h1 .fe-no-bell {
content: "\ea05";
}
.fe-audio {
position: relative;
top: 1px;
left: 0;
}
.fe-audio:before {
content: "\e9fc";
}
.fe-poll {
position: relative;
top: 2px;
@ -398,6 +407,12 @@ fieldset legend {
background-color: #d8e5ee;
}
@media (min-width: 768px) {
html {
scroll-padding-top: 80px;
}
}
.container {
width: 100%;
padding-right: 0.75rem;
@ -427,6 +442,13 @@ fieldset legend {
}
}
@media (min-width: 992px) {
.navbar-expand-lg .navbar-nav .dropdown-menu {
overflow-y: auto;
max-height: 90vh;
overflow-x: hidden;
}
}
.low_bandwidth .dropdown-toggle::after {
display: none;
}

View file

@ -5,6 +5,12 @@ nav, etc which are used site-wide */
@import "scss/typography";
@import "scss/controls";
html {
@include breakpoint(phablet) {
scroll-padding-top: 80px;
}
}
.container {
width: 100%;
padding-right: 0.75rem;
@ -29,6 +35,14 @@ nav, etc which are used site-wide */
}
}
@include breakpoint(tablet) {
.navbar-expand-lg .navbar-nav .dropdown-menu {
overflow-y: auto;
max-height: 90vh;
overflow-x: hidden;
}
}
.low_bandwidth {
.dropdown-toggle::after {
display: none;

View file

@ -313,6 +313,15 @@ h1 .fe-bell, h1 .fe-no-bell {
content: "\ea05";
}
.fe-audio {
position: relative;
top: 1px;
left: 0;
}
.fe-audio:before {
content: "\e9fc";
}
.fe-poll {
position: relative;
top: 2px;

View file

@ -86,6 +86,9 @@
{% if post.type == POST_TYPE_LINK %}
<p><a href="{{ post.url }}" rel="nofollow ugc" target="_blank" class="post_link">{{ post.url|shorten_url }}
<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>
{% endif %}
{% if 'youtube.com' in post.url %}
<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 %}

View file

@ -42,6 +42,8 @@
{% if post.type == POST_TYPE_LINK and post.domain_id %}
{% if post.url and '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>
{% endif %}
<span class="domain_link" aria-hidden="true">(<a href="/d/{{ post.domain_id }}" aria-label="{{ _('All posts about this domain') }}">{{ post.domain.name }}</a>)</span>
{% endif %}

View file

@ -463,13 +463,13 @@ def ban_purge_profile(actor):
# federate deletion
if user.is_local():
purge_user_then_delete(user.id)
flash(f'{actor} has been banned, deleted and all their content deleted. This might take a few minutes.')
else:
user.deleted = True
user.delete_dependencies()
user.purge_content()
db.session.commit()
flash(f'{actor} has been banned, deleted and all their content deleted.')
flash(f'{actor} has been banned, deleted and all their content deleted.')
else:
abort(401)

View file

@ -82,8 +82,7 @@ def purge_user_then_delete_task(user_id):
}
for instance in instances:
if instance.inbox and instance.id != 1:
post_request(instance.inbox, payload, site.private_key,
f"https://{current_app.config['SERVER_NAME']}#main-key")
post_request(instance.inbox, payload, user.private_key, user.ap_profile_id + '#main-key')
sleep(100) # wait a while for any related activitypub traffic to die down.
user.deleted = True

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
import random
import urllib
from collections import defaultdict
@ -705,3 +706,18 @@ def theme_list():
theme_settings = json.loads(file_get_contents(f'app/templates/themes/{dir}/{dir}.json'))
result.append((dir, theme_settings['name']))
return result
def sha256_digest(input_string):
"""
Compute the SHA-256 hash digest of a given string.
Args:
- input_string: The string to compute the hash digest for.
Returns:
- A hexadecimal string representing the SHA-256 hash digest.
"""
sha256_hash = hashlib.sha256()
sha256_hash.update(input_string.encode('utf-8'))
return sha256_hash.hexdigest()

42
compose.yaml Normal file
View file

@ -0,0 +1,42 @@
services:
db:
shm_size: 128mb
image: postgres
env_file:
- ./.env.docker
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis
env_file:
- ./.env.docker
celery:
build:
context: .
target: builder
env_file:
- ./.env.docker
entrypoint: ./entrypoint_celery.sh
web:
build:
context: .
target: builder
depends_on:
- db
- redis
env_file:
- ./.env.docker
volumes:
- ./.env:/app/.env
- ./.gunicorn.conf.py:/app/gunicorn.conf.py
ports:
- '8080:5000'
adminer:
image: adminer
restart: always
ports:
- 8888:8080
volumes:
pgdata:

3
entrypoint.sh Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env sh
flask db upgrade
gunicorn --config gunicorn.conf.py --preload pyfedi:app

3
entrypoint_celery.sh Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
celery -A celery_worker.celery worker