remote admins can delete remote posts (not just moderators)

This commit is contained in:
rimu 2024-02-14 12:31:44 +13:00
parent 77a0ee9b5d
commit fef3a1e995
5 changed files with 168 additions and 37 deletions

View file

@ -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 = []

View file

@ -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)
if isinstance(user, User):
refresh_user_profile(user.id) 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,6 +691,7 @@ def find_instance_id(server):
def refresh_instance_profile(instance_id: int): def refresh_instance_profile(instance_id: int):
if instance_id:
if current_app.debug: if current_app.debug:
refresh_instance_profile_task(instance_id) refresh_instance_profile_task(instance_id)
else: else:
@ -696,6 +701,7 @@ def refresh_instance_profile(instance_id: int):
@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)
if instance.updated_at < utcnow() - timedelta(days=7):
try: try:
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'}) instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
except: except:
@ -723,6 +729,39 @@ def refresh_instance_profile_task(instance_id: int):
instance.updated_at = utcnow() instance.updated_at = utcnow()
db.session.commit() 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
@cache.memoize(timeout=50) @cache.memoize(timeout=50)
@ -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()

View file

@ -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():

View file

@ -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

View 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 ###