mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
remote admins can delete remote posts (not just moderators)
This commit is contained in:
parent
77a0ee9b5d
commit
fef3a1e995
5 changed files with 168 additions and 37 deletions
|
@ -124,7 +124,7 @@ def lemmy_site():
|
||||||
@bp.route('/api/v3/federated_instances')
|
@bp.route('/api/v3/federated_instances')
|
||||||
@cache.cached(timeout=600)
|
@cache.cached(timeout=600)
|
||||||
def lemmy_federated_instances():
|
def lemmy_federated_instances():
|
||||||
instances = Instance.query.all()
|
instances = Instance.query.filter(Instance.id != 1).all()
|
||||||
linked = []
|
linked = []
|
||||||
allowed = []
|
allowed = []
|
||||||
blocked = []
|
blocked = []
|
||||||
|
|
|
@ -10,7 +10,7 @@ from flask_babel import _
|
||||||
from sqlalchemy import text, func
|
from sqlalchemy import text, func
|
||||||
from app import db, cache, constants, celery
|
from app import db, cache, constants, celery
|
||||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
|
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
|
||||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember
|
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
|
@ -211,14 +211,18 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]:
|
||||||
user = Community.query.filter(Community.ap_profile_id == actor).first()
|
user = Community.query.filter(Community.ap_profile_id == actor).first()
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
if not user.is_local() and user.ap_fetched_at < utcnow() - timedelta(days=7):
|
if not user.is_local() and (user.ap_fetched_at is None or user.ap_fetched_at < utcnow() - timedelta(days=7)):
|
||||||
# To reduce load on remote servers, refreshing the user profile happens after a delay of 1 to 10 seconds. Meanwhile, subsequent calls to
|
# To reduce load on remote servers, refreshing the user profile happens after a delay of 1 to 10 seconds. Meanwhile, subsequent calls to
|
||||||
# find_actor_or_create() which happen to be for the same actor might queue up refreshes of the same user. To avoid this, set a flag to
|
# find_actor_or_create() which happen to be for the same actor might queue up refreshes of the same user. To avoid this, set a flag to
|
||||||
# indicate that user is currently being refreshed.
|
# indicate that user is currently being refreshed.
|
||||||
refresh_in_progress = cache.get(f'refreshing_{user.id}')
|
refresh_in_progress = cache.get(f'refreshing_{user.id}')
|
||||||
if not refresh_in_progress:
|
if not refresh_in_progress:
|
||||||
cache.set(f'refreshing_{user.id}', True, timeout=300)
|
cache.set(f'refreshing_{user.id}', True, timeout=300)
|
||||||
refresh_user_profile(user.id)
|
if isinstance(user, User):
|
||||||
|
refresh_user_profile(user.id)
|
||||||
|
elif isinstance(user, Community):
|
||||||
|
# todo: refresh community profile also, not just instance_profile
|
||||||
|
refresh_instance_profile(user.instance_id)
|
||||||
return user
|
return user
|
||||||
else: # User does not exist in the DB, it's going to need to be created from it's remote home instance
|
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://'):
|
if actor.startswith('https://'):
|
||||||
|
@ -687,41 +691,76 @@ def find_instance_id(server):
|
||||||
|
|
||||||
|
|
||||||
def refresh_instance_profile(instance_id: int):
|
def refresh_instance_profile(instance_id: int):
|
||||||
if current_app.debug:
|
if instance_id:
|
||||||
refresh_instance_profile_task(instance_id)
|
if current_app.debug:
|
||||||
else:
|
refresh_instance_profile_task(instance_id)
|
||||||
refresh_instance_profile_task.apply_async(args=(instance_id,), countdown=randint(1, 10))
|
else:
|
||||||
|
refresh_instance_profile_task.apply_async(args=(instance_id,), countdown=randint(1, 10))
|
||||||
|
|
||||||
|
|
||||||
@celery.task
|
@celery.task
|
||||||
def refresh_instance_profile_task(instance_id: int):
|
def refresh_instance_profile_task(instance_id: int):
|
||||||
instance = Instance.query.get(instance_id)
|
instance = Instance.query.get(instance_id)
|
||||||
try:
|
if instance.updated_at < utcnow() - timedelta(days=7):
|
||||||
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
if instance_data.status_code == 200:
|
|
||||||
try:
|
try:
|
||||||
instance_json = instance_data.json()
|
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
|
||||||
instance_data.close()
|
except:
|
||||||
except requests.exceptions.JSONDecodeError as ex:
|
return
|
||||||
instance_json = {}
|
if instance_data.status_code == 200:
|
||||||
if 'type' in instance_json and instance_json['type'] == 'Application':
|
try:
|
||||||
if instance_json['name'].lower() == 'kbin':
|
instance_json = instance_data.json()
|
||||||
software = 'Kbin'
|
instance_data.close()
|
||||||
elif instance_json['name'].lower() == 'mbin':
|
except requests.exceptions.JSONDecodeError as ex:
|
||||||
software = 'Mbin'
|
instance_json = {}
|
||||||
else:
|
if 'type' in instance_json and instance_json['type'] == 'Application':
|
||||||
software = 'Lemmy'
|
if instance_json['name'].lower() == 'kbin':
|
||||||
instance.inbox = instance_json['inbox']
|
software = 'Kbin'
|
||||||
instance.outbox = instance_json['outbox']
|
elif instance_json['name'].lower() == 'mbin':
|
||||||
instance.software = software
|
software = 'Mbin'
|
||||||
if instance.inbox.endswith('/site_inbox'): # Lemmy provides a /site_inbox but it always returns 400 when trying to POST to it. wtf.
|
else:
|
||||||
instance.inbox = instance.inbox.replace('/site_inbox', '/inbox')
|
software = 'Lemmy'
|
||||||
else: # it's pretty much always /inbox so just assume that it is for whatever this instance is running (mostly likely Mastodon)
|
instance.inbox = instance_json['inbox']
|
||||||
instance.inbox = f"https://{instance.domain}/inbox"
|
instance.outbox = instance_json['outbox']
|
||||||
instance.updated_at = utcnow()
|
instance.software = software
|
||||||
db.session.commit()
|
if instance.inbox.endswith('/site_inbox'): # Lemmy provides a /site_inbox but it always returns 400 when trying to POST to it. wtf.
|
||||||
|
instance.inbox = instance.inbox.replace('/site_inbox', '/inbox')
|
||||||
|
else: # it's pretty much always /inbox so just assume that it is for whatever this instance is running (mostly likely Mastodon)
|
||||||
|
instance.inbox = f"https://{instance.domain}/inbox"
|
||||||
|
instance.updated_at = utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# retrieve list of Admins from /api/v3/site, update InstanceRole
|
||||||
|
try:
|
||||||
|
response = get_request(f'https://{instance.domain}/api/v3/site')
|
||||||
|
except:
|
||||||
|
response = None
|
||||||
|
|
||||||
|
if response and response.status_code == 200:
|
||||||
|
try:
|
||||||
|
instance_data = response.json()
|
||||||
|
except:
|
||||||
|
instance_data = None
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
if instance_data:
|
||||||
|
if 'admins' in instance_data:
|
||||||
|
admin_profile_ids = []
|
||||||
|
for admin in instance_data['admins']:
|
||||||
|
admin_profile_ids.append(admin['person']['actor_id'].lower())
|
||||||
|
user = find_actor_or_create(admin['person']['actor_id'])
|
||||||
|
if user and not instance.user_is_admin(user.id):
|
||||||
|
new_instance_role = InstanceRole(instance_id=instance.id, user_id=user.id, role='admin')
|
||||||
|
db.session.add(new_instance_role)
|
||||||
|
db.session.commit()
|
||||||
|
# remove any InstanceRoles that are no longer part of instance-data['admins']
|
||||||
|
for instance_admin in InstanceRole.query.filter_by(instance_id=instance.id):
|
||||||
|
if instance_admin.user.profile_id() not in admin_profile_ids:
|
||||||
|
db.session.query(InstanceRole).filter(
|
||||||
|
InstanceRole.user_id == instance_admin.user.id,
|
||||||
|
InstanceRole.instance_id == instance.id,
|
||||||
|
InstanceRole.role == 'admin').delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
# alter the effect of upvotes based on their instance. Default to 1.0
|
# alter the effect of upvotes based on their instance. Default to 1.0
|
||||||
|
@ -897,7 +936,7 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
|
||||||
to_delete = find_liked_object(to_be_deleted_ap_id)
|
to_delete = find_liked_object(to_be_deleted_ap_id)
|
||||||
|
|
||||||
if deletor and community and to_delete:
|
if deletor and community and to_delete:
|
||||||
if deletor.is_admin() or community.is_moderator(deletor) or to_delete.author.id == deletor.id:
|
if deletor.is_admin() or community.is_moderator(deletor) or community.is_instance_admin(deletor) or to_delete.author.id == deletor.id:
|
||||||
if isinstance(to_delete, Post):
|
if isinstance(to_delete, Post):
|
||||||
to_delete.delete_dependencies()
|
to_delete.delete_dependencies()
|
||||||
to_delete.flush_cache()
|
to_delete.flush_cache()
|
||||||
|
|
|
@ -6,7 +6,7 @@ from random import randint
|
||||||
from sqlalchemy.sql.operators import or_
|
from sqlalchemy.sql.operators import or_
|
||||||
|
|
||||||
from app import db, cache
|
from app import db, cache
|
||||||
from app.activitypub.util import default_context, make_image_sizes_async, refresh_user_profile
|
from app.activitypub.util import default_context, make_image_sizes_async, refresh_user_profile, find_actor_or_create
|
||||||
from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER, POST_TYPE_IMAGE, POST_TYPE_LINK, \
|
from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER, POST_TYPE_IMAGE, POST_TYPE_LINK, \
|
||||||
SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR
|
SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR
|
||||||
from app.inoculation import inoculation
|
from app.inoculation import inoculation
|
||||||
|
@ -19,8 +19,8 @@ from sqlalchemy import select, desc, text
|
||||||
from sqlalchemy_searchable import search
|
from sqlalchemy_searchable import search
|
||||||
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
||||||
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
|
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
|
||||||
joined_communities, moderating_communities, parse_page, theme_list
|
joined_communities, moderating_communities, parse_page, theme_list, get_request
|
||||||
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File
|
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, InstanceRole
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import pytesseract
|
import pytesseract
|
||||||
|
|
||||||
|
@ -257,6 +257,42 @@ def list_files(directory):
|
||||||
@bp.route('/test')
|
@bp.route('/test')
|
||||||
def test():
|
def test():
|
||||||
|
|
||||||
|
instance = Instance.query.get(3)
|
||||||
|
if instance.updated_at < utcnow() - timedelta(days=7):
|
||||||
|
try:
|
||||||
|
response = get_request(f'https://{instance.domain}/api/v3/site')
|
||||||
|
except:
|
||||||
|
response = None
|
||||||
|
|
||||||
|
if response and response.status_code == 200:
|
||||||
|
try:
|
||||||
|
instance_data = response.json()
|
||||||
|
except:
|
||||||
|
instance_data = None
|
||||||
|
finally:
|
||||||
|
response.close()
|
||||||
|
|
||||||
|
if instance_data:
|
||||||
|
if 'admins' in instance_data:
|
||||||
|
admin_profile_ids = []
|
||||||
|
for admin in instance_data['admins']:
|
||||||
|
admin_profile_ids.append(admin['person']['actor_id'].lower())
|
||||||
|
user = find_actor_or_create(admin['person']['actor_id'])
|
||||||
|
if user and not instance.user_is_admin(user.id):
|
||||||
|
new_instance_role = InstanceRole(instance_id=instance.id, user_id=user.id, role='admin')
|
||||||
|
db.session.add(new_instance_role)
|
||||||
|
db.session.commit()
|
||||||
|
# remove any InstanceRoles that are no longer part of instance-data['admins']
|
||||||
|
for instance_admin in InstanceRole.query.filter_by(instance_id=instance.id):
|
||||||
|
if instance_admin.user.profile_id() not in admin_profile_ids:
|
||||||
|
db.session.query(InstanceRole).filter(
|
||||||
|
InstanceRole.user_id == instance_admin.user.id,
|
||||||
|
InstanceRole.instance_id == instance.id,
|
||||||
|
InstanceRole.role == 'admin').delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return 'Ok'
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
retval = ''
|
retval = ''
|
||||||
for user in User.query.all():
|
for user in User.query.all():
|
||||||
|
|
|
@ -69,6 +69,18 @@ class Instance(db.Model):
|
||||||
def online(self):
|
def online(self):
|
||||||
return not self.dormant and not self.gone_forever
|
return not self.dormant and not self.gone_forever
|
||||||
|
|
||||||
|
def user_is_admin(self, user_id):
|
||||||
|
role = InstanceRole.query.filter_by(instance_id=self.id, user_id=user_id).first()
|
||||||
|
return role and role.role == 'admin'
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceRole(db.Model):
|
||||||
|
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||||
|
role = db.Column(db.String(50), default='admin')
|
||||||
|
|
||||||
|
user = db.relationship('User', lazy='joined')
|
||||||
|
|
||||||
|
|
||||||
class InstanceBlock(db.Model):
|
class InstanceBlock(db.Model):
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||||
|
@ -269,6 +281,15 @@ class Community(db.Model):
|
||||||
else:
|
else:
|
||||||
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
|
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
|
||||||
|
|
||||||
|
def is_instance_admin(self, user):
|
||||||
|
if self.instance_id:
|
||||||
|
instance_role = InstanceRole.query.filter(InstanceRole.instance_id == self.instance_id,
|
||||||
|
InstanceRole.user_id == user.id,
|
||||||
|
InstanceRole.role == 'admin').first()
|
||||||
|
return instance_role is not None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def user_is_banned(self, user):
|
def user_is_banned(self, user):
|
||||||
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
|
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
|
||||||
return membership.is_banned if membership else False
|
return membership.is_banned if membership else False
|
||||||
|
|
35
migrations/versions/75f5b458c2f9_instance_admins.py
Normal file
35
migrations/versions/75f5b458c2f9_instance_admins.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
"""instance admins
|
||||||
|
|
||||||
|
Revision ID: 75f5b458c2f9
|
||||||
|
Revises: a8fc7f7ba539
|
||||||
|
Create Date: 2024-02-14 11:12:09.271117
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '75f5b458c2f9'
|
||||||
|
down_revision = 'a8fc7f7ba539'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('instance_role',
|
||||||
|
sa.Column('instance_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('role', sa.String(length=50), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['instance_id'], ['instance.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('instance_id', 'user_id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('instance_role')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue