mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-02-03 00:31:25 -08:00
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:
commit
1fcefd6628
17 changed files with 229 additions and 45 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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
22
Dockerfile
Normal 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"]
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -291,6 +291,15 @@ h1 {
|
|||
}
|
||||
}
|
||||
|
||||
.fe-audio {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 0;
|
||||
&:before {
|
||||
content: "\e9fc";
|
||||
}
|
||||
}
|
||||
|
||||
.fe-poll {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
16
app/utils.py
16
app/utils.py
|
@ -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
42
compose.yaml
Normal 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
3
entrypoint.sh
Normal 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
3
entrypoint_celery.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
celery -A celery_worker.celery worker
|
Loading…
Add table
Reference in a new issue