From f164bb56baa5832041fd211c9719bb5a0bc13205 Mon Sep 17 00:00:00 2001 From: Min Khant Kyaw Date: Wed, 10 Sep 2025 17:46:28 +0630 Subject: [PATCH 1/3] Add Bluesky Bridge integration and settings Introduces Bluesky Bridge support, including a new ChannelBlueskyBridgeService, migration for user fields (did_value, bluesky_bridge_enabled), and updates to server settings, seeds, and related logic. Refactors dashboard checks and scheduler to use the new service and settings, enabling conditional Bluesky Bridge processing. --- app/controllers/application_controller.rb | 20 ++ app/controllers/server_settings_controller.rb | 8 +- app/helpers/app_version_helper.rb | 2 +- .../scheduler/follow_bluesky_bot_scheduler.rb | 155 +-------------- app/models/user.rb | 2 + .../channel_bluesky_bridge_service.rb | 178 ++++++++++++++++++ ...lue_and_bluesky_bridge_enabled_to_users.rb | 10 + db/schema.rb | 4 +- db/seeds.rb | 3 + db/seeds/01_server_setttings.rb | 4 + lib/tasks/insert_server_setting_data.rake | 36 +++- 11 files changed, 258 insertions(+), 164 deletions(-) create mode 100644 app/services/channel_bluesky_bridge_service.rb create mode 100644 db/migrate/20250910000001_add_did_value_and_bluesky_bridge_enabled_to_users.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2febee7e..b3e53f99 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,8 @@ class ApplicationController < ActionController::Base helper_method :current_account + helper_method :is_channel_dashboard? + rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized require 'httparty' @@ -81,4 +83,22 @@ def current_account @current_account = current_user&.account end + + def is_channel_dashboard? + if Rails.env.development? + return true + end + + mastodon_url = ENV['MASTODON_INSTANCE_URL'] + return false if mastodon_url.nil? + + case mastodon_url + when /channel/ + true + when /staging\.patchwork\.online/ + true + else + false + end + end end diff --git a/app/controllers/server_settings_controller.rb b/app/controllers/server_settings_controller.rb index 2fd4344c..c77cc6cf 100644 --- a/app/controllers/server_settings_controller.rb +++ b/app/controllers/server_settings_controller.rb @@ -34,15 +34,15 @@ def server_setting_params end def prepare_server_setting - @parent_settings = ENV['MASTODON_INSTANCE_URL']&.include?('channel') ? ServerSetting.where(parent_id: nil).includes(:children).order(:id) : ServerSetting.where(parent_id: nil).order(:id) + @parent_settings = is_channel_dashboard? ? ServerSetting.where(parent_id: nil).includes(:children).order(:id) : ServerSetting.where(parent_id: nil).order(:id) @parent_settings = @parent_settings.where("lower(name) LIKE ?", "%#{@q.downcase}%") if @q.present? - desired_order = ['Local Features', 'User Management', 'Content filters', 'Spam filters', 'Federation', 'Plug-ins'] - desired_child_name = ['Spam filters', 'Content filters', 'Bluesky', 'Search opt-out', 'Long posts and markdown', 'e-Newsletters'] + desired_order = ['Local Features', 'User Management', 'Content filters', 'Spam filters', 'Federation', 'Plug-ins', 'Bluesky Bridge'] + desired_child_name = ['Spam filters', 'Content filters', 'Bluesky', 'Search opt-out', 'Long posts and markdown', 'e-Newsletters', 'Enable bluesky bridge'] @data = @parent_settings.map do |parent_setting| - child_setting_query = ENV['MASTODON_INSTANCE_URL']&.include?('channel') ? parent_setting.children.sort_by(&:position) : parent_setting.children.where(name: desired_child_name).sort_by(&:position) + child_setting_query = is_channel_dashboard? ? parent_setting.children.sort_by(&:position) : parent_setting.children.where(name: desired_child_name).sort_by(&:position) { name: parent_setting.name, settings: child_setting_query.map do |child_setting| diff --git a/app/helpers/app_version_helper.rb b/app/helpers/app_version_helper.rb index 9c0403b0..89dbf102 100644 --- a/app/helpers/app_version_helper.rb +++ b/app/helpers/app_version_helper.rb @@ -28,7 +28,7 @@ def application_name(app_name) humanized_name = app_name_key.humanize.capitalize # Check if the instance URL contains "channel" and app is Patchwork - if humanized_name == 'Patchwork' && ENV['MASTODON_INSTANCE_URL']&.include?('channel') + if humanized_name == 'Patchwork' && is_channel_dashboard? 'Channels' else humanized_name diff --git a/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb b/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb index 795023ea..243d499c 100644 --- a/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb +++ b/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb @@ -1,162 +1,19 @@ module Scheduler - class FollowBlueskyBotScheduler include Sidekiq::Worker + include ApplicationHelper + sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 15.minutes.to_i, queue: :scheduler def perform - return if ENV.fetch('RAILS_ENV', nil).eql?('staging') - - communities = Community.where(did_value: nil).exclude_incomplete_channels.exclude_deleted_channels - return unless communities.any? - - communities.each do |community| - - community_admin = CommunityAdmin.find_by(patchwork_community_id: community&.id, is_boost_bot: true) - next if community_admin.nil? - - account = community_admin&.account - next if account.nil? - - user = User.find_by(email: community_admin&.email, account_id: account&.id) - next if user.nil? - - token = fetch_oauth_token(user) - next if token.nil? - - target_account_id = Rails.cache.fetch('bluesky_bridge_bot_account_id', expires_in: 24.hours) do - search_target_account_id(token) - end - target_account = Account.find_by(id: target_account_id) - next if target_account.nil? - - account_relationship_array = handle_relationship(account, target_account.id) - next unless account_relationship_array.present? && account_relationship_array&.last - - if account_relationship_array&.last['requested'] - UnfollowService.new.call(account, target_account) - end - - next unless enable_bride_bluesky?(account) - - if account_relationship_array&.last['following'] == true && account_relationship_array&.last['requested'] == false - process_did_value(community, token, account) - else - FollowService.new.call(account, target_account) - account_relationship_array = handle_relationship(account, target_account.id) - process_did_value(community, token, account) if account_relationship_array.present? && account_relationship_array&.last && account_relationship_array&.last['following'] - end - end - end - - private - - def enable_bride_bluesky?(account) - account&.username.present? && account&.display_name.present? && - account&.avatar.present? && account&.header.present? - end - - def search_target_account_id(token) - query = '@bsky.brid.gy@bsky.brid.gy' - retries = 5 - result = nil + return unless ServerSetting.find_by(name: 'Enable bluesky bridge')&.value - while retries >= 0 - result = ContributorSearchService.new(query, url: ENV['MASTODON_INSTANCE_URL'], token: token).call - if result.any? - return result.last['id'] - end - retries -= 1 - end - nil - end - - def fetch_oauth_token(user) - GenerateAdminAccessTokenService.new(user&.id).call - end - - def process_did_value(community, token, account) - did_value = FetchDidValueService.new.call(account, community) - - if did_value - begin - create_dns_record(did_value, community) - sleep 1.minutes - create_direct_message(token, community) - community.update!(did_value: did_value) - rescue StandardError => e - Rails.logger.error("Error processing did_value for community #{community.id}: #{e.message}") - end - end - end - - def create_dns_record(did_value, community) - route53 = Aws::Route53::Client.new - hosted_zones = route53.list_hosted_zones - - env = ENV.fetch('RAILS_ENV', nil) - channel_zone = case env - when 'staging' - hosted_zones.hosted_zones.find { |zone| zone.name == 'staging.patchwork.online.' } - when 'production' - hosted_zones.hosted_zones.find { |zone| zone.name == 'channel.org.' } + if is_channel_dashboard? + ChannelBlueskyBridgeService.new.process_communities else - hosted_zones.hosted_zones.find { |zone| zone.name == 'localhost.3000.' } + # NYL bro end - if channel_zone - name = if community&.is_custom_domain? - "_atproto.#{community.slug}" - else - "_atproto.#{community&.slug}.channel.org" - end - route53.change_resource_record_sets({ - hosted_zone_id: channel_zone.id, # Hosted Zone for channel.org - change_batch: { - changes: [ - { - action: 'UPSERT', - resource_record_set: { - name: name, - type: 'TXT', - ttl: 60, - resource_records: [ - { value: "\"did=#{did_value}\"" }, - ], - }, - }, - ], - }, - }) - else - Rails.logger.error("Hosted zone for #{ENV.fetch('RAILS_ENV', nil)} not found.") - end - end - - def create_direct_message(token, community) - - name = if community&.is_custom_domain? - community&.slug - else - "#{community&.slug}.channel.org" - end - - status_params = { - "in_reply_to_id": nil, - "language": "en", - "media_ids": [], - "poll": nil, - "sensitive": false, - "spoiler_text": "", - "status": "@bsky.brid.gy@bsky.brid.gy username #{name}", - "visibility": "direct" - } - - PostStatusService.new.call(token: token, options: status_params) - end - - def handle_relationship(account, target_account_id) - AccountRelationshipsService.new.call(account, target_account_id) end end diff --git a/app/models/user.rb b/app/models/user.rb index 5f845101..9cc84200 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,12 +4,14 @@ # # id :bigint not null, primary key # approved :boolean default(TRUE), not null +# bluesky_bridge_enabled :boolean default(FALSE), not null # chosen_languages :string is an Array # confirmation_sent_at :datetime # confirmation_token :string # confirmed_at :datetime # consumed_timestep :integer # current_sign_in_at :datetime +# did_value :string # disabled :boolean default(FALSE), not null # email :string default(""), not null # encrypted_otp_secret :string diff --git a/app/services/channel_bluesky_bridge_service.rb b/app/services/channel_bluesky_bridge_service.rb new file mode 100644 index 00000000..3b5583f8 --- /dev/null +++ b/app/services/channel_bluesky_bridge_service.rb @@ -0,0 +1,178 @@ +class ChannelBlueskyBridgeService + include ApplicationHelper + + def initialize + end + + def process_communities + communities = Community.where(did_value: nil).exclude_incomplete_channels.exclude_deleted_channels + return unless communities.any? + + communities.each do |community| + process_community(community) + end + end + + private + + def process_community(community) + community_admin = CommunityAdmin.find_by(patchwork_community_id: community&.id, is_boost_bot: true) + return if community_admin.nil? + + account = community_admin&.account + return if account.nil? + + user = User.find_by(email: community_admin&.email, account_id: account&.id) + return if user.nil? + + token = fetch_oauth_token(user) + return if token.nil? + + target_account_id = Rails.cache.fetch('bluesky_bridge_bot_account_id', expires_in: 24.hours) do + search_target_account_id(token) + end + target_account = Account.find_by(id: target_account_id) + return if target_account.nil? + + account_relationship_array = handle_relationship(account, target_account.id) + return unless account_relationship_array.present? && account_relationship_array&.last + + if account_relationship_array&.last['requested'] + UnfollowService.new.call(account, target_account) + end + + return unless enable_bridge_bluesky?(account) + + if account_relationship_array&.last['following'] == true && account_relationship_array&.last['requested'] == false + process_did_value(community, token, account) + else + FollowService.new.call(account, target_account) + account_relationship_array = handle_relationship(account, target_account.id) + process_did_value(community, token, account) if account_relationship_array.present? && account_relationship_array&.last && account_relationship_array&.last['following'] + end + end + + def enable_bridge_bluesky?(account) + account&.username.present? && account&.display_name.present? && + account&.avatar.present? && account&.header.present? + end + + def search_target_account_id(token) + query = '@bsky.brid.gy@bsky.brid.gy' + retries = 5 + result = nil + + while retries >= 0 + result = ContributorSearchService.new(query, url: ENV['MASTODON_INSTANCE_URL'], token: token).call + if result.any? + return result.last['id'] + end + retries -= 1 + end + nil + end + + def fetch_oauth_token(user) + GenerateAdminAccessTokenService.new(user&.id).call + end + + def process_did_value(community, token, account) + did_value = FetchDidValueService.new.call(account, community) + + if did_value + begin + create_dns_record(did_value, community) + sleep 1.minutes + create_direct_message(token, community) + community.update!(did_value: did_value) + rescue StandardError => e + Rails.logger.error("Error processing did_value for community #{community.id}: #{e.message}") + end + end + end + + def create_dns_record(did_value, community) + route53 = Aws::Route53::Client.new + hosted_zones = route53.list_hosted_zones + + env = ENV.fetch('RAILS_ENV', nil) + channel_zone = case env + when 'staging' + hosted_zones.hosted_zones.find { |zone| zone.name == 'staging.patchwork.online.' } + when 'production' + hosted_zones.hosted_zones.find { |zone| zone.name == 'channel.org.' } + else + hosted_zones.hosted_zones.find { |zone| zone.name == 'localhost.3000.' } + end + + if channel_zone + name = if community&.is_custom_domain? + "_atproto.#{community.slug}" + else + "_atproto.#{community&.slug}.channel.org" + end + + # Determine the correct domain based on environment and custom domain + domain_name = determine_domain_name(community, env) + name = "_atproto.#{domain_name}" + + route53.change_resource_record_sets({ + hosted_zone_id: channel_zone.id, + change_batch: { + changes: [ + { + action: 'UPSERT', + resource_record_set: { + name: name, + type: 'TXT', + ttl: 60, + resource_records: [ + { value: "\"did=#{did_value}\"" }, + ], + }, + }, + ], + }, + }) + else + Rails.logger.error("Hosted zone for #{ENV.fetch('RAILS_ENV', nil)} not found.") + end + end + + def create_direct_message(token, community) + env = ENV.fetch('RAILS_ENV', nil) + domain_name = determine_domain_name(community, env) + + status_params = { + "in_reply_to_id": nil, + "language": "en", + "media_ids": [], + "poll": nil, + "sensitive": false, + "spoiler_text": "", + "status": "@bsky.brid.gy@bsky.brid.gy username #{domain_name}", + "visibility": "direct" + } + + PostStatusService.new.call(token: token, options: status_params) + end + + def handle_relationship(account, target_account_id) + AccountRelationshipsService.new.call(account, target_account_id) + end + + def determine_domain_name(community, env) + if community&.is_custom_domain? + community&.slug + else + case env + when 'staging' + "#{community&.slug}.staging.patchwork.online" + when 'production' + "#{community&.slug}.channel.org" + else + "#{community&.slug}.channel.org" + end + end + end +end diff --git a/db/migrate/20250910000001_add_did_value_and_bluesky_bridge_enabled_to_users.rb b/db/migrate/20250910000001_add_did_value_and_bluesky_bridge_enabled_to_users.rb new file mode 100644 index 00000000..792f7522 --- /dev/null +++ b/db/migrate/20250910000001_add_did_value_and_bluesky_bridge_enabled_to_users.rb @@ -0,0 +1,10 @@ +class AddDidValueAndBlueskyBridgeEnabledToUsers < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + safety_assured do + add_column :users, :did_value, :string, default: nil, null: true unless column_exists?(:users, :did_value) + add_column :users, :bluesky_bridge_enabled, :boolean, default: false, null: false unless column_exists?(:users, :bluesky_bridge_enabled) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 12ae9e70..b7d0f89c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_09_02_101613) do +ActiveRecord::Schema[7.1].define(version: 2025_09_10_000001) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1517,6 +1517,8 @@ t.text "settings" t.string "time_zone" t.string "otp_secret" + t.string "did_value" + t.boolean "bluesky_bridge_enabled", default: false, null: false t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)" diff --git a/db/seeds.rb b/db/seeds.rb index a94e28ae..5edcb616 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,3 +10,6 @@ load seed load 'db/seeds/community_user_role.rb' end + +# Run the insert_server_setting_data rake task +Rake::Task['db:insert_server_setting_data'].invoke diff --git a/db/seeds/01_server_setttings.rb b/db/seeds/01_server_setttings.rb index 30878737..4f3c24ce 100644 --- a/db/seeds/01_server_setttings.rb +++ b/db/seeds/01_server_setttings.rb @@ -24,6 +24,10 @@ { name: 'Plug-ins', options: [] + }, + { + name: 'Bluesky Bridge', + options: ['Enable bluesky bridge'] } ] diff --git a/lib/tasks/insert_server_setting_data.rake b/lib/tasks/insert_server_setting_data.rake index d6ace1cb..7b32b5c0 100644 --- a/lib/tasks/insert_server_setting_data.rake +++ b/lib/tasks/insert_server_setting_data.rake @@ -1,16 +1,16 @@ -# lib/tasks/insert_server_setting_data.rake - namespace :db do desc "Seed parent and child settings data" task insert_server_setting_data: :environment do - if ServerSetting.all.count > 0 - ServerSetting.destroy_all - end + # if ServerSetting.all.count > 0 + # ServerSetting.destroy_all + # end + + # if KeywordFilter.all.count > 0 + # KeywordFilter.destroy_all + # end + puts "Inserting server settings & keywords..." - if KeywordFilter.all.count > 0 - KeywordFilter.destroy_all - end # Sample data for parent settings parent_settings_data = { "Spam Block" => [], @@ -18,11 +18,19 @@ namespace :db do "Federation" => [], "Local Features" => [], "User Management" => [], - "Plug-ins" => [] + "Plug-ins" => [], + "Bluesky Bridge" => [] } # Create parent settings and set positions parent_settings_data.each_with_index do |(parent_name, _), index| + # Skip if parent setting already exists + existing_parent = ServerSetting.find_by(name: parent_name, parent_id: nil) + if existing_parent + puts "Skipping existing parent setting: #{parent_name}" + next + end + parent_setting = ServerSetting.create!(name: parent_name, value: nil) # Sample data for child settings with parent associations @@ -53,10 +61,20 @@ namespace :db do ], "Plug-ins" => [ + ], + "Bluesky Bridge" => [ + { name: "Enable bluesky bridge", value: false } ] } child_settings_data[parent_name].each_with_index do |child, child_index| + # Skip if child setting already exists + existing_child = ServerSetting.find_by(name: child[:name], parent_id: parent_setting.id) + if existing_child + puts "Skipping existing child setting: #{child[:name]}" + next + end + ServerSetting.create!(name: child[:name], value: child[:value], position: child_index + 1, parent_id: parent_setting.id) end end From 0882396647ac5c8bb07305555148d8b7af7adf34 Mon Sep 17 00:00:00 2001 From: Nyan Lin Htut Date: Wed, 10 Sep 2025 22:05:24 +0700 Subject: [PATCH 2/3] add: non-channel bluesky bridge service --- .../scheduler/follow_bluesky_bot_scheduler.rb | 2 +- .../non_channel_bluesky_bridge_service.rb | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 app/services/non_channel_bluesky_bridge_service.rb diff --git a/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb b/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb index 243d499c..6a0534f8 100644 --- a/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb +++ b/app/jobs/scheduler/follow_bluesky_bot_scheduler.rb @@ -11,7 +11,7 @@ def perform if is_channel_dashboard? ChannelBlueskyBridgeService.new.process_communities else - # NYL bro + NonChannelBlueskyBridgeService.new.process_users end end diff --git a/app/services/non_channel_bluesky_bridge_service.rb b/app/services/non_channel_bluesky_bridge_service.rb new file mode 100644 index 00000000..67767343 --- /dev/null +++ b/app/services/non_channel_bluesky_bridge_service.rb @@ -0,0 +1,149 @@ +class NonChannelBlueskyBridgeService + include ApplicationHelper + + def initialize + end + + def process_users + users = User.where(did_value: nil, bluesky_bridge_enabled: true) + return unless users.any? + + users.each do |user| + process_user(user) + end + end + + private + + def process_user(user) + + account = user&.account + next if account.nil? + + token = fetch_oauth_token(user) + next if token.nil? + + target_account_id = Rails.cache.fetch('bluesky_bridge_bot_account_id', expires_in: 24.hours) do + search_target_account_id(token) + end + target_account = Account.find_by(id: target_account_id) + next if target_account.nil? + + account_relationship_array = handle_relationship(account, target_account.id) + next unless account_relationship_array.present? && account_relationship_array&.last + + if account_relationship_array&.last['requested'] + UnfollowService.new.call(account, target_account) + end + + next unless bluesky_bridge_enabled?(account) + + if account_relationship_array&.last['following'] == true && account_relationship_array&.last['requested'] == false + process_did_value(user, token, account) + else + FollowService.new.call(account, target_account) + account_relationship_array = handle_relationship(account, target_account.id) + process_did_value(user, token, account) if account_relationship_array.present? && account_relationship_array&.last && account_relationship_array&.last['following'] + end + end + + def bluesky_bridge_enabled?(account) + account&.username.present? && account&.display_name.present? && + account&.avatar.present? && account&.header.present? + end + + def search_target_account_id(token) + query = '@bsky.brid.gy@bsky.brid.gy' + retries = 5 + result = nil + + while retries >= 0 + result = ContributorSearchService.new(query, url: ENV['MASTODON_INSTANCE_URL'], token: token).call + if result.any? + return result.last['id'] + end + retries -= 1 + end + nil + end + + def fetch_oauth_token(user) + GenerateAdminAccessTokenService.new(user&.id).call + end + + def process_did_value(user, token, account) + did_value = FetchDidValueService.new.call(account, user) + + if did_value + begin + create_dns_record(did_value, account) + sleep 1.minutes + create_direct_message(token, account) + user.update!(did_value: did_value) + rescue StandardError => e + Rails.logger.error("Error processing did_value for user #{account.username}: #{e.message}") + end + end + end + + def create_dns_record(did_value, account) + route53 = Aws::Route53::Client.new + hosted_zones = route53.list_hosted_zones + + env = ENV.fetch('RAILS_ENV', nil) + channel_zone = case env + when 'staging' + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } + when 'production' + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } + else + hosted_zones.hosted_zones.find { |zone| zone.name == 'localhost:3000.' } + end + + if channel_zone + name = "_atproto.#{account&.username}.#{ENV['LOCAL_DOMAIN']}" + route53.change_resource_record_sets({ + hosted_zone_id: channel_zone.id, + change_batch: { + changes: [ + { + action: 'UPSERT', + resource_record_set: { + name: name, + type: 'TXT', + ttl: 60, + resource_records: [ + { value: "\"did=#{did_value}\"" }, + ], + }, + }, + ], + }, + }) + else + Rails.logger.error("Hosted zone for #{ENV.fetch('RAILS_ENV', nil)} not found.") + end + end + + def create_direct_message(token, account) + + name = "#{account&.username}@#{ENV['LOCAL_DOMAIN']}" + + status_params = { + "in_reply_to_id": nil, + "language": "en", + "media_ids": [], + "poll": nil, + "sensitive": false, + "spoiler_text": "", + "status": "@bsky.brid.gy@bsky.brid.gy username #{name}", + "visibility": "direct" + } + + PostStatusService.new.call(token: token, options: status_params) + end + + def handle_relationship(account, target_account_id) + AccountRelationshipsService.new.call(account, target_account_id) + end +end \ No newline at end of file From b2b6972db089049de8fd139b84496e7af3db6a3b Mon Sep 17 00:00:00 2001 From: Min Khant Kyaw Date: Wed, 10 Sep 2025 22:48:08 +0630 Subject: [PATCH 3/3] Implement Bluesky Bridge settings update and validation in UsersController; adjust domain handling in services and add translations for error messages. --- app/controllers/api/v1/users_controller.rb | 60 +++++++++++++++++++ .../channel_bluesky_bridge_service.rb | 14 ++--- .../non_channel_bluesky_bridge_service.rb | 14 ++--- config/locales/cy.yml | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/es.yml | 1 + config/locales/fr.yml | 1 + config/locales/it.yml | 1 + config/locales/ja.yml | 1 + config/locales/pt.yml | 1 + config/locales/pt_BR.yml | 1 + config/locales/ru.yml | 1 + config/routes/api_v1.rb | 2 + 14 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 app/controllers/api/v1/users_controller.rb diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 00000000..ea8781a7 --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Api + module V1 + class UsersController < ApiController + skip_before_action :verify_key! + before_action :check_authorization_header + before_action :set_authenticated_account + + def update_bluesky_bridge_setting + return render_not_found unless @account + + desired_value = parse_boolean_param(user_params[:bluesky_bridge_enabled]) + + # Validate parameter presence first + if desired_value.nil? + return render_error('api.errors.invalid_request', :bad_request) + end + + # Check if user meets the requirements for Bluesky Bridge + unless meets_bluesky_bridge_requirements? + return render_errors('api.account.errors.unable_to_bridge', :unprocessable_entity) + end + + if current_user.update(bluesky_bridge_enabled: desired_value) + render_success({id: current_user.id, bluesky_bridge_enabled: current_user.bluesky_bridge_enabled}, 'api.messages.updated') + else + render_validation_failed(current_user.errors, 'api.errors.validation_failed') + end + end + + private + + def parse_boolean_param(value) + ActiveModel::Type::Boolean.new.cast(value) + end + + def meets_bluesky_bridge_requirements? + @account&.username.present? && @account&.display_name.present? && + @account&.avatar.present? && @account&.header.present? + end + + def set_authenticated_account + if params[:instance_domain].present? + @account = current_remote_account + else + @account = current_account + end + + return render_unauthorized unless @account + + @account + end + + def user_params + params.permit(:bluesky_bridge_enabled) + end + end + end +end \ No newline at end of file diff --git a/app/services/channel_bluesky_bridge_service.rb b/app/services/channel_bluesky_bridge_service.rb index 3b5583f8..7186b2a4 100644 --- a/app/services/channel_bluesky_bridge_service.rb +++ b/app/services/channel_bluesky_bridge_service.rb @@ -98,18 +98,18 @@ def create_dns_record(did_value, community) env = ENV.fetch('RAILS_ENV', nil) channel_zone = case env when 'staging' - hosted_zones.hosted_zones.find { |zone| zone.name == 'staging.patchwork.online.' } + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } when 'production' - hosted_zones.hosted_zones.find { |zone| zone.name == 'channel.org.' } + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } else - hosted_zones.hosted_zones.find { |zone| zone.name == 'localhost.3000.' } + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } end if channel_zone name = if community&.is_custom_domain? "_atproto.#{community.slug}" else - "_atproto.#{community&.slug}.channel.org" + "_atproto.#{community&.slug}.#{ENV['LOCAL_DOMAIN']}" end # Determine the correct domain based on environment and custom domain @@ -167,11 +167,11 @@ def determine_domain_name(community, env) else case env when 'staging' - "#{community&.slug}.staging.patchwork.online" + "#{community&.slug}.#{ENV['LOCAL_DOMAIN']}" when 'production' - "#{community&.slug}.channel.org" + "#{community&.slug}.#{ENV['LOCAL_DOMAIN']}" else - "#{community&.slug}.channel.org" + "#{community&.slug}.#{ENV['LOCAL_DOMAIN']}" end end end diff --git a/app/services/non_channel_bluesky_bridge_service.rb b/app/services/non_channel_bluesky_bridge_service.rb index 67767343..a2f2d9f9 100644 --- a/app/services/non_channel_bluesky_bridge_service.rb +++ b/app/services/non_channel_bluesky_bridge_service.rb @@ -18,25 +18,25 @@ def process_users def process_user(user) account = user&.account - next if account.nil? + return if account.nil? token = fetch_oauth_token(user) - next if token.nil? + return if token.nil? target_account_id = Rails.cache.fetch('bluesky_bridge_bot_account_id', expires_in: 24.hours) do search_target_account_id(token) end target_account = Account.find_by(id: target_account_id) - next if target_account.nil? + return if target_account.nil? account_relationship_array = handle_relationship(account, target_account.id) - next unless account_relationship_array.present? && account_relationship_array&.last + return unless account_relationship_array.present? && account_relationship_array&.last if account_relationship_array&.last['requested'] UnfollowService.new.call(account, target_account) end - - next unless bluesky_bridge_enabled?(account) + + return unless bluesky_bridge_enabled?(account) if account_relationship_array&.last['following'] == true && account_relationship_array&.last['requested'] == false process_did_value(user, token, account) @@ -97,7 +97,7 @@ def create_dns_record(did_value, account) when 'production' hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } else - hosted_zones.hosted_zones.find { |zone| zone.name == 'localhost:3000.' } + hosted_zones.hosted_zones.find { |zone| zone.name == ENV['LOCAL_DOMAIN'] } end if channel_zone diff --git a/config/locales/cy.yml b/config/locales/cy.yml index a62d0e1a..97877444 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -51,6 +51,7 @@ cy: account_not_found: "Ni ddaethpwyd o hyd i'r cyfrif." account_suspended: "Mae'r cyfrif wedi'i atal." email_not_verified: "Nid yw'r cyfeiriad e-bost wedi'i ddilysu." + unable_to_bridge: "Nid yw'r defnyddiwr yn bodloni gofynion Pont Bluesky. Rhaid cael enw defnyddiwr, enw arddangos, afatar a phennawd." setting: errors: diff --git a/config/locales/de.yml b/config/locales/de.yml index 5f063112..47893c6a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -51,6 +51,7 @@ de: account_not_found: "Konto nicht gefunden." account_suspended: "Konto wurde gesperrt." email_not_verified: "E-Mail-Adresse nicht verifiziert." + unable_to_bridge: "Benutzer erfüllt nicht die Bluesky Bridge-Anforderungen. Muss Benutzername, Anzeigename, Avatar und Header haben." setting: errors: diff --git a/config/locales/en.yml b/config/locales/en.yml index ab3b6a6e..c6ca84c3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -51,6 +51,7 @@ en: account_not_found: "Account not found" account_suspended: "Account has been suspended" email_not_verified: "Email address not verified" + unable_to_bridge: "User does not meet Bluesky Bridge requirements. Must have username, display_name, avatar, and header." setting: errors: diff --git a/config/locales/es.yml b/config/locales/es.yml index 79b33e32..2e7eab4f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -51,6 +51,7 @@ es: account_not_found: "Cuenta no encontrada" account_suspended: "La cuenta ha sido suspendida" email_not_verified: "Dirección de correo electrónico no verificada" + unable_to_bridge: "El usuario no cumple con los requisitos de Bluesky Bridge. Debe tener nombre de usuario, nombre para mostrar, avatar y encabezado." setting: errors: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f7f40de2..40a51dd9 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -51,6 +51,7 @@ fr: account_not_found: "Compte introuvable." account_suspended: "Le compte a été suspendu." email_not_verified: "Adresse e-mail non vérifiée." + unable_to_bridge: "L'utilisateur ne répond pas aux exigences du pont Bluesky. Doit avoir un nom d'utilisateur, un nom d'affichage, un avatar et un en-tête." setting: errors: diff --git a/config/locales/it.yml b/config/locales/it.yml index aa1d70f8..588dc819 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -51,6 +51,7 @@ it: account_not_found: "Account non trovato." account_suspended: "L'account è stato sospeso." email_not_verified: "Indirizzo email non verificato." + unable_to_bridge: "L'utente non soddisfa i requisiti di Bluesky Bridge. Deve avere nome utente, nome visualizzato, avatar e intestazione." setting: errors: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 93439125..c1713bdd 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -51,6 +51,7 @@ ja: account_not_found: "アカウントが見つかりません。" account_suspended: "アカウントが停止されています。" email_not_verified: "メールアドレスが確認されていません。" + unable_to_bridge: "ユーザーはBluesky Bridgeの要件を満たしていません。ユーザー名、表示名、アバター、およびヘッダーが必要です。" setting: errors: diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 0f0ad308..17df2a59 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -51,6 +51,7 @@ pt: account_not_found: "Conta não encontrada." account_suspended: "A conta foi suspensa." email_not_verified: "Endereço de correio electrónico não verificado." + unable_to_bridge: "O utilizador não cumpre os requisitos da Ponte Bluesky. Deve ter nome de utilizador, nome de exibição, avatar e cabeçalho." setting: errors: diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 10d2889e..69c48094 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -51,6 +51,7 @@ pt_BR: account_not_found: "Conta não encontrada." account_suspended: "A conta foi suspensa." email_not_verified: "Endereço de e-mail não verificado." + unable_to_bridge: "O usuário não atende aos requisitos da Ponte Bluesky. Deve ter nome de usuário, nome de exibição, avatar e cabeçalho." setting: errors: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index baf57219..153bd333 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -51,6 +51,7 @@ ru: account_not_found: "Аккаунт не найден" account_suspended: "Аккаунт заблокирован" email_not_verified: "Email адрес не подтверждён" + unable_to_bridge: "Пользователь не соответствует требованиям Bluesky Bridge. Должен иметь имя пользователя, отображаемое имя, аватар и заголовок." setting: errors: diff --git a/config/routes/api_v1.rb b/config/routes/api_v1.rb index 3893ed64..abc579f4 100644 --- a/config/routes/api_v1.rb +++ b/config/routes/api_v1.rb @@ -131,5 +131,7 @@ post :boost_post end end + + post 'users/bluesky_bridge', to: 'users#update_bluesky_bridge_setting' end end