Skip to content

Commit

Permalink
FEATURE: Add reject follower action (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
angusmcleod authored Oct 23, 2024
1 parent 2d6db5a commit 9051c43
Show file tree
Hide file tree
Showing 21 changed files with 480 additions and 41 deletions.
16 changes: 14 additions & 2 deletions app/controllers/discourse_activity_pub/actor_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class ActorController < ApplicationController

include DiscourseActivityPub::EnabledVerification

before_action :ensure_admin, only: %i[follow unfollow find_by_handle]
before_action :ensure_admin, only: %i[follow unfollow reject find_by_handle]
before_action :ensure_site_enabled
before_action :find_actor
before_action :ensure_model_enabled
before_action :ensure_can_access
before_action :find_target_actor, only: %i[follow unfollow]
before_action :find_target_actor, only: %i[follow unfollow reject]

def show
render_serialized(@actor, DiscourseActivityPub::ActorSerializer, include_model: true)
Expand Down Expand Up @@ -44,6 +44,18 @@ def unfollow
end
end

def reject
# Currently, we only process rejections of existing follows.
# See further https://github.com/mastodon/mastodon/issues/5708
return render_actor_error("not_following_actor", 404) if !@target_actor.following?(@actor)

if FollowHandler.reject(@actor.id, @target_actor.id)
render json: success_json
else
render json: failed_json
end
end

def find_by_handle
params.require(:handle)

Expand Down
21 changes: 19 additions & 2 deletions app/models/discourse_activity_pub_activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,31 @@ def before_deliver
end

def after_deliver(delivered = true)
return self.destroy! if !delivered && local? && ap.follow?
return unless local?
return self.destroy! if !delivered && ap.follow?

if delivered && local? && ap.undo? && object.ap.follow?
if delivered && undo_follow?
DiscourseActivityPubFollow.where(
follower_id: actor_id,
followed_id: object.object.id,
).destroy_all
end

# We destroy our local follow whether or not our follow rejection was delivered.
if reject_follow?
DiscourseActivityPubFollow.where(
follower_id: object.actor.id,
followed_id: actor_id,
).destroy_all
end
end

def undo_follow?
ap.undo? && object.ap.follow?
end

def reject_follow?
ap.reject? && object.ap.follow?
end

def after_scheduled(scheduled_at, _activity = nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { dasherize } from "@ember/string";
import DButton from "discourse/components/d-button";
import I18n from "I18n";
import ActivityPubActorFollowModal from "../components/modal/activity-pub-actor-follow";
import ActivityPubActorRejectModal from "../components/modal/activity-pub-actor-reject";
import ActivityPubActorUnfollowModal from "../components/modal/activity-pub-actor-unfollow";
import ActivityPubFollowModal from "../components/modal/activity-pub-follow";

const modalMap = {
follow: ActivityPubFollowModal,
actor_follow: ActivityPubActorFollowModal,
actor_unfollow: ActivityPubActorUnfollowModal,
actor_reject: ActivityPubActorRejectModal,
};

export default class ActivityPubFollowBtn extends Component {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<DModal
@closeModal={{@closeModal}}
@title={{this.title}}
class="activity-pub-actor-reject-modal"
>
<:body>
<div class="activity-pub-actor-reject">
{{i18n
"discourse_activity_pub.actor_reject.confirm"
actor=@model.actor.name
follower=@model.follower.handle
}}
</div>
</:body>

<:footer>
<DButton
@action={{action "reject"}}
@label="discourse_activity_pub.actor_reject.label"
class="btn-primary"
/>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import I18n from "I18n";

export default class ActivityPubActorReject extends Component {
get title() {
return I18n.t("discourse_activity_pub.actor_reject.modal_title", {
actor: this.args.model.actor?.name,
});
}

@action
reject() {
const model = this.args.model;
model.reject(model.actor, model.follower);
this.args.closeModal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export default class ActivityPubActorFollowers extends Controller {

@notEmpty("actors") hasActors;

get tableClass() {
let result = "activity-pub-follow-table followers";
if (this.currentUser.admin) {
result += " show-controls";
}
return result;
}

@action
loadMore() {
if (!this.loadMoreUrl || this.total <= this.actors.length) {
Expand Down
12 changes: 12 additions & 0 deletions assets/javascripts/discourse/models/activity-pub-actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ ActivityPubActor.reopenClass({
.catch(popupAjaxError);
},

reject(actorId, targetActorId) {
return ajax({
url: `${actorClientPath}/${actorId}/reject`,
type: "POST",
data: {
target_actor_id: targetActorId,
},
})
.then((response) => !!response?.success)
.catch(popupAjaxError);
},

list(actorId, params, listType) {
const queryParams = new URLSearchParams();

Expand Down
28 changes: 19 additions & 9 deletions assets/javascripts/discourse/routes/activity-pub-actor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { inject as service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
import ActivityPubActor from "../models/activity-pub-actor";

export default DiscourseRoute.extend({
router: service(),
site: service(),
store: service(),
export default class ActivityPubActorRoute extends DiscourseRoute {
@service router;
@service site;
@service store;

model(params) {
return ActivityPubActor.find(params.actor_id);
},
}

setupController(controller, model) {
const actor = model;
Expand All @@ -34,7 +34,7 @@ export default DiscourseRoute.extend({
props.canCreateTopicOnTag = !actor.model.staff || this.currentUser?.staff;
}
controller.setProperties(props);
},
}

@action
follow(actor, followActor) {
Expand All @@ -44,7 +44,7 @@ export default DiscourseRoute.extend({
);
return result;
});
},
}

@action
unfollow(actor, followedActor) {
Expand All @@ -56,5 +56,15 @@ export default DiscourseRoute.extend({
return result;
}
);
},
});
}

@action
reject(actor, follower) {
return ActivityPubActor.reject(actor.id, follower.id).then((result) => {
this.controllerFor(this.router.currentRouteName).actors.removeObject(
follower
);
return result;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class="activity-pub-followers-container"
>
{{#if this.hasActors}}
<ResponsiveTable @className="activity-pub-follow-table followers">
<ResponsiveTable @className={{this.tableClass}}>
<:header>
<TableHeaderToggle
@field="actor"
Expand All @@ -27,29 +27,50 @@
@order={{this.order}}
@asc={{this.asc}}
/>
{{#if this.currentUser.admin}}
<div
class="directory-table__column-header activity-pub-follow-table-actions"
>
<span class="text">{{i18n
"discourse_activity_pub.follow_table.actions"
}}</span>
</div>
{{/if}}
</:header>
<:body>
{{#each this.actors as |actor|}}
{{#each this.actors as |follower|}}
<div class="directory-table__row activity-pub-follow-table-row">
<div class="directory-table__cell activity-pub-follow-table-actor">
<ActivityPubActor @actor={{actor}} />
<ActivityPubActor @actor={{follower}} />
</div>
<div class="directory-table__cell activity-pub-follow-table-user">
{{#if actor.model}}
{{#if follower.model}}
<a
class="avatar"
href={{concat "/u/" actor.model.username}}
data-user-card={{actor.model.username}}
href={{concat "/u/" follower.model.username}}
data-user-card={{follower.model.username}}
>
{{avatar actor.model imageSize="small"}}
{{avatar follower.model imageSize="small"}}
</a>
{{/if}}
</div>
<div
class="directory-table__cell activity-pub-follow-table-followed-at"
>
{{bound-date actor.followed_at}}
{{bound-date follower.followed_at}}
</div>
{{#if this.currentUser.admin}}
<div
class="directory-table__cell activity-pub-follow-table-actions"
>
<ActivityPubFollowBtn
@actor={{actor}}
@follower={{follower}}
@reject={{route-action "reject"}}
@type="actor_reject"
/>
</div>
{{/if}}
</div>
{{/each}}
</:body>
Expand Down
3 changes: 3 additions & 0 deletions assets/stylesheets/common/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,9 @@ body.user-preferences-activity-pub-page {
.activity-pub-follow-table {
&.followers {
grid-template-columns: minmax(13em, 3fr) repeat(2, 110px);
&.show-controls {
grid-template-columns: minmax(13em, 3fr) repeat(3, 110px);
}
}
&.follows {
grid-template-columns: minmax(13em, 3fr) repeat(3, 110px);
Expand Down
6 changes: 6 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,17 @@ en:
title: Unfollow for %{actor} via ActivityPub
modal_title: Confirm Unfollow
confirm: Are you sure you want %{actor} to unfollow %{followed_actor}?
actor_reject:
label: Reject
title: Reject follow of %{actor} via ActivityPub
modal_title: Confirm Reject Follow
confirm: Are you sure you want to reject %{follower} from following %{actor}?
follow_table:
actor: Actor
user: User
followed_at: Followed
follow_pending: pending
actions: Actions
visibility:
label:
private: Followers Only
Expand Down
2 changes: 2 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ en:
cant_follow_target_actor: "Actor cant follow the target actor"
username_required: An ActivityPub username is required to create an actor.
full_topic_must_be_public: "Full topic publication requires public publication."
not_following_actor: "Target actor is not following the actor"
warning:
cant_create_model_for_actor_type: "Cannot create a model for %{actor_type} %{actor_id}"
cant_create_actor_for_model_type: "Cannot create an actor for %{model_type} %{model_id}"
not_allowed_to_create_actor_for_model: "Not allowed to create an actor for %{model_id}"
no_options: "No options passed to ActorHandler"
invalid_username: "Username must be %{min_length} to %{max_length} letters, numbers, dashes or underscores."
username_taken: "That username is already taken on this server."
user_not_authorized: "User not authorized"
auth:
error:
invalid_oauth_domain: "Invalid ActivityPub OAuth domain"
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
get ":actor_id/follows" => "actor#follows"
post ":actor_id/follow" => "actor#follow", :defaults => { format: :json }
delete ":actor_id/follow" => "actor#unfollow", :defaults => { format: :json }
post ":actor_id/reject" => "actor#reject", :defaults => { format: :json }
get ":actor_id/find-by-handle" => "actor#find_by_handle", :defaults => { format: :json }
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/discourse_activity_pub/ap/activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ def follow?
type == Follow.type
end

def reject?
type == Reject.type
end

def announce?
type == Announce.type
end
Expand Down
Loading

0 comments on commit 9051c43

Please sign in to comment.