Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions app/controllers/server_settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/app_version_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
155 changes: 6 additions & 149 deletions app/jobs/scheduler/follow_bluesky_bot_scheduler.rb
Original file line number Diff line number Diff line change
@@ -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.' }
NonChannelBlueskyBridgeService.new.process_users
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
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading