monitor bsky user button

This commit is contained in:
Dylan Knutson
2025-08-16 21:27:55 +00:00
parent 8d6953c758
commit cdcd574d02
7 changed files with 170 additions and 6 deletions

View File

@@ -3,7 +3,8 @@ class Domain::UsersController < DomainController
extend T::Sig
extend T::Helpers
before_action :set_user!, only: %i[show followed_by following]
before_action :set_user!,
only: %i[show followed_by following monitor_bluesky_user]
before_action :set_post!, only: %i[users_faving_post]
skip_before_action :authenticate_user!,
only: %i[
@@ -167,6 +168,23 @@ class Domain::UsersController < DomainController
}
end
sig { void }
def monitor_bluesky_user
user = T.cast(@user, Domain::User::BlueskyUser)
authorize user
monitor = Domain::Bluesky::MonitoredObject.build_for_user(user)
if monitor.save
Domain::Bluesky::Job::ScanUserJob.perform_later(user:)
Domain::Bluesky::Job::ScanPostsJob.perform_later(user:)
flash[:notice] = "User is now being monitored"
else
flash[
:alert
] = "Error monitoring user: #{monitor.errors.full_messages.join(", ")}"
end
redirect_to domain_user_path(user)
end
private
sig { override.returns(DomainController::DomainParamConfig) }

View File

@@ -1,3 +1,12 @@
# typed: strict
class Domain::User::BlueskyUserPolicy < Domain::UserPolicy
sig { returns(T::Boolean) }
def view_is_bluesky_user_monitored?
is_role_admin? || is_role_moderator?
end
sig { returns(T::Boolean) }
def monitor_bluesky_user?
is_role_admin?
end
end

View File

@@ -3,4 +3,22 @@
<span class="font-medium italic text-slate-500">State</span>
<span class=""><%= user.account_state_for_view %></span>
</div>
<% if policy(user).view_is_bluesky_user_monitored? %>
<div class="flex flex-col">
<span class="font-medium italic text-slate-500">Monitored</span>
<% if Domain::Bluesky::MonitoredObject.exists?(kind: :user_did, value: user.did!) %>
<span class="">Yes</span>
<% else %>
<% if policy(user).monitor_bluesky_user? %>
<%= button_to(
"Monitor",
monitor_bluesky_user_domain_user_path(user),
class: "inline-block text-blue-400 border border-blue-400 hover:bg-blue-600 px-1 hover:text-white rounded-md transition-colors duration-200"
) %>
<% else %>
<span class="">No</span>
<% end %>
<% end %>
</div>
<% end %>
<% end %>

View File

@@ -180,11 +180,11 @@
<% end %>
</header>
<main class="flex flex-col grow bg-slate-200">
<% if notice %>
<p class="notice bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative" role="alert"><%= notice %></p>
<% end %>
<% if alert %>
<p class="alert bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert"><%= alert %></p>
<% if notice || alert %>
<span class="max-w-2xl mx-auto mt-4 flex flex-col gap-2">
<%= render "shared/flash_message", value: notice, class: "bg-green-100 border border-green-400 text-green-700" %>
<%= render "shared/flash_message", value: alert, class: "bg-red-100 border border-red-400 text-red-700" %>
</span>
<% end %>
<%= yield %>
</main>

View File

@@ -0,0 +1,10 @@
<% if (value = local_assigns[:value]) && value.present? %>
<div class="relative flex items-center justify-between gap-2 w-full <%= local_assigns[:class] %> px-4 py-3 rounded">
<p role="alert"><%= value %></p>
<button onclick="this.parentElement.remove()"
type="button"
class="opacity-50 hover:opacity-100 transition-opacity duration-200 border border-inherit p-1 leading-none w-6 h-6 rounded-sm flex items-center justify-center">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<% end %>

View File

@@ -33,6 +33,7 @@ Rails.application.routes.draw do
get "followed_by", on: :member, action: :followed_by
get "following", on: :member, action: :following
post "monitor_bluesky_user", on: :member, action: :monitor_bluesky_user
resources :job_events, only: [], controller: "domain/user_job_events" do
get "tracked_objects",

View File

@@ -312,4 +312,112 @@ RSpec.describe Domain::UsersController, type: :controller do
end
end
end
describe "POST #monitor_bluesky_user" do
let(:bluesky_user) { create(:domain_user_bluesky_user) }
let(:composite_param) { "bsky@#{bluesky_user.handle}" }
before do
# Sign in admin user for authentication
sign_in admin_user
# Mock authorization to allow the action
allow(controller).to receive(:authorize).and_return(true)
# Set @user instance variable as the controller does in find_user
controller.instance_variable_set(:@user, bluesky_user)
end
context "when monitoring succeeds" do
it "creates a monitored object" do
expect {
post :monitor_bluesky_user, params: { id: composite_param }
}.to change(Domain::Bluesky::MonitoredObject, :count).by(1)
monitor = Domain::Bluesky::MonitoredObject.last
expect(monitor.value).to eq(bluesky_user.did)
expect(monitor.kind).to eq("user_did")
end
it "enqueues scan user job" do
post :monitor_bluesky_user, params: { id: composite_param }
enqueued_jobs =
SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanUserJob)
expect(enqueued_jobs).to contain_exactly(
hash_including(user: bluesky_user),
)
end
it "enqueues scan posts job" do
post :monitor_bluesky_user, params: { id: composite_param }
enqueued_jobs =
SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanPostsJob)
expect(enqueued_jobs).to contain_exactly(
hash_including(user: bluesky_user),
)
end
it "sets success flash message" do
post :monitor_bluesky_user, params: { id: composite_param }
expect(flash[:notice]).to eq("User is now being monitored")
end
it "redirects to user page" do
post :monitor_bluesky_user, params: { id: composite_param }
expect(response).to redirect_to(domain_user_path(bluesky_user))
end
it "authorizes the user" do
expect(controller).to receive(:authorize).with(bluesky_user)
post :monitor_bluesky_user, params: { id: composite_param }
end
end
context "when monitoring fails due to validation errors" do
before do
# Create existing monitor to trigger uniqueness validation failure
Domain::Bluesky::MonitoredObject.create!(
value: bluesky_user.did,
kind: :user_did,
)
end
it "does not create a new monitored object" do
expect {
post :monitor_bluesky_user, params: { id: composite_param }
}.not_to change(Domain::Bluesky::MonitoredObject, :count)
end
it "fails to save the monitor due to uniqueness" do
monitor = Domain::Bluesky::MonitoredObject.build_for_user(bluesky_user)
expect(monitor.save).to be_falsey
expect(monitor.errors[:value]).to include("has already been taken")
end
it "does not enqueue any jobs" do
SpecUtil.clear_enqueued_jobs!
post :monitor_bluesky_user, params: { id: composite_param }
expect(
SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanUserJob),
).to be_empty
expect(
SpecUtil.enqueued_job_args(Domain::Bluesky::Job::ScanPostsJob),
).to be_empty
end
it "sets error flash message with validation errors" do
post :monitor_bluesky_user, params: { id: composite_param }
expect(flash[:alert]).to match(
/Error monitoring user: .*has already been taken/,
)
end
it "redirects to user page" do
post :monitor_bluesky_user, params: { id: composite_param }
expect(response).to redirect_to(domain_user_path(bluesky_user))
end
end
end
end