diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 99b59451..05d7107e 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -892,7 +892,10 @@ def process_inbox_request(request_json, store_ap_json): if not blocked: log_incoming_ap(announce_id, APLOG_USERBAN, APLOG_IGNORED, request_json if store_ap_json else None, 'Does not exist here') return - block_from_ap_id = request_json['target'] + + # target = request_json['target'] # target is supposed to determine the scope - whether it is an instance-wide ban or just one community. Lemmy doesn't use it right though + # community = find_actor_or_create(target, create_if_not_found=False, community_only=True) + remove_data = request_json['removeData'] if 'removeData' in request_json else False # Lemmy currently only sends userbans for admins banning local users @@ -902,15 +905,31 @@ def process_inbox_request(request_json, store_ap_json): log_incoming_ap(announce_id, APLOG_USERBAN, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission') return - # request_json includes 'expires' and 'endTime' (same thing) but nowhere to record this and check in future for end in ban. + if blocked.banned: # We may have already banned them - we don't want remote temp bans to over-ride our permanent bans + return - if remove_data == True: + if blocked.is_local(): # Sanity check + current_app.logger.error('Attempt to ban local user: ' + str(request_json)) + return + + blocked.banned = True + db.session.commit() + if 'expires' in request_json: + blocked.banned_until = request_json['expires'] + elif 'endTime' in request_json: + blocked.banned_until = request_json['endTime'] + try: + db.session.commit() + except: # I don't know the format of expires or endTime so let's see how this goes + db.session.rollback() + current_app.logger.error('could not save banned_until value: ' + str(request_json)) + + if remove_data: site_ban_remove_data(blocker.id, blocked) log_incoming_ap(announce_id, APLOG_USERBAN, APLOG_SUCCESS, request_json if store_ap_json else None) else: - #blocked.banned = True # uncommented until there's a mechanism for processing ban expiry date - #db.session.commit() log_incoming_ap(announce_id, APLOG_USERBAN, APLOG_IGNORED, request_json if store_ap_json else None, 'Banned, but content retained') + return if request_json['type'] == 'Undo': @@ -1438,8 +1457,8 @@ def user_followers(actor): @bp.route('/comment/', methods=['GET', 'HEAD']) def comment_ap(comment_id): + reply = PostReply.query.get_or_404(comment_id) if is_activitypub_request(): - reply = PostReply.query.get_or_404(comment_id) reply_data = comment_model_to_json(reply) if request.method == 'GET' else [] resp = jsonify(reply_data) resp.content_type = 'application/activity+json' @@ -1447,7 +1466,6 @@ def comment_ap(comment_id): resp.headers.set('Link', f'; rel="alternate"; type="text/html"') return resp else: - reply = PostReply.query.get_or_404(comment_id) return continue_discussion(reply.post.id, comment_id) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index b1b99ea8..6090844b 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -1453,7 +1453,6 @@ def site_ban_remove_data(blocker_id, blocked): if blocked.cover_id: blocked.cover.delete_from_disk() blocked.cover.source_url = '' - # blocked.banned = True # uncommented until there's a mechanism for processing ban expiry date db.session.commit() diff --git a/app/cli.py b/app/cli.py index 093d5097..717a1dff 100644 --- a/app/cli.py +++ b/app/cli.py @@ -209,6 +209,11 @@ def register(app): db.session.execute(text('DELETE FROM "post_reply_vote" WHERE created_at < :cutoff'), {'cutoff': utcnow() - timedelta(days=28 * 6)}) db.session.commit() + # Un-ban after ban expires + db.session.execute(text('UPDATE "user" SET banned = false WHERE banned is true AND banned_until < :cutoff AND banned_until is not null'), + {'cutoff': utcnow()}) + db.session.commit() + # Check for dormant or dead instances try: # Check for dormant or dead instances diff --git a/app/models.py b/app/models.py index 4ab841bf..282d79c9 100644 --- a/app/models.py +++ b/app/models.py @@ -135,12 +135,20 @@ class InstanceRole(db.Model): user = db.relationship('User', lazy='joined') +# Instances that this user has blocked class InstanceBlock(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True) created_at = db.Column(db.DateTime, default=utcnow) +# Instances that have banned this user +class InstanceBan(db.Model): + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True) + banned_until = db.Column(db.DateTime) + + class Conversation(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) @@ -649,7 +657,8 @@ class User(UserMixin, db.Model): password_hash = db.Column(db.String(128)) verified = db.Column(db.Boolean, default=False) verification_token = db.Column(db.String(16), index=True) - banned = db.Column(db.Boolean, default=False) + banned = db.Column(db.Boolean, default=False, index=True) + banned_until = db.Column(db.DateTime) # null == permanent ban deleted = db.Column(db.Boolean, default=False) deleted_by = db.Column(db.Integer, index=True) about = db.Column(db.Text) # markdown @@ -2010,6 +2019,7 @@ class CommunityBan(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # person who is banned, not the banner community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True) banned_by = db.Column(db.Integer, db.ForeignKey('user.id')) + banned_until = db.Column(db.DateTime) reason = db.Column(db.String(256)) created_at = db.Column(db.DateTime, default=utcnow) ban_until = db.Column(db.DateTime) diff --git a/app/post/routes.py b/app/post/routes.py index a1762a98..2410938a 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -470,8 +470,13 @@ def poll_vote(post_id): def continue_discussion(post_id, comment_id): post = Post.query.get_or_404(post_id) comment = PostReply.query.get_or_404(comment_id) + if post.community.banned or post.deleted or comment.deleted: - abort(404) + if current_user.is_anonymous or not (current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff())): + abort(404) + else: + flash(_('This comment has been deleted and is only visible to staff and admins.'), 'warning') + mods = post.community.moderators() is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) if post.community.private_mods: diff --git a/migrations/versions/20a3bae71dd7_banned_until.py b/migrations/versions/20a3bae71dd7_banned_until.py new file mode 100644 index 00000000..0e1768e5 --- /dev/null +++ b/migrations/versions/20a3bae71dd7_banned_until.py @@ -0,0 +1,49 @@ +"""banned until + +Revision ID: 20a3bae71dd7 +Revises: d88b49617de0 +Create Date: 2024-11-30 08:52:27.584637 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '20a3bae71dd7' +down_revision = 'd88b49617de0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('instance_ban', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('instance_id', sa.Integer(), nullable=False), + sa.Column('banned_until', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['instance_id'], ['instance.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'instance_id') + ) + with op.batch_alter_table('community_ban', schema=None) as batch_op: + batch_op.add_column(sa.Column('banned_until', sa.DateTime(), nullable=True)) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('banned_until', sa.DateTime(), nullable=True)) + batch_op.create_index(batch_op.f('ix_user_banned'), ['banned'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_banned')) + batch_op.drop_column('banned_until') + + with op.batch_alter_table('community_ban', schema=None) as batch_op: + batch_op.drop_column('banned_until') + + op.drop_table('instance_ban') + # ### end Alembic commands ###