MastodnのPsychを4にする旅 (未完)
MastodonのRubyを3.1にするためには、Ruby 3.1に添付の、読み込む型に制限の加わったPsych 4でYamlをパースできるのがうれしいです。
Mastodonでは、rails-settings-cached gem (0.6.6)がYAML.load
メソッド経由でPsychを使っています。
Psychの動作のバージョンによる違い
Psych 4でパースできないYamlは、例えばsettings
テーブルにあるようです。
> SELECT var,value FROM settings WHERE var='interactions';
var | value
--------------+---------------------------------------------------------
interactions | --- !ruby/hash:ActiveSupport::HashWithIndifferentAccess+
| must_be_follower: false +
| must_be_following: false +
|
(1 row)
このようにRubyの型が指定されているYamlはPsych 4ではそのままでは読めません。
# test.rb
require 'yaml'
module ActiveSupport
class HashWithIndifferentAccess < Hash
end
end
yaml = <<_END
--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess
must_be_follower: false
must_be_following: false
_END
puts "Psych::VERSION: #{Psych::VERSION}"
p YAML.load(yaml)
Ruby 3.0
$ rbenv local 3.0.4
$ ruby test.rb
Psych::VERSION: 3.3.2
{"must_be_follower"=>false, "must_be_following"=>false}
Ruby 3.1
$ rbenv local 3.1.2
$ ruby test.rb
Psych::VERSION: 4.0.3
/home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/class_loader.rb:99:in `find': Tried to load unspecified class: ActiveSupport::HashWithIndifferentAccess (Psych::DisallowedClass)
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/class_loader.rb:28:in `load'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:424:in `resolve_class'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:288:in `visit_Psych_Nodes_Mapping'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/visitor.rb:30:in `visit'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/visitor.rb:6:in `accept'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:35:in `accept'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:318:in `visit_Psych_Nodes_Document'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/visitor.rb:30:in `visit'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/visitor.rb:6:in `accept'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:35:in `accept'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych.rb:335:in `safe_load'
from /home/zunda/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych.rb:370:in `load'
from test.rb:15:in `<main>'
型の指定されたYamlはどこで生成されたのか
must_be_follower: false
がどこで扱われてるのか探してみます。
$ grep -rl must_be_follower | grep -v locales
config/settings.yml
spec/support/examples/lib/settings/scoped_settings.rb
spec/lib/user_settings_decorator_spec.rb
spec/controllers/settings/preferences/notifications_controller_spec.rb
log/test.log
coverage/index.html
app/services/notify_service.rb
app/views/settings/preferences/notifications/show.html.haml
app/controllers/settings/preferences_controller.rb
デフォルトの値はYamlのファイルに定義されているみたい。
# config/settings.yml
$ cat config/settings.yml
# This file contains default values, and does not need to be edited. All
# important settings can be changed from the admin interface.
defaults: &defaults
site_title: Mastodon
site_short_description: ''
site_description: ''
site_extended_description: ''
site_terms: ''
site_contact_username: ''
site_contact_email: ''
registrations_mode: 'open'
profile_directory: true
closed_registrations_message: ''
open_deletion: true
min_invite_role: 'admin'
timeline_preview: true
show_staff_badge: true
default_sensitive: false
unfollow_modal: false
boost_modal: false
delete_modal: true
auto_play_gif: false
display_media: 'default'
expand_spoilers: false
preview_sensitive_media: false
reduce_motion: false
disable_swiping: false
show_application: true
system_font_ui: false
noindex: false
theme: 'default'
aggregate_reblogs: true
advanced_layout: false
use_blurhash: true
use_pending_items: false
trends: true
trendable_by_default: false
crop_images: true
notification_emails:
follow: true
reblog: false
favourite: false
mention: true
follow_request: true
digest: true
report: true
pending_account: true
trending_tag: true
appeal: true
always_send_emails: false
interactions:
must_be_follower: false
must_be_following: false
must_be_following_dm: false
reserved_usernames:
- admin
- support
- help
- root
- webmaster
- administrator
- mod
- moderator
disallowed_hashtags: # space separated string or list of hashtags without the hash
bootstrap_timeline_accounts: ''
activity_api_enabled: true
peers_api_enabled: true
show_domain_blocks: 'disabled'
show_domain_blocks_rationale: 'disabled'
require_invite_text: false
backups_retention_period: 7
development:
<<: *defaults
test:
<<: *defaults
production:
<<: *defaults
/settings/preferences/notifications
で設定できる項目のようです。
<h4>その他の通知設定</h4>
<div class='fields-group'>
<div class="input with_label boolean optional user_interactions_must_be_follower"><div class="label_input"><label class="boolean optional" for="user_interactions_must_be_follower">フォロワー以外からの通知をブロック</label><div class="label_input__wrapper"><input value="0" autocomplete="off" type="hidden" name="user[interactions][must_be_follower]" /><label class="checkbox"><input class="boolean optional" type="checkbox" value="1" name="user[interactions][must_be_follower]" id="user_interactions_must_be_follower" /></label></div></div></div>
Specを書く
以前、branding_controller_spec.rb
を参考にmedia_cache_retention_period
を整数にするSpecを書いた。似たものをユーザー用に書く必要がありそう。
# spec/controllers/admin/settings/content_retention_controller_spec.rb
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Admin::Settings::ContentRetentionController, type: :controller do
render_views
describe 'When signed in as an admin' do
before do
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
before do
allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
end
around do |example|
before = Setting.media_cache_retention_period
example.run
Setting.media_cache_retention_period = before
end
it 'saves a settings value as an Integer' do
patch :update, params: { form_admin_settings: { media_cache_retention_period: '42' } }
expect(response).to redirect_to(admin_settings_content_retention_path)
expect(Setting.media_cache_retention_period).to eq 42
end
it 'saves a blank value as a blank' do
patch :update, params: { form_admin_settings: { media_cache_retention_period: '' } }
expect(response).to redirect_to(admin_settings_content_retention_path)
expect(Setting.media_cache_retention_period).to eq ''
end
end
end
end
既存のspec/controllers/settings/preferences/notifications_controller_spec.rb
が落ちてくれそうだ。
# spec/controllers/settings/preferences/notifications_controller_spec.rb
require 'rails_helper'
describe Settings::Preferences::NotificationsController do
render_views
let(:user) { Fabricate(:user) }
before do
sign_in user, scope: :user
end
describe 'GET #show' do
it 'returns http success' do
get :show
expect(response).to have_http_status(200)
end
end
describe 'PUT #update' do
it 'updates notifications settings' do
user.settings['notification_emails'] = user.settings['notification_emails'].merge('follow' => false)
user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true)
put :update, params: {
user: {
notification_emails: { follow: '1' },
interactions: { must_be_follower: '0' },
}
}
expect(response).to redirect_to(settings_preferences_notifications_path)
user.reload
expect(user.settings['notification_emails']['follow']).to be true
expect(user.settings['interactions']['must_be_follower']).to be false
end
end
end
Rubyを更新してテストを落とす
$ vi Gemfile
$ git diff Gemfile
diff --git a/Gemfile b/Gemfile
index 07c300510..34123eac8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org'
-ruby '>= 2.6.0', '< 3.1.0'
+ruby '>= 2.6.0', '< 3.2.0'
gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
@@ -12,6 +12,8 @@ gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.4'
+gem 'mail', git: 'https://github.com/Shopify/mail.git', branch: 'net-smtp-dependency'
+
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.4'
gem 'makara', '~> 0.5'
$ rbenv local 3.1.2
$ bundle install --path=vendor/bundle
:
Your bundle is locked to mail (2.7.1) from https://github.com/Shopify/mail.git (at net-smtp-dependency@651a59a), but that version
can no longer be found in that source. That means the author of mail (2.7.1) has removed it. You'll need to update your bundle to
a version other than mail (2.7.1) that hasn't been removed in order to install.
ぎゃー。なにか警告が出てる。
https://github.com/mastodon/mastodon/pull/21061 を参考に、Gemfileを下記のように編集した。
diff --git a/Gemfile b/Gemfile
index 07c300510..00459226e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org'
-ruby '>= 2.6.0', '< 3.1.0'
+ruby '>= 2.6.0', '< 3.2.0'
gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
@@ -12,6 +12,9 @@ gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.4'
+gem 'net-smtp', require: false
+gem 'premailer-rails'
+
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.4'
gem 'makara', '~> 0.5'
$ bundle install --path=vendor/bundle
$ bundle exec rspec spec/controllers/settings/preferences/notifications_controller_spec.rb
Calling `DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please call `DidYouMean.correct_error(error_name, spell_checker)' instead.
Randomized with seed 905
1) Settings::Preferences::NotificationsController PUT #update updates notifications settings
Failure/Error:
val = Rails.cache.fetch(cache_key(key, nil)) do
db_val = object(key)
if db_val
default_value = default_settings[key]
return default_value.with_indifferent_access.merge!(db_val.value) if default_value.is_a?(Hash)
db_val.value
else
default_settings[key]
Psych::BadAlias:
Unknown alias: defaults
# ./app/models/setting.rb:26:in `[]'
# ./app/validators/unreserved_username_validator.rb:21:in `reserved_username?'
# ./app/validators/unreserved_username_validator.rb:9:in `validate'
# ./spec/controllers/settings/preferences/notifications_controller_spec.rb:6:in `block (2 levels) in <top (required)>'
# ./spec/controllers/settings/preferences/notifications_controller_spec.rb:9:in `block (2 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# NoMethodError:
# undefined method `reserved_usernames' for Setting:Class
# Did you mean? reset_sequence_name
# ./app/validators/unreserved_username_validator.rb:21:in `reserved_username?'
2) Settings::Preferences::NotificationsController GET #show returns http success
Failure/Error:
val = Rails.cache.fetch(cache_key(key, nil)) do
db_val = object(key)
if db_val
default_value = default_settings[key]
return default_value.with_indifferent_access.merge!(db_val.value) if default_value.is_a?(Hash)
db_val.value
else
default_settings[key]
Psych::BadAlias:
Unknown alias: defaults
# ./app/models/setting.rb:26:in `[]'
# ./app/validators/unreserved_username_validator.rb:21:in `reserved_username?'
# ./app/validators/unreserved_username_validator.rb:9:in `validate'
# ./spec/controllers/settings/preferences/notifications_controller_spec.rb:6:in `block (2 levels) in <top (required)>'
# ./spec/controllers/settings/preferences/notifications_controller_spec.rb:9:in `block (2 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# NoMethodError:
# undefined method `reserved_usernames' for Setting:Class
# Did you mean? reset_sequence_name
# ./app/validators/unreserved_username_validator.rb:21:in `reserved_username?'
2/2 |================================================== 100 ===================================================>| Time: 00:00:04
Finished in 4.1 seconds (files took 3.62 seconds to load)
2 examples, 2 failures
Failed examples:
rspec ./spec/controllers/settings/preferences/notifications_controller_spec.rb:20 # Settings::Preferences::NotificationsController PUT #update updates notifications settings
rspec ./spec/controllers/settings/preferences/notifications_controller_spec.rb:13 # Settings::Preferences::NotificationsController GET #show returns http success
Randomized with seed 905
Coverage report generated for RSpec to /home/zunda/c/src/github.com/zunda/mastodon/coverage. 1397 / 35020 LOC (3.99%) covered.
Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected
よしよし。
$ rbenv local 3.0.4
$ bundle install --path=vendor/bundle
$ bundle exec rspec spec/controllers/settings/preferences/notifications_controller_spec.rb
Randomized with seed 26161
2/2 |================================================== 100 ===================================================>| Time: 00:00:04
Finished in 4.61 seconds (files took 3.44 seconds to load)
2 examples, 0 failures
Randomized with seed 26161
Coverage report generated for RSpec to /home/zunda/c/src/github.com/zunda/mastodon/coverage. 2191 / 34521 LOC (6.35%) covered.
zunda@misoan:~/c/src/github.com/zunda/mastodon (update-rails-settings-cached *)
Ruby 3.0では通る。よしよし。
Ruby 3.1でテストを通す
下記の変更でテストが通るようになるが、Ruby 3.0では走らない。だが、rails-settings-cachedのみが問題であることはわかる。
diff -ur rails-settings-cached-0.6.6.orig/lib/rails-settings/default.rb rails-settings-cached-0.6.6/lib/rails-settings/default.rb
--- rails-settings-cached-0.6.6.orig/lib/rails-settings/default.rb 2022-11-20 11:16:19.242409196 -1000
+++ rails-settings-cached-0.6.6/lib/rails-settings/default.rb 2022-11-20 14:18:58.867986771 -1000
@@ -38,7 +38,7 @@
def initialize
content = open(self.class.source_path).read
- hash = content.empty? ? {} : YAML.load(ERB.new(content).result).to_hash
+ hash = content.empty? ? {} : YAML.unsafe_load(ERB.new(content).result).to_hash
hash = hash[Rails.env] || {}
replace hash
end
diff -ur rails-settings-cached-0.6.6.orig/lib/rails-settings/settings.rb rails-settings-cached-0.6.6/lib/rails-settings/settings.rb
--- rails-settings-cached-0.6.6.orig/lib/rails-settings/settings.rb 2022-11-20 11:16:19.242409196 -1000
+++ rails-settings-cached-0.6.6/lib/rails-settings/settings.rb 2022-11-20 14:19:07.748102237 -1000
@@ -8,7 +8,7 @@
# get the value field, YAML decoded
def value
- YAML.load(self[:value]) if self[:value].present?
+ YAML.unsafe_load(self[:value]) if self[:value].present?
end
# set the value field, YAML encoded
パース対象をp
するとRubyの型指定のあるものはなく、&default
みたいな参照を扱えていないようだ。
rails-settings-cachedを更新する
- New design release.
- No more scope support (RailsSettings::Extend has removed);
- No more YAML file.
- Requuire Ruby 2.5+, Rails 5.0+
- You must use field method to statement the setting keys before use.
例えばspec/controllers/settings/preferences/notifications_controller_spec.rb
を通そうとするとconfig/settings.yml
の代わりに、
diff --git a/app/models/setting.rb b/app/models/setting.rb
index 4bcaa060f..6dd645c77 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -13,7 +13,23 @@
#
class Setting < RailsSettings::Base
- source Rails.root.join('config', 'settings.yml')
+ field :interactions, type: :hash, default: {
+ must_be_follower: false,
+ must_be_following: false,
+ must_be_following_dm: false
+ }
+ field :reserved_usernames, type: :array, default: %w(
+ admin
+ support
+ help
+ root
+ webmaster
+ administrator
+ mod
+ moderator
+ )
+ field :require_invite_text, type: :boolean, default: false
+ field :registrations_mode, default: 'open'
def to_param
var
のような記述をすることになるようだ。
下記のような変更で設定にスコープを導入できるのだろうか。
diff --git a/app/lib/settings/scoped_settings.rb b/app/lib/settings/scoped_settings.rb
index 1e18d6d46..fd6da0d38 100644
--- a/app/lib/settings/scoped_settings.rb
+++ b/app/lib/settings/scoped_settings.rb
@@ -7,6 +7,15 @@ module Settings
noindex
).freeze
+ # rails-settings-cached-0.6.6/lib/rails-settings/base.rb:26
+ def self.cache_key(var_name, scope_object)
+ key_parts = [Setting.cache_key]
+ key_parts << "#{scope_object.class.name}-#{scope_object.id}" if scope_object
+ key_parts << var_name.to_s
+ key_parts.join('/')
+ end
+
+
def initialize(object)
@object = object
end
@@ -45,11 +54,11 @@ module Settings
record = thing_scoped.find_or_initialize_by(var: key)
record.update!(value: value)
- Rails.cache.write(Setting.cache_key(key, @object), value)
+ Rails.cache.write(ScopedSettings.cache_key(key, @object), value)
end
def [](key)
- Rails.cache.fetch(Setting.cache_key(key, @object)) do
+ Rails.cache.fetch(ScopedSettings.cache_key(key, @object)) do
db_val = thing_scoped.find_by(var: key.to_s)
if db_val
default_value = ScopedSettings.default_settings[key]
diff --git a/app/models/setting.rb b/app/models/setting.rb
index 4bcaa060f..f46cea567 100644
--- a/app/models/setting.rb
+++ b/app/models/setting.rb
@@ -13,7 +13,23 @@
#
class Setting < RailsSettings::Base
- source Rails.root.join('config', 'settings.yml')
+ field :interactions, type: :hash, default: {
+ must_be_follower: false,
+ must_be_following: false,
+ must_be_following_dm: false
+ }
+ field :reserved_usernames, type: :array, default: %w(
+ admin
+ support
+ help
+ root
+ webmaster
+ administrator
+ mod
+ moderator
+ )
+ field :require_invite_text, type: :boolean, default: false
+ field :registrations_mode, default: 'open'
def to_param
var
@@ -23,7 +39,7 @@ class Setting < RailsSettings::Base
def [](key)
return super(key) unless rails_initialized?
- val = Rails.cache.fetch(cache_key(key, nil)) do
+ val = Rails.cache.fetch(Settings::ScopedSettings.cache_key(key, nil)) do
db_val = object(key)
if db_val
この件は、Upgrade to Ruby 3.2でrails-settings-cached gemがPsych 4と互換なものに置き換えられた後、Inline what remains of the rails-settings-cached gemで必要なコードだけが本体に取り込まれることで解決しました。