Merge pull request #1327 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
signup-info-prompt
ThibG 2020-05-13 23:46:09 +02:00 committed by GitHub
commit e1d2820234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 2766 additions and 1987 deletions

View File

@ -30,7 +30,7 @@ plugins:
channel: eslint-6 channel: eslint-6
rubocop: rubocop:
enabled: true enabled: true
channel: rubocop-0-76 channel: rubocop-0-82
sass-lint: sass-lint:
enabled: true enabled: true
exclude_patterns: exclude_patterns:

View File

@ -33,7 +33,7 @@ LOCAL_DOMAIN=example.com
# ALTERNATE_DOMAINS=example1.com,example2.com # ALTERNATE_DOMAINS=example1.com,example2.com
# Application secrets # Application secrets
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose) # Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose)
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
@ -42,7 +42,7 @@ OTP_SECRET=
# You should only generate this once per instance. If you later decide to change it, all push subscription will # You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe. # be invalidated, requiring the users to access the website again to resubscribe.
# #
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose) # Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose)
# #
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html # For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=

View File

@ -2,7 +2,7 @@ require:
- rubocop-rails - rubocop-rails
AllCops: AllCops:
TargetRubyVersion: 2.3 TargetRubyVersion: 2.4
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
- 'db/**/*' - 'db/**/*'
@ -46,7 +46,7 @@ Metrics/ClassLength:
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 25 Max: 25
Metrics/LineLength: Layout/LineLength:
AllowURI: true AllowURI: true
Enabled: false Enabled: false

24
Gemfile
View File

@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.4' gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.63', require: false gem 'aws-sdk-s3', '~> 1.64', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -49,7 +49,7 @@ gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.3' gem 'doorkeeper', '~> 5.4'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'goldfinger', '~> 2.1' gem 'goldfinger', '~> 2.1'
@ -57,12 +57,12 @@ gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.7' gem 'redis-namespace', '~> 1.7'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b' gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.3' gem 'http', '~> 4.4'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
gem 'httplog', '~> 1.4.2' gem 'httplog', '~> 1.4.2'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
@ -75,7 +75,7 @@ gem 'parallel', '~> 1.19'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.2' gem 'rack-attack', '~> 6.3'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
@ -96,8 +96,8 @@ gem 'strong_migrations', '~> 0.6'
gem 'tty-command', '~> 0.9', require: false gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.21', require: false gem 'tty-prompt', '~> 0.21', require: false
gem 'twitter-text', '~> 1.14' gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019' gem 'tzinfo-data', '~> 1.2020'
gem 'webpacker', '~> 4.2' gem 'webpacker', '~> 5.1'
gem 'webpush' gem 'webpush'
gem 'json-ld' gem 'json-ld'
@ -110,7 +110,7 @@ group :development, :test do
gem 'fabrication', '~> 2.21' gem 'fabrication', '~> 2.21'
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.8' gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 4.0' gem 'rspec-rails', '~> 4.0'
end end
@ -120,7 +120,7 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.31' gem 'capybara', '~> 3.32'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.11' gem 'faker', '~> 2.11'
gem 'microformats', '~> 4.2' gem 'microformats', '~> 4.2'
@ -135,18 +135,18 @@ end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.7' gem 'active_record_query_trace', '~> 1.7'
gem 'annotate', '~> 3.1' gem 'annotate', '~> 3.1'
gem 'better_errors', '~> 2.6' gem 'better_errors', '~> 2.7'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1' gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4' gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.79', require: false gem 'rubocop', '~> 0.82', require: false
gem 'rubocop-rails', '~> 2.5', require: false gem 'rubocop-rails', '~> 2.5', require: false
gem 'brakeman', '~> 4.8', require: false gem 'brakeman', '~> 4.8', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'capistrano', '~> 3.13' gem 'capistrano', '~> 3.14'
gem 'capistrano-rails', '~> 1.4' gem 'capistrano-rails', '~> 1.4'
gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'

View File

@ -92,23 +92,23 @@ GEM
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.303.0) aws-partitions (1.312.0)
aws-sdk-core (3.94.0) aws-sdk-core (3.95.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.30.0) aws-sdk-kms (1.31.0)
aws-sdk-core (~> 3, >= 3.71.0) aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.63.0) aws-sdk-s3 (1.64.0)
aws-sdk-core (~> 3, >= 3.83.0) aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.2) aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
bcrypt (3.1.13) bcrypt (3.1.13)
better_errors (2.6.0) better_errors (2.7.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
@ -118,8 +118,8 @@ GEM
ffi (~> 1.10.0) ffi (~> 1.10.0)
bootsnap (1.4.6) bootsnap (1.4.6)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.8.0) brakeman (4.8.1)
browser (4.0.0) browser (4.1.0)
builder (3.2.4) builder (3.2.4)
bullet (6.1.0) bullet (6.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -127,8 +127,8 @@ GEM
bundler-audit (0.6.1) bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (~> 0.18)
byebug (11.1.1) byebug (11.1.3)
capistrano (3.13.0) capistrano (3.14.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@ -143,7 +143,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.31.0) capybara (3.32.1)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -194,7 +194,7 @@ GEM
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.3.1) doorkeeper (5.4.0)
railties (>= 5) railties (>= 5)
dotenv (2.7.5) dotenv (2.7.5)
dotenv-rails (2.7.5) dotenv-rails (2.7.5)
@ -213,7 +213,7 @@ GEM
encryptor (3.0.0) encryptor (3.0.0)
equatable (0.6.1) equatable (0.6.1)
erubi (1.9.0) erubi (1.9.0)
et-orbi (1.2.3) et-orbi (1.2.4)
tzinfo tzinfo
excon (0.73.0) excon (0.73.0)
fabrication (2.21.1) fabrication (2.21.1)
@ -240,7 +240,7 @@ GEM
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
formatador (0.2.5) formatador (0.2.5)
fugit (1.3.3) fugit (1.3.5)
et-orbi (~> 1.1, >= 1.1.8) et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.1) raabro (~> 1.1)
fuubar (2.5.0) fuubar (2.5.0)
@ -270,7 +270,7 @@ GEM
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (4.3.0) http (4.4.1)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
@ -303,7 +303,7 @@ GEM
jmespath (1.4.0) jmespath (1.4.0)
json (2.3.0) json (2.3.0)
json-canonicalization (0.2.0) json-canonicalization (0.2.0)
json-ld (3.1.3) json-ld (3.1.4)
htmlentities (~> 4.3) htmlentities (~> 4.3)
json-canonicalization (~> 0.2) json-canonicalization (~> 0.2)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
@ -314,21 +314,21 @@ GEM
json-ld (~> 3.1) json-ld (~> 3.1)
rdf (~> 3.1) rdf (~> 3.1)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.1.0) jwt (2.2.1)
kaminari (1.1.1) kaminari (1.2.0)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1) kaminari-actionview (= 1.2.0)
kaminari-activerecord (= 1.1.1) kaminari-activerecord (= 1.2.0)
kaminari-core (= 1.1.1) kaminari-core (= 1.2.0)
kaminari-actionview (1.1.1) kaminari-actionview (1.2.0)
actionview actionview
kaminari-core (= 1.1.1) kaminari-core (= 1.2.0)
kaminari-activerecord (1.1.1) kaminari-activerecord (1.2.0)
activerecord activerecord
kaminari-core (= 1.1.1) kaminari-core (= 1.2.0)
kaminari-core (1.1.1) kaminari-core (1.2.0)
launchy (2.4.3) launchy (2.5.0)
addressable (~> 2.3) addressable (~> 2.7)
letter_opener (1.7.0) letter_opener (1.7.0)
launchy (~> 2.2) launchy (~> 2.2)
letter_opener_web (1.4.0) letter_opener_web (1.4.0)
@ -353,14 +353,14 @@ GEM
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
memory_profiler (0.9.14) memory_profiler (0.9.14)
method_source (0.9.2) method_source (1.0.0)
microformats (4.2.0) microformats (4.2.0)
json (~> 2.2) json (~> 2.2)
nokogiri (~> 1.10) nokogiri (~> 1.10)
mime-types (3.3.1) mime-types (3.3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2020.0425) mime-types-data (3.2020.0425)
mimemagic (0.3.4) mimemagic (0.3.5)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.14.0) minitest (5.14.0)
@ -369,9 +369,9 @@ GEM
multipart-post (2.1.1) multipart-post (2.1.1)
necromancer (0.5.1) necromancer (0.5.1)
net-ldap (0.16.2) net-ldap (0.16.2)
net-scp (2.0.0) net-scp (3.0.0)
net-ssh (>= 2.6.5, < 6.0.0) net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (5.2.0) net-ssh (6.0.2)
nio4r (2.5.2) nio4r (2.5.2)
nokogiri (1.10.9) nokogiri (1.10.9)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
@ -407,40 +407,40 @@ GEM
parallel (1.19.1) parallel (1.19.1)
parallel_tests (2.32.0) parallel_tests (2.32.0)
parallel parallel
parser (2.7.1.1) parser (2.7.1.2)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (2.0.0) parslet (2.0.0)
pastel (0.7.3) pastel (0.7.4)
equatable (~> 0.6) equatable (~> 0.6)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.2.3) pg (1.2.3)
pghero (2.4.1) pghero (2.4.2)
activerecord (>= 5) activerecord (>= 5)
pkg-config (1.4.1) pkg-config (1.4.1)
premailer (1.11.1) premailer (1.11.1)
addressable addressable
css_parser (>= 1.6.0) css_parser (>= 1.6.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.10.3) premailer-rails (1.11.1)
actionmailer (>= 3) actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
pry (0.12.2) pry (0.13.1)
coderay (~> 1.1.0) coderay (~> 1.1)
method_source (~> 0.9.0) method_source (~> 1.0)
pry-byebug (3.8.0) pry-byebug (3.9.0)
byebug (~> 11.0) byebug (~> 11.0)
pry (~> 0.10) pry (~> 0.13.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.4) public_suffix (4.0.5)
puma (4.3.3) puma (4.3.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.1.6) raabro (1.3.1)
rack (2.2.2) rack (2.2.2)
rack-attack (6.2.2) rack-attack (6.3.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
@ -491,13 +491,13 @@ GEM
rdf-normalize (0.4.0) rdf-normalize (0.4.0)
rdf (~> 3.1) rdf (~> 3.1)
redcarpet (3.5.0) redcarpet (3.5.0)
redis (4.1.3) redis (4.1.4)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3) redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.4) redis-activesupport (5.2.0)
activesupport (>= 3, < 6) activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.7.0) redis-namespace (1.7.0)
redis (>= 3.0.4) redis (>= 3.0.4)
@ -516,15 +516,16 @@ GEM
responders (3.0.0) responders (3.0.0)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
rexml (3.2.4)
rotp (2.1.2) rotp (2.1.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (1.1.2) rqrcode (1.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 0.1) rqrcode_core (~> 0.1)
rqrcode_core (0.1.2) rqrcode_core (0.1.2)
rspec-core (3.9.1) rspec-core (3.9.2)
rspec-support (~> 3.9.1) rspec-support (~> 3.9.3)
rspec-expectations (3.9.1) rspec-expectations (3.9.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.9.0)
rspec-mocks (3.9.1) rspec-mocks (3.9.1)
@ -541,16 +542,17 @@ GEM
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.9.2) rspec-support (3.9.3)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.79.0) rubocop (0.82.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.7.0.1) parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 1.4.0, < 2.0)
rubocop-rails (2.5.2) rubocop-rails (2.5.2)
activesupport activesupport
rack (>= 1.1) rack (>= 1.1)
@ -565,9 +567,10 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.8.0) nokogiri (>= 1.8.0)
nokogumbo (~> 2.0) nokogumbo (~> 2.0)
sidekiq (6.0.4) semantic_range (2.3.0)
sidekiq (6.0.7)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (>= 2.0.0) rack (~> 2.0)
rack-protection (>= 2.0.0) rack-protection (>= 2.0.0)
redis (>= 4.1.0) redis (>= 4.1.0)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
@ -607,7 +610,7 @@ GEM
stoplight (2.2.0) stoplight (2.2.0)
streamio-ffmpeg (3.0.2) streamio-ffmpeg (3.0.2)
multi_json (~> 1.8) multi_json (~> 1.8)
strong_migrations (0.6.2) strong_migrations (0.6.6)
activerecord (>= 5) activerecord (>= 5)
temple (0.8.2) temple (0.8.2)
terminal-table (1.8.0) terminal-table (1.8.0)
@ -635,12 +638,12 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.7) tzinfo (1.2.7)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2019.3) tzinfo-data (1.2020.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.6) unf_ext (0.0.7.7)
unicode-display_width (1.6.1) unicode-display_width (1.7.0)
uniform_notifier (1.13.0) uniform_notifier (1.13.0)
warden (1.2.8) warden (1.2.8)
rack (>= 2.0.6) rack (>= 2.0.6)
@ -648,10 +651,11 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.2.2) webpacker (5.1.1)
activesupport (>= 4.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 5.2)
semantic_range (>= 2.3.0)
webpush (0.3.8) webpush (0.3.8)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) jwt (~> 2.0)
@ -670,8 +674,8 @@ DEPENDENCIES
active_record_query_trace (~> 1.7) active_record_query_trace (~> 1.7)
addressable (~> 2.7) addressable (~> 2.7)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.63) aws-sdk-s3 (~> 1.64)
better_errors (~> 2.6) better_errors (~> 2.7)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.4) bootsnap (~> 1.4)
@ -679,11 +683,11 @@ DEPENDENCIES
browser browser
bullet (~> 6.1) bullet (~> 6.1)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
capistrano (~> 3.13) capistrano (~> 3.14)
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.31) capybara (~> 3.32)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 5.1) chewy (~> 5.1)
cld3 (~> 3.3.0) cld3 (~> 3.3.0)
@ -694,7 +698,7 @@ DEPENDENCIES
devise-two-factor (~> 3.1) devise-two-factor (~> 3.1)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2) discard (~> 1.2)
doorkeeper (~> 5.3) doorkeeper (~> 5.4)
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
e2mmap (~> 0.1.0) e2mmap (~> 0.1.0)
fabrication (~> 2.21) fabrication (~> 2.21)
@ -709,7 +713,7 @@ DEPENDENCIES
health_check! health_check!
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 4.3) http (~> 4.4)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)! http_parser.rb (~> 0.6)!
httplog (~> 1.4.2) httplog (~> 1.4.2)
@ -718,7 +722,7 @@ DEPENDENCIES
iso-639 iso-639
json-ld json-ld
json-ld-preloaded (~> 3.1) json-ld-preloaded (~> 3.1)
kaminari (~> 1.1) kaminari (~> 1.2)
letter_opener (~> 1.7) letter_opener (~> 1.7)
letter_opener_web (~> 1.4) letter_opener_web (~> 1.4)
link_header (~> 0.0) link_header (~> 0.0)
@ -748,12 +752,12 @@ DEPENDENCIES
posix-spawn! posix-spawn!
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.8) pry-byebug (~> 3.9)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 4.3) puma (~> 4.3)
pundit (~> 2.1) pundit (~> 2.1)
rack (~> 2.2.2) rack (~> 2.2.2)
rack-attack (~> 6.2) rack-attack (~> 6.3)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rails (~> 5.2.4.2) rails (~> 5.2.4.2)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
@ -768,7 +772,7 @@ DEPENDENCIES
rspec-rails (~> 4.0) rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 0.79) rubocop (~> 0.82)
rubocop-rails (~> 2.5) rubocop-rails (~> 2.5)
ruby-progressbar (~> 1.10) ruby-progressbar (~> 1.10)
sanitize (~> 5.1) sanitize (~> 5.1)
@ -790,7 +794,7 @@ DEPENDENCIES
tty-command (~> 0.9) tty-command (~> 0.9)
tty-prompt (~> 0.21) tty-prompt (~> 0.21)
twitter-text (~> 1.14) twitter-text (~> 1.14)
tzinfo-data (~> 1.2019) tzinfo-data (~> 1.2020)
webmock (~> 3.8) webmock (~> 3.8)
webpacker (~> 4.2) webpacker (~> 5.1)
webpush webpush

View File

@ -41,7 +41,7 @@ class AccountsController < ApplicationController
format.rss do format.rss do
expires_in 1.minute, public: true expires_in 1.minute, public: true
@statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE) @statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end end
@ -130,11 +130,11 @@ class AccountsController < ApplicationController
end end
def media_requested? def media_requested?
request.path.ends_with?('/media') && !tag_requested? request.path.split('.').first.ends_with?('/media') && !tag_requested?
end end
def replies_requested? def replies_requested?
request.path.ends_with?('/with_replies') && !tag_requested? request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
end end
def tag_requested? def tag_requested?

View File

@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
return [] if hide_results? return [] if hide_results?
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
scope.merge(paginated_follows).to_a scope.merge(paginated_follows).to_a
end end

View File

@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
return [] if hide_results? return [] if hide_results?
scope = default_accounts scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
scope.merge(paginated_follows).to_a scope.merge(paginated_follows).to_a
end end

View File

@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def public_timeline_statuses def public_timeline_statuses
Status.as_public_timeline(current_account, truthy_param?(:local)) Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
end end
def insert_pagination_headers def insert_pagination_headers
@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
end end
def next_path def next_path

View File

@ -113,6 +113,13 @@ class Auth::SessionsController < Devise::SessionsController
render :two_factor render :two_factor
end end
def require_no_authentication
super
# Delete flash message that isn't entirely useful and may be confusing in
# most cases because /web doesn't display/clear flash messages.
flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated')
end
private private
def set_pack def set_pack

View File

@ -28,18 +28,6 @@ module Localized
end end
def request_locale def request_locale
preferred_locale || compatible_locale http_accept_language.language_region_compatible_from(I18n.available_locales)
end
def preferred_locale
http_accept_language.preferred_language_from(available_locales)
end
def compatible_locale
http_accept_language.compatible_language_from(available_locales)
end
def available_locales
I18n.available_locales.reverse
end end
end end

View File

@ -22,8 +22,7 @@ class Settings::IdentityProofsController < Settings::BaseController
if current_account.username.casecmp(params[:username]).zero? if current_account.username.casecmp(params[:username]).zero?
render layout: 'auth' render layout: 'auth'
else else
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
redirect_to settings_identity_proofs_path
end end
end end
@ -35,11 +34,16 @@ class Settings::IdentityProofsController < Settings::BaseController
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
redirect_to @proof.on_success_path(params[:user_agent]) redirect_to @proof.on_success_path(params[:user_agent])
else else
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
redirect_to settings_identity_proofs_path
end end
end end
def destroy
@proof = current_account.identity_proofs.find(params[:id])
@proof.destroy!
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
end
private private
def check_enabled def check_enabled

View File

@ -7,13 +7,13 @@ module HomeHelper
} }
end end
def account_link_to(account, button = '', size: 36, path: nil) def account_link_to(account, button = '', path: nil)
content_tag(:div, class: 'account') do content_tag(:div, class: 'account') do
content_tag(:div, class: 'account__wrapper') do content_tag(:div, class: 'account__wrapper') do
section = if account.nil? section = if account.nil?
content_tag(:div, class: 'account__display-name') do content_tag(:div, class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})") image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'account__avatar')
end + end +
content_tag(:span, class: 'display-name') do content_tag(:span, class: 'display-name') do
content_tag(:strong, t('about.contact_missing')) + content_tag(:strong, t('about.contact_missing')) +
@ -23,7 +23,7 @@ module HomeHelper
else else
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})") image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
end + end +
content_tag(:span, class: 'display-name') do content_tag(:span, class: 'display-name') do
content_tag(:bdi) do content_tag(:bdi) do

View File

@ -68,6 +68,7 @@ module SettingsHelper
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
ur: 'اُردُو', ur: 'اُردُو',
vi: 'Tiếng Việt',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)', 'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)', 'zh-TW': '繁體中文(臺灣)',

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module WebfingerHelper
def webfinger!(uri)
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
opts = {
ssl: !hidden_service_uri,
headers: {
'User-Agent': Mastodon::Version.user_agent,
},
}
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
end
end

View File

@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View File

@ -121,7 +121,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
}; };
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });

View File

@ -186,10 +186,12 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else { } else {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) { if (!account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); if (account.getIn(['relationship', 'showing_reblogs'])) {
} else { menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); } else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
} }
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });

View File

@ -28,6 +28,7 @@ class Option extends React.PureComponent {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
isPollMultiple: PropTypes.bool, isPollMultiple: PropTypes.bool,
autoFocus: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list, suggestions: ImmutablePropTypes.list,
@ -58,7 +59,7 @@ class Option extends React.PureComponent {
} }
render () { render () {
const { isPollMultiple, title, index, intl } = this.props; const { isPollMultiple, title, index, autoFocus, intl } = this.props;
return ( return (
<li> <li>
@ -75,6 +76,7 @@ class Option extends React.PureComponent {
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
searchTokens={[':']} searchTokens={[':']}
autoFocus={autoFocus}
/> />
</label> </label>
@ -125,10 +127,12 @@ class PollForm extends ImmutablePureComponent {
return null; return null;
} }
const autoFocusIndex = options.indexOf('');
return ( return (
<div className='compose-form__poll-wrapper'> <div className='compose-form__poll-wrapper'>
<ul> <ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} {...other} />)} {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
{options.size < pollLimits.max_options && ( {options.size < pollLimits.max_options && (
<label className='poll__text editable'> <label className='poll__text editable'>
<span className={classNames('poll__input')} style={{ opacity: 0 }} /> <span className={classNames('poll__input')} style={{ opacity: 0 }} />

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
};
render () {
const { settings, onChange } = this.props;
return (
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings'; import ColumnSettings from '../components/column_settings';
import { changeSetting } from 'flavours/glitch/actions/settings'; import { changeSetting } from 'flavours/glitch/actions/settings';
import { changeColumnParams } from 'flavours/glitch/actions/columns'; import { changeColumnParams } from 'flavours/glitch/actions/columns';

View File

@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']); const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid); const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: !!timelineState && timelineState.get('unread') > 0, hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia, onlyMedia,
onlyRemote,
}; };
}; };
@ -46,15 +48,16 @@ class PublicTimeline extends React.PureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool, onlyMedia: PropTypes.bool,
onlyRemote: PropTypes.bool,
}; };
handlePin = () => { handlePin = () => {
const { columnId, dispatch, onlyMedia } = this.props; const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
} }
} }
@ -68,19 +71,19 @@ class PublicTimeline extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) { if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
this.disconnect(); this.disconnect();
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
} }
@ -96,13 +99,13 @@ class PublicTimeline extends React.PureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia })); dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
} }
render () { render () {
const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -121,7 +124,7 @@ class PublicTimeline extends React.PureComponent {
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
timelineId={`public${onlyMedia ? ':media' : ''}`} timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`} scrollKey={`public_timeline-${columnId}`}

View File

@ -37,6 +37,7 @@ const componentMap = {
'HOME': HomeTimeline, 'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications, 'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline, 'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline, 'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline, 'DIRECT': DirectTimeline,

View File

@ -545,13 +545,6 @@ $small-breakpoint: 960px;
flex: 0 0 auto; flex: 0 0 auto;
} }
&__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
@include avatar-size(44px);
}
.display-name { .display-name {
font-size: 15px; font-size: 15px;
@ -752,12 +745,6 @@ $small-breakpoint: 960px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
} }
&__counters__wrapper { &__counters__wrapper {

View File

@ -38,9 +38,14 @@
.account__avatar { .account__avatar {
@include avatar-radius(); @include avatar-radius();
display: block;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
width: 36px;
height: 36px;
background-size: 36px 36px;
&-inline { &-inline {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View File

@ -145,6 +145,11 @@
&__avatar { &__avatar {
left: 15px; left: 15px;
top: 17px; top: 17px;
.account__avatar {
width: 48px;
height: 48px;
}
} }
&__content { &__content {

View File

@ -93,12 +93,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
} }
.trends__item { .trends__item {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View File

@ -107,7 +107,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
}; };
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });

View File

@ -192,10 +192,12 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else { } else {
if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) { if (!account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); if (account.getIn(['relationship', 'showing_reblogs'])) {
} else { menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); } else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
} }
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });

View File

@ -27,6 +27,7 @@ class Option extends React.PureComponent {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
isPollMultiple: PropTypes.bool, isPollMultiple: PropTypes.bool,
autoFocus: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
onToggleMultiple: PropTypes.func.isRequired, onToggleMultiple: PropTypes.func.isRequired,
@ -71,7 +72,7 @@ class Option extends React.PureComponent {
} }
render () { render () {
const { isPollMultiple, title, index, intl } = this.props; const { isPollMultiple, title, index, autoFocus, intl } = this.props;
return ( return (
<li> <li>
@ -96,6 +97,7 @@ class Option extends React.PureComponent {
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
searchTokens={[':']} searchTokens={[':']}
autoFocus={autoFocus}
/> />
</label> </label>
@ -146,10 +148,12 @@ class PollForm extends ImmutablePureComponent {
return null; return null;
} }
const autoFocusIndex = options.indexOf('');
return ( return (
<div className='compose-form__poll-wrapper'> <div className='compose-form__poll-wrapper'>
<ul> <ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} {...other} />)} {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
};
render () {
const { settings, onChange } = this.props;
return (
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings'; import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings'; import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns'; import { changeColumnParams } from '../../../actions/columns';

View File

@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']); const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid); const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: !!timelineState && timelineState.get('unread') > 0, hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia, onlyMedia,
onlyRemote,
}; };
}; };
@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool, onlyMedia: PropTypes.bool,
onlyRemote: PropTypes.bool,
}; };
handlePin = () => { handlePin = () => {
const { columnId, dispatch, onlyMedia } = this.props; const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
} }
} }
@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) { if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
this.disconnect(); this.disconnect();
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
} }
@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia })); dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
} }
render () { render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent {
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
timelineId={`public${onlyMedia ? ':media' : ''}`} timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`} scrollKey={`public_timeline-${columnId}`}

View File

@ -5,30 +5,21 @@ import ColumnHeader from '../column_header';
describe('<Column />', () => { describe('<Column />', () => {
describe('<ColumnHeader /> click handler', () => { describe('<ColumnHeader /> click handler', () => {
const originalRaf = global.requestAnimationFrame;
beforeEach(() => {
global.requestAnimationFrame = jest.fn();
});
afterAll(() => {
global.requestAnimationFrame = originalRaf;
});
it('runs the scroll animation if the column contains scrollable content', () => { it('runs the scroll animation if the column contains scrollable content', () => {
const wrapper = mount( const wrapper = mount(
<Column heading='notifications'> <Column heading='notifications'>
<div className='scrollable' /> <div className='scrollable' />
</Column>, </Column>,
); );
const scrollToMock = jest.fn();
wrapper.find(Column).find('.scrollable').getDOMNode().scrollTo = scrollToMock;
wrapper.find(ColumnHeader).find('button').simulate('click'); wrapper.find(ColumnHeader).find('button').simulate('click');
expect(global.requestAnimationFrame.mock.calls.length).toEqual(1); expect(scrollToMock).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
}); });
it('does not try to scroll if there is no scrollable content', () => { it('does not try to scroll if there is no scrollable content', () => {
const wrapper = mount(<Column heading='notifications' />); const wrapper = mount(<Column heading='notifications' />);
wrapper.find(ColumnHeader).find('button').simulate('click'); wrapper.find(ColumnHeader).find('button').simulate('click');
expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
}); });
}); });
}); });

View File

@ -37,6 +37,7 @@ const componentMap = {
'HOME': HomeTimeline, 'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications, 'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline, 'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline, 'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline, 'DIRECT': DirectTimeline,

View File

@ -543,12 +543,6 @@ $small-breakpoint: 960px;
flex: 0 0 auto; flex: 0 0 auto;
} }
&__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
.display-name { .display-name {
font-size: 15px; font-size: 15px;
@ -749,12 +743,6 @@ $small-breakpoint: 960px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
} }
&__counters__wrapper { &__counters__wrapper {

View File

@ -1318,8 +1318,13 @@
.account__avatar { .account__avatar {
@include avatar-radius; @include avatar-radius;
display: block;
position: relative; position: relative;
width: 36px;
height: 36px;
background-size: 36px 36px;
&-inline { &-inline {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View File

@ -149,6 +149,11 @@
&__avatar { &__avatar {
left: 15px; left: 15px;
top: 17px; top: 17px;
.account__avatar {
width: 48px;
height: 48px;
}
} }
&__content { &__content {

View File

@ -93,12 +93,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.account__avatar {
width: 44px;
height: 44px;
background-size: 44px 44px;
}
} }
.trends__item { .trends__item {

View File

@ -22,7 +22,12 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
end end
def logo def logo
{ svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')), svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')) } {
svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')),
svg_white: full_asset_url(asset_pack_path('media/images/logo_transparent_white.svg')),
svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')),
svg_full_darkmode: full_asset_url(asset_pack_path('media/images/logo.svg')),
}
end end
def brand_color def brand_color

View File

@ -73,8 +73,6 @@ class Request
response.body_with_limit if http_client.persistent? response.body_with_limit if http_client.persistent?
yield response if block_given? yield response if block_given?
rescue => e
raise e.class, e.message, e.backtrace[0]
ensure ensure
http_client.close unless http_client.persistent? http_client.close unless http_client.persistent?
end end

38
app/lib/rss/serializer.rb Normal file
View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class RSS::Serializer
private
def render_statuses(builder, statuses)
statuses.each do |status|
builder.item do |item|
item.title(status_title(status))
.link(ActivityPub::TagManager.instance.url_for(status))
.pub_date(status.created_at)
.description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
status.media_attachments.each do |media|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
end
end
end
end
def status_title(status)
return "#{status.account.acct} deleted status" if status.destroyed?
preview = status.proper.spoiler_text.presence || status.proper.text
if preview.length > 30 || preview[0, 30].include?("\n")
preview = preview[0, 30]
preview = preview[0, preview.index("\n").presence || 30] + '…'
end
preview = "#{status.proper.spoiler_text.present? ? 'CW ' : ''}#{preview}#{status.proper.sensitive? ? ' (sensitive)' : ''}"
if status.reblog?
"#{status.account.acct} boosted #{status.reblog.account.acct}: #{preview}"
else
"#{status.account.acct}: #{preview}"
end
end
end

View File

@ -1,13 +1,24 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidekiqErrorHandler class SidekiqErrorHandler
BACKTRACE_LIMIT = 3
def call(*) def call(*)
yield yield
rescue Mastodon::HostValidationError rescue Mastodon::HostValidationError
# Do not retry # Do not retry
rescue => e
limit_backtrace_and_raise(e)
ensure ensure
socket = Thread.current[:statsd_socket] socket = Thread.current[:statsd_socket]
socket&.close socket&.close
Thread.current[:statsd_socket] = nil Thread.current[:statsd_socket] = nil
end end
private
def limit_backtrace_and_raise(e)
e.set_backtrace(e.backtrace.first(BACKTRACE_LIMIT))
raise e
end
end end

View File

@ -23,7 +23,7 @@ class RelationshipFilter
scope = scope_for('relationship', params['relationship'].to_s.strip) scope = scope_for('relationship', params['relationship'].to_s.strip)
params.each do |key, value| params.each do |key, value|
next if key.to_s == 'page' next if %w(relationship page).include?(key)
scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present? scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
end end

View File

@ -3,6 +3,7 @@
class RemoteFollow class RemoteFollow
include ActiveModel::Validations include ActiveModel::Validations
include RoutingHelper include RoutingHelper
include WebfingerHelper
attr_accessor :acct, :addressable_template attr_accessor :acct, :addressable_template
@ -71,7 +72,7 @@ class RemoteFollow
end end
def acct_resource def acct_resource
@acct_resource ||= Goldfinger.finger("acct:#{acct}") @acct_resource ||= webfinger!("acct:#{acct}")
rescue Goldfinger::Error, HTTP::ConnectionError rescue Goldfinger::Error, HTTP::ConnectionError
nil nil
end end

View File

@ -203,14 +203,6 @@ class Status < ApplicationRecord
preview_cards.first preview_cards.first
end end
def title
if destroyed?
"#{account.acct} deleted status"
else
reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
end
end
def hidden? def hidden?
!distributable? !distributable?
end end
@ -342,7 +334,7 @@ class Status < ApplicationRecord
query = timeline_scope(local_only) query = timeline_scope(local_only)
query = query.without_replies unless Setting.show_replies_in_public_timelines query = query.without_replies unless Setting.show_replies_in_public_timelines
apply_timeline_filters(query, account, local_only) apply_timeline_filters(query, account, [:local, true].include?(local_only))
end end
def as_tag_timeline(tag, account = nil, local_only = false) def as_tag_timeline(tag, account = nil, local_only = false)
@ -434,8 +426,15 @@ class Status < ApplicationRecord
private private
def timeline_scope(local_only = false) def timeline_scope(scope = false)
starting_scope = local_only ? Status.local : Status starting_scope = case scope
when :local, true
Status.local
when :remote
Status.remote
else
Status
end
starting_scope = starting_scope.with_public_visibility starting_scope = starting_scope.with_public_visibility
if Setting.show_reblogs_in_public_timelines if Setting.show_reblogs_in_public_timelines
starting_scope starting_scope

View File

@ -94,11 +94,11 @@ class Web::PushSubscription < ApplicationRecord
def find_or_create_access_token def find_or_create_access_token
Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::AccessToken.find_or_create_for(
Doorkeeper::Application.find_by(superapp: true), application: Doorkeeper::Application.find_by(superapp: true),
session_activation.user_id, resource_owner: session_activation.user_id,
Doorkeeper::OAuth::Scopes.from_string('read write follow push'), scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
Doorkeeper.configuration.access_token_expires_in, expires_in: Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled? use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
) )
end end
end end

View File

@ -4,7 +4,7 @@ class OEmbedSerializer < ActiveModel::Serializer
include RoutingHelper include RoutingHelper
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
attributes :type, :version, :title, :author_name, attributes :type, :version, :author_name,
:author_url, :provider_name, :provider_url, :author_url, :provider_name, :provider_url,
:cache_age, :html, :width, :height :cache_age, :html, :width, :height

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class RSS::AccountSerializer class RSS::AccountSerializer < RSS::Serializer
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include AccountsHelper include AccountsHelper
include RoutingHelper include RoutingHelper
@ -17,18 +17,7 @@ class RSS::AccountSerializer
builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar? builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
builder.cover(full_asset_url(account.header.url(:original))) if account.header? builder.cover(full_asset_url(account.header.url(:original))) if account.header?
statuses.each do |status| render_statuses(builder, statuses)
builder.item do |item|
item.title(status.title)
.link(ActivityPub::TagManager.instance.url_for(status))
.pub_date(status.created_at)
.description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str)
status.media_attachments.each do |media|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
end
end
end
builder.to_xml builder.to_xml
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class RSS::TagSerializer class RSS::TagSerializer < RSS::Serializer
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
include RoutingHelper include RoutingHelper
@ -14,18 +14,7 @@ class RSS::TagSerializer
.logo(full_pack_url('media/images/logo.svg')) .logo(full_pack_url('media/images/logo.svg'))
.accent_color('2b90d9') .accent_color('2b90d9')
statuses.each do |status| render_statuses(builder, statuses)
builder.item do |item|
item.title(status.title)
.link(ActivityPub::TagManager.instance.url_for(status))
.pub_date(status.created_at)
.description(status.spoiler_text.presence || Formatter.instance.format(status).to_str)
status.media_attachments.each do |media|
item.enclosure(full_asset_url(media.file.url(:original, false)), media.file.content_type, media.file.size)
end
end
end
builder.to_xml builder.to_xml
end end

View File

@ -3,6 +3,7 @@
class ActivityPub::FetchRemoteAccountService < BaseService class ActivityPub::FetchRemoteAccountService < BaseService
include JsonLdHelper include JsonLdHelper
include DomainControlHelper include DomainControlHelper
include WebfingerHelper
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
@ -35,12 +36,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService
private private
def verified_webfinger? def verified_webfinger?
webfinger = Goldfinger.finger("acct:#{@username}@#{@domain}") webfinger = webfinger!("acct:#{@username}@#{@domain}")
confirmed_username, confirmed_domain = split_acct(webfinger.subject) confirmed_username, confirmed_domain = split_acct(webfinger.subject)
return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero? return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
webfinger = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}") webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
@username, @domain = split_acct(webfinger.subject) @username, @domain = split_acct(webfinger.subject)
self_reference = webfinger.link('self') self_reference = webfinger.link('self')

View File

@ -73,11 +73,18 @@ class BatchedRemoveStatusService < BaseService
redis.pipelined do redis.pipelined do
redis.publish('timeline:public', payload) redis.publish('timeline:public', payload)
redis.publish('timeline:public:local', payload) if status.local? if status.local?
redis.publish('timeline:public:local', payload)
else
redis.publish('timeline:public:remote', payload)
end
if status.media_attachments.any? if status.media_attachments.any?
redis.publish('timeline:public:media', payload) redis.publish('timeline:public:media', payload)
redis.publish('timeline:public:local:media', payload) if status.local? if status.local?
redis.publish('timeline:public:local:media', payload)
else
redis.publish('timeline:public:remote:media', payload)
end
end end
@tags[status.id].each do |hashtag| @tags[status.id].each do |hashtag|

View File

@ -86,14 +86,22 @@ class FanOutOnWriteService < BaseService
Rails.logger.debug "Delivering status #{status.id} to public timeline" Rails.logger.debug "Delivering status #{status.id} to public timeline"
Redis.current.publish('timeline:public', @payload) Redis.current.publish('timeline:public', @payload)
Redis.current.publish('timeline:public:local', @payload) if status.local? if status.local?
Redis.current.publish('timeline:public:local', @payload)
else
Redis.current.publish('timeline:public:remote', @payload)
end
end end
def deliver_to_media(status) def deliver_to_media(status)
Rails.logger.debug "Delivering status #{status.id} to media timeline" Rails.logger.debug "Delivering status #{status.id} to media timeline"
Redis.current.publish('timeline:public:media', @payload) Redis.current.publish('timeline:public:media', @payload)
Redis.current.publish('timeline:public:local:media', @payload) if status.local? if status.local?
Redis.current.publish('timeline:public:local:media', @payload)
else
Redis.current.publish('timeline:public:remote:media', @payload)
end
end end
def deliver_to_direct_timelines(status) def deliver_to_direct_timelines(status)

View File

@ -142,14 +142,22 @@ class RemoveStatusService < BaseService
return unless @status.public_visibility? return unless @status.public_visibility?
redis.publish('timeline:public', @payload) redis.publish('timeline:public', @payload)
redis.publish('timeline:public:local', @payload) if @status.local? if @status.local?
redis.publish('timeline:public:local', @payload)
else
redis.publish('timeline:public:remote', @payload)
end
end end
def remove_from_media def remove_from_media
return unless @status.public_visibility? return unless @status.public_visibility?
redis.publish('timeline:public:media', @payload) redis.publish('timeline:public:media', @payload)
redis.publish('timeline:public:local:media', @payload) if @status.local? if @status.local?
redis.publish('timeline:public:local:media', @payload)
else
redis.publish('timeline:public:remote:media', @payload)
end
end end
def remove_from_direct def remove_from_direct

View File

@ -3,6 +3,7 @@
class ResolveAccountService < BaseService class ResolveAccountService < BaseService
include JsonLdHelper include JsonLdHelper
include DomainControlHelper include DomainControlHelper
include WebfingerHelper
class WebfingerRedirectError < StandardError; end class WebfingerRedirectError < StandardError; end
@ -76,7 +77,7 @@ class ResolveAccountService < BaseService
end end
def process_webfinger!(uri, redirected = false) def process_webfinger!(uri, redirected = false)
@webfinger = Goldfinger.finger("acct:#{uri}") @webfinger = webfinger!("acct:#{uri}")
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?

View File

@ -27,7 +27,7 @@
.avatar-stack .avatar-stack
- @instance_presenter.sample_accounts.each do |account| - @instance_presenter.sample_accounts.each do |account|
= image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar' = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, alt: '', class: 'account__avatar'
- if Setting.timeline_preview - if Setting.timeline_preview
.directory__tag .directory__tag

View File

@ -25,7 +25,7 @@
- target_account = reports.first.target_account - target_account = reports.first.target_account
.report-card .report-card
.report-card__profile .report-card__profile
= account_link_to target_account, '', size: 36, path: admin_account_path(target_account.id) = account_link_to target_account, '', path: admin_account_path(target_account.id)
.report-card__profile__stats .report-card__profile__stats
= link_to t('admin.reports.account.notes', count: target_account.targeted_moderation_notes.count), admin_account_path(target_account.id) = link_to t('admin.reports.account.notes', count: target_account.targeted_moderation_notes.count), admin_account_path(target_account.id)
%br/ %br/

View File

@ -25,7 +25,7 @@
.directory__card__bar .directory__card__bar
= link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
.avatar .avatar
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' = image_tag account.avatar.url, alt: '', class: 'u-photo'
.display-name .display-name
%bdi %bdi

View File

@ -28,6 +28,8 @@
= javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
= csrf_meta_tags = csrf_meta_tags
= stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
= yield :header_tags = yield :header_tags
-# These must come after :header_tags to ensure our initial state has been defined. -# These must come after :header_tags to ensure our initial state has been defined.

View File

@ -18,3 +18,4 @@
%td %td
= table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url = table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url
= table_link_to 'trash', t('identity_proofs.remove'), settings_identity_proof_path(proof), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View File

@ -3,9 +3,9 @@
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do
.detailed-status__display-avatar .detailed-status__display-avatar
- if current_account&.user&.setting_auto_play_gif || autoplay - if current_account&.user&.setting_auto_play_gif || autoplay
= image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' = image_tag status.account.avatar_original_url, alt: '', class: 'account__avatar u-photo'
- else - else
= image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' = image_tag status.account.avatar_static_url, alt: '', class: 'account__avatar u-photo'
%span.display-name %span.display-name
%bdi %bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)

View File

@ -9,9 +9,9 @@
.status__avatar .status__avatar
%div %div
- if current_account&.user&.setting_auto_play_gif || autoplay - if current_account&.user&.setting_auto_play_gif || autoplay
= image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' = image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
- else - else
= image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' = image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
%span.display-name %span.display-name
%bdi %bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)

View File

@ -52,13 +52,9 @@ class ActivityPub::DeliveryWorker
end end
end end
begin light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD) .with_cool_off_time(STOPLIGHT_COOLDOWN)
.with_cool_off_time(STOPLIGHT_COOLDOWN) .run
.run
rescue Stoplight::Error::RedLight => e
raise e.class, e.message, e.backtrace.first(3)
end
end end
def failure_tracker def failure_tracker

View File

@ -11,7 +11,7 @@ class RedownloadMediaWorker
return if media_attachment.remote_url.blank? return if media_attachment.remote_url.blank?
media_attachment.reset_file! media_attachment.file_remote_url = media_attachment.remote_url
media_attachment.save media_attachment.save
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true

View File

@ -55,8 +55,8 @@ module Mastodon
:el, :el,
:en, :en,
:eo, :eo,
:'es-AR',
:es, :es,
:'es-AR',
:et, :et,
:eu, :eu,
:fa, :fa,
@ -97,8 +97,8 @@ module Mastodon
:sk, :sk,
:sl, :sl,
:sq, :sq,
:'sr-Latn',
:sr, :sr,
:'sr-Latn',
:sv, :sv,
:ta, :ta,
:te, :te,
@ -106,6 +106,7 @@ module Mastodon
:tr, :tr,
:uk, :uk,
:ur, :ur,
:vi,
:'zh-CN', :'zh-CN',
:'zh-HK', :'zh-HK',
:'zh-TW', :'zh-TW',

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
lock '3.12.1' lock '3.14.0'
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git') set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'master') set :branch, ENV.fetch('BRANCH', 'master')
@ -12,3 +12,21 @@ set :migration_role, :app
append :linked_files, '.env.production', 'public/robots.txt' append :linked_files, '.env.production', 'public/robots.txt'
append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system' append :linked_dirs, 'vendor/bundle', 'node_modules', 'public/system'
namespace :systemd do
%i[sidekiq streaming web].each do |service|
%i[reload restart status].each do |action|
desc "Perform a #{action} on #{service} service"
task "#{service}:#{action}".to_sym do
on roles(:app) do
# runs e.g. "sudo restart mastodon-sidekiq.service"
sudo :systemctl, action, "#{fetch(:application)}-#{service}.service"
end
end
end
end
end
after 'deploy:publishing', 'systemd:web:reload'
after 'deploy:publishing', 'systemd:sidekiq:restart'
after 'deploy:publishing', 'systemd:streaming:restart'

View File

@ -34,7 +34,7 @@ if Rails.env.production?
p.script_src :self, assets_host p.script_src :self, assets_host
p.font_src :self, assets_host p.font_src :self, assets_host
p.img_src :self, :data, :blob, *data_hosts p.img_src :self, :data, :blob, *data_hosts
p.style_src :self, :unsafe_inline, assets_host p.style_src :self, assets_host
p.media_src :self, :data, *data_hosts p.media_src :self, :data, *data_hosts
p.frame_src :self, :https p.frame_src :self, :https
p.child_src :self, :blob, assets_host p.child_src :self, :blob, assets_host
@ -48,3 +48,8 @@ end
# For further information see the following documentation: # For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true # Rails.application.config.content_security_policy_report_only = true
PgHero::HomeController.content_security_policy do |p|
p.script_src :self, :unsafe_inline, assets_host
p.style_src :self, :unsafe_inline, assets_host
end

View File

@ -1,24 +1,22 @@
Rails.application.configure do Rails.application.configure do
config.x.http_client_proxy = {} config.x.http_client_proxy = {}
if ENV['http_proxy'].present? if ENV['http_proxy'].present?
proxy = URI.parse(ENV['http_proxy']) proxy = URI.parse(ENV['http_proxy'])
raise "Unsupported proxy type: #{proxy.scheme}" unless %w(http https).include? proxy.scheme raise "Unsupported proxy type: #{proxy.scheme}" unless %w(http https).include? proxy.scheme
raise "No proxy host" unless proxy.host raise "No proxy host" unless proxy.host
host = proxy.host host = proxy.host
host = host[1...-1] if host[0] == '[' # for IPv6 address host = host[1...-1] if host[0] == '[' # for IPv6 address
config.x.http_client_proxy[:proxy] = { proxy_address: host, proxy_port: proxy.port, proxy_username: proxy.user, proxy_password: proxy.password }.compact
config.x.http_client_proxy[:proxy] = {
proxy_address: host,
proxy_port: proxy.port,
proxy_username: proxy.user,
proxy_password: proxy.password,
}.compact
end end
config.x.access_to_hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' config.x.access_to_hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
end end
module Goldfinger
def self.finger(uri, opts = {})
to_hidden = /\.(onion|i2p)(:\d+)?$/.match(uri)
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && to_hidden
opts = { ssl: !to_hidden, headers: {} }.merge(Rails.configuration.x.http_client_proxy).merge(opts)
opts[:headers]['User-Agent'] ||= Mastodon::Version.user_agent
Goldfinger::Client.new(uri, opts).finger
end
end

View File

@ -19,4 +19,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'ActivityStreams' inflect.acronym 'ActivityStreams'
inflect.acronym 'JsonLd' inflect.acronym 'JsonLd'
inflect.acronym 'NodeInfo' inflect.acronym 'NodeInfo'
inflect.singular 'data', 'data'
end end

View File

@ -858,12 +858,14 @@ en:
invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters
verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase.
wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again. wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again.
explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them. explanation_html: Here you can cryptographically connect your other identities from other platforms, such as Keybase. This lets other people send you encrypted messages on those platforms and allows them to trust that the content you send them comes from you.
i_am_html: I am %{username} on %{service}. i_am_html: I am %{username} on %{service}.
identity: Identity identity: Identity
inactive: Inactive inactive: Inactive
publicize_checkbox: 'And toot this:' publicize_checkbox: 'And toot this:'
publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}' publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}'
remove: Remove proof from account
removed: Successfully removed proof from account
status: Verification status status: Verification status
view_proof: View proof view_proof: View proof
imports: imports:
@ -918,7 +920,7 @@ en:
cancelled_msg: Successfully cancelled the redirect. cancelled_msg: Successfully cancelled the redirect.
errors: errors:
already_moved: is the same account you have already moved to already_moved: is the same account you have already moved to
missing_also_known_as: is not back-referencing this account missing_also_known_as: is not an alias of this account
move_to_self: cannot be current account move_to_self: cannot be current account
not_found: could not be found not_found: could not be found
on_cooldown: You are on cooldown on_cooldown: You are on cooldown

