mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-07 21:15:12 +00:00
Merge remote-tracking branch 'upstream/master' into master
This commit is contained in:
commit
ef09081456
174 changed files with 2198 additions and 1506 deletions
20
Gemfile
20
Gemfile
|
|
@ -5,10 +5,10 @@ ruby '>= 2.5.0', '< 3.0.0'
|
|||
|
||||
gem 'pkg-config', '~> 1.4'
|
||||
|
||||
gem 'puma', '~> 4.3'
|
||||
gem 'rails', '~> 5.2.4.3'
|
||||
gem 'puma', '~> 5.0'
|
||||
gem 'rails', '~> 5.2.4.4'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 0.20'
|
||||
gem 'thor', '~> 1.0'
|
||||
gem 'rack', '~> 2.2.3'
|
||||
|
||||
gem 'thwait', '~> 0.2.0'
|
||||
|
|
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.7'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.79', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.81', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
|
@ -121,27 +121,27 @@ end
|
|||
group :test do
|
||||
gem 'capybara', '~> 3.33'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.13'
|
||||
gem 'faker', '~> 2.14'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
gem 'simplecov', '~> 0.19', require: false
|
||||
gem 'webmock', '~> 3.8'
|
||||
gem 'parallel_tests', '~> 3.2'
|
||||
gem 'webmock', '~> 3.9'
|
||||
gem 'parallel_tests', '~> 3.3'
|
||||
gem 'rspec_junit_formatter', '~> 0.4'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'better_errors', '~> 2.7'
|
||||
gem 'better_errors', '~> 2.8'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.88', require: false
|
||||
gem 'rubocop-rails', '~> 2.6', require: false
|
||||
gem 'rubocop', '~> 0.91', require: false
|
||||
gem 'rubocop-rails', '~> 2.8', require: false
|
||||
gem 'brakeman', '~> 4.9', require: false
|
||||
gem 'bundler-audit', '~> 0.7', require: false
|
||||
|
||||
|
|
|
|||
168
Gemfile.lock
168
Gemfile.lock
|
|
@ -16,25 +16,25 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
actioncable (5.2.4.4)
|
||||
actionpack (= 5.2.4.4)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activejob (= 5.2.4.3)
|
||||
actionmailer (5.2.4.4)
|
||||
actionpack (= 5.2.4.4)
|
||||
actionview (= 5.2.4.4)
|
||||
activejob (= 5.2.4.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
actionpack (5.2.4.4)
|
||||
actionview (= 5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
actionview (5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
|
@ -45,20 +45,20 @@ GEM
|
|||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.7)
|
||||
activejob (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
activejob (5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
activerecord (5.2.4.3)
|
||||
activemodel (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
activemodel (5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
activerecord (5.2.4.4)
|
||||
activemodel (= 5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
activerecord (= 5.2.4.3)
|
||||
activestorage (5.2.4.4)
|
||||
actionpack (= 5.2.4.4)
|
||||
activerecord (= 5.2.4.4)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (5.2.4.3)
|
||||
activesupport (5.2.4.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
|
@ -79,23 +79,23 @@ GEM
|
|||
cocaine (~> 0.5.3)
|
||||
awrence (1.1.1)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.363.0)
|
||||
aws-sdk-core (3.105.0)
|
||||
aws-partitions (1.373.0)
|
||||
aws-sdk-core (3.107.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.37.0)
|
||||
aws-sdk-kms (1.38.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.79.1)
|
||||
aws-sdk-s3 (1.81.0)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
bcrypt (3.1.15)
|
||||
better_errors (2.7.1)
|
||||
bcrypt (3.1.16)
|
||||
better_errors (2.8.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
|
@ -106,7 +106,7 @@ GEM
|
|||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.8)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.9.0)
|
||||
brakeman (4.9.1)
|
||||
browser (4.2.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
|
|
@ -160,13 +160,12 @@ GEM
|
|||
cose (1.0.0)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 0.4.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crack (0.4.4)
|
||||
crass (1.0.6)
|
||||
css_parser (1.7.1)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
devise (4.7.2)
|
||||
devise (4.7.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
|
|
@ -210,7 +209,7 @@ GEM
|
|||
tzinfo
|
||||
excon (0.76.0)
|
||||
fabrication (2.21.1)
|
||||
faker (2.13.0)
|
||||
faker (2.14.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
|
|
@ -233,7 +232,7 @@ GEM
|
|||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.2.5)
|
||||
fugit (1.3.8)
|
||||
fugit (1.3.9)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.3)
|
||||
fuubar (2.5.0)
|
||||
|
|
@ -355,7 +354,7 @@ GEM
|
|||
mimemagic (0.3.5)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.1)
|
||||
minitest (5.14.2)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.1.1)
|
||||
|
|
@ -363,7 +362,7 @@ GEM
|
|||
net-scp (3.0.0)
|
||||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
nio4r (2.5.2)
|
||||
nio4r (2.5.4)
|
||||
nokogiri (1.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.2)
|
||||
|
|
@ -373,7 +372,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.10.13)
|
||||
oj (3.10.14)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
|
@ -387,7 +386,7 @@ GEM
|
|||
openssl (2.2.0)
|
||||
openssl-signature_algorithm (0.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.13.2)
|
||||
ox (2.13.4)
|
||||
paperclip (6.0.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
|
|
@ -398,7 +397,7 @@ GEM
|
|||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parallel (1.19.2)
|
||||
parallel_tests (3.2.0)
|
||||
parallel_tests (3.3.0)
|
||||
parallel
|
||||
parser (2.7.1.4)
|
||||
ast (~> 2.4.1)
|
||||
|
|
@ -406,11 +405,11 @@ GEM
|
|||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.2.3)
|
||||
pghero (2.7.0)
|
||||
pghero (2.7.2)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.4.2)
|
||||
pkg-config (1.4.3)
|
||||
posix-spawn (0.3.15)
|
||||
premailer (1.13.1)
|
||||
premailer (1.14.2)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
|
|
@ -426,8 +425,8 @@ GEM
|
|||
pry (~> 0.13.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.5)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.0.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
|
@ -441,18 +440,18 @@ GEM
|
|||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.4.3)
|
||||
actioncable (= 5.2.4.3)
|
||||
actionmailer (= 5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activejob (= 5.2.4.3)
|
||||
activemodel (= 5.2.4.3)
|
||||
activerecord (= 5.2.4.3)
|
||||
activestorage (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
rails (5.2.4.4)
|
||||
actioncable (= 5.2.4.4)
|
||||
actionmailer (= 5.2.4.4)
|
||||
actionpack (= 5.2.4.4)
|
||||
actionview (= 5.2.4.4)
|
||||
activejob (= 5.2.4.4)
|
||||
activemodel (= 5.2.4.4)
|
||||
activerecord (= 5.2.4.4)
|
||||
activestorage (= 5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.4.3)
|
||||
railties (= 5.2.4.4)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
|
@ -468,20 +467,20 @@ GEM
|
|||
railties (>= 5.0, < 6)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails (>= 4.2.0)
|
||||
railties (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
railties (5.2.4.4)
|
||||
actionpack (= 5.2.4.4)
|
||||
activesupport (= 5.2.4.4)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rdf (3.1.5)
|
||||
rdf (3.1.6)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redis (4.2.1)
|
||||
redis (4.2.2)
|
||||
redis-actionpack (5.2.0)
|
||||
actionpack (>= 5, < 7)
|
||||
redis-rack (>= 2.1.0, < 3)
|
||||
|
|
@ -500,7 +499,7 @@ GEM
|
|||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.1)
|
||||
regexp_parser (1.8.0)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.1)
|
||||
|
|
@ -535,27 +534,26 @@ GEM
|
|||
rspec-support (3.9.3)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (0.88.0)
|
||||
rubocop (0.91.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
rexml
|
||||
rubocop-ast (>= 0.1.0, < 1.0)
|
||||
rubocop-ast (>= 0.4.0, < 1.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.3.0)
|
||||
rubocop-ast (0.4.2)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-rails (2.6.0)
|
||||
rubocop-rails (2.8.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop (>= 0.87.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-saml (1.11.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
rufus-scheduler (3.6.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safe_yaml (1.0.5)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sanitize (5.2.1)
|
||||
|
|
@ -564,7 +562,7 @@ GEM
|
|||
nokogumbo (~> 2.0)
|
||||
securecompare (1.0.0)
|
||||
semantic_range (2.3.0)
|
||||
sidekiq (6.1.1)
|
||||
sidekiq (6.1.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
|
|
@ -577,10 +575,10 @@ GEM
|
|||
sidekiq (>= 3)
|
||||
thwait
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.22)
|
||||
sidekiq-unique-jobs (6.0.23)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
thor (>= 0.20, < 2.0)
|
||||
simple-navigation (4.1.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (5.0.2)
|
||||
|
|
@ -593,7 +591,7 @@ GEM
|
|||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.1)
|
||||
sprockets-rails (3.2.2)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
|
|
@ -612,7 +610,7 @@ GEM
|
|||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
thor (0.20.3)
|
||||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
thwait (0.2.0)
|
||||
e2mmap
|
||||
|
|
@ -641,8 +639,8 @@ GEM
|
|||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webauthn (3.0.0.alpha1)
|
||||
android_key_attestation (~> 0.3.0)
|
||||
awrence (~> 1.1)
|
||||
|
|
@ -653,7 +651,7 @@ GEM
|
|||
safety_net_attestation (~> 0.4.0)
|
||||
securecompare (~> 1.0)
|
||||
tpm-key_attestation (~> 0.9.0)
|
||||
webmock (3.8.3)
|
||||
webmock (3.9.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
|
@ -680,8 +678,8 @@ DEPENDENCIES
|
|||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.79)
|
||||
better_errors (~> 2.7)
|
||||
aws-sdk-s3 (~> 1.81)
|
||||
better_errors (~> 2.8)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
|
|
@ -710,7 +708,7 @@ DEPENDENCIES
|
|||
e2mmap (~> 0.1.0)
|
||||
ed25519 (~> 1.2)
|
||||
fabrication (~> 2.21)
|
||||
faker (~> 2.13)
|
||||
faker (~> 2.14)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
|
|
@ -751,7 +749,7 @@ DEPENDENCIES
|
|||
paperclip (~> 6.0)
|
||||
paperclip-av-transcoder (~> 0.6)
|
||||
parallel (~> 1.19)
|
||||
parallel_tests (~> 3.2)
|
||||
parallel_tests (~> 3.3)
|
||||
parslet
|
||||
pg (~> 1.2)
|
||||
pghero (~> 2.7)
|
||||
|
|
@ -761,12 +759,12 @@ DEPENDENCIES
|
|||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.3)
|
||||
puma (~> 5.0)
|
||||
pundit (~> 2.1)
|
||||
rack (~> 2.2.3)
|
||||
rack-attack (~> 6.3)
|
||||
rack-cors (~> 1.1)
|
||||
rails (~> 5.2.4.3)
|
||||
rails (~> 5.2.4.4)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 5.1)
|
||||
rails-settings-cached (~> 0.6)
|
||||
|
|
@ -778,8 +776,8 @@ DEPENDENCIES
|
|||
rspec-rails (~> 4.0)
|
||||
rspec-sidekiq (~> 3.1)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 0.88)
|
||||
rubocop-rails (~> 2.6)
|
||||
rubocop (~> 0.91)
|
||||
rubocop-rails (~> 2.8)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.2)
|
||||
sidekiq (~> 6.1)
|
||||
|
|
@ -795,12 +793,12 @@ DEPENDENCIES
|
|||
stoplight (~> 2.2.1)
|
||||
streamio-ffmpeg (~> 3.0)
|
||||
strong_migrations (~> 0.7)
|
||||
thor (~> 0.20)
|
||||
thor (~> 1.0)
|
||||
thwait (~> 0.2.0)
|
||||
tty-prompt (~> 0.22)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2020)
|
||||
webauthn (~> 3.0.0.alpha1)
|
||||
webmock (~> 3.8)
|
||||
webmock (~> 3.9)
|
||||
webpacker (~> 5.2)
|
||||
webpush
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class AccountsController < ApplicationController
|
|||
include AccountControllerConcern
|
||||
include SignatureAuthentication
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :set_cache_headers
|
||||
before_action :set_body_classes
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ class AccountsController < ApplicationController
|
|||
|
||||
format.json do
|
||||
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
||||
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
|
||||
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -153,12 +154,4 @@ class AccountsController < ApplicationController
|
|||
def params_slice(*keys)
|
||||
params.slice(*keys).permit(*keys)
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
if signed_request_account.present? || public_fetch_mode?
|
||||
# Return all fields
|
||||
else
|
||||
%i(id type preferred_username inbox public_key endpoints)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,9 +57,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
def set_statuses
|
||||
return unless page_requested?
|
||||
|
||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
||||
@statuses = cache_collection_paginated_by_id(
|
||||
@statuses,
|
||||
@account.statuses.permitted_for(@account, signed_request_account),
|
||||
Status,
|
||||
LIMIT,
|
||||
params_slice(:max_id, :min_id, :since_id)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :set_account, except: [:index]
|
||||
before_action :require_remote_account!, only: [:redownload]
|
||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||
|
||||
|
|
@ -14,49 +14,58 @@ module Admin
|
|||
def show
|
||||
authorize @account, :show?
|
||||
|
||||
@deletion_request = @account.deletion_request
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||
end
|
||||
|
||||
def memorialize
|
||||
authorize @account, :memorialize?
|
||||
@account.memorialize!
|
||||
log_action :memorialize, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def enable
|
||||
authorize @account.user, :enable?
|
||||
@account.user.enable!
|
||||
log_action :enable, @account.user
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def approve
|
||||
authorize @account.user, :approve?
|
||||
@account.user.approve!
|
||||
redirect_to admin_pending_accounts_path
|
||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
redirect_to admin_pending_accounts_path
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :destroy?
|
||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def unsilence
|
||||
authorize @account, :unsilence?
|
||||
@account.unsilence!
|
||||
log_action :unsilence, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def unsuspend
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||
log_action :unsuspend, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def redownload
|
||||
|
|
@ -65,7 +74,7 @@ module Admin
|
|||
@account.update!(last_webfingered_at: nil)
|
||||
ResolveAccountService.new.call(@account)
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def remove_avatar
|
||||
|
|
@ -76,7 +85,7 @@ module Admin
|
|||
|
||||
log_action :remove_avatar, @account.user
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def remove_header
|
||||
|
|
@ -87,7 +96,7 @@ module Admin
|
|||
|
||||
log_action :remove_header, @account.user
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController
|
|||
def require_user!
|
||||
if !current_user
|
||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||
elsif current_user.disabled?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
elsif !current_user.confirmed?
|
||||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
||||
elsif !current_user.approved?
|
||||
render json: { error: 'Your login is currently pending approval' }, status: 403
|
||||
elsif !current_user.functional?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
else
|
||||
set_user_activity
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
|
|||
end
|
||||
|
||||
def set_featured_tags
|
||||
@featured_tags = @account.featured_tags
|
||||
@featured_tags = @account.suspended? ? @account.featured_tags : []
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
@account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def hide_results?
|
||||
(@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
@account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||
end
|
||||
|
||||
def default_accounts
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||
before_action :set_account
|
||||
|
||||
def index
|
||||
@proofs = @account.identity_proofs.active
|
||||
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController
|
|||
before_action :set_account
|
||||
|
||||
def index
|
||||
@lists = @account.lists.where(account: current_account)
|
||||
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account)
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
accounts = Account.where(id: account_ids).select('id')
|
||||
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
# we requested them, so return the "right" order to the requestor.
|
||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||
end
|
||||
|
||||
def load_statuses
|
||||
cached_account_statuses
|
||||
@account.suspended? ? [] : cached_account_statuses
|
||||
end
|
||||
|
||||
def cached_account_statuses
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
|
||||
before_action :require_user!, except: [:show, :create]
|
||||
before_action :set_account, except: [:create]
|
||||
before_action :check_account_suspension, only: [:show]
|
||||
before_action :check_enabled_registrations, only: [:create]
|
||||
|
||||
skip_before_action :require_authenticated_user!, only: :create
|
||||
|
|
@ -31,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def follow
|
||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
|
||||
|
||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
||||
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
|
||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
|
||||
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||
end
|
||||
|
|
@ -73,10 +71,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||
end
|
||||
|
||||
def check_account_suspension
|
||||
gone if @account.suspended?
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :destroy?
|
||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
|
|
@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||
def unsuspend
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||
log_action :unsuspend, @account
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController
|
|||
|
||||
def paginated_blocks
|
||||
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
|
|||
end
|
||||
|
||||
def endorsed_accounts
|
||||
current_account.endorsed_accounts.includes(:account_stat)
|
||||
current_account.endorsed_accounts.includes(:account_stat).without_suspended
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
before_action :require_user!
|
||||
before_action :set_most_used_tags, only: :index
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
def index
|
||||
render json: @most_used_tags, each_serializer: REST::TagSerializer
|
||||
render json: @recently_used_tags, each_serializer: REST::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_most_used_tags
|
||||
@most_used_tags = Tag.most_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: current_account.featured_tags).limit(10)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
|
||||
def authorize
|
||||
AuthorizeFollowService.new.call(account, current_account)
|
||||
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
|
||||
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
|
||||
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:follow_requests, :account_stat).references(:follow_requests)
|
||||
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests)
|
||||
end
|
||||
|
||||
def paginated_follow_requests
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||
|
||||
def load_accounts
|
||||
if unlimited?
|
||||
@list.accounts.includes(:account_stat).all
|
||||
@list.accounts.without_suspended.includes(:account_stat).all
|
||||
else
|
||||
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Api::V1::MutesController < Api::BaseController
|
|||
|
||||
def paginated_mutes
|
||||
@paginated_mutes ||= Mute.eager_load(:target_account)
|
||||
.joins(:target_account)
|
||||
.merge(Account.without_suspended)
|
||||
.where(account: current_account)
|
||||
.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
end
|
||||
|
||||
def show
|
||||
@notification = current_account.notifications.find(params[:id])
|
||||
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||
render json: @notification, serializer: REST::NotificationSerializer
|
||||
end
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
end
|
||||
|
||||
def browserable_account_notifications
|
||||
current_account.notifications.browserable(exclude_types, from_account)
|
||||
current_account.notifications.without_suspended.browserable(exclude_types, from_account)
|
||||
end
|
||||
|
||||
def target_statuses_from_notifications
|
||||
|
|
|
|||
|
|
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
|||
|
||||
def default_accounts
|
||||
Account
|
||||
.without_suspended
|
||||
.includes(:favourites, :account_stat)
|
||||
.references(:favourites)
|
||||
.where(favourites: { status_id: @status.id })
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def default_accounts
|
||||
Account.includes(:statuses, :account_stat).references(:statuses)
|
||||
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses)
|
||||
end
|
||||
|
||||
def paginated_statuses
|
||||
|
|
|
|||
|
|
@ -20,26 +20,25 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
def cached_public_statuses_page
|
||||
cache_collection_paginated_by_id(
|
||||
public_statuses,
|
||||
Status,
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
cache_collection(public_statuses, Status)
|
||||
end
|
||||
|
||||
def public_statuses
|
||||
statuses = public_timeline_statuses
|
||||
|
||||
if truthy_param?(:only_media)
|
||||
statuses.joins(:media_attachments).group(:id)
|
||||
else
|
||||
statuses
|
||||
end
|
||||
public_feed.get(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id],
|
||||
params[:min_id]
|
||||
)
|
||||
end
|
||||
|
||||
def public_timeline_statuses
|
||||
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
|
||||
def public_feed
|
||||
PublicFeed.new(
|
||||
current_account,
|
||||
local: truthy_param?(:local),
|
||||
remote: truthy_param?(:remote),
|
||||
only_media: truthy_param?(:only_media)
|
||||
)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
|
|
|||
|
|
@ -20,23 +20,29 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
end
|
||||
|
||||
def cached_tagged_statuses
|
||||
if @tag.nil?
|
||||
[]
|
||||
else
|
||||
statuses = tag_timeline_statuses
|
||||
statuses = statuses.joins(:media_attachments) if truthy_param?(:only_media)
|
||||
|
||||
cache_collection_paginated_by_id(
|
||||
statuses,
|
||||
Status,
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
end
|
||||
@tag.nil? ? [] : cache_collection(tag_timeline_statuses, Status)
|
||||
end
|
||||
|
||||
def tag_timeline_statuses
|
||||
HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, truthy_param?(:local))
|
||||
tag_feed.get(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id],
|
||||
params[:min_id]
|
||||
)
|
||||
end
|
||||
|
||||
def tag_feed
|
||||
TagFeed.new(
|
||||
@tag,
|
||||
current_account,
|
||||
any: params[:any],
|
||||
all: params[:all],
|
||||
none: params[:none],
|
||||
local: truthy_param?(:local),
|
||||
remote: truthy_param?(:remote),
|
||||
only_media: truthy_param?(:only_media)
|
||||
)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
reblog: alerts_enabled,
|
||||
mention: alerts_enabled,
|
||||
poll: alerts_enabled,
|
||||
status: alerts_enabled,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
def data_params
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ module ExportControllerConcern
|
|||
|
||||
included do
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :load_export
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
|
@ -30,8 +29,4 @@ module ExportControllerConcern
|
|||
def export_filename
|
||||
"#{controller_name}.csv"
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
before_action :require_not_suspended!, only: :destroy
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
|
@ -25,4 +26,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
def store_current_location
|
||||
store_location_for(:user, request.url)
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::AliasesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :set_aliases, except: :destroy
|
||||
before_action :set_alias, only: :destroy
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ApplicationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
||||
before_action :prepare_scopes, only: [:create, :update]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::BaseController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
|
@ -13,4 +16,8 @@ class Settings::BaseController < ApplicationController
|
|||
def set_cache_headers
|
||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::DeletesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :check_enabled_deletion
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_not_suspended!
|
||||
before_action :check_enabled_deletion
|
||||
|
||||
def show
|
||||
@confirmation = Form::DeleteConfirmation.new
|
||||
end
|
||||
|
|
@ -46,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||
|
||||
def destroy_account!
|
||||
current_account.suspend!
|
||||
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
|
||||
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||
sign_out
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Settings
|
||||
module Exports
|
||||
class BlockedAccountsController < ApplicationController
|
||||
class BlockedAccountsController < BaseController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Settings
|
||||
module Exports
|
||||
class BlockedDomainsController < ApplicationController
|
||||
class BlockedDomainsController < BaseController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Settings
|
||||
module Exports
|
||||
class FollowingAccountsController < ApplicationController
|
||||
class FollowingAccountsController < BaseController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Settings
|
||||
module Exports
|
||||
class ListsController < ApplicationController
|
||||
class ListsController < BaseController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Settings
|
||||
module Exports
|
||||
class MutedAccountsController < ApplicationController
|
||||
class MutedAccountsController < BaseController
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@
|
|||
class Settings::ExportsController < Settings::BaseController
|
||||
include Authorization
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
|
|
@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
raise Mastodon::NotPermittedError unless user_signed_in?
|
||||
|
||||
backup = nil
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
|
|
@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController
|
|||
def lock_options
|
||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::FeaturedTagsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_featured_tags, only: :index
|
||||
before_action :set_featured_tag, except: [:index, :create]
|
||||
before_action :set_most_used_tags, only: :index
|
||||
before_action :set_recently_used_tags, only: :index
|
||||
|
||||
def index
|
||||
@featured_tag = FeaturedTag.new
|
||||
|
|
@ -20,7 +17,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||
redirect_to settings_featured_tags_path
|
||||
else
|
||||
set_featured_tags
|
||||
set_most_used_tags
|
||||
set_recently_used_tags
|
||||
|
||||
render :index
|
||||
end
|
||||
|
|
@ -41,8 +38,8 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
|
||||
end
|
||||
|
||||
def set_most_used_tags
|
||||
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||
end
|
||||
|
||||
def featured_tag_params
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::IdentityProofsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :check_required_params, only: :new
|
||||
|
||||
def index
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ImportsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
|
||||
def show
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::Migration::RedirectsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_not_suspended!
|
||||
|
||||
def new
|
||||
@redirect = Form::Redirect.new
|
||||
end
|
||||
|
|
@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||
def resource_params
|
||||
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::MigrationsController < Settings::BaseController
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_not_suspended!
|
||||
before_action :set_migrations
|
||||
before_action :set_cooldown
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@migration = current_account.migrations.build
|
||||
end
|
||||
|
|
@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController
|
|||
def on_cooldown?
|
||||
@cooldown.present?
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.suspended?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
module Settings
|
||||
class PicturesController < BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
before_action :set_picture
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::PreferencesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::ProfilesController < Settings::BaseController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
|
||||
def show
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::SessionsController < Settings::BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_session, only: :destroy
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_not_suspended!
|
||||
before_action :set_session, only: :destroy
|
||||
|
||||
def destroy
|
||||
@session.destroy!
|
||||
flash[:notice] = I18n.t('sessions.revoke_success')
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ module Settings
|
|||
class ConfirmationsController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_challenge!
|
||||
before_action :ensure_otp_secret
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def new
|
||||
prepare_two_factor_form
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ module Settings
|
|||
class OtpAuthenticationController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_otp_not_enabled, only: [:show]
|
||||
before_action :require_challenge!, only: [:create]
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def show
|
||||
@confirmation = Form::TwoFactorConfirmation.new
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ module Settings
|
|||
class RecoveryCodesController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_challenge!, on: :create
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_challenge!, on: :create
|
||||
|
||||
def create
|
||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||
current_user.save!
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
module Settings
|
||||
module TwoFactorAuthentication
|
||||
class WebauthnCredentialsController < BaseController
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_otp_enabled
|
||||
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,11 @@ module Settings
|
|||
class TwoFactorAuthenticationMethodsController < BaseController
|
||||
include ChallengableConcern
|
||||
|
||||
layout 'admin'
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_challenge!, only: :disable
|
||||
before_action :require_otp_enabled
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
def index; end
|
||||
|
||||
def disable
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ class TagsController < ApplicationController
|
|||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_tag
|
||||
before_action :set_local
|
||||
before_action :set_tag
|
||||
before_action :set_statuses
|
||||
before_action :set_body_classes
|
||||
before_action :set_instance_presenter
|
||||
|
||||
|
|
@ -25,20 +26,11 @@ class TagsController < ApplicationController
|
|||
|
||||
format.rss do
|
||||
expires_in 0, public: true
|
||||
|
||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||
end
|
||||
|
||||
format.json do
|
||||
expires_in 3.minutes, public: public_fetch_mode?
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
end
|
||||
|
|
@ -54,6 +46,15 @@ class TagsController < ApplicationController
|
|||
@local = truthy_param?(:local)
|
||||
end
|
||||
|
||||
def set_statuses
|
||||
case request.format&.to_sym
|
||||
when :json
|
||||
@statuses = cache_collection(TagFeed.new(@tag, current_account, local: @local).get(PAGE_SIZE, params[:max_id], params[:since_id], params[:min_id]), Status)
|
||||
when :rss
|
||||
@statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status)
|
||||
end
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'with-modals'
|
||||
end
|
||||
|
|
@ -62,16 +63,16 @@ class TagsController < ApplicationController
|
|||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def limit_param
|
||||
params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
end
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: tag_url(@tag, filter_params),
|
||||
id: tag_url(@tag),
|
||||
type: :ordered,
|
||||
size: @tag.statuses.count,
|
||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||
)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.slice(:any, :all, :none).permit(:any, :all, :none)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) {
|
|||
};
|
||||
};
|
||||
|
||||
export function followAccount(id, reblogs = true) {
|
||||
export function followAccount(id, options = { reblogs: true }) {
|
||||
return (dispatch, getState) => {
|
||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||
|
||||
dispatch(followAccountRequest(id, locked));
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
||||
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||
}).catch(error => {
|
||||
dispatch(followAccountFail(error, locked));
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
|||
const _buildParams = (state) => {
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
|
||||
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
|
||||
let filtered = false;
|
||||
|
||||
if (notification.type === 'mention') {
|
||||
if (['mention', 'status'].includes(notification.type)) {
|
||||
const dropRegex = filters[0];
|
||||
const regex = filters[1];
|
||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||
|
|
|
|||
|
|
@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { hasError, copied } = this.state;
|
||||
const { hasError, copied, errorMessage } = this.state;
|
||||
|
||||
if (!hasError) {
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
|
||||
|
||||
return (
|
||||
<div className='error-boundary'>
|
||||
<div>
|
||||
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
|
||||
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
|
||||
<p className='error-boundary__error'>
|
||||
{ likelyBrowserAddonIssue ? (
|
||||
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
|
||||
) : (
|
||||
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{ likelyBrowserAddonIssue ? (
|
||||
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||
) : (
|
||||
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||
)}
|
||||
</p>
|
||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
|
|
@ -35,6 +36,8 @@ const messages = defineMessages({
|
|||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
|
|
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
|
|||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onReblogToggle: PropTypes.func.isRequired,
|
||||
onNotifyToggle: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
onUnblockDomain: PropTypes.func.isRequired,
|
||||
|
|
@ -140,8 +144,11 @@ class Header extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const suspended = account.get('suspended');
|
||||
|
||||
let info = [];
|
||||
let actionBtn = '';
|
||||
let bellBtn = '';
|
||||
let lockedIcon = '';
|
||||
let menu = [];
|
||||
|
||||
|
|
@ -171,6 +178,10 @@ class Header extends ImmutablePureComponent {
|
|||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
}
|
||||
|
||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
|
@ -268,7 +279,7 @@ class Header extends ImmutablePureComponent {
|
|||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
{info}
|
||||
{!suspended && info}
|
||||
</div>
|
||||
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
|
|
@ -282,11 +293,14 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='spacer' />
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
{!suspended && (
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
|
|
@ -298,7 +312,7 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
||||
{(fields.size > 0 || identity_proofs.size > 0) && (
|
||||
<div className='account__header__fields'>
|
||||
{identity_proofs.map((proof, i) => (
|
||||
<dl key={i}>
|
||||
|
|
@ -324,33 +338,35 @@ class Header extends ImmutablePureComponent {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{account.get('id') !== me && <AccountNoteContainer account={account} />}
|
||||
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
|
||||
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
||||
</div>
|
||||
|
||||
<div className='account__header__extra__links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('statuses_count')}
|
||||
renderer={counterRenderer('statuses')}
|
||||
/>
|
||||
</NavLink>
|
||||
{!suspended && (
|
||||
<div className='account__header__extra__links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('statuses_count')}
|
||||
renderer={counterRenderer('statuses')}
|
||||
/>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('following_count')}
|
||||
renderer={counterRenderer('following')}
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('following_count')}
|
||||
renderer={counterRenderer('following')}
|
||||
/>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('followers_count')}
|
||||
renderer={counterRenderer('followers')}
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('followers_count')}
|
||||
renderer={counterRenderer('followers')}
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
|||
import LoadMore from 'mastodon/components/load_more';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
attachments: getAccountGallery(state, props.params.accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
|
@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
@ -119,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount) {
|
||||
|
|
@ -152,15 +157,21 @@ class AccountGallery extends ImmutablePureComponent {
|
|||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
{(suspended || blockedBy) ? (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
) : (
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
{loadOlder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onReblogToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleNotifyToggle = () => {
|
||||
this.props.onNotifyToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
|
|
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onMention={this.handleMention}
|
||||
onDirect={this.handleDirect}
|
||||
onReblogToggle={this.handleReblogToggle}
|
||||
onNotifyToggle={this.handleNotifyToggle}
|
||||
onReport={this.handleReport}
|
||||
onMute={this.handleMute}
|
||||
onBlockDomain={this.handleBlockDomain}
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
|
||||
onReblogToggle (account) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
dispatch(followAccount(account.get('id'), false));
|
||||
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id'), true));
|
||||
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onNotifyToggle (account) {
|
||||
if (account.getIn(['relationship', 'notifying'])) {
|
||||
dispatch(followAccount(account.get('id'), { notify: false }));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id'), { notify: true }));
|
||||
}
|
||||
},
|
||||
|
||||
onReport (account) {
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
|
|||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
};
|
||||
|
|
@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
withReplies: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
|
@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
|
|
@ -134,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
|
||||
let emptyMessage;
|
||||
|
||||
if (blockedBy) {
|
||||
if (suspended || blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (remote && statusIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
|
|
@ -153,7 +155,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
scrollKey='account_timeline'
|
||||
statusIds={blockedBy ? emptyList : statusIds}
|
||||
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
|
|||
};
|
||||
|
||||
// Emoji requiring extra borders depending on theme
|
||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞']);
|
||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
|
||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
||||
|
||||
const emojiFilename = (filename) => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const tooltips = defineMessages({
|
|||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
|
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
|
|||
>
|
||||
<Icon id='tasks' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'status' ? 'active' : ''}
|
||||
onClick={this.onClick('status')}
|
||||
title={intl.formatMessage(tooltips.statuses)}
|
||||
>
|
||||
<Icon id='home' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'follow' ? 'active' : ''}
|
||||
onClick={this.onClick('follow')}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const messages = defineMessages({
|
|||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||
});
|
||||
|
||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||
|
|
@ -237,6 +238,38 @@ class Notification extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderStatus (notification, link) {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-status focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='home' fixedWidth />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
getScrollPosition={this.props.getScrollPosition}
|
||||
updateScrollBottom={this.props.updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
renderPoll (notification, account) {
|
||||
const { intl } = this.props;
|
||||
const ownPoll = me === account.get('id');
|
||||
|
|
@ -292,6 +325,8 @@ class Notification extends ImmutablePureComponent {
|
|||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'status':
|
||||
return this.renderStatus(notification, link);
|
||||
case 'poll':
|
||||
return this.renderPoll(notification, account);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const getNotifications = createSelector([
|
|||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||
}
|
||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
||||
return notifications.filter(item => item === null || allowedType === item.get('type'));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
|
|||
|
|
@ -75,3 +75,8 @@
|
|||
.public-layout .public-account-header__tabs__tabs .counter.active::after {
|
||||
border-bottom: 4px solid $ui-highlight-color;
|
||||
}
|
||||
|
||||
.compose-form .autosuggest-textarea__textarea::placeholder,
|
||||
.compose-form .spoiler-input__input::placeholder {
|
||||
color: $inverted-text-color;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6502,6 +6502,10 @@ noscript {
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
& > .icon-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,13 +118,13 @@ class ActivityPub::Activity
|
|||
end
|
||||
|
||||
def notify_about_reblog(status)
|
||||
NotifyService.new.call(status.reblog.account, status)
|
||||
NotifyService.new.call(status.reblog.account, :reblog, status)
|
||||
end
|
||||
|
||||
def notify_about_mentions(status)
|
||||
status.active_mentions.includes(:account).each do |mention|
|
||||
next unless mention.account.local? && audience_includes?(mention.account)
|
||||
NotifyService.new.call(mention.account, mention)
|
||||
NotifyService.new.call(mention.account, :mention, mention)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
|
||||
def delete_person
|
||||
lock_or_return("delete_in_progress:#{@account.id}") do
|
||||
SuspendAccountService.new.call(@account, reserve_username: false)
|
||||
DeleteAccountService.new.call(@account, reserve_username: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
|||
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
|
||||
|
||||
if target_account.locked? || @account.silenced?
|
||||
NotifyService.new.call(target_account, follow_request)
|
||||
NotifyService.new.call(target_account, :follow_request, follow_request)
|
||||
else
|
||||
AuthorizeFollowService.new.call(@account, target_account)
|
||||
NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account))
|
||||
NotifyService.new.call(target_account, :follow, ::Follow.find_by(account: @account, target_account: target_account))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
|
||||
|
||||
favourite = original_status.favourites.create!(account: @account)
|
||||
NotifyService.new.call(original_status.account, favourite)
|
||||
NotifyService.new.call(original_status.account, :favourite, favourite)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,31 +6,54 @@ class FeedManager
|
|||
include Singleton
|
||||
include Redisable
|
||||
|
||||
# Maximum number of items stored in a single feed
|
||||
MAX_ITEMS = 400
|
||||
|
||||
# Must be <= MAX_ITEMS or the tracking sets will grow forever
|
||||
# Number of items in the feed since last reblog of status
|
||||
# before the new reblog will be inserted. Must be <= MAX_ITEMS
|
||||
# or the tracking sets will grow forever
|
||||
REBLOG_FALLOFF = 40
|
||||
|
||||
# Execute block for every active account
|
||||
# @yield [Account]
|
||||
# @return [void]
|
||||
def with_active_accounts(&block)
|
||||
Account.joins(:user).where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago).find_each(&block)
|
||||
end
|
||||
|
||||
# Redis key of a feed
|
||||
# @param [Symbol] type
|
||||
# @param [Integer] id
|
||||
# @param [Symbol] subtype
|
||||
# @return [String]
|
||||
def key(type, id, subtype = nil)
|
||||
return "feed:#{type}:#{id}" unless subtype
|
||||
|
||||
"feed:#{type}:#{id}:#{subtype}"
|
||||
end
|
||||
|
||||
def filter?(timeline_type, status, receiver_id)
|
||||
if timeline_type == :home
|
||||
filter_from_home?(status, receiver_id, build_crutches(receiver_id, [status]))
|
||||
elsif timeline_type == :mentions
|
||||
filter_from_mentions?(status, receiver_id)
|
||||
# Check if the status should not be added to a feed
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Status] status
|
||||
# @param [Account|List] receiver
|
||||
# @return [Boolean]
|
||||
def filter?(timeline_type, status, receiver)
|
||||
case timeline_type
|
||||
when :home
|
||||
filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]))
|
||||
when :list
|
||||
filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]))
|
||||
when :mentions
|
||||
filter_from_mentions?(status, receiver.id)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Add a status to a home feed and send a streaming API update
|
||||
# @param [Account] account
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def push_to_home(account, status)
|
||||
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
|
||||
|
|
@ -39,6 +62,10 @@ class FeedManager
|
|||
true
|
||||
end
|
||||
|
||||
# Remove a status from a home feed and send a streaming API update
|
||||
# @param [Account] account
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_home(account, status)
|
||||
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
|
||||
|
||||
|
|
@ -46,21 +73,22 @@ class FeedManager
|
|||
true
|
||||
end
|
||||
|
||||
# Add a status to a list feed and send a streaming API update
|
||||
# @param [List] list
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def push_to_list(list, status)
|
||||
if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
should_filter = status.in_reply_to_account_id != list.account_id
|
||||
should_filter &&= !list.show_all_replies?
|
||||
should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
|
||||
return false if should_filter
|
||||
end
|
||||
|
||||
return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
|
||||
trim(:list, list.id)
|
||||
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
|
||||
true
|
||||
end
|
||||
|
||||
# Remove a status from a list feed and send a streaming API update
|
||||
# @param [List] list
|
||||
# @param [Status] status
|
||||
# @return [Boolean]
|
||||
def unpush_from_list(list, status)
|
||||
return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
|
||||
|
|
@ -68,32 +96,11 @@ class FeedManager
|
|||
true
|
||||
end
|
||||
|
||||
def trim(type, account_id)
|
||||
timeline_key = key(type, account_id)
|
||||
reblog_key = key(type, account_id, 'reblogs')
|
||||
|
||||
# Remove any items past the MAX_ITEMS'th entry in our feed
|
||||
redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
|
||||
|
||||
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
|
||||
# tracking anything after it for deduplication purposes.
|
||||
falloff_rank = FeedManager::REBLOG_FALLOFF - 1
|
||||
falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
|
||||
falloff_score = falloff_range&.first&.last&.to_i || 0
|
||||
|
||||
# Get any reblogs we might have to clean up after.
|
||||
redis.zrangebyscore(reblog_key, 0, falloff_score).each do |reblogged_id|
|
||||
# Remove it from the set of reblogs we're tracking *first* to avoid races.
|
||||
redis.zrem(reblog_key, reblogged_id)
|
||||
# Just drop any set we might have created to track additional reblogs.
|
||||
# This means that if this reblog is deleted, we won't automatically insert
|
||||
# another reblog, but also that any new reblog can be inserted into the
|
||||
# feed.
|
||||
redis.del(key(type, account_id, "reblogs:#{reblogged_id}"))
|
||||
end
|
||||
end
|
||||
|
||||
def merge_into_timeline(from_account, into_account)
|
||||
# Fill a home feed with an account's statuses
|
||||
# @param [Account] from_account
|
||||
# @param [Account] into_account
|
||||
# @return [void]
|
||||
def merge_into_home(from_account, into_account)
|
||||
timeline_key = key(:home, into_account.id)
|
||||
aggregate = into_account.user&.aggregates_reblogs?
|
||||
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
||||
|
|
@ -115,7 +122,37 @@ class FeedManager
|
|||
trim(:home, into_account.id)
|
||||
end
|
||||
|
||||
def unmerge_from_timeline(from_account, into_account)
|
||||
# Fill a list feed with an account's statuses
|
||||
# @param [Account] from_account
|
||||
# @param [List] list
|
||||
# @return [void]
|
||||
def merge_into_list(from_account, list)
|
||||
timeline_key = key(:list, list.id)
|
||||
aggregate = list.account.user&.aggregates_reblogs?
|
||||
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
|
||||
|
||||
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
|
||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i
|
||||
query = query.where('id > ?', oldest_home_score)
|
||||
end
|
||||
|
||||
statuses = query.to_a
|
||||
crutches = build_crutches(list.account_id, statuses)
|
||||
|
||||
statuses.each do |status|
|
||||
next if filter_from_home?(status, list.account_id, crutches) || filter_from_list?(status, list)
|
||||
|
||||
add_to_feed(:list, list.id, status, aggregate)
|
||||
end
|
||||
|
||||
trim(:list, list.id)
|
||||
end
|
||||
|
||||
# Remove an account's statuses from a home feed
|
||||
# @param [Account] from_account
|
||||
# @param [Account] into_account
|
||||
# @return [void]
|
||||
def unmerge_from_home(from_account, into_account)
|
||||
timeline_key = key(:home, into_account.id)
|
||||
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
||||
|
||||
|
|
@ -124,14 +161,31 @@ class FeedManager
|
|||
end
|
||||
end
|
||||
|
||||
def clear_from_timeline(account, target_account)
|
||||
# Clear from timeline all statuses from or mentionning target_account
|
||||
# Remove an account's statuses from a list feed
|
||||
# @param [Account] from_account
|
||||
# @param [List] list
|
||||
# @return [void]
|
||||
def unmerge_from_list(from_account, list)
|
||||
timeline_key = key(:list, list.id)
|
||||
oldest_list_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
|
||||
|
||||
from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_list_score).reorder(nil).find_each do |status|
|
||||
remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
|
||||
end
|
||||
end
|
||||
|
||||
# Clear all statuses from or mentioning target_account from a home feed
|
||||
# @param [Account] account
|
||||
# @param [Account] target_account
|
||||
# @return [void]
|
||||
def clear_from_home(account, target_account)
|
||||
timeline_key = key(:home, account.id)
|
||||
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||
statuses = Status.where(id: timeline_status_ids).select(:id, :reblog_of_id, :account_id).to_a
|
||||
reblogged_ids = Status.where(id: statuses.map(&:reblog_of_id).compact, account: target_account).pluck(:id)
|
||||
with_mentions_ids = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact, account: target_account).pluck(:status_id)
|
||||
target_statuses = statuses.filter do |status|
|
||||
|
||||
target_statuses = statuses.select do |status|
|
||||
status.account_id == target_account.id || reblogged_ids.include?(status.reblog_of_id) || with_mentions_ids.include?(status.id) || with_mentions_ids.include?(status.reblog_of_id)
|
||||
end
|
||||
|
||||
|
|
@ -140,7 +194,10 @@ class FeedManager
|
|||
end
|
||||
end
|
||||
|
||||
def populate_feed(account)
|
||||
# Populate home feed of account from scratch
|
||||
# @param [Account] account
|
||||
# @return [void]
|
||||
def populate_home(account)
|
||||
limit = FeedManager::MAX_ITEMS / 2
|
||||
aggregate = account.user&.aggregates_reblogs?
|
||||
timeline_key = key(:home, account.id)
|
||||
|
|
@ -175,15 +232,59 @@ class FeedManager
|
|||
|
||||
private
|
||||
|
||||
def push_update_required?(timeline_id)
|
||||
redis.exists?("subscribed:#{timeline_id}")
|
||||
# Trim a feed to maximum size by removing older items
|
||||
# @param [Symbol] type
|
||||
# @param [Integer] timeline_id
|
||||
# @return [void]
|
||||
def trim(type, timeline_id)
|
||||
timeline_key = key(type, timeline_id)
|
||||
reblog_key = key(type, timeline_id, 'reblogs')
|
||||
|
||||
# Remove any items past the MAX_ITEMS'th entry in our feed
|
||||
redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
|
||||
|
||||
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
|
||||
# tracking anything after it for deduplication purposes.
|
||||
falloff_rank = FeedManager::REBLOG_FALLOFF
|
||||
falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true)
|
||||
falloff_score = falloff_range&.first&.last&.to_i
|
||||
|
||||
return if falloff_score.nil?
|
||||
|
||||
# Get any reblogs we might have to clean up after.
|
||||
redis.zrangebyscore(reblog_key, 0, falloff_score).each do |reblogged_id|
|
||||
# Remove it from the set of reblogs we're tracking *first* to avoid races.
|
||||
redis.zrem(reblog_key, reblogged_id)
|
||||
# Just drop any set we might have created to track additional reblogs.
|
||||
# This means that if this reblog is deleted, we won't automatically insert
|
||||
# another reblog, but also that any new reblog can be inserted into the
|
||||
# feed.
|
||||
redis.del(key(type, timeline_id, "reblogs:#{reblogged_id}"))
|
||||
end
|
||||
end
|
||||
|
||||
# Check if there is a streaming API client connected
|
||||
# for the given feed
|
||||
# @param [String] timeline_key
|
||||
# @return [Boolean]
|
||||
def push_update_required?(timeline_key)
|
||||
redis.exists?("subscribed:#{timeline_key}")
|
||||
end
|
||||
|
||||
# Check if the account is blocking or muting any of the given accounts
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Array<Integer>] account_ids
|
||||
# @param [Symbol] context
|
||||
def blocks_or_mutes?(receiver_id, account_ids, context)
|
||||
Block.where(account_id: receiver_id, target_account_id: account_ids).any? ||
|
||||
(context == :home ? Mute.where(account_id: receiver_id, target_account_id: account_ids).any? : Mute.where(account_id: receiver_id, target_account_id: account_ids, hide_notifications: true).any?)
|
||||
end
|
||||
|
||||
# Check if status should not be added to the home feed
|
||||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Hash] crutches
|
||||
# @return [Boolean]
|
||||
def filter_from_home?(status, receiver_id, crutches)
|
||||
return false if receiver_id == status.account_id
|
||||
return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
|
||||
|
|
@ -216,6 +317,11 @@ class FeedManager
|
|||
false
|
||||
end
|
||||
|
||||
# Check if status should not be added to the mentions feed
|
||||
# @see NotifyService
|
||||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @return [Boolean]
|
||||
def filter_from_mentions?(status, receiver_id)
|
||||
return true if receiver_id == status.account_id
|
||||
return true if phrase_filtered?(status, receiver_id, :notifications)
|
||||
|
|
@ -232,6 +338,27 @@ class FeedManager
|
|||
should_filter
|
||||
end
|
||||
|
||||
# Check if status should not be added to the list feed
|
||||
# @param [Status] status
|
||||
# @param [List] list
|
||||
# @return [Boolean]
|
||||
def filter_from_list?(status, list)
|
||||
if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
should_filter = status.in_reply_to_account_id != list.account_id
|
||||
should_filter &&= !list.show_all_replies?
|
||||
should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
|
||||
|
||||
return !!should_filter
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Check if the status hits a phrase filter
|
||||
# @param [Status] status
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Symbol] context
|
||||
# @return [Boolean]
|
||||
def phrase_filtered?(status, receiver_id, context)
|
||||
active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a
|
||||
|
||||
|
|
@ -267,6 +394,11 @@ class FeedManager
|
|||
# added, and false if it was not added to the feed. Note that this is
|
||||
# an internal helper: callers must call trim or push updates if
|
||||
# either action is appropriate.
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Integer] account_id
|
||||
# @param [Status] status
|
||||
# @param [Boolean] aggregate_reblogs
|
||||
# @return [Boolean]
|
||||
def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||
timeline_key = key(timeline_type, account_id)
|
||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||
|
|
@ -279,14 +411,12 @@ class FeedManager
|
|||
|
||||
return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF
|
||||
|
||||
reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id)
|
||||
|
||||
if reblog_rank.nil?
|
||||
# The ordered set at `reblog_key` holds statuses which have a reblog
|
||||
# in the top `REBLOG_FALLOFF` statuses of the timeline
|
||||
if redis.zadd(reblog_key, status.id, status.reblog_of_id, nx: true)
|
||||
# This is not something we've already seen reblogged, so we
|
||||
# can just add it to the feed (and note that we're
|
||||
# reblogging it).
|
||||
# can just add it to the feed (and note that we're reblogging it).
|
||||
redis.zadd(timeline_key, status.id, status.id)
|
||||
redis.zadd(reblog_key, status.id, status.reblog_of_id)
|
||||
else
|
||||
# Another reblog of the same status was already in the
|
||||
# REBLOG_FALLOFF most recent statuses, so we note that this
|
||||
|
|
@ -300,9 +430,7 @@ class FeedManager
|
|||
# delay of the worker deliverying the original status, the late addition
|
||||
# by merging timelines, and other reasons.
|
||||
# If such a reblog already exists, just do not re-insert it into the feed.
|
||||
rank = redis.zrevrank(reblog_key, status.id)
|
||||
|
||||
return false unless rank.nil?
|
||||
return false unless redis.zscore(reblog_key, status.id).nil?
|
||||
|
||||
redis.zadd(timeline_key, status.id, status.id)
|
||||
end
|
||||
|
|
@ -314,6 +442,11 @@ class FeedManager
|
|||
# with reblogs, and returning true if a status was removed. As with
|
||||
# `add_to_feed`, this does not trigger push updates, so callers must
|
||||
# do so if appropriate.
|
||||
# @param [Symbol] timeline_type
|
||||
# @param [Integer] account_id
|
||||
# @param [Status] status
|
||||
# @param [Boolean] aggregate_reblogs
|
||||
# @return [Boolean]
|
||||
def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true)
|
||||
timeline_key = key(timeline_type, account_id)
|
||||
reblog_key = key(timeline_type, account_id, 'reblogs')
|
||||
|
|
@ -348,6 +481,11 @@ class FeedManager
|
|||
redis.zrem(timeline_key, status.id)
|
||||
end
|
||||
|
||||
# Pre-fetch various objects and relationships for given statuses that
|
||||
# are going to be checked by the filtering methods
|
||||
# @param [Integer] receiver_id
|
||||
# @param [Array<Status>] statuses
|
||||
# @return [Hash]
|
||||
def build_crutches(receiver_id, statuses)
|
||||
crutches = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
|
@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@account = notification.from_account
|
||||
|
||||
return if @me.user.disabled?
|
||||
return unless @me.user.functional?
|
||||
|
||||
locale_for_account(@me) do
|
||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
||||
|
|
@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@account = notification.from_account
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
|
@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@account = notification.from_account
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
|
@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@account = notification.from_account
|
||||
|
||||
return if @me.user.disabled?
|
||||
return unless @me.user.functional?
|
||||
|
||||
locale_for_account(@me) do
|
||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
||||
|
|
@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer
|
|||
end
|
||||
|
||||
def digest(recipient, **opts)
|
||||
return if recipient.user.disabled?
|
||||
return unless recipient.user.functional?
|
||||
|
||||
@me = recipient
|
||||
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
|
||||
|
|
@ -88,8 +88,10 @@ class NotificationMailer < ApplicationMailer
|
|||
|
||||
def thread_by_conversation(conversation)
|
||||
return if conversation.nil?
|
||||
|
||||
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"
|
||||
|
||||
headers['In-Reply-To'] = msg_id
|
||||
headers['References'] = msg_id
|
||||
headers['References'] = msg_id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer
|
|||
@token = token
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.unconfirmed_email.presence || @resource.email,
|
||||
|
|
@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
|
|||
@token = token
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
||||
|
|
@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
||||
|
|
@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
|
||||
|
|
@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
|
||||
|
|
@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
|
||||
|
|
@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
|
||||
|
|
@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
|
||||
|
|
@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
|
||||
|
|
@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@webauthn_credential = webauthn_credential
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
|
||||
|
|
@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@webauthn_credential = webauthn_credential
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
|
||||
|
|
@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
||||
|
|
@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@backup = backup
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
|
||||
|
|
@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer
|
|||
@detection = Browser.new(user_agent)
|
||||
@timestamp = timestamp.to_time.utc
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email,
|
||||
|
|
|
|||
|
|
@ -222,23 +222,20 @@ class Account < ApplicationRecord
|
|||
|
||||
def suspend!(date = Time.now.utc)
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
create_deletion_request!
|
||||
update!(suspended_at: date)
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend!
|
||||
transaction do
|
||||
user&.enable! if local?
|
||||
deletion_request&.destroy!
|
||||
update!(suspended_at: nil)
|
||||
end
|
||||
end
|
||||
|
||||
def memorialize!
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
update!(memorial: true)
|
||||
end
|
||||
update!(memorial: true)
|
||||
end
|
||||
|
||||
def sign?
|
||||
|
|
|
|||
|
|
@ -38,15 +38,16 @@ class AccountConversation < ApplicationRecord
|
|||
class << self
|
||||
def to_a_paginated_by_id(limit, options = {})
|
||||
if options[:min_id]
|
||||
paginate_by_min_id(limit, options[:min_id]).reverse
|
||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||
else
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||
end
|
||||
end
|
||||
|
||||
def paginate_by_min_id(limit, min_id = nil)
|
||||
def paginate_by_min_id(limit, min_id = nil, max_id = nil)
|
||||
query = order(arel_table[:last_status_id].asc).limit(limit)
|
||||
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
|
||||
query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present?
|
||||
query
|
||||
end
|
||||
|
||||
|
|
|
|||
20
app/models/account_deletion_request.rb
Normal file
20
app/models/account_deletion_request.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_deletion_requests
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class AccountDeletionRequest < ApplicationRecord
|
||||
DELAY_TO_DELETION = 30.days.freeze
|
||||
|
||||
belongs_to :account
|
||||
|
||||
def due_at
|
||||
created_at + DELAY_TO_DELETION
|
||||
end
|
||||
end
|
||||
|
|
@ -134,7 +134,7 @@ class Admin::AccountAction
|
|||
end
|
||||
|
||||
def process_email!
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def warnable?
|
||||
|
|
@ -142,7 +142,7 @@ class Admin::AccountAction
|
|||
end
|
||||
|
||||
def status_ids
|
||||
@report.status_ids if @report && include_statuses
|
||||
report.status_ids if report && include_statuses
|
||||
end
|
||||
|
||||
def reports
|
||||
|
|
|
|||
|
|
@ -60,5 +60,8 @@ module AccountAssociations
|
|||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Account deletion requests
|
||||
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ module AccountInteractions
|
|||
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
||||
mapping[follow.target_account_id] = {
|
||||
reblogs: follow.show_reblogs?,
|
||||
notify: follow.notify?,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -36,6 +37,7 @@ module AccountInteractions
|
|||
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
||||
mapping[follow_request.target_account_id] = {
|
||||
reblogs: follow_request.show_reblogs?,
|
||||
notify: follow_request.notify?,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -95,25 +97,29 @@ module AccountInteractions
|
|||
has_many :announcement_mutes, dependent: :destroy
|
||||
end
|
||||
|
||||
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
|
||||
rel.save! if rel.changed?
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
end
|
||||
|
||||
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
||||
reblogs = true if reblogs.nil?
|
||||
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
||||
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||
.find_or_create_by!(target_account: other_account)
|
||||
|
||||
rel.update!(show_reblogs: reblogs)
|
||||
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||
rel.notify = notify unless notify.nil?
|
||||
|
||||
rel.save! if rel.changed?
|
||||
|
||||
remove_potential_friendship(other_account)
|
||||
|
||||
rel
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ module Paginable
|
|||
# Differs from :paginate_by_max_id in that it gives the results immediately following min_id,
|
||||
# whereas since_id gives the items with largest id, but with since_id as a cutoff.
|
||||
# Results will be in ascending order by id.
|
||||
scope :paginate_by_min_id, ->(limit, min_id = nil) {
|
||||
scope :paginate_by_min_id, ->(limit, min_id = nil, max_id = nil) {
|
||||
query = reorder(arel_table[:id]).limit(limit)
|
||||
query = query.where(arel_table[:id].gt(min_id)) if min_id.present?
|
||||
query = query.where(arel_table[:id].lt(max_id)) if max_id.present?
|
||||
query
|
||||
}
|
||||
|
||||
def self.to_a_paginated_by_id(limit, options = {})
|
||||
if options[:min_id].present?
|
||||
paginate_by_min_id(limit, options[:min_id]).reverse
|
||||
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||
else
|
||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ class Feed
|
|||
protected
|
||||
|
||||
def from_redis(limit, max_id, since_id, min_id)
|
||||
max_id = '+inf' if max_id.blank?
|
||||
if min_id.blank?
|
||||
max_id = '+inf' if max_id.blank?
|
||||
since_id = '-inf' if since_id.blank?
|
||||
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
else
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", '+inf', limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
unhydrated = redis.zrangebyscore(key, "(#{min_id}", "(#{max_id}", limit: [0, limit], with_scores: true).map(&:first).map(&:to_i)
|
||||
end
|
||||
|
||||
Status.where(id: unhydrated).cache_ids
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
# target_account_id :bigint(8) not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
# uri :string
|
||||
# notify :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Follow < ApplicationRecord
|
||||
|
|
@ -34,7 +35,7 @@ class Follow < ApplicationRecord
|
|||
end
|
||||
|
||||
def revoke_request!
|
||||
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri)
|
||||
FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, notify: notify, uri: uri)
|
||||
destroy!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
# target_account_id :bigint(8) not null
|
||||
# show_reblogs :boolean default(TRUE), not null
|
||||
# uri :string
|
||||
# notify :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class FollowRequest < ApplicationRecord
|
||||
|
|
@ -28,7 +29,7 @@ class FollowRequest < ApplicationRecord
|
|||
validates_with FollowLimitValidator, on: :create
|
||||
|
||||
def authorize!
|
||||
account.follow!(target_account, reblogs: show_reblogs, uri: uri)
|
||||
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
|
||||
MergeWorker.perform_async(target_account.id, account.id) if account.local?
|
||||
destroy!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -69,6 +69,6 @@ class Form::AccountBatch
|
|||
records = accounts.includes(:user)
|
||||
|
||||
records.each { |account| authorize(account.user, :reject?) }
|
||||
.each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class Invite < ApplicationRecord
|
|||
before_validation :set_code
|
||||
|
||||
def valid_for_use?
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?)
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -10,21 +10,34 @@
|
|||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
# from_account_id :bigint(8) not null
|
||||
# type :string
|
||||
#
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
self.inheritance_column = nil
|
||||
|
||||
include Paginable
|
||||
include Cacheable
|
||||
|
||||
TYPE_CLASS_MAP = {
|
||||
mention: 'Mention',
|
||||
reblog: 'Status',
|
||||
follow: 'Follow',
|
||||
follow_request: 'FollowRequest',
|
||||
favourite: 'Favourite',
|
||||
poll: 'Poll',
|
||||
LEGACY_TYPE_CLASS_MAP = {
|
||||
'Mention' => :mention,
|
||||
'Status' => :reblog,
|
||||
'Follow' => :follow,
|
||||
'FollowRequest' => :follow_request,
|
||||
'Favourite' => :favourite,
|
||||
'Poll' => :poll,
|
||||
}.freeze
|
||||
|
||||
TYPES = %i(
|
||||
mention
|
||||
status
|
||||
reblog
|
||||
follow
|
||||
follow_request
|
||||
favourite
|
||||
poll
|
||||
).freeze
|
||||
|
||||
STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze
|
||||
|
||||
belongs_to :account, optional: true
|
||||
|
|
@ -38,26 +51,30 @@ class Notification < ApplicationRecord
|
|||
belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id', optional: true
|
||||
belongs_to :poll, foreign_type: 'Poll', foreign_key: 'activity_id', optional: true
|
||||
|
||||
validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
|
||||
validates :activity_type, inclusion: { in: TYPE_CLASS_MAP.values }
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
|
||||
scope :without_suspended, -> { joins(:from_account).merge(Account.without_suspended) }
|
||||
|
||||
scope :browserable, ->(exclude_types = [], account_id = nil) {
|
||||
types = TYPE_CLASS_MAP.values - activity_types_from_types(exclude_types)
|
||||
types = TYPES - exclude_types.map(&:to_sym)
|
||||
|
||||
if account_id.nil?
|
||||
where(activity_type: types)
|
||||
where(type: types)
|
||||
else
|
||||
where(activity_type: types, from_account_id: account_id)
|
||||
where(type: types, from_account_id: account_id)
|
||||
end
|
||||
}
|
||||
|
||||
cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account, follow_request: :account, poll: [status: STATUS_INCLUDES]
|
||||
|
||||
def type
|
||||
@type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
|
||||
@type ||= (super || LEGACY_TYPE_CLASS_MAP[activity_type]).to_sym
|
||||
end
|
||||
|
||||
def target_status
|
||||
case type
|
||||
when :status
|
||||
status
|
||||
when :reblog
|
||||
status&.reblog
|
||||
when :favourite
|
||||
|
|
@ -86,10 +103,6 @@ class Notification < ApplicationRecord
|
|||
item.target_status.account = accounts[item.target_status.account_id] if item.target_status
|
||||
end
|
||||
end
|
||||
|
||||
def activity_types_from_types(types)
|
||||
types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
|
||||
end
|
||||
end
|
||||
|
||||
after_initialize :set_from_account
|
||||
|
|
|
|||
90
app/models/public_feed.rb
Normal file
90
app/models/public_feed.rb
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PublicFeed < Feed
|
||||
# @param [Account] account
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :with_replies
|
||||
# @option [Boolean] :with_reblogs
|
||||
# @option [Boolean] :local
|
||||
# @option [Boolean] :remote
|
||||
# @option [Boolean] :only_media
|
||||
def initialize(account, options = {})
|
||||
@account = account
|
||||
@options = options
|
||||
end
|
||||
|
||||
# @param [Integer] limit
|
||||
# @param [Integer] max_id
|
||||
# @param [Integer] since_id
|
||||
# @param [Integer] min_id
|
||||
# @return [Array<Status>]
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||
scope = public_scope
|
||||
|
||||
scope.merge!(without_replies_scope) unless with_replies?
|
||||
scope.merge!(without_reblogs_scope) unless with_reblogs?
|
||||
scope.merge!(local_only_scope) if local_only?
|
||||
scope.merge!(remote_only_scope) if remote_only?
|
||||
scope.merge!(account_filters_scope) if account?
|
||||
scope.merge!(media_only_scope) if media_only?
|
||||
|
||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_reblogs?
|
||||
@options[:with_reblogs]
|
||||
end
|
||||
|
||||
def with_replies?
|
||||
@options[:with_replies]
|
||||
end
|
||||
|
||||
def local_only?
|
||||
@options[:local]
|
||||
end
|
||||
|
||||
def remote_only?
|
||||
@options[:remote]
|
||||
end
|
||||
|
||||
def account?
|
||||
@account.present?
|
||||
end
|
||||
|
||||
def media_only?
|
||||
@options[:only_media]
|
||||
end
|
||||
|
||||
def public_scope
|
||||
Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced)
|
||||
end
|
||||
|
||||
def local_only_scope
|
||||
Status.local
|
||||
end
|
||||
|
||||
def remote_only_scope
|
||||
Status.remote
|
||||
end
|
||||
|
||||
def without_replies_scope
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def without_reblogs_scope
|
||||
Status.without_reblogs
|
||||
end
|
||||
|
||||
def media_only_scope
|
||||
Status.joins(:media_attachments).group(:id)
|
||||
end
|
||||
|
||||
def account_filters_scope
|
||||
Status.not_excluded_by_account(@account).tap do |scope|
|
||||
scope.merge!(Status.not_domain_blocked_by_account(@account)) unless local_only?
|
||||
scope.merge!(Status.in_chosen_languages(@account)) if @account.chosen_languages.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -85,23 +85,23 @@ class Status < ApplicationRecord
|
|||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :remote, -> { where(local: false).where.not(uri: nil) }
|
||||
scope :local, -> { where(local: true).or(where(uri: nil)) }
|
||||
|
||||
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
|
||||
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
|
||||
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
|
||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
||||
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
|
||||
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
||||
scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) }
|
||||
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||
scope :tagged_with_all, ->(tags) {
|
||||
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
|
||||
scope :tagged_with_all, ->(tag_ids) {
|
||||
Array(tag_ids).reduce(self) do |result, id|
|
||||
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||
end
|
||||
}
|
||||
scope :tagged_with_none, ->(tags) {
|
||||
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
|
||||
scope :tagged_with_none, ->(tag_ids) {
|
||||
Array(tag_ids).reduce(self) do |result, id|
|
||||
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
||||
.where("t#{id}.tag_id IS NULL")
|
||||
end
|
||||
|
|
@ -277,26 +277,6 @@ class Status < ApplicationRecord
|
|||
visibilities.keys - %w(direct limited)
|
||||
end
|
||||
|
||||
def in_chosen_languages(account)
|
||||
where(language: nil).or where(language: account.chosen_languages)
|
||||
end
|
||||
|
||||
def as_public_timeline(account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).without_replies
|
||||
|
||||
apply_timeline_filters(query, account, [:local, true].include?(local_only))
|
||||
end
|
||||
|
||||
def as_tag_timeline(tag, account = nil, local_only = false)
|
||||
query = timeline_scope(local_only).tagged_with(tag)
|
||||
|
||||
apply_timeline_filters(query, account, local_only)
|
||||
end
|
||||
|
||||
def as_outbox_timeline(account)
|
||||
where(account: account, visibility: :public)
|
||||
end
|
||||
|
||||
def favourites_map(status_ids, account_id)
|
||||
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
|
||||
end
|
||||
|
|
@ -373,51 +353,6 @@ class Status < ApplicationRecord
|
|||
status&.distributable? ? status : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def timeline_scope(scope = false)
|
||||
starting_scope = case scope
|
||||
when :local, true
|
||||
Status.local
|
||||
when :remote
|
||||
Status.remote
|
||||
else
|
||||
Status
|
||||
end
|
||||
|
||||
starting_scope
|
||||
.with_public_visibility
|
||||
.without_reblogs
|
||||
end
|
||||
|
||||
def apply_timeline_filters(query, account, local_only)
|
||||
if account.nil?
|
||||
filter_timeline_default(query)
|
||||
else
|
||||
filter_timeline_for_account(query, account, local_only)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_timeline_for_account(query, account, local_only)
|
||||
query = query.not_excluded_by_account(account)
|
||||
query = query.not_domain_blocked_by_account(account) unless local_only
|
||||
query = query.in_chosen_languages(account) if account.chosen_languages.present?
|
||||
query.merge(account_silencing_filter(account))
|
||||
end
|
||||
|
||||
def filter_timeline_default(query)
|
||||
query.excluding_silenced_accounts
|
||||
end
|
||||
|
||||
def account_silencing_filter(account)
|
||||
if account.silenced?
|
||||
including_myself = left_outer_joins(:account).where(account_id: account.id).references(:accounts)
|
||||
excluding_silenced_accounts.or(including_myself)
|
||||
else
|
||||
excluding_silenced_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def status_stat
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class Tag < ApplicationRecord
|
|||
scope :listable, -> { where(listable: [true, nil]) }
|
||||
scope :trendable, -> { Setting.trendable_by_default ? where(trendable: [true, nil]) : where(trendable: true) }
|
||||
scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :recently_used, ->(account) { joins(:statuses).where(statuses: { id: account.statuses.select(:id).limit(1000) }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) }
|
||||
|
||||
delegate :accounts_count,
|
||||
|
|
|
|||
57
app/models/tag_feed.rb
Normal file
57
app/models/tag_feed.rb
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagFeed < PublicFeed
|
||||
LIMIT_PER_MODE = 4
|
||||
|
||||
# @param [Tag] tag
|
||||
# @param [Account] account
|
||||
# @param [Hash] options
|
||||
# @option [Enumerable<String>] :any
|
||||
# @option [Enumerable<String>] :all
|
||||
# @option [Enumerable<String>] :none
|
||||
# @option [Boolean] :local
|
||||
# @option [Boolean] :remote
|
||||
# @option [Boolean] :only_media
|
||||
def initialize(tag, account, options = {})
|
||||
@tag = tag
|
||||
@account = account
|
||||
@options = options
|
||||
end
|
||||
|
||||
# @param [Integer] limit
|
||||
# @param [Integer] max_id
|
||||
# @param [Integer] since_id
|
||||
# @param [Integer] min_id
|
||||
# @return [Array<Status>]
|
||||
def get(limit, max_id = nil, since_id = nil, min_id = nil)
|
||||
scope = public_scope
|
||||
|
||||
scope.merge!(tagged_with_any_scope)
|
||||
scope.merge!(tagged_with_all_scope)
|
||||
scope.merge!(tagged_with_none_scope)
|
||||
scope.merge!(local_only_scope) if local_only?
|
||||
scope.merge!(remote_only_scope) if remote_only?
|
||||
scope.merge!(account_filters_scope) if account?
|
||||
scope.merge!(media_only_scope) if media_only?
|
||||
|
||||
scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tagged_with_any_scope
|
||||
Status.group(:id).tagged_with(tags_for(Array(@tag.name) | Array(@options[:any])))
|
||||
end
|
||||
|
||||
def tagged_with_all_scope
|
||||
Status.group(:id).tagged_with_all(tags_for(@options[:all]))
|
||||
end
|
||||
|
||||
def tagged_with_none_scope
|
||||
Status.group(:id).tagged_with_none(tags_for(@options[:none]))
|
||||
end
|
||||
|
||||
def tags_for(names)
|
||||
Tag.matching_name(Array(names).take(LIMIT_PER_MODE)).pluck(:id) if names.present?
|
||||
end
|
||||
end
|
||||
|
|
@ -168,7 +168,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def active_for_authentication?
|
||||
true
|
||||
!account.memorial?
|
||||
end
|
||||
|
||||
def suspicious_sign_in?(ip)
|
||||
|
|
@ -176,7 +176,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def functional?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ class WebauthnCredential < ApplicationRecord
|
|||
validates :external_id, uniqueness: true
|
||||
validates :nickname, uniqueness: { scope: :user_id }
|
||||
validates :sign_count,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**63 - 1 }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ class AccountPolicy < ApplicationPolicy
|
|||
staff? && !record.user&.staff?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
record.suspended? && record.deletion_request.present? && admin?
|
||||
end
|
||||
|
||||
def unsuspend?
|
||||
staff?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class StatusPolicy < ApplicationPolicy
|
|||
end
|
||||
|
||||
def show?
|
||||
return false if author.suspended?
|
||||
|
||||
if requires_mention?
|
||||
owned? || mention_exists?
|
||||
elsif private?
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
:followers_count, :following_count, :statuses_count, :last_status_at
|
||||
|
||||
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
|
||||
|
||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||
|
||||
attribute :suspended, if: :suspended?
|
||||
|
||||
class FieldSerializer < ActiveModel::Serializer
|
||||
attributes :name, :value, :verified_at
|
||||
|
||||
|
|
@ -29,7 +32,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def note
|
||||
Formatter.instance.simplified_format(object)
|
||||
object.suspended? ? '' : Formatter.instance.simplified_format(object)
|
||||
end
|
||||
|
||||
def url
|
||||
|
|
@ -37,26 +40,60 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def avatar
|
||||
full_asset_url(object.avatar_original_url)
|
||||
full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_original_url)
|
||||
end
|
||||
|
||||
def avatar_static
|
||||
full_asset_url(object.avatar_static_url)
|
||||
full_asset_url(object.suspended? ? object.avatar.default_url : object.avatar_static_url)
|
||||
end
|
||||
|
||||
def header
|
||||
full_asset_url(object.header_original_url)
|
||||
full_asset_url(object.suspended? ? object.header.default_url : object.header_original_url)
|
||||
end
|
||||
|
||||
def header_static
|
||||
full_asset_url(object.header_static_url)
|
||||
end
|
||||
|
||||
def moved_and_not_nested?
|
||||
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
||||
full_asset_url(object.suspended? ? object.header.default_url : object.header_static_url)
|
||||
end
|
||||
|
||||
def last_status_at
|
||||
object.last_status_at&.to_date&.iso8601
|
||||
end
|
||||
|
||||
def display_name
|
||||
object.suspended? ? '' : object.display_name
|
||||
end
|
||||
|
||||
def locked
|
||||
object.suspended? ? false : object.locked
|
||||
end
|
||||
|
||||
def bot
|
||||
object.suspended? ? false : object.bot
|
||||
end
|
||||
|
||||
def discoverable
|
||||
object.suspended? ? false : object.discoverable
|
||||
end
|
||||
|
||||
def moved_to_account
|
||||
object.suspended? ? nil : object.moved_to_account
|
||||
end
|
||||
|
||||
def emojis
|
||||
object.suspended? ? [] : object.emojis
|
||||
end
|
||||
|
||||
def fields
|
||||
object.suspended? ? [] : object.fields
|
||||
end
|
||||
|
||||
def suspended
|
||||
object.suspended?
|
||||
end
|
||||
|
||||
delegate :suspended?, to: :object
|
||||
|
||||
def moved_and_not_nested?
|
||||
object.moved? && object.moved_to_account.moved_to_account_id.nil?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def status_type?
|
||||
[:favourite, :reblog, :mention, :poll].include?(object.type)
|
||||
[:favourite, :reblog, :status, :mention, :poll].include?(object.type)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue