Merge tag 'v3.5.3'

master v3.5.3-cw1
Mike Barnes 2022-05-27 18:31:42 +10:00
commit ed34f4b9a4
292 changed files with 11462 additions and 3972 deletions

7
.browserslistrc 100644
View File

@ -0,0 +1,7 @@
[production]
defaults
not IE 11
not dead
[development]
supports es6-module

View File

@ -11,7 +11,8 @@
"extensions": [
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"rebornix.Ruby"
"rebornix.Ruby",
"webben.browserslist"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.

View File

@ -3,6 +3,53 @@ Changelog
All notable changes to this project will be documented in this file.
## [3.5.3] - 2022-05-26
### Added
- **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460))
- **Add warning for limited accounts in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18344))
- Add `limited` attribute to accounts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18344))
### Changed
- **Change RSS feeds** ([Gargron](https://github.com/mastodon/mastodon/pull/18356), [tribela](https://github.com/mastodon/mastodon/pull/18406))
- Titles are now date and time of post
- Bodies now render all content faithfully, including polls and emojis
- All media attachments are included with Media RSS
- Change "dangerous" to "sensitive" in privacy policy and web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18515))
- Change unconfirmed accounts to not be visible in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17530))
- Change `tootctl search deploy` to improve performance ([Gargron](https://github.com/mastodon/mastodon/pull/18463), [Gargron](https://github.com/mastodon/mastodon/pull/18514))
- Change search indexing to use batches to minimize resource usage ([Gargron](https://github.com/mastodon/mastodon/pull/18451))
### Fixed
- Fix follower and other counters being able to go negative ([Gargron](https://github.com/mastodon/mastodon/pull/18517))
- Fix unnecessary query on when creating a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17901))
- Fix warning an account outside of a report closing all reports for that account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18387))
- Fix error when resolving a link that redirects to a local post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18314))
- Fix preferred posting language returning unusable value in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/18428))
- Fix race condition error when external status is reblogged ([ykzts](https://github.com/mastodon/mastodon/pull/18424))
- Fix missing string for appeal validation error ([Gargron](https://github.com/mastodon/mastodon/pull/18410))
- Fix block/mute lists showing a follow button in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18364))
- Fix Redis configuration not being changed by `mastodon:setup` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18383))
- Fix streaming notifications not using quick filter logic in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18316))
- Fix ambiguous wording on appeal actions in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18328))
- Fix floating action button obscuring last element in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18332))
- Fix account warnings not being recorded in audit log ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18338))
- Fix leftover icons for direct visibility statuses ([Steffo99](https://github.com/mastodon/mastodon/pull/18305))
- Fix link verification requiring case sensitivity on links ([sgolemon](https://github.com/mastodon/mastodon/pull/18320))
- Fix embeds not setting their height correctly ([rinsuki](https://github.com/mastodon/mastodon/pull/18301))
### Security
- Fix concurrent unfollowing decrementing follower count more than once ([Gargron](https://github.com/mastodon/mastodon/pull/18527))
- Fix being able to appeal a strike unlimited times ([Gargron](https://github.com/mastodon/mastodon/pull/18529))
- Fix being able to report otherwise inaccessible statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18528))
- Fix empty votes arbitrarily increasing voters count in polls ([Gargron](https://github.com/mastodon/mastodon/pull/18526))
- Fix moderator identity leak when approving appeal of sensitive marked statuses ([Gargron](https://github.com/mastodon/mastodon/pull/18525))
- Fix suspended users being able to access APIs that don't require a user ([Gargron](https://github.com/mastodon/mastodon/pull/18524))
- Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523))
## [3.5.2] - 2022-05-04
### Added

16
Gemfile
View File

@ -7,7 +7,7 @@ gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.6'
gem 'rails', '~> 6.1.5'
gem 'rails', '~> 6.1.6'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.3'
@ -18,7 +18,7 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.113', require: false
gem 'aws-sdk-s3', '~> 1.114', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1'
@ -79,13 +79,13 @@ gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.6'
gem 'sidekiq', '~> 6.4'
gem 'sidekiq-scheduler', '~> 3.2'
gem 'sidekiq-scheduler', '~> 4.0'
gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~>0.2.0'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1'
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1'
gem 'stoplight', '~> 3.0.0'
gem 'strong_migrations', '~> 0.7'
gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
@ -112,9 +112,9 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.36'
gem 'capybara', '~> 3.37'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.20'
gem 'faker', '~> 2.21'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1'
@ -132,7 +132,7 @@ group :development do
gem 'letter_opener', '~> 1.8'
gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler'
gem 'rubocop', '~> 1.27', require: false
gem 'rubocop', '~> 1.29', require: false
gem 'rubocop-rails', '~> 2.14', require: false
gem 'brakeman', '~> 5.2', require: false
gem 'bundler-audit', '~> 0.9', require: false

View File

@ -1,40 +1,40 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
actioncable (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
actionmailbox (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
mail (>= 2.7.1)
actionmailer (6.1.5.1)
actionpack (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activesupport (= 6.1.5.1)
actionmailer (6.1.6)
actionpack (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activesupport (= 6.1.6)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.5.1)
actionview (= 6.1.5.1)
activesupport (= 6.1.5.1)
actionpack (6.1.6)
actionview (= 6.1.6)
activesupport (= 6.1.6)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.5.1)
actionpack (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
actiontext (6.1.6)
actionpack (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
nokogiri (>= 1.8.5)
actionview (6.1.5.1)
activesupport (= 6.1.5.1)
actionview (6.1.6)
activesupport (= 6.1.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -45,22 +45,22 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8)
activejob (6.1.5.1)
activesupport (= 6.1.5.1)
activejob (6.1.6)
activesupport (= 6.1.6)
globalid (>= 0.3.6)
activemodel (6.1.5.1)
activesupport (= 6.1.5.1)
activerecord (6.1.5.1)
activemodel (= 6.1.5.1)
activesupport (= 6.1.5.1)
activestorage (6.1.5.1)
actionpack (= 6.1.5.1)
activejob (= 6.1.5.1)
activerecord (= 6.1.5.1)
activesupport (= 6.1.5.1)
activemodel (6.1.6)
activesupport (= 6.1.6)
activerecord (6.1.6)
activemodel (= 6.1.6)
activesupport (= 6.1.6)
activestorage (6.1.6)
actionpack (= 6.1.6)
activejob (= 6.1.6)
activerecord (= 6.1.6)
activesupport (= 6.1.6)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.5.1)
activesupport (6.1.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -81,7 +81,7 @@ GEM
attr_required (1.0.1)
awrence (1.1.1)
aws-eventstream (1.2.0)
aws-partitions (1.582.0)
aws-partitions (1.587.0)
aws-sdk-core (3.130.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
@ -90,7 +90,7 @@ GEM
aws-sdk-kms (1.56.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.2)
aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -116,7 +116,7 @@ GEM
ffi (~> 1.14)
bootsnap (1.11.1)
msgpack (~> 1.2)
brakeman (5.2.2)
brakeman (5.2.3)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
@ -144,7 +144,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.36.0)
capybara (3.37.1)
addressable
matrix
mini_mime (>= 0.1.3)
@ -203,7 +203,6 @@ GEM
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
e2mmap (0.1.0)
ed25519 (1.3.0)
elasticsearch (7.13.3)
elasticsearch-api (= 7.13.3)
@ -220,7 +219,7 @@ GEM
tzinfo
excon (0.76.0)
fabrication (2.28.0)
faker (2.20.0)
faker (2.21.0)
i18n (>= 1.8.11, < 2)
faraday (1.9.3)
faraday-em_http (~> 1.0)
@ -307,7 +306,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.9)
i18n-tasks (1.0.10)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
better_html (~> 1.0)
@ -375,7 +374,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.17.0)
loofah (2.18.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -405,7 +404,7 @@ GEM
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0)
nio4r (2.5.8)
nokogiri (1.13.4)
nokogiri (1.13.6)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nsa (0.2.8)
@ -442,7 +441,7 @@ GEM
orm_adapter (0.5.0)
ox (2.14.11)
parallel (1.22.1)
parser (3.1.1.0)
parser (3.1.2.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
@ -468,7 +467,7 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
public_suffix (4.0.7)
puma (5.6.4)
nio4r (~> 2.0)
pundit (2.2.0)
@ -490,20 +489,20 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.1.5.1)
actioncable (= 6.1.5.1)
actionmailbox (= 6.1.5.1)
actionmailer (= 6.1.5.1)
actionpack (= 6.1.5.1)
actiontext (= 6.1.5.1)
actionview (= 6.1.5.1)
activejob (= 6.1.5.1)
activemodel (= 6.1.5.1)
activerecord (= 6.1.5.1)
activestorage (= 6.1.5.1)
activesupport (= 6.1.5.1)
rails (6.1.6)
actioncable (= 6.1.6)
actionmailbox (= 6.1.6)
actionmailer (= 6.1.6)
actionpack (= 6.1.6)
actiontext (= 6.1.6)
actionview (= 6.1.6)
activejob (= 6.1.6)
activemodel (= 6.1.6)
activerecord (= 6.1.6)
activestorage (= 6.1.6)
activesupport (= 6.1.6)
bundler (>= 1.15.0)
railties (= 6.1.5.1)
railties (= 6.1.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -519,9 +518,9 @@ GEM
railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (6.1.5.1)
actionpack (= 6.1.5.1)
activesupport (= 6.1.5.1)
railties (6.1.6)
actionpack (= 6.1.6)
activesupport (= 6.1.6)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -534,7 +533,7 @@ GEM
redis (4.5.1)
redis-namespace (1.8.2)
redis (>= 3.0.4)
regexp_parser (2.3.0)
regexp_parser (2.4.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
@ -569,16 +568,16 @@ GEM
rspec-support (3.11.0)
rspec_junit_formatter (0.5.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.27.0)
rubocop (1.29.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.16.0, < 2.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.17.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.17.0)
rubocop-ast (1.18.0)
parser (>= 3.1.1.0)
rubocop-rails (2.14.2)
activesupport (>= 4.2.0)
@ -601,20 +600,18 @@ GEM
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (3.0.0)
sidekiq (6.4.1)
sidekiq (6.4.2)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (3.2.0)
e2mmap
redis (>= 3, < 5)
sidekiq-scheduler (4.0.0)
redis (>= 4.2.0)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
thwait
sidekiq (>= 4)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.21)
sidekiq-unique-jobs (7.1.22)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 8.0)
@ -643,7 +640,7 @@ GEM
net-ssh (>= 2.8.0)
stackprof (0.2.19)
statsd-ruby (1.5.0)
stoplight (2.2.1)
stoplight (3.0.0)
strong_migrations (0.7.9)
activerecord (>= 5)
swd (1.3.0)
@ -656,8 +653,6 @@ GEM
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (1.2.1)
thwait (0.2.0)
e2mmap
tilt (2.0.10)
tpm-key_attestation (0.9.0)
bindata (~> 2.4)
@ -734,7 +729,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.8)
addressable (~> 2.8)
annotate (~> 3.2)
aws-sdk-s3 (~> 1.113)
aws-sdk-s3 (~> 1.114)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
@ -747,7 +742,7 @@ DEPENDENCIES
capistrano-rails (~> 1.6)
capistrano-rbenv (~> 2.2)
capistrano-yarn (~> 2.0)
capybara (~> 3.36)
capybara (~> 3.37)
charlock_holmes (~> 0.7.7)
chewy (~> 7.2)
climate_control (~> 0.2)
@ -762,7 +757,7 @@ DEPENDENCIES
dotenv-rails (~> 2.7)
ed25519 (~> 1.3)
fabrication (~> 2.28)
faker (~> 2.20)
faker (~> 2.21)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -813,7 +808,7 @@ DEPENDENCIES
rack (~> 2.2.3)
rack-attack (~> 6.6)
rack-cors (~> 1.1)
rails (~> 6.1.5)
rails (~> 6.1.6)
rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6)
@ -825,14 +820,14 @@ DEPENDENCIES
rspec-rails (~> 5.1)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.5)
rubocop (~> 1.27)
rubocop (~> 1.29)
rubocop-rails (~> 2.14)
ruby-progressbar (~> 1.11)
sanitize (~> 6.0)
scenic (~> 1.6)
sidekiq (~> 6.4)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.2)
sidekiq-scheduler (~> 4.0)
sidekiq-unique-jobs (~> 7.1)
simple-navigation (~> 4.3)
simple_form (~> 5.1)
@ -840,7 +835,7 @@ DEPENDENCIES
sprockets (~> 3.7.2)
sprockets-rails (~> 3.4)
stackprof
stoplight (~> 2.2.1)
stoplight (~> 3.0.0)
strong_migrations (~> 0.7)
thor (~> 1.2)
tty-prompt (~> 0.23)

View File

@ -14,7 +14,7 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| ------- | ------------------ |
| 3.5.x | Yes |
| 3.4.x | Yes |
| 3.3.x | Yes |
| 3.3.x | No |
| < 3.3 | No |
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '5m' }, analysis: {
settings index: { refresh_interval: '30s' }, analysis: {
analyzer: {
content: {
tokenizer: 'whitespace',
@ -23,7 +23,7 @@ class AccountsIndex < Chewy::Index
},
}
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
index_scope ::Account.searchable.includes(:account_stat)
root date_detection: false do
field :id, type: 'long'
@ -36,8 +36,8 @@ class AccountsIndex < Chewy::Index
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :following_count, type: 'long', value: ->(account) { account.following_count }
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end
end

View File

@ -3,7 +3,7 @@
class StatusesIndex < Chewy::Index
include FormattingHelper
settings index: { refresh_interval: '15m' }, analysis: {
settings index: { refresh_interval: '30s' }, analysis: {
filter: {
english_stop: {
type: 'stop',
@ -33,6 +33,8 @@ class StatusesIndex < Chewy::Index
},
}
# We do not use delete_if option here because it would call a method that we
# expect to be called with crutches without crutches, causing n+1 queries
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
crutch :mentions do |collection|

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
settings index: { refresh_interval: '15m' }, analysis: {
settings index: { refresh_interval: '30s' }, analysis: {
analyzer: {
content: {
tokenizer: 'keyword',
@ -23,7 +23,11 @@ class TagsIndex < Chewy::Index
},
}
index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? }
index_scope ::Tag.listable
crutch :time_period do
7.days.ago.to_date..0.days.ago.to_date
end
root date_detection: false do
field :name, type: 'text', analyzer: 'content' do
@ -31,7 +35,7 @@ class TagsIndex < Chewy::Index
end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end
end

View File

@ -44,7 +44,6 @@ class AccountsController < ApplicationController
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
@statuses = filtered_statuses.without_reblogs.limit(limit)
@statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end
format.json do

View File

@ -2,6 +2,7 @@
class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!
skip_before_action :require_not_suspended!
skip_around_action :set_locale
private

View File

@ -11,6 +11,7 @@ class Api::BaseController < ApplicationController
skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :require_not_suspended!
before_action :set_cache_headers
protect_from_forgery with: :null_session
@ -97,6 +98,10 @@ class Api::BaseController < ApplicationController
render json: { error: 'This method requires an authenticated user' }, status: 401 unless current_user
end
def require_not_suspended!
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.suspended?
end
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422

View File

@ -9,6 +9,8 @@ class Api::V1::AccountsController < Api::BaseController
before_action :require_user!, except: [:show, :create]
before_action :set_account, except: [:create]
before_action :check_account_approval, except: [:create]
before_action :check_account_confirmation, except: [:create]
before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create
@ -74,6 +76,14 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id])
end
def check_account_approval
raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending?
end
def check_account_confirmation
raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed?
end
def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
end

View File

@ -40,7 +40,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.redirect_uri
user.created_by_application.confirmation_redirect_uri
else
super
end

View File

@ -4,6 +4,7 @@ class MediaProxyController < ApplicationController
include RoutingHelper
include Authorization
include Redisable
include Lockable
skip_before_action :store_current_location
skip_before_action :require_functional!
@ -16,14 +17,10 @@ class MediaProxyController < ApplicationController
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
authorize @media_attachment.status, :show?
redownload! if @media_attachment.needs_redownload? && !reject_media?
else
raise Mastodon::RaceConditionError
end
with_lock("media_download:#{params[:id]}") do
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
authorize @media_attachment.status, :show?
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
redirect_to full_asset_url(@media_attachment.file.url(version))
@ -45,10 +42,6 @@ class MediaProxyController < ApplicationController
end
end
def lock_options
{ redis: redis, key: "media_download:#{params[:id]}", autorelease: 15.minutes.seconds }
end
def reject_media?
DomainBlock.reject_media?(@media_attachment.account.domain)
end

View File

@ -3,6 +3,7 @@
class Settings::ExportsController < Settings::BaseController
include Authorization
include Redisable
include Lockable
skip_before_action :require_functional!
@ -14,21 +15,13 @@ class Settings::ExportsController < Settings::BaseController
def create
backup = nil
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
authorize :backup, :create?
backup = current_user.backups.create!
else
raise Mastodon::RaceConditionError
end
with_lock("backup:#{current_user.id}") do
authorize :backup, :create?
backup = current_user.backups.create!
end
BackupWorker.perform_async(backup.id)
redirect_to settings_export_path
end
def lock_options
{ redis: redis, key: "backup:#{current_user.id}" }
end
end

View File

@ -26,7 +26,6 @@ class TagsController < ApplicationController
format.rss do
expires_in 0, public: true
render xml: RSS::TagSerializer.render(@tag, @statuses)
end
format.json do

View File

@ -132,7 +132,7 @@ module ApplicationHelper
elsif status.private_visibility? || status.limited_visibility?
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
elsif status.direct_visibility?
fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
fa_icon('at', title: I18n.t('statuses.visibilities.direct'))
end
end
@ -243,7 +243,7 @@ module ApplicationHelper
end.values
end
def prerender_custom_emojis(html, custom_emojis)
EmojiFormatter.new(html, custom_emojis, animate: prefers_autoplay?).to_s
def prerender_custom_emojis(html, custom_emojis, other_options = {})
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
end
end

View File

@ -18,6 +18,32 @@ module FormattingHelper
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []))
end
def rss_status_content_format(status)
html = status_content_format(status)
before_html = begin
if status.spoiler_text?
"<p><strong>#{I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)}</strong> #{h(status.spoiler_text)}</p><hr />"
else
''
end
end.html_safe # rubocop:disable Rails/OutputSafety
after_html = begin
if status.preloadable_poll
"<p>#{status.preloadable_poll.options.map { |o| "<input type=#{status.preloadable_poll.multiple? ? 'checkbox' : 'radio'} disabled /> #{h(o)}" }.join('<br />')}</p>"
else
''
end
end.html_safe # rubocop:disable Rails/OutputSafety
prerender_custom_emojis(
safe_join([before_html, html, after_html]),
status.emojis,
style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex'
).to_str
end
def account_bio_format(account)
html_aware_format(account.note, account.local?)
end

View File

@ -254,4 +254,8 @@ module LanguagesHelper
def valid_locale?(locale)
locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
end
def available_locale_or_nil(locale_name)
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
end
end

View File

@ -101,7 +101,7 @@ module StatusesHelper
when 'private'
fa_icon 'lock fw'
when 'direct'
fa_icon 'envelope fw'
fa_icon 'at fw'
end
end

View File

@ -77,6 +77,8 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
@ -780,3 +782,8 @@ export function unpinAccountFail(error) {
error,
};
};
export const revealAccount = id => ({
type: ACCOUNT_REVEAL,
id,
});

View File

@ -45,12 +45,12 @@ export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
@ -169,6 +169,7 @@ export function submitCompose(routerHistory) {
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
language: getState().getIn(['compose', 'language']),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@ -635,6 +636,11 @@ export function changeComposeSensitivity() {
};
};
export const changeComposeLanguage = language => ({
type: COMPOSE_LANGUAGE_CHANGE,
language,
});
export function changeComposeSpoilerness() {
return {
type: COMPOSE_SPOILERNESS_CHANGE,

View File

@ -0,0 +1,12 @@
import { saveSettings } from './settings';
export const LANGUAGE_USE = 'LANGUAGE_USE';
export const useLanguage = language => dispatch => {
dispatch({
type: LANGUAGE_USE,
language,
});
dispatch(saveSettings());
};

View File

@ -58,7 +58,8 @@ export const loadPending = () => ({
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type;
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
const filters = getFiltersRegex(getState(), { contextType: 'notifications' });

View File

@ -18,6 +18,8 @@ const messages = defineMessages({
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
});
export default @injectIntl
@ -33,6 +35,7 @@ class Account extends ImmutablePureComponent {
hidden: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
};
@ -61,7 +64,7 @@ class Account extends ImmutablePureComponent {
}
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
if (!account) {
return <div />;
@ -105,6 +108,10 @@ class Account extends ImmutablePureComponent {
{hidingNotificationsButton}
</Fragment>
);
} else if (defaultAction === 'mute') {
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}

View File

@ -2,11 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
import classNames from 'classnames';
export default class Avatar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map,
size: PropTypes.number.isRequired,
style: PropTypes.object,
inline: PropTypes.bool,
@ -37,15 +38,6 @@ export default class Avatar extends React.PureComponent {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
const src = account.get('avatar');
const staticSrc = account.get('avatar_static');
let className = 'account__avatar';
if (inline) {
className = className + ' account__avatar-inline';
}
const style = {
...this.props.style,
width: `${size}px`,
@ -53,15 +45,21 @@ export default class Avatar extends React.PureComponent {
backgroundSize: `${size}px ${size}px`,
};
if (hovering || animate) {
style.backgroundImage = `url(${src})`;
} else {
style.backgroundImage = `url(${staticSrc})`;
if (account) {
const src = account.get('avatar');
const staticSrc = account.get('avatar_static');
if (hovering || animate) {
style.backgroundImage = `url(${src})`;
} else {
style.backgroundImage = `url(${staticSrc})`;
}
}
return (
<div
className={className}
className={classNames('account__avatar', { 'account__avatar-inline': inline })}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}

View File

@ -468,7 +468,7 @@ class Status extends ImmutablePureComponent {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];

View File

@ -82,6 +82,7 @@ class Header extends ImmutablePureComponent {
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
};
openEditProfile = () => {
@ -123,7 +124,7 @@ class Header extends ImmutablePureComponent {
}
render () {
const { account, intl, domain } = this.props;
const { account, hidden, intl, domain } = this.props;
if (!account) {
return null;
@ -267,21 +268,25 @@ class Header extends ImmutablePureComponent {
{!suspended && info}
</div>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
</div>
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} />
<Avatar account={suspended || hidden ? undefined : account} size={90} />
</a>
<div className='spacer' />
{!suspended && (
<div className='account__header__tabs__buttons'>
{actionBtn}
{bellBtn}
{!hidden && (
<React.Fragment>
{actionBtn}
{bellBtn}
</React.Fragment>
)}
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
@ -295,30 +300,30 @@ class Header extends ImmutablePureComponent {
</h1>
</div>
<div className='account__header__extra'>
<div className='account__header__bio'>
{fields.size > 0 && (
<div className='account__header__fields'>
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
{!(suspended || hidden) && (
<div className='account__header__extra'>
<div className='account__header__bio'>
{fields.size > 0 && (
<div className='account__header__fields'>
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
)}
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
)}
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
{account.get('id') !== me && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
</div>
<div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
</div>
{!suspended && (
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
@ -341,8 +346,8 @@ class Header extends ImmutablePureComponent {
/>
</NavLink>
</div>
)}
</div>
</div>
)}
</div>
</div>
);

View File

@ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent {
onAddToList: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
};
static contextTypes = {
@ -91,7 +92,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account, hideTabs } = this.props;
const { account, hidden, hideTabs } = this.props;
if (account === null) {
return null;
@ -99,7 +100,7 @@ export default class Header extends ImmutablePureComponent {
return (
<div className='account-timeline__header'>
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
<InnerHeader
account={account}
@ -117,9 +118,10 @@ export default class Header extends ImmutablePureComponent {
onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
hidden={hidden}
/>
{!hideTabs && (
{!(hideTabs || hidden) && (
<div className='account__section-headline'>
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { revealAccount } from 'mastodon/actions/accounts';
import { FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
const mapDispatchToProps = (dispatch, { accountId }) => ({
reveal () {
dispatch(revealAccount(accountId));
},
});
export default @connect(() => {}, mapDispatchToProps)
class LimitedAccountHint extends React.PureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
reveal: PropTypes.func,
}
render () {
const { reveal } = this.props;
return (
<div className='limited-account-hint'>
<p><FormattedMessage id='limited_account_hint.title'