View File

@ -38,4 +38,4 @@ databases:
# aws_secret_access_key: ... # aws_secret_access_key: ...
# aws_region: us-east-1 # aws_region: us-east-1
override_csp: true override_csp: false

View File

@ -130,7 +130,7 @@ Rails.application.routes.draw do
resource :confirmation, only: [:new, :create] resource :confirmation, only: [:new, :create]
end end
resources :identity_proofs, only: [:index, :show, :new, :create, :update] resources :identity_proofs, only: [:index, :new, :create, :destroy]
resources :applications, except: [:edit] do resources :applications, except: [:edit] do
member do member do

View File

@ -1,5 +1,5 @@
class AddInviteIdToUsers < ActiveRecord::Migration[5.1] class AddInviteIdToUsers < ActiveRecord::Migration[5.1]
def change def change
add_reference :users, :invite, null: true, default: nil, foreign_key: { on_delete: :nullify }, index: false safety_assured { add_reference :users, :invite, null: true, default: nil, foreign_key: { on_delete: :nullify }, index: false }
end end
end end

View File

@ -1,5 +1,5 @@
class AddAssignedAccountIdToReports < ActiveRecord::Migration[5.1] class AddAssignedAccountIdToReports < ActiveRecord::Migration[5.1]
def change def change
add_reference :reports, :assigned_account, null: true, default: nil, foreign_key: { on_delete: :nullify, to_table: :accounts }, index: false safety_assured { add_reference :reports, :assigned_account, null: true, default: nil, foreign_key: { on_delete: :nullify, to_table: :accounts }, index: false }
end end
end end

View File

@ -1,6 +1,8 @@
class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2] class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2]
def change def change
add_reference :web_push_subscriptions, :access_token, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :oauth_access_tokens }, index: false safety_assured do
add_reference :web_push_subscriptions, :user, null: true, default: nil, foreign_key: { on_delete: :cascade }, index: false add_reference :web_push_subscriptions, :access_token, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :oauth_access_tokens }, index: false
add_reference :web_push_subscriptions, :user, null: true, default: nil, foreign_key: { on_delete: :cascade }, index: false
end
end end
end end

View File

@ -2,7 +2,7 @@ class AddCreatedByApplicationIdToUsers < ActiveRecord::Migration[5.2]
disable_ddl_transaction! disable_ddl_transaction!
def change def change
add_reference :users, :created_by_application, foreign_key: { to_table: 'oauth_applications', on_delete: :nullify }, index: false safety_assured { add_reference :users, :created_by_application, foreign_key: { to_table: 'oauth_applications', on_delete: :nullify }, index: false }
add_index :users, :created_by_application_id, algorithm: :concurrently add_index :users, :created_by_application_id, algorithm: :concurrently
end end
end end

View File

@ -2,7 +2,7 @@ class AddScheduledStatusIdToMediaAttachments < ActiveRecord::Migration[5.2]
disable_ddl_transaction! disable_ddl_transaction!
def change def change
add_reference :media_attachments, :scheduled_status, foreign_key: { on_delete: :nullify }, index: false safety_assured { add_reference :media_attachments, :scheduled_status, foreign_key: { on_delete: :nullify }, index: false }
add_index :media_attachments, :scheduled_status_id, algorithm: :concurrently add_index :media_attachments, :scheduled_status_id, algorithm: :concurrently
end end
end end

