forked from treehouse/mastodon
Merge pull request #1327 from ThibG/glitch-soc/merge-upstream
Merge upstream changesrebase/4.0.0rc2
commit
e1d2820234
|
@ -30,7 +30,7 @@ plugins:
|
|||
channel: eslint-6
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-0-76
|
||||
channel: rubocop-0-82
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
|
|
|
@ -33,7 +33,7 @@ LOCAL_DOMAIN=example.com
|
|||
# ALTERNATE_DOMAINS=example1.com,example2.com
|
||||
|
||||
# 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=
|
||||
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
|
||||
# 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
|
||||
VAPID_PRIVATE_KEY=
|
||||
|
|
|
@ -2,7 +2,7 @@ require:
|
|||
- rubocop-rails
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
TargetRubyVersion: 2.4
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'db/**/*'
|
||||
|
@ -46,7 +46,7 @@ Metrics/ClassLength:
|
|||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
AllowURI: true
|
||||
Enabled: false
|
||||
|
||||
|
|
24
Gemfile
24
Gemfile
|
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.4'
|
||||
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-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -49,7 +49,7 @@ gem 'omniauth-saml', '~> 1.10'
|
|||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.3'
|
||||
gem 'doorkeeper', '~> 5.4'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
|
@ -57,12 +57,12 @@ gem 'hiredis', '~> 0.6'
|
|||
gem 'redis-namespace', '~> 1.7'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 4.3'
|
||||
gem 'http', '~> 4.4'
|
||||
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 'httplog', '~> 1.4.2'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.1'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
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 'pundit', '~> 2.1'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.2'
|
||||
gem 'rack-attack', '~> 6.3'
|
||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
|
@ -96,8 +96,8 @@ gem 'strong_migrations', '~> 0.6'
|
|||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.21', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2019'
|
||||
gem 'webpacker', '~> 4.2'
|
||||
gem 'tzinfo-data', '~> 1.2020'
|
||||
gem 'webpacker', '~> 5.1'
|
||||
gem 'webpush'
|
||||
|
||||
gem 'json-ld'
|
||||
|
@ -110,7 +110,7 @@ group :development, :test do
|
|||
gem 'fabrication', '~> 2.21'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.8'
|
||||
gem 'pry-byebug', '~> 3.9'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 4.0'
|
||||
end
|
||||
|
@ -120,7 +120,7 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.31'
|
||||
gem 'capybara', '~> 3.32'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.11'
|
||||
gem 'microformats', '~> 4.2'
|
||||
|
@ -135,18 +135,18 @@ end
|
|||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'better_errors', '~> 2.6'
|
||||
gem 'better_errors', '~> 2.7'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.79', require: false
|
||||
gem 'rubocop', '~> 0.82', require: false
|
||||
gem 'rubocop-rails', '~> 2.5', require: false
|
||||
gem 'brakeman', '~> 4.8', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.13'
|
||||
gem 'capistrano', '~> 3.14'
|
||||
gem 'capistrano-rails', '~> 1.4'
|
||||
gem 'capistrano-rbenv', '~> 2.1'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
|
154
Gemfile.lock
154
Gemfile.lock
|
@ -92,23 +92,23 @@ GEM
|
|||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.303.0)
|
||||
aws-sdk-core (3.94.0)
|
||||
aws-partitions (1.312.0)
|
||||
aws-sdk-core (3.95.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.30.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.2)
|
||||
aws-sigv4 (1.1.3)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
bcrypt (3.1.13)
|
||||
better_errors (2.6.0)
|
||||
better_errors (2.7.0)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
@ -118,8 +118,8 @@ GEM
|
|||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.0)
|
||||
browser (4.0.0)
|
||||
brakeman (4.8.1)
|
||||
browser (4.1.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -127,8 +127,8 @@ GEM
|
|||
bundler-audit (0.6.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.1.1)
|
||||
capistrano (3.13.0)
|
||||
byebug (11.1.3)
|
||||
capistrano (3.14.0)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
|
@ -143,7 +143,7 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.31.0)
|
||||
capybara (3.32.1)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -194,7 +194,7 @@ GEM
|
|||
docile (1.3.2)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.3.1)
|
||||
doorkeeper (5.4.0)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
|
@ -213,7 +213,7 @@ GEM
|
|||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
erubi (1.9.0)
|
||||
et-orbi (1.2.3)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
excon (0.73.0)
|
||||
fabrication (2.21.1)
|
||||
|
@ -240,7 +240,7 @@ GEM
|
|||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.2.5)
|
||||
fugit (1.3.3)
|
||||
fugit (1.3.5)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.1)
|
||||
fuubar (2.5.0)
|
||||
|
@ -270,7 +270,7 @@ GEM
|
|||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (4.3.0)
|
||||
http (4.4.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
|
@ -303,7 +303,7 @@ GEM
|
|||
jmespath (1.4.0)
|
||||
json (2.3.0)
|
||||
json-canonicalization (0.2.0)
|
||||
json-ld (3.1.3)
|
||||
json-ld (3.1.4)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
|
@ -314,21 +314,21 @@ GEM
|
|||
json-ld (~> 3.1)
|
||||
rdf (~> 3.1)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.1.0)
|
||||
kaminari (1.1.1)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.0)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
kaminari-activerecord (= 1.1.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-actionview (1.1.1)
|
||||
kaminari-actionview (= 1.2.0)
|
||||
kaminari-activerecord (= 1.2.0)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-actionview (1.2.0)
|
||||
actionview
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-activerecord (1.1.1)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-activerecord (1.2.0)
|
||||
activerecord
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-core (1.1.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
kaminari-core (= 1.2.0)
|
||||
kaminari-core (1.2.0)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.4.0)
|
||||
|
@ -353,14 +353,14 @@ GEM
|
|||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
memory_profiler (0.9.14)
|
||||
method_source (0.9.2)
|
||||
method_source (1.0.0)
|
||||
microformats (4.2.0)
|
||||
json (~> 2.2)
|
||||
nokogiri (~> 1.10)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2020.0425)
|
||||
mimemagic (0.3.4)
|
||||
mimemagic (0.3.5)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.0)
|
||||
|
@ -369,9 +369,9 @@ GEM
|
|||
multipart-post (2.1.1)
|
||||
necromancer (0.5.1)
|
||||
net-ldap (0.16.2)
|
||||
net-scp (2.0.0)
|
||||
net-ssh (>= 2.6.5, < 6.0.0)
|
||||
net-ssh (5.2.0)
|
||||
net-scp (3.0.0)
|
||||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.0.2)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.9)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
|
@ -407,40 +407,40 @@ GEM
|
|||
parallel (1.19.1)
|
||||
parallel_tests (2.32.0)
|
||||
parallel
|
||||
parser (2.7.1.1)
|
||||
parser (2.7.1.2)
|
||||
ast (~> 2.4.0)
|
||||
parslet (2.0.0)
|
||||
pastel (0.7.3)
|
||||
pastel (0.7.4)
|
||||
equatable (~> 0.6)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.2.3)
|
||||
pghero (2.4.1)
|
||||
pghero (2.4.2)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.4.1)
|
||||
premailer (1.11.1)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.10.3)
|
||||
premailer-rails (1.11.1)
|
||||
actionmailer (>= 3)
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-byebug (3.8.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-byebug (3.9.0)
|
||||
byebug (~> 11.0)
|
||||
pry (~> 0.10)
|
||||
pry (~> 0.13.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.4)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
raabro (1.3.1)
|
||||
rack (2.2.2)
|
||||
rack-attack (6.2.2)
|
||||
rack-attack (6.3.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
|
@ -491,13 +491,13 @@ GEM
|
|||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redcarpet (3.5.0)
|
||||
redis (4.1.3)
|
||||
redis (4.1.4)
|
||||
redis-actionpack (5.2.0)
|
||||
actionpack (>= 5, < 7)
|
||||
redis-rack (>= 2.1.0, < 3)
|
||||
redis-store (>= 1.1.0, < 2)
|
||||
redis-activesupport (5.0.4)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-activesupport (5.2.0)
|
||||
activesupport (>= 3, < 7)
|
||||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.7.0)
|
||||
redis (>= 3.0.4)
|
||||
|
@ -516,15 +516,16 @@ GEM
|
|||
responders (3.0.0)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.4)
|
||||
rotp (2.1.2)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec-core (3.9.1)
|
||||
rspec-support (~> 3.9.1)
|
||||
rspec-expectations (3.9.1)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
|
@ -541,16 +542,17 @@ GEM
|
|||
rspec-sidekiq (3.0.3)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.9.2)
|
||||
rspec-support (3.9.3)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (0.79.0)
|
||||
rubocop (0.82.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
rexml
|
||||
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)
|
||||
activesupport
|
||||
rack (>= 1.1)
|
||||
|
@ -565,9 +567,10 @@ GEM
|
|||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
sidekiq (6.0.4)
|
||||
semantic_range (2.3.0)
|
||||
sidekiq (6.0.7)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (>= 2.0.0)
|
||||
rack (~> 2.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
redis (>= 4.1.0)
|
||||
sidekiq-bulk (0.2.0)
|
||||
|
@ -607,7 +610,7 @@ GEM
|
|||
stoplight (2.2.0)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.6.2)
|
||||
strong_migrations (0.6.6)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
|
@ -635,12 +638,12 @@ GEM
|
|||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.1)
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
|
@ -648,10 +651,11 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (4.2.2)
|
||||
activesupport (>= 4.2)
|
||||
webpacker (5.1.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
|
@ -670,8 +674,8 @@ DEPENDENCIES
|
|||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.63)
|
||||
better_errors (~> 2.6)
|
||||
aws-sdk-s3 (~> 1.64)
|
||||
better_errors (~> 2.7)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
|
@ -679,11 +683,11 @@ DEPENDENCIES
|
|||
browser
|
||||
bullet (~> 6.1)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.13)
|
||||
capistrano (~> 3.14)
|
||||
capistrano-rails (~> 1.4)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.31)
|
||||
capybara (~> 3.32)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 5.1)
|
||||
cld3 (~> 3.3.0)
|
||||
|
@ -694,7 +698,7 @@ DEPENDENCIES
|
|||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.2)
|
||||
doorkeeper (~> 5.3)
|
||||
doorkeeper (~> 5.4)
|
||||
dotenv-rails (~> 2.7)
|
||||
e2mmap (~> 0.1.0)
|
||||
fabrication (~> 2.21)
|
||||
|
@ -709,7 +713,7 @@ DEPENDENCIES
|
|||
health_check!
|
||||
hiredis (~> 0.6)
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 4.3)
|
||||
http (~> 4.4)
|
||||
http_accept_language (~> 2.1)
|
||||
http_parser.rb (~> 0.6)!
|
||||
httplog (~> 1.4.2)
|
||||
|
@ -718,7 +722,7 @@ DEPENDENCIES
|
|||
iso-639
|
||||
json-ld
|
||||
json-ld-preloaded (~> 3.1)
|
||||
kaminari (~> 1.1)
|
||||
kaminari (~> 1.2)
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.4)
|
||||
link_header (~> 0.0)
|
||||
|
@ -748,12 +752,12 @@ DEPENDENCIES
|
|||
posix-spawn!
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.8)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.3)
|
||||
pundit (~> 2.1)
|
||||
rack (~> 2.2.2)
|
||||
rack-attack (~> 6.2)
|
||||
rack-attack (~> 6.3)
|
||||
rack-cors (~> 1.1)
|
||||
rails (~> 5.2.4.2)
|
||||
rails-controller-testing (~> 1.0)
|
||||
|
@ -768,7 +772,7 @@ DEPENDENCIES
|
|||
rspec-rails (~> 4.0)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 0.79)
|
||||
rubocop (~> 0.82)
|
||||
rubocop-rails (~> 2.5)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
|
@ -790,7 +794,7 @@ DEPENDENCIES
|
|||
tty-command (~> 0.9)
|
||||
tty-prompt (~> 0.21)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
tzinfo-data (~> 1.2020)
|
||||
webmock (~> 3.8)
|
||||
webpacker (~> 4.2)
|
||||
webpacker (~> 5.1)
|
||||
webpush
|
||||
|
|
|
@ -41,7 +41,7 @@ class AccountsController < ApplicationController
|
|||
format.rss do
|
||||
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)
|
||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
||||
end
|
||||
|
@ -130,11 +130,11 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def media_requested?
|
||||
request.path.ends_with?('/media') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/media') && !tag_requested?
|
||||
end
|
||||
|
||||
def replies_requested?
|
||||
request.path.ends_with?('/with_replies') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
|
||||
end
|
||||
|
||||
def tag_requested?
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def next_path
|
||||
|
|
|
@ -113,6 +113,13 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
render :two_factor
|
||||
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
|
||||
|
||||
def set_pack
|
||||
|
|
|
@ -28,18 +28,6 @@ module Localized
|
|||
end
|
||||
|
||||
def request_locale
|
||||
preferred_locale || compatible_locale
|
||||
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
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,8 +22,7 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
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?
|
||||
redirect_to @proof.on_success_path(params[:user_agent])
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
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
|
||||
|
||||
def check_enabled
|
||||
|
|
|
@ -7,13 +7,13 @@ module HomeHelper
|
|||
}
|
||||
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__wrapper') do
|
||||
section = if account.nil?
|
||||
content_tag(:div, class: 'account__display-name') 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 +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:strong, t('about.contact_missing')) +
|
||||
|
@ -23,7 +23,7 @@ module HomeHelper
|
|||
else
|
||||
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', 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 +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:bdi) do
|
||||
|
|
|
@ -68,6 +68,7 @@ module SettingsHelper
|
|||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
|
|
|
@ -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
|
|
@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
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 connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
|
|
@ -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 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 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 });
|
||||
|
|
|
@ -186,11 +186,13 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { 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(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
|
|
|
@ -28,6 +28,7 @@ class Option extends React.PureComponent {
|
|||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
|
@ -58,7 +59,7 @@ class Option extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
const { isPollMultiple, title, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
@ -75,6 +76,7 @@ class Option extends React.PureComponent {
|
|||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
@ -125,10 +127,12 @@ class PollForm extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<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 && (
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input')} style={{ opacity: 0 }} />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
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 { changeColumnParams } from 'flavours/glitch/actions/columns';
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
const columns = state.getIn(['settings', 'columns']);
|
||||
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 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' : ''}`]);
|
||||
|
||||
return {
|
||||
hasUnread: !!timelineState && timelineState.get('unread') > 0,
|
||||
onlyMedia,
|
||||
onlyRemote,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -46,15 +48,16 @@ class PublicTimeline extends React.PureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
onlyRemote: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch, onlyMedia } = this.props;
|
||||
const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
|
||||
dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,19 +71,19 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,13 +99,13 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
|
||||
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
|
@ -121,7 +124,7 @@ class PublicTimeline extends React.PureComponent {
|
|||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
||||
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`public_timeline-${columnId}`}
|
||||
|
|
|
@ -37,6 +37,7 @@ const componentMap = {
|
|||
'HOME': HomeTimeline,
|
||||
'NOTIFICATIONS': Notifications,
|
||||
'PUBLIC': PublicTimeline,
|
||||
'REMOTE': PublicTimeline,
|
||||
'COMMUNITY': CommunityTimeline,
|
||||
'HASHTAG': HashtagTimeline,
|
||||
'DIRECT': DirectTimeline,
|
||||
|
|
|
@ -545,13 +545,6 @@ $small-breakpoint: 960px;
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
@include avatar-size(44px);
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 15px;
|
||||
|
||||
|
@ -752,12 +745,6 @@ $small-breakpoint: 960px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
&__counters__wrapper {
|
||||
|
|
|
@ -38,9 +38,14 @@
|
|||
|
||||
.account__avatar {
|
||||
@include avatar-radius();
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-size: 36px 36px;
|
||||
|
||||
&-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -145,6 +145,11 @@
|
|||
&__avatar {
|
||||
left: 15px;
|
||||
top: 17px;
|
||||
|
||||
.account__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
|
|
@ -93,12 +93,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.trends__item {
|
||||
|
|
|
@ -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 |
|
@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
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 connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
|
|
@ -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 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 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 });
|
||||
|
|
|
@ -192,11 +192,13 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { 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(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
|
|
|
@ -27,6 +27,7 @@ class Option extends React.PureComponent {
|
|||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
|
@ -71,7 +72,7 @@ class Option extends React.PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, index, intl } = this.props;
|
||||
const { isPollMultiple, title, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
@ -96,6 +97,7 @@ class Option extends React.PureComponent {
|
|||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
@ -146,10 +148,12 @@ class PollForm extends ImmutablePureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<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>
|
||||
|
||||
<div className='poll__footer'>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
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 { changeColumnParams } from '../../../actions/columns';
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
|
|||
const columns = state.getIn(['settings', 'columns']);
|
||||
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 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' : ''}`]);
|
||||
|
||||
return {
|
||||
hasUnread: !!timelineState && timelineState.get('unread') > 0,
|
||||
onlyMedia,
|
||||
onlyRemote,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
onlyRemote: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch, onlyMedia } = this.props;
|
||||
const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
|
||||
dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
const { dispatch, onlyMedia, onlyRemote } = this.props;
|
||||
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
|
||||
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
|
@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent {
|
|||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
timelineId={`public${onlyMedia ? ':media' : ''}`}
|
||||
timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`public_timeline-${columnId}`}
|
||||
|
|
|
@ -5,30 +5,21 @@ import ColumnHeader from '../column_header';
|
|||
|
||||
describe('<Column />', () => {
|
||||
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', () => {
|
||||
const wrapper = mount(
|
||||
<Column heading='notifications'>
|
||||
<div className='scrollable' />
|
||||
</Column>,
|
||||
);
|
||||
const scrollToMock = jest.fn();
|
||||
wrapper.find(Column).find('.scrollable').getDOMNode().scrollTo = scrollToMock;
|
||||
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', () => {
|
||||
const wrapper = mount(<Column heading='notifications' />);
|
||||
wrapper.find(ColumnHeader).find('button').simulate('click');
|
||||
expect(global.requestAnimationFrame.mock.calls.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ const componentMap = {
|
|||
'HOME': HomeTimeline,
|
||||
'NOTIFICATIONS': Notifications,
|
||||
'PUBLIC': PublicTimeline,
|
||||
'REMOTE': PublicTimeline,
|
||||
'COMMUNITY': CommunityTimeline,
|
||||
'HASHTAG': HashtagTimeline,
|
||||
'DIRECT': DirectTimeline,
|
||||
|
|
|
@ -543,12 +543,6 @@ $small-breakpoint: 960px;
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 15px;
|
||||
|
||||
|
@ -749,12 +743,6 @@ $small-breakpoint: 960px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
&__counters__wrapper {
|
||||
|
|
|
@ -1318,8 +1318,13 @@
|
|||
|
||||
.account__avatar {
|
||||
@include avatar-radius;
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-size: 36px 36px;
|
||||
|
||||
&-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -149,6 +149,11 @@
|
|||
&__avatar {
|
||||
left: 15px;
|
||||
top: 17px;
|
||||
|
||||
.account__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
|
|
@ -93,12 +93,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background-size: 44px 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.trends__item {
|
||||
|
|
|
@ -22,7 +22,12 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def brand_color
|
||||
|
|
|
@ -73,8 +73,6 @@ class Request
|
|||
response.body_with_limit if http_client.persistent?
|
||||
|
||||
yield response if block_given?
|
||||
rescue => e
|
||||
raise e.class, e.message, e.backtrace[0]
|
||||
ensure
|
||||
http_client.close unless http_client.persistent?
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -1,13 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SidekiqErrorHandler
|
||||
BACKTRACE_LIMIT = 3
|
||||
|
||||
def call(*)
|
||||
yield
|
||||
rescue Mastodon::HostValidationError
|
||||
# Do not retry
|
||||
rescue => e
|
||||
limit_backtrace_and_raise(e)
|
||||
ensure
|
||||
socket = Thread.current[:statsd_socket]
|
||||
socket&.close
|
||||
Thread.current[:statsd_socket] = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def limit_backtrace_and_raise(e)
|
||||
e.set_backtrace(e.backtrace.first(BACKTRACE_LIMIT))
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ class RelationshipFilter
|
|||
scope = scope_for('relationship', params['relationship'].to_s.strip)
|
||||
|
||||
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?
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class RemoteFollow
|
||||
include ActiveModel::Validations
|
||||
include RoutingHelper
|
||||
include WebfingerHelper
|
||||
|
||||
attr_accessor :acct, :addressable_template
|
||||
|
||||
|
@ -71,7 +72,7 @@ class RemoteFollow
|
|||
end
|
||||
|
||||
def acct_resource
|
||||
@acct_resource ||= Goldfinger.finger("acct:#{acct}")
|
||||
@acct_resource ||= webfinger!("acct:#{acct}")
|
||||
rescue Goldfinger::Error, HTTP::ConnectionError
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -203,14 +203,6 @@ class Status < ApplicationRecord
|
|||
preview_cards.first
|
||||
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?
|
||||
!distributable?
|
||||
end
|
||||
|
@ -342,7 +334,7 @@ class Status < ApplicationRecord
|
|||
query = timeline_scope(local_only)
|
||||
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
|
||||
|
||||
def as_tag_timeline(tag, account = nil, local_only = false)
|
||||
|
@ -434,8 +426,15 @@ class Status < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def timeline_scope(local_only = false)
|
||||
starting_scope = local_only ? Status.local : Status
|
||||
def timeline_scope(scope = false)
|
||||
starting_scope = case scope
|
||||
when :local, true
|
||||
Status.local
|
||||
when :remote
|
||||
Status.remote
|
||||
else
|
||||
Status
|
||||
end
|
||||
starting_scope = starting_scope.with_public_visibility
|
||||
if Setting.show_reblogs_in_public_timelines
|
||||
starting_scope
|
||||
|
|
|
@ -94,11 +94,11 @@ class Web::PushSubscription < ApplicationRecord
|
|||
|
||||
def find_or_create_access_token
|
||||
Doorkeeper::AccessToken.find_or_create_for(
|
||||
Doorkeeper::Application.find_by(superapp: true),
|
||||
session_activation.user_id,
|
||||
Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
|
||||
Doorkeeper.configuration.access_token_expires_in,
|
||||
Doorkeeper.configuration.refresh_token_enabled?
|
||||
application: Doorkeeper::Application.find_by(superapp: true),
|
||||
resource_owner: session_activation.user_id,
|
||||
scopes: Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
|
||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class OEmbedSerializer < ActiveModel::Serializer
|
|||
include RoutingHelper
|
||||
include ActionView::Helpers::TagHelper
|
||||
|
||||
attributes :type, :version, :title, :author_name,
|
||||
attributes :type, :version, :author_name,
|
||||
:author_url, :provider_name, :provider_url,
|
||||
:cache_age, :html, :width, :height
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RSS::AccountSerializer
|
||||
class RSS::AccountSerializer < RSS::Serializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include AccountsHelper
|
||||
include RoutingHelper
|
||||
|
@ -17,18 +17,7 @@ class RSS::AccountSerializer
|
|||
builder.image(full_asset_url(account.avatar.url(:original))) if account.avatar?
|
||||
builder.cover(full_asset_url(account.header.url(:original))) if account.header?
|
||||
|
||||
statuses.each do |status|
|
||||
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
|
||||
render_statuses(builder, statuses)
|
||||
|
||||
builder.to_xml
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RSS::TagSerializer
|
||||
class RSS::TagSerializer < RSS::Serializer
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
include RoutingHelper
|
||||
|
@ -14,18 +14,7 @@ class RSS::TagSerializer
|
|||
.logo(full_pack_url('media/images/logo.svg'))
|
||||
.accent_color('2b90d9')
|
||||
|
||||
statuses.each do |status|
|
||||
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
|
||||
render_statuses(builder, statuses)
|
||||
|
||||
builder.to_xml
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ActivityPub::FetchRemoteAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
include DomainControlHelper
|
||||
include WebfingerHelper
|
||||
|
||||
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
|
||||
|
||||
|
@ -35,12 +36,12 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||
private
|
||||
|
||||
def verified_webfinger?
|
||||
webfinger = Goldfinger.finger("acct:#{@username}@#{@domain}")
|
||||
webfinger = webfinger!("acct:#{@username}@#{@domain}")
|
||||
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?
|
||||
|
||||
webfinger = Goldfinger.finger("acct:#{confirmed_username}@#{confirmed_domain}")
|
||||
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
|
||||
@username, @domain = split_acct(webfinger.subject)
|
||||
self_reference = webfinger.link('self')
|
||||
|
||||
|
|
|
@ -73,11 +73,18 @@ class BatchedRemoveStatusService < BaseService
|
|||
|
||||
redis.pipelined do
|
||||
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?
|
||||
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
|
||||
|
||||
@tags[status.id].each do |hashtag|
|
||||
|
|
|
@ -86,14 +86,22 @@ class FanOutOnWriteService < BaseService
|
|||
Rails.logger.debug "Delivering status #{status.id} to public timeline"
|
||||
|
||||
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
|
||||
|
||||
def deliver_to_media(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to media timeline"
|
||||
|
||||
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
|
||||
|
||||
def deliver_to_direct_timelines(status)
|
||||
|
|
|
@ -142,14 +142,22 @@ class RemoveStatusService < BaseService
|
|||
return unless @status.public_visibility?
|
||||
|
||||
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
|
||||
|
||||
def remove_from_media
|
||||
return unless @status.public_visibility?
|
||||
|
||||
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
|
||||
|
||||
def remove_from_direct
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ResolveAccountService < BaseService
|
||||
include JsonLdHelper
|
||||
include DomainControlHelper
|
||||
include WebfingerHelper
|
||||
|
||||
class WebfingerRedirectError < StandardError; end
|
||||
|
||||
|
@ -76,7 +77,7 @@ class ResolveAccountService < BaseService
|
|||
end
|
||||
|
||||
def process_webfinger!(uri, redirected = false)
|
||||
@webfinger = Goldfinger.finger("acct:#{uri}")
|
||||
@webfinger = webfinger!("acct:#{uri}")
|
||||
confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@')
|
||||
|
||||
if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
.avatar-stack
|
||||
- @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
|
||||
.directory__tag
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
- target_account = reports.first.target_account
|
||||
.report-card
|
||||
.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
|
||||
= link_to t('admin.reports.account.notes', count: target_account.targeted_moderation_notes.count), admin_account_path(target_account.id)
|
||||
%br/
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
.directory__card__bar
|
||||
= link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
|
||||
.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
|
||||
%bdi
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
= javascript_pack_tag "locales/#{@theme[:flavour]}/en", integrity: true, crossorigin: 'anonymous'
|
||||
= csrf_meta_tags
|
||||
|
||||
= stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
|
||||
|
||||
= yield :header_tags
|
||||
|
||||
-# These must come after :header_tags to ensure our initial state has been defined.
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
|
||||
%td
|
||||
= 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') }
|
||||
|
|
|
@ -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
|
||||
.detailed-status__display-avatar
|
||||
- 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
|
||||
= 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
|
||||
%bdi
|
||||
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
.status__avatar
|
||||
%div
|
||||
- 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
|
||||
= 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
|
||||
%bdi
|
||||
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)
|
||||
|
|
|
@ -52,13 +52,9 @@ class ActivityPub::DeliveryWorker
|
|||
end
|
||||
end
|
||||
|
||||
begin
|
||||
light.with_threshold(STOPLIGHT_FAILURE_THRESHOLD)
|
||||
.with_cool_off_time(STOPLIGHT_COOLDOWN)
|
||||
.run
|
||||
rescue Stoplight::Error::RedLight => e
|
||||
raise e.class, e.message, e.backtrace.first(3)
|
||||
end
|
||||
end
|
||||
|
||||
def failure_tracker
|
||||
|
|
|
@ -11,7 +11,7 @@ class RedownloadMediaWorker
|
|||
|
||||
return if media_attachment.remote_url.blank?
|
||||
|
||||
media_attachment.reset_file!
|
||||
media_attachment.file_remote_url = media_attachment.remote_url
|
||||
media_attachment.save
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
|
|
|
@ -55,8 +55,8 @@ module Mastodon
|
|||
:el,
|
||||
:en,
|
||||
:eo,
|
||||
:'es-AR',
|
||||
:es,
|
||||
:'es-AR',
|
||||
:et,
|
||||
:eu,
|
||||
:fa,
|
||||
|
@ -97,8 +97,8 @@ module Mastodon
|
|||
:sk,
|
||||
:sl,
|
||||
:sq,
|
||||
:'sr-Latn',
|
||||
:sr,
|
||||
:'sr-Latn',
|
||||
:sv,
|
||||
:ta,
|
||||
:te,
|
||||
|
@ -106,6 +106,7 @@ module Mastodon
|
|||
:tr,
|
||||
:uk,
|
||||
:ur,
|
||||
:vi,
|
||||
:'zh-CN',
|
||||
:'zh-HK',
|
||||
:'zh-TW',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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 :branch, ENV.fetch('BRANCH', 'master')
|
||||
|
@ -12,3 +12,21 @@ set :migration_role, :app
|
|||
|
||||
append :linked_files, '.env.production', 'public/robots.txt'
|
||||
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'
|
||||
|
|
|
@ -34,7 +34,7 @@ if Rails.env.production?
|
|||
p.script_src :self, assets_host
|
||||
p.font_src :self, assets_host
|
||||
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.frame_src :self, :https
|
||||
p.child_src :self, :blob, assets_host
|
||||
|
@ -48,3 +48,8 @@ end
|
|||
# For further information see the following documentation:
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
|
||||
# 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
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
Rails.application.configure do
|
||||
config.x.http_client_proxy = {}
|
||||
|
||||
if ENV['http_proxy'].present?
|
||||
proxy = URI.parse(ENV['http_proxy'])
|
||||
|
||||
raise "Unsupported proxy type: #{proxy.scheme}" unless %w(http https).include? proxy.scheme
|
||||
raise "No proxy host" unless proxy.host
|
||||
|
||||
host = proxy.host
|
||||
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
|
||||
|
||||
config.x.access_to_hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
||||
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
|
||||
|
|
|
@ -19,4 +19,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
inflect.acronym 'ActivityStreams'
|
||||
inflect.acronym 'JsonLd'
|
||||
inflect.acronym 'NodeInfo'
|
||||
|
||||
inflect.singular 'data', 'data'
|
||||
end
|
||||
|
|
|
@ -858,12 +858,14 @@ en:
|
|||
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.
|
||||
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}.
|
||||
identity: Identity
|
||||
inactive: Inactive
|
||||
publicize_checkbox: 'And toot this:'
|
||||
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
|
||||
view_proof: View proof
|
||||
imports:
|
||||
|
@ -918,7 +920,7 @@ en:
|
|||
cancelled_msg: Successfully cancelled the redirect.
|
||||
errors:
|
||||
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
|
||||
not_found: could not be found
|
||||
on_cooldown: You are on cooldown
|
||||
|
|
|
@ -38,4 +38,4 @@ databases:
|
|||
# aws_secret_access_key: ...
|
||||
# aws_region: us-east-1
|
||||
|
||||
override_csp: true
|
||||
override_csp: false
|
||||
|
|
|
@ -130,7 +130,7 @@ Rails.application.routes.draw do
|
|||
resource :confirmation, only: [:new, :create]
|
||||
end
|
||||
|
||||
resources :identity_proofs, only: [:index, :show, :new, :create, :update]
|
||||
resources :identity_proofs, only: [:index, :new, :create, :destroy]
|
||||
|
||||
resources :applications, except: [:edit] do
|
||||
member do
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class AddInviteIdToUsers < ActiveRecord::Migration[5.1]
|
||||
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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class AddAssignedAccountIdToReports < ActiveRecord::Migration[5.1]
|
||||
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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class AddAccessTokenIdToWebPushSubscriptions < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
safety_assured do
|
||||
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
|
||||
|
|
|
@ -2,7 +2,7 @@ class AddCreatedByApplicationIdToUsers < ActiveRecord::Migration[5.2]
|
|||
disable_ddl_transaction!
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ class AddScheduledStatusIdToMediaAttachments < ActiveRecord::Migration[5.2]
|
|||
disable_ddl_transaction!
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class AddParentIdToEmailDomainBlocks < ActiveRecord::Migration[5.2]
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
|
|
|
@ -144,7 +144,14 @@ module Mastodon
|
|||
begin
|
||||
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
|
||||
removed += 1
|
||||
|
|
|
@ -121,7 +121,7 @@ module Mastodon
|
|||
FileUtils.mv(previous_path, upgraded_path)
|
||||
|
||||
begin
|
||||
FileUtils.rmdir(previous_path, parents: true)
|
||||
FileUtils.rmdir(File.dirname(previous_path), parents: true)
|
||||
rescue Errno::ENOTEMPTY
|
||||
# OK
|
||||
end
|
||||
|
@ -131,7 +131,7 @@ module Mastodon
|
|||
|
||||
unless dry_run?
|
||||
begin
|
||||
FileUtils.rmdir(upgraded_path, parents: true)
|
||||
FileUtils.rmdir(File.dirname(upgraded_path), parents: true)
|
||||
rescue Errno::ENOTEMPTY
|
||||
# OK
|
||||
end
|
||||
|
|
26
package.json
26
package.json
|
@ -60,17 +60,17 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.3",
|
||||
"@babel/plugin-transform-react-inline-elements": "^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/runtime": "^7.8.4",
|
||||
"@clusterws/cws": "^0.17.3",
|
||||
"@clusterws/cws": "^2.0.0",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@rails/ujs": "^6.0.2",
|
||||
"@rails/ujs": "^6.0.3",
|
||||
"array-includes": "^3.1.1",
|
||||
"arrow-key-navigation": "^1.1.0",
|
||||
"atrament": "0.2.4",
|
||||
|
@ -79,7 +79,7 @@
|
|||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"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-runtime": "^6.26.0",
|
||||
"blurhash": "^1.1.3",
|
||||
|
@ -128,7 +128,7 @@
|
|||
"prop-types": "^15.5.10",
|
||||
"punycode": "^2.1.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
"react-immutable-proptypes": "^2.2.0",
|
||||
"react-immutable-pure-component": "^1.1.1",
|
||||
|
@ -159,15 +159,15 @@
|
|||
"stacktrace-js": "^2.0.2",
|
||||
"stringz": "^2.1.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
"terser-webpack-plugin": "^2.3.5",
|
||||
"terser-webpack-plugin": "^3.0.1",
|
||||
"tesseract.js": "^2.0.0-alpha.16",
|
||||
"throng": "^4.0.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uuid": "^7.0.3",
|
||||
"wavesurfer.js": "^3.3.1",
|
||||
"webpack": "^4.42.1",
|
||||
"uuid": "^8.0.0",
|
||||
"wavesurfer.js": "^3.3.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-bundle-analyzer": "^3.7.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"wicg-inert": "^3.0.2"
|
||||
|
@ -182,10 +182,10 @@
|
|||
"eslint-plugin-jsx-a11y": "~6.2.3",
|
||||
"eslint-plugin-promise": "~4.2.1",
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest": "^25.4.0",
|
||||
"raf": "^3.4.1",
|
||||
"react-intl-translations-manager": "^5.0.3",
|
||||
"react-test-renderer": "^16.13.0",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"sass-lint": "^1.13.1",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"yargs": "^15.3.1"
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -3,108 +3,608 @@ require 'rails_helper'
|
|||
RSpec.describe AccountsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:alice) { Fabricate(:account, username: 'alice', user: Fabricate(:user)) }
|
||||
let(:eve) { Fabricate(:user) }
|
||||
let(:account) { Fabricate(:user).account }
|
||||
|
||||
describe 'GET #show' do
|
||||
let!(:status1) { Status.create!(account: alice, text: 'Hello world') }
|
||||
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(:format) { 'html' }
|
||||
|
||||
let!(:status_pin1) { StatusPin.create!(account: alice, status: status5, created_at: 5.days.ago) }
|
||||
let!(:status_pin2) { StatusPin.create!(account: alice, status: status6, created_at: 2.years.ago) }
|
||||
let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) }
|
||||
let!(:status) { Fabricate(:status, account: account) }
|
||||
let!(:status_reply) { Fabricate(:status, account: account, thread: Fabricate(:status)) }
|
||||
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
|
||||
alice.block!(eve.account)
|
||||
status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg'))
|
||||
status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image)
|
||||
account.pinned_statuses << status_pinned
|
||||
end
|
||||
|
||||
shared_examples 'responses' do
|
||||
shared_examples 'preliminary checks' do
|
||||
context 'when account is not approved' do
|
||||
before do
|
||||
sign_in(current_user) if defined? current_user
|
||||
get :show, params: {
|
||||
username: alice.username,
|
||||
max_id: (max_id if defined? max_id),
|
||||
since_id: (since_id if defined? since_id),
|
||||
current_user: (current_user if defined? current_user),
|
||||
}, format: format
|
||||
account.user.update(approved: false)
|
||||
end
|
||||
|
||||
it 'assigns @account' do
|
||||
expect(assigns(:account)).to eq alice
|
||||
it 'returns http not found' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is suspended' do
|
||||
before do
|
||||
account.suspend!
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
get :show, params: { username: account.username, format: format }
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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' }
|
||||
|
||||
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 correct format' do
|
||||
expect(response.content_type).to eq content_type
|
||||
end
|
||||
it 'returns application/activity+json' do
|
||||
expect(response.content_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
context 'activitystreams2' do
|
||||
let(:format) { 'json' }
|
||||
let(:content_type) { 'application/activity+json' }
|
||||
|
||||
include_examples 'responses'
|
||||
it 'returns public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
end
|
||||
|
||||
context 'html' do
|
||||
let(:format) { nil }
|
||||
let(:content_type) { 'text/html' }
|
||||
|
||||
shared_examples 'responsed statuses' do
|
||||
it 'assigns @pinned_statuses' do
|
||||
pinned_statuses = assigns(:pinned_statuses).to_a
|
||||
expect(pinned_statuses.size).to eq expected_pinned_statuses.size
|
||||
pinned_statuses.each.zip(expected_pinned_statuses.each) do |pinned_status, expected_pinned_status|
|
||||
expect(pinned_status).to eq expected_pinned_status
|
||||
end
|
||||
it 'renders account' do
|
||||
json = body_as_json
|
||||
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey, :name, :summary)
|
||||
end
|
||||
|
||||
it 'assigns @statuses' do
|
||||
statuses = assigns(:statuses).to_a
|
||||
expect(statuses.size).to eq expected_statuses.size
|
||||
statuses.each.zip(expected_statuses.each) do |status, expected_status|
|
||||
expect(status).to eq expected_status
|
||||
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
|
||||
|
||||
include_examples 'responses'
|
||||
context 'when signed in' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
context 'with anonymous visitor' do
|
||||
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'
|
||||
before do
|
||||
sign_in(user)
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
context 'with since_id nor max_id' do
|
||||
let(:max_id) { status4.id }
|
||||
let(:since_id) { status1.id }
|
||||
let(:expected_statuses) { [status3, status2] }
|
||||
let(:expected_pinned_statuses) { [] }
|
||||
it 'returns http success' do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
include_examples 'responsed statuses'
|
||||
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 blocked visitor' do
|
||||
let(:current_user) { eve }
|
||||
context 'with signature' do
|
||||
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||
|
||||
context 'without since_id nor max_id' do
|
||||
let(:expected_statuses) { [] }
|
||||
let(:expected_pinned_statuses) { [] }
|
||||
before do
|
||||
allow(controller).to receive(:signed_request_account).and_return(remote_account)
|
||||
get :show, params: { username: account.username, format: format }
|
||||
end
|
||||
|
||||
include_examples 'responsed statuses'
|
||||
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
|
||||
|
||||
context 'as RSS' do
|
||||
let(:format) { 'rss' }
|
||||
|
||||
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 public Cache-Control header' do
|
||||
expect(response.headers['Cache-Control']).to include 'public'
|
||||
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 '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 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 '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
|
||||
|
|
|
@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowerAccountsController do
|
|||
expect(body_as_json.size).to eq 1
|
||||
expect(body_as_json[0][:id]).to eq alice.id.to_s
|
||||
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
|
||||
|
|
|
@ -36,5 +36,28 @@ describe Api::V1::Accounts::FollowingAccountsController do
|
|||
expect(body_as_json.size).to eq 1
|
||||
expect(body_as_json[0][:id]).to eq alice.id.to_s
|
||||
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
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
|||
|
||||
describe 'POST #create' do
|
||||
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 }
|
||||
|
||||
before do
|
||||
|
|
|
@ -16,10 +16,16 @@ describe ApplicationController, type: :controller do
|
|||
end
|
||||
|
||||
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
|
||||
request.headers['Accept-Language'] = 'ca-ES, fa'
|
||||
get 'success'
|
||||
expect(response.body).to eq 'fa'
|
||||
expect(response.body).to eq 'ca'
|
||||
end
|
||||
|
||||
it 'sets available and compatible language if none of available languages are preferred' do
|
||||
|
|
|
@ -41,11 +41,11 @@ RSpec.describe Oauth::AuthorizationsController, type: :controller do
|
|||
context 'when app is already authorized' do
|
||||
before do
|
||||
Doorkeeper::AccessToken.find_or_create_for(
|
||||
app,
|
||||
user.id,
|
||||
app.scopes,
|
||||
Doorkeeper.configuration.access_token_expires_in,
|
||||
Doorkeeper.configuration.refresh_token_enabled?
|
||||
application: app,
|
||||
resource_owner: user.id,
|
||||
scopes: app.scopes,
|
||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ require 'rails_helper'
|
|||
RSpec.describe Oauth::TokensController, type: :controller do
|
||||
describe 'POST #revoke' do
|
||||
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) }
|
||||
|
||||
before do
|
||||
post :revoke, params: { token: access_token.token }
|
||||
post :revoke, params: { client_id: application.uid, token: access_token.token }
|
||||
end
|
||||
|
||||
it 'revokes the token' do
|
||||
|
|
|
@ -35,7 +35,7 @@ describe RemoteFollowController do
|
|||
context 'when webfinger values are wrong' do
|
||||
it 'renders new when redirect url is nil' do
|
||||
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' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
|
@ -45,7 +45,7 @@ describe RemoteFollowController do
|
|||
it 'renders new when template is nil' do
|
||||
link_with_nil_template = double(template: nil)
|
||||
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' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
|
@ -57,7 +57,7 @@ describe RemoteFollowController do
|
|||
before do
|
||||
link_with_template = double(template: 'http://example.com/follow_me?acct={uri}')
|
||||
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' } }
|
||||
end
|
||||
|
||||
|
@ -79,7 +79,7 @@ describe RemoteFollowController do
|
|||
end
|
||||
|
||||
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' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
|
@ -87,7 +87,7 @@ describe RemoteFollowController do
|
|||
end
|
||||
|
||||
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' } }
|
||||
|
||||
expect(response).to render_template(:new)
|
||||
|
|
|
@ -165,4 +165,22 @@ describe Settings::IdentityProofsController do
|
|||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -82,35 +82,6 @@ RSpec.describe Status, type: :model do
|
|||
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
|
||||
context 'if private_visibility?' do
|
||||
it 'returns true' do
|
||||
|
@ -490,6 +461,33 @@ RSpec.describe Status, type: :model do
|
|||
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
|
||||
before do
|
||||
@account = Fabricate(:account)
|
||||
|
|
|
@ -266,6 +266,8 @@ const startWorker = (workerId) => {
|
|||
'public:media',
|
||||
'public:local',
|
||||
'public:local:media',
|
||||
'public:remote',
|
||||
'public:remote:media',
|
||||
'hashtag',
|
||||
'hashtag:local',
|
||||
];
|
||||
|
@ -297,6 +299,7 @@ const startWorker = (workerId) => {
|
|||
const PUBLIC_ENDPOINTS = [
|
||||
'/api/v1/streaming/public',
|
||||
'/api/v1/streaming/public/local',
|
||||
'/api/v1/streaming/public/remote',
|
||||
'/api/v1/streaming/hashtag',
|
||||
'/api/v1/streaming/hashtag/local',
|
||||
];
|
||||
|
@ -541,6 +544,13 @@ const startWorker = (workerId) => {
|
|||
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) => {
|
||||
const channel = `timeline:direct:${req.accountId}`;
|
||||
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true);
|
||||
|
@ -605,12 +615,18 @@ const startWorker = (workerId) => {
|
|||
case 'public:local':
|
||||
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'public:remote':
|
||||
streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'public:media':
|
||||
streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'public:local:media':
|
||||
streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'public:remote:media':
|
||||
streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'direct':
|
||||
channel = `timeline:direct:${req.accountId}`;
|
||||
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
Loading…
Reference in New Issue