View File

@ -1,5 +1,5 @@
class AddParentIdToEmailDomainBlocks < ActiveRecord::Migration[5.2] class AddParentIdToEmailDomainBlocks < ActiveRecord::Migration[5.2]
def change def change
add_reference :email_domain_blocks, :parent, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :email_domain_blocks }, index: false safety_assured { add_reference :email_domain_blocks, :parent, null: true, default: nil, foreign_key: { on_delete: :cascade, to_table: :email_domain_blocks }, index: false }
end end
end end

View File

@ -0,0 +1,12 @@
class ResetUniqueJobsLocks < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
# We do this to clean up unique job digests that were not properly
# disposed of prior to https://github.com/tootsuite/mastodon/pull/13361
SidekiqUniqueJobs::Digests.delete_by_pattern('*', count: SidekiqUniqueJobs::Digests.count)
end
def down; end
end

View File

@ -0,0 +1,15 @@
class ResetWebAppSecret < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
web_app = Doorkeeper::Application.find_by(superapp: true)
return if web_app.nil?
web_app.renew_secret
web_app.save!
end
def down
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_04_17_125749) do ActiveRecord::Schema.define(version: 2020_05_10_110808) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"

View File

@ -144,7 +144,14 @@ module Mastodon
begin begin
size = File.size(path) size = File.size(path)
File.delete(path) unless options[:dry_run] unless options[:dry_run]
File.delete(path)
begin
FileUtils.rmdir(File.dirname(path), parents: true)
rescue Errno::ENOTEMPTY
# OK
end
end
reclaimed_bytes += size reclaimed_bytes += size
removed += 1 removed += 1

View File

@ -121,7 +121,7 @@ module Mastodon
FileUtils.mv(previous_path, upgraded_path) FileUtils.mv(previous_path, upgraded_path)
begin begin
FileUtils.rmdir(previous_path, parents: true) FileUtils.rmdir(File.dirname(previous_path), parents: true)
rescue Errno::ENOTEMPTY rescue Errno::ENOTEMPTY
# OK # OK
end end
@ -131,7 +131,7 @@ module Mastodon
unless dry_run? unless dry_run?
begin begin
FileUtils.rmdir(upgraded_path, parents: true) FileUtils.rmdir(File.dirname(upgraded_path), parents: true)
rescue Errno::ENOTEMPTY rescue Errno::ENOTEMPTY
# OK # OK
end end

View File

@ -60,17 +60,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-transform-react-inline-elements": "^7.9.0", "@babel/plugin-transform-react-inline-elements": "^7.9.0",
"@babel/plugin-transform-runtime": "^7.9.0", "@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.8.4",
"@clusterws/cws": "^0.17.3", "@clusterws/cws": "^2.0.0",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@rails/ujs": "^6.0.2", "@rails/ujs": "^6.0.3",
"array-includes": "^3.1.1", "array-includes": "^3.1.1",
"arrow-key-navigation": "^1.1.0", "arrow-key-navigation": "^1.1.0",
"atrament": "0.2.4", "atrament": "0.2.4",
@ -79,7 +79,7 @@
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-lodash": "^3.3.4", "babel-plugin-lodash": "^3.3.4",
"babel-plugin-preval": "^5.0.0", "babel-plugin-preval": "^5.0.0",
"babel-plugin-react-intl": "^3.4.1", "babel-plugin-react-intl": "^6.2.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"blurhash": "^1.1.3", "blurhash": "^1.1.3",
@ -128,7 +128,7 @@
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"punycode": "^2.1.0", "punycode": "^2.1.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.0", "react-dom": "^16.13.1",
"react-hotkeys": "^1.1.4", "react-hotkeys": "^1.1.4",
"react-immutable-proptypes": "^2.2.0", "react-immutable-proptypes": "^2.2.0",
"react-immutable-pure-component": "^1.1.1", "react-immutable-pure-component": "^1.1.1",
@ -159,15 +159,15 @@
"stacktrace-js": "^2.0.2", "stacktrace-js": "^2.0.2",
"stringz": "^2.1.0", "stringz": "^2.1.0",
"substring-trie": "^1.0.2", "substring-trie": "^1.0.2",
"terser-webpack-plugin": "^2.3.5", "terser-webpack-plugin": "^3.0.1",
"tesseract.js": "^2.0.0-alpha.16", "tesseract.js": "^2.0.0-alpha.16",
"throng": "^4.0.0", "throng": "^4.0.0",
"tiny-queue": "^0.2.1", "tiny-queue": "^0.2.1",
"uuid": "^7.0.3", "uuid": "^8.0.0",
"wavesurfer.js": "^3.3.1", "wavesurfer.js": "^3.3.3",
"webpack": "^4.42.1", "webpack": "^4.43.0",
"webpack-assets-manifest": "^3.1.1", "webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.6.1", "webpack-bundle-analyzer": "^3.7.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-merge": "^4.2.1", "webpack-merge": "^4.2.1",
"wicg-inert": "^3.0.2" "wicg-inert": "^3.0.2"
@ -182,10 +182,10 @@
"eslint-plugin-jsx-a11y": "~6.2.3", "eslint-plugin-jsx-a11y": "~6.2.3",
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",
"eslint-plugin-react": "~7.19.0", "eslint-plugin-react": "~7.19.0",
"jest": "^24.9.0", "jest": "^25.4.0",
"raf": "^3.4.1", "raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3", "react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.13.0", "react-test-renderer": "^16.13.1",
"sass-lint": "^1.13.1", "sass-lint": "^1.13.1",
"webpack-dev-server": "^3.10.3", "webpack-dev-server": "^3.10.3",
"yargs": "^15.3.1" "yargs": "^15.3.1"

11
public/inert.css Normal file
View File

@ -0,0 +1,11 @@
[inert] {
pointer-events: none;
cursor: default;
}
[inert], [inert] * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

View File

@ -3,108 +3,608 @@ require 'rails_helper'
RSpec.describe AccountsController, type: :controller do RSpec.describe AccountsController, type: :controller do
render_views render_views
let(:alice) { Fabricate(:account, username: 'alice', user: Fabricate(:user)) } let(:account) { Fabricate(:user).account }
let(:eve) { Fabricate(:user) }
describe 'GET #show' do describe 'GET #show' do
let!(:status1) { Status.create!(account: alice, text: 'Hello world') } let(:format) { 'html' }
let!(:status2) { Status.create!(account: alice, text: 'Boop', thread: status1) }
let!(:status3) { Status.create!(account: alice, text: 'Picture!') }
let!(:status4) { Status.create!(account: alice, text: 'Mentioning @alice') }
let!(:status5) { Status.create!(account: alice, text: 'Kitsune') }
let!(:status6) { Status.create!(account: alice, text: 'Neko') }
let!(:status7) { Status.create!(account: alice, text: 'Tanuki') }
let!(:status_pin1) { StatusPin.create!(account: alice, status: status5, created_at: 5.days.ago) } let!(:status) { Fabricate(:status, account: account) }
let!(:status_pin2) { StatusPin.create!(account: alice, status: status6, created_at: 2.years.ago) } let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) } let!(:status_self_reply) { Fabricate(:status, account: account, thread: status) }
let!(:status_media) { Fabricate(:status, account: account) }
let!(:status_pinned) { Fabricate(:status, account: account) }
let!(:status_private) { Fabricate(:status, account: account, visibility: :private) }
let!(:status_direct) { Fabricate(:status, account: account, visibility: :direct) }
let!(:status_reblog) { Fabricate(:status, account: account, reblog: Fabricate(:status)) }
before do before do
alice.block!(eve.account) status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image)
status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg')) account.pinned_statuses << status_pinned
end end
shared_examples 'responses' do shared_examples 'preliminary checks' do
before do context 'when account is not approved' do
sign_in(current_user) if defined? current_user before do
get :show, params: { account.user.update(approved: false)
username: alice.username, end
max_id: (max_id if defined? max_id),
since_id: (since_id if defined? since_id), it 'returns http not found' do
current_user: (current_user if defined? current_user), get :show, params: { username: account.username, format: format }
}, format: format expect(response).to have_http_status(404)
end
end end
it 'assigns @account' do context 'when account is suspended' do
expect(assigns(:account)).to eq alice before do
end account.suspend!
end
it 'returns http success' do it 'returns http gone' do
expect(response).to have_http_status(200) get :show, params: { username: account.username, format: format }
end expect(response).to have_http_status(410)
end
it 'returns correct format' do
expect(response.content_type).to eq content_type
end end
end end
context 'activitystreams2' do context 'as HTML' do
let(:format) { 'html' }
it_behaves_like 'preliminary checks'
shared_examples 'common response characteristics' do
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns Link header' do
expect(response.headers['Link'].to_s).to include ActivityPub::TagManager.instance.uri_for(account)
end
it 'renders show template' do
expect(response).to render_template(:show)
end
end
context do
before do
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'renders public status' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
end
it 'renders self-reply' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'renders reblog' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'renders pinned status' do
expect(response.body).to include(I18n.t('stream_entries.pinned'))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'when signed-in' do
let(:user) { Fabricate(:user) }
before do
sign_in(user)
end
context 'when user follows account' do
before do
user.account.follow!(account)
get :show, params: { username: account.username, format: format }
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
end
context 'when user is blocked' do
before do
account.block!(user.account)
get :show, params: { username: account.username, format: format }
end
it 'renders unavailable message' do
expect(response.body).to include(I18n.t('accounts.unavailable'))
end
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'does not render status with media' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render pinned status' do
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
end
context 'with replies' do
before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'renders public status' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
end
it 'renders self-reply' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'renders reblog' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render pinned status' do
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'renders reply to someone else' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with media' do
before do
allow(controller).to receive(:media_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render pinned status' do
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with tag' do
let(:tag) { Fabricate(:tag) }
let!(:status_tag) { Fabricate(:status, account: account) }
before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'does not render status with media' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render pinned status' do
expect(response.body).to_not include(I18n.t('stream_entries.pinned'))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
it 'renders status with tag' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag))
end
end
end
context 'as JSON' do
let(:authorized_fetch_mode) { false }
let(:format) { 'json' } let(:format) { 'json' }
let(:content_type) { 'application/activity+json' }
include_examples 'responses' before do
allow(controller).to receive(:authorized_fetch_mode?).and_return(authorized_fetch_mode)
end
it_behaves_like 'preliminary checks'
context do
before do
get :show, params: { username: account.username, format: format }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public'
end
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
context 'in authorized fetch mode' do
let(:authorized_fetch_mode) { true }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public'
end
it 'returns Vary header with Signature' do
expect(response.headers['Vary']).to include 'Signature'
end
it 'renders bare minimum account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey)
expect(json).to_not include(:name, :summary)
end
end
end
context 'when signed in' do
let(:user) { Fabricate(:user) }
before do
sign_in(user)
get :show, params: { username: account.username, format: format }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public'
end
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
end
context 'with signature' do
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
before do
allow(controller).to receive(:signed_request_account).and_return(remote_account)
get :show, params: { username: account.username, format: format }
end
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it 'returns public Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'public'
end
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
context 'in authorized fetch mode' do
let(:authorized_fetch_mode) { true }
it 'returns http success' do
expect(response).to have_http_status(200)
end
it 'returns application/activity+json' do
expect(response.content_type).to eq 'application/activity+json'
end
it 'returns private Cache-Control header' do
expect(response.headers['Cache-Control']).to include 'private'
end
it 'returns Vary header with Signature' do
expect(response.headers['Vary']).to include 'Signature'
end
it 'renders account' do
json = body_as_json
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
end
end
end
end end
context 'html' do context 'as RSS' do
let(:format) { nil } let(:format) { 'rss' }
let(:content_type) { 'text/html' }
shared_examples 'responsed statuses' do it_behaves_like 'preliminary checks'
it 'assigns @pinned_statuses' do
pinned_statuses = assigns(:pinned_statuses).to_a shared_examples 'common response characteristics' do
expect(pinned_statuses.size).to eq expected_pinned_statuses.size it 'returns http success' do
pinned_statuses.each.zip(expected_pinned_statuses.each) do |pinned_status, expected_pinned_status| expect(response).to have_http_status(200)
expect(pinned_status).to eq expected_pinned_status
end
end end
it 'assigns @statuses' do it 'returns public Cache-Control header' do
statuses = assigns(:statuses).to_a expect(response.headers['Cache-Control']).to include 'public'
expect(statuses.size).to eq expected_statuses.size
statuses.each.zip(expected_statuses.each) do |status, expected_status|
expect(status).to eq expected_status
end
end end
end end
include_examples 'responses' context do
before do
context 'with anonymous visitor' do get :show, params: { username: account.username, format: format }
context 'without since_id nor max_id' do
let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] }
let(:expected_pinned_statuses) { [status7, status5, status6] }
include_examples 'responsed statuses'
end end
context 'with since_id nor max_id' do it_behaves_like 'common response characteristics'
let(:max_id) { status4.id }
let(:since_id) { status1.id }
let(:expected_statuses) { [status3, status2] }
let(:expected_pinned_statuses) { [] }
include_examples 'responsed statuses' it 'renders public status' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
end
it 'renders self-reply' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end end
end end
context 'with blocked visitor' do context 'with replies' do
let(:current_user) { eve } before do
allow(controller).to receive(:replies_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
context 'without since_id nor max_id' do it_behaves_like 'common response characteristics'
let(:expected_statuses) { [] }
let(:expected_pinned_statuses) { [] }
include_examples 'responsed statuses' it 'renders public status' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status))
end
it 'renders self-reply' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'renders reply to someone else' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with media' do
before do
allow(controller).to receive(:media_requested?).and_return(true)
get :show, params: { username: account.username, format: format }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'renders status with media' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
end
context 'with tag' do
let(:tag) { Fabricate(:tag) }
let!(:status_tag) { Fabricate(:status, account: account) }
before do
allow(controller).to receive(:tag_requested?).and_return(true)
status_tag.tags << tag
get :show, params: { username: account.username, format: format, tag: tag.to_param }
end
it_behaves_like 'common response characteristics'
it 'does not render public status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status))
end
it 'does not render self-reply' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_self_reply))
end
it 'does not render status with media' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_media))
end
it 'does not render reblog' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reblog.reblog))
end
it 'does not render private status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_private))
end
it 'does not render direct status' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_direct))
end
it 'does not render reply to someone else' do
expect(response.body).to_not include(ActivityPub::TagManager.instance.url_for(status_reply))
end
it 'renders status with tag' do
expect(response.body).to include(ActivityPub::TagManager.instance.url_for(status_tag))
end end
end end
end end

View File

@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowerAccountsController do
expect(body_as_json.size).to eq 1 expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s expect(body_as_json[0][:id]).to eq alice.id.to_s
end end
context 'when requesting user is blocked' do
before do
account.block!(user.account)
end
it 'hides results' do
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 0
end
end
context 'when requesting user is the account owner' do
let(:user) { Fabricate(:user, account: account) }
it 'returns all accounts, including muted accounts' do
user.account.mute!(bob)
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
end
end end
end end

View File

@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowingAccountsController do
expect(body_as_json.size).to eq 1 expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s expect(body_as_json[0][:id]).to eq alice.id.to_s
end end
context 'when requesting user is blocked' do
before do
account.block!(user.account)
end
it 'hides results' do
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 0
end
end
context 'when requesting user is the account owner' do
let(:user) { Fabricate(:user, account: account) }
it 'returns all accounts, including muted accounts' do
user.account.mute!(bob)
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
end
end end
end end

View File

@ -21,7 +21,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
describe 'POST #create' do describe 'POST #create' do
let(:app) { Fabricate(:application) } let(:app) { Fabricate(:application) }
let(:token) { Doorkeeper::AccessToken.find_or_create_for(app, nil, 'read write', nil, false) } let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
let(:agreement) { nil } let(:agreement) { nil }
before do before do

View File

@ -16,10 +16,16 @@ describe ApplicationController, type: :controller do
end end
shared_examples 'default locale' do shared_examples 'default locale' do
it 'sets available and preferred language' do
request.headers['Accept-Language'] = 'sr-Latn'
get 'success'
expect(response.body).to eq 'sr-Latn'
end
it 'sets available and preferred language' do it 'sets available and preferred language' do
request.headers['Accept-Language'] = 'ca-ES, fa' request.headers['Accept-Language'] = 'ca-ES, fa'
get 'success' get 'success'
expect(response.body).to eq 'fa' expect(response.body).to eq 'ca'
end end
it 'sets available and compatible language if none of available languages are preferred' do it 'sets available and compatible language if none of available languages are preferred' do

View File

@ -41,11 +41,11 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
context 'when app is already authorized' do context 'when app is already authorized' do
before do before do
Doorkeeper::AccessToken.find_or_create_for( Doorkeeper::AccessToken.find_or_create_for(
app, application: app,
user.id, resource_owner: user.id,
app.scopes, scopes: app.scopes,
Doorkeeper.configuration.access_token_expires_in, expires_in: Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled? use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
) )
end end

View File

@ -5,11 +5,12 @@ require 'rails_helper'
RSpec.describe Oauth::TokensController, type: :controller do RSpec.describe Oauth::TokensController, type: :controller do
describe 'POST #revoke' do describe 'POST #revoke' do
let!(:user) { Fabricate(:user) } let!(:user) { Fabricate(:user) }
let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } let!(:application) { Fabricate(:application, confidential: false) }
let!(:access_token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: application) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) } let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
before do before do
post :revoke, params: { token: access_token.token } post :revoke, params: { client_id: application.uid, token: access_token.token }
end end
it 'revokes the token' do it 'revokes the token' do

View File

@ -35,7 +35,7 @@ describe RemoteFollowController do
context 'when webfinger values are wrong' do context 'when webfinger values are wrong' do
it 'renders new when redirect url is nil' do it 'renders new when redirect url is nil' do
resource_with_nil_link = double(link: nil) resource_with_nil_link = double(link: nil)
allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_nil_link) allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_nil_link)
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
expect(response).to render_template(:new) expect(response).to render_template(:new)
@ -45,7 +45,7 @@ describe RemoteFollowController do
it 'renders new when template is nil' do it 'renders new when template is nil' do
link_with_nil_template = double(template: nil) link_with_nil_template = double(template: nil)
resource_with_link = double(link: link_with_nil_template) resource_with_link = double(link: link_with_nil_template)
allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_link) allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
expect(response).to render_template(:new) expect(response).to render_template(:new)
@ -57,7 +57,7 @@ describe RemoteFollowController do
before do before do
link_with_template = double(template: 'http://example.com/follow_me?acct={uri}') link_with_template = double(template: 'http://example.com/follow_me?acct={uri}')
resource_with_link = double(link: link_with_template) resource_with_link = double(link: link_with_template)
allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_return(resource_with_link) allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
end end
@ -79,7 +79,7 @@ describe RemoteFollowController do
end end
it 'renders new with error when goldfinger fails' do it 'renders new with error when goldfinger fails' do
allow(Goldfinger).to receive(:finger).with('acct:user@example.com').and_raise(Goldfinger::Error) allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Goldfinger::Error)
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } } post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
expect(response).to render_template(:new) expect(response).to render_template(:new)
@ -87,7 +87,7 @@ describe RemoteFollowController do
end end
it 'renders new when occur HTTP::ConnectionError' do it 'renders new when occur HTTP::ConnectionError' do
allow(Goldfinger).to receive(:finger).with('acct:user@unknown').and_raise(HTTP::ConnectionError) allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@unknown').and_raise(HTTP::ConnectionError)
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } } post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@unknown' } }
expect(response).to render_template(:new) expect(response).to render_template(:new)

View File

@ -151,7 +151,7 @@ describe Settings::IdentityProofsController do
@proof1 = Fabricate(:account_identity_proof, account: user.account) @proof1 = Fabricate(:account_identity_proof, account: user.account)
@proof2 = Fabricate(:account_identity_proof, account: user.account) @proof2 = Fabricate(:account_identity_proof, account: user.account)
allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') } allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') }
allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) { } allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {}
end end
it 'has the first proof username on the page' do it 'has the first proof username on the page' do
@ -165,4 +165,22 @@ describe Settings::IdentityProofsController do
end end
end end
end end
describe 'DELETE #destroy' do
before do
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
@proof1 = Fabricate(:account_identity_proof, account: user.account)
allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') }
allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {}
delete :destroy, params: { id: @proof1.id }
end
it 'redirects to :index' do
expect(response).to redirect_to settings_identity_proofs_path
end
it 'removes the proof' do
expect(AccountIdentityProof.where(id: @proof1.id).count).to eq 0
end
end
end end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'rails_helper'
describe RSS::Serializer do
describe '#status_title' do
let(:text) { 'This is a toot' }
let(:spoiler) { '' }
let(:sensitive) { false }
let(:reblog) { nil }
let(:account) { Fabricate(:account) }
let(:status) { Fabricate(:status, account: account, text: text, spoiler_text: spoiler, sensitive: sensitive, reblog: reblog) }
subject { RSS::Serializer.new.send(:status_title, status) }
context 'if destroyed?' do
it 'returns "#{account.acct} deleted status"' do
status.destroy!
expect(subject).to eq "#{account.acct} deleted status"
end
end
context 'on a toot with long text' do
let(:text) { "This toot's text is longer than the allowed number of characters" }
it 'truncates toot text appropriately' do
expect(subject).to eq "#{account.acct}: “This toot's text is longer tha…”"
end
end
context 'on a toot with long text with a newline' do
let(:text) { "This toot's text is longer\nthan the allowed number of characters" }
it 'truncates toot text appropriately' do
expect(subject).to eq "#{account.acct}: “This toot's text is longer…”"
end
end
context 'on a toot with a content warning' do
let(:spoiler) { 'long toot' }
it 'displays spoiler text instead of toot content' do
expect(subject).to eq "#{account.acct}: CW “long toot”"
end
end
context 'on a toot with sensitive media' do
let(:sensitive) { true }
it 'displays that the media is sensitive' do
expect(subject).to eq "#{account.acct}: “This is a toot” (sensitive)"
end
end
context 'on a reblog' do
let(:reblog) { Fabricate(:status, text: 'This is a toot') }
it 'display that the toot is a reblog' do
expect(subject).to eq "#{account.acct} boosted #{reblog.account.acct}: “This is a toot”"
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'rails_helper'
describe RelationshipFilter do
let(:account) { Fabricate(:account) }
describe '#results' do
context 'when default params are used' do
let(:subject) do
RelationshipFilter.new(account, 'order' => 'active').results
end
before do
add_following_account_with(last_status_at: 7.days.ago)
add_following_account_with(last_status_at: 1.day.ago)
add_following_account_with(last_status_at: 3.days.ago)
end
it 'returns followings ordered by last activity' do
expected_result = account.following.eager_load(:account_stat).reorder(nil).by_recent_status
expect(subject).to eq expected_result
end
end
end
def add_following_account_with(last_status_at:)
following_account = Fabricate(:account)
Fabricate(:account_stat, account: following_account,
last_status_at: last_status_at,
statuses_count: 1,
following_count: 0,
followers_count: 0)
Fabricate(:follow, account: account, target_account: following_account).account
end
end

View File

@ -82,35 +82,6 @@ RSpec.describe Status, type: :model do
end end
end end
describe '#title' do
# rubocop:disable Style/InterpolationCheck
let(:account) { subject.account }
context 'if destroyed?' do
it 'returns "#{account.acct} deleted status"' do
subject.destroy!
expect(subject.title).to eq "#{account.acct} deleted status"
end
end
context 'unless destroyed?' do
context 'if reblog?' do
it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do
reblog = subject.reblog = other
expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}"
end
end
context 'unless reblog?' do
it 'returns "New status by #{account.acct}"' do
subject.reblog = nil
expect(subject.title).to eq "New status by #{account.acct}"
end
end
end
end
describe '#hidden?' do describe '#hidden?' do
context 'if private_visibility?' do context 'if private_visibility?' do
it 'returns true' do it 'returns true' do
@ -490,6 +461,33 @@ RSpec.describe Status, type: :model do
end end
end end
context 'with a remote_only option set' do
let!(:local_account) { Fabricate(:account, domain: nil) }
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
let!(:local_status) { Fabricate(:status, account: local_account) }
let!(:remote_status) { Fabricate(:status, account: remote_account) }
subject { Status.as_public_timeline(viewer, :remote) }
context 'without a viewer' do
let(:viewer) { nil }
it 'does not include local instances statuses' do
expect(subject).not_to include(local_status)
expect(subject).to include(remote_status)
end
end
context 'with a viewer' do
let(:viewer) { Fabricate(:account, username: 'viewer') }
it 'does not include local instances statuses' do
expect(subject).not_to include(local_status)
expect(subject).to include(remote_status)
end
end
end
describe 'with an account passed in' do describe 'with an account passed in' do
before do before do
@account = Fabricate(:account) @account = Fabricate(:account)

View File

@ -266,6 +266,8 @@ const startWorker = (workerId) => {
'public:media', 'public:media',
'public:local', 'public:local',
'public:local:media', 'public:local:media',
'public:remote',
'public:remote:media',
'hashtag', 'hashtag',
'hashtag:local', 'hashtag:local',
]; ];
@ -297,6 +299,7 @@ const startWorker = (workerId) => {
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
'/api/v1/streaming/public', '/api/v1/streaming/public',
'/api/v1/streaming/public/local', '/api/v1/streaming/public/local',
'/api/v1/streaming/public/remote',
'/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag',
'/api/v1/streaming/hashtag/local', '/api/v1/streaming/hashtag/local',
]; ];
@ -541,6 +544,13 @@ const startWorker = (workerId) => {
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true);
}); });
app.get('/api/v1/streaming/public/remote', (req, res) => {
const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true';
const channel = onlyMedia ? 'timeline:public:remote:media' : 'timeline:public:remote';
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true);
});
app.get('/api/v1/streaming/direct', (req, res) => { app.get('/api/v1/streaming/direct', (req, res) => {
const channel = `timeline:direct:${req.accountId}`; const channel = `timeline:direct:${req.accountId}`;
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true);
@ -605,12 +615,18 @@ const startWorker = (workerId) => {
case 'public:local': case 'public:local':
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:remote':
streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'public:media': case 'public:media':
streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:local:media': case 'public:local:media':
streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:remote:media':
streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'direct': case 'direct':
channel = `timeline:direct:${req.accountId}`; channel = `timeline:direct:${req.accountId}`;
streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true);

Some files were not shown because too many files have changed in this diff Show More