Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/activitypub/collections_controller.rb`: Conflict due to glitch-soc having to take care of local-only pinned toots in that controller. Took upstream's changes and restored the local-only special handling. - `app/controllers/auth/sessions_controller.rb`: Minor conflicts due to the theming system, applied upstream changes, adapted the following two files for glitch-soc's theming system: - `app/controllers/concerns/sign_in_token_authentication_concern.rb` - `app/controllers/concerns/two_factor_authentication_concern.rb` - `app/services/backup_service.rb`: Minor conflict due to glitch-soc having to handle local-only toots specially. Applied upstream changes and restored the local-only special handling. - `app/views/admin/custom_emojis/index.html.haml`: Minor conflict due to the theming system. - `package.json`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file. - `yarn.lock`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file.
This commit is contained in:
commit
12c8ac9e14
246 changed files with 5027 additions and 1354 deletions
|
@ -1,28 +0,0 @@
|
||||||
version: 1
|
|
||||||
|
|
||||||
update_configs:
|
|
||||||
- package_manager: "ruby:bundler"
|
|
||||||
directory: "/"
|
|
||||||
update_schedule: "weekly"
|
|
||||||
# Supported update schedule: live daily weekly monthly
|
|
||||||
version_requirement_updates: "auto"
|
|
||||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
|
||||||
allowed_updates:
|
|
||||||
- match:
|
|
||||||
dependency_type: "all"
|
|
||||||
# Supported dependency types: all indirect direct production development
|
|
||||||
update_type: "all"
|
|
||||||
# Supported update types: all security
|
|
||||||
|
|
||||||
- package_manager: "javascript"
|
|
||||||
directory: "/"
|
|
||||||
update_schedule: "weekly"
|
|
||||||
# Supported update schedule: live daily weekly monthly
|
|
||||||
version_requirement_updates: "auto"
|
|
||||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
|
||||||
allowed_updates:
|
|
||||||
- match:
|
|
||||||
dependency_type: "all"
|
|
||||||
# Supported dependency types: all indirect direct production development
|
|
||||||
update_type: "all"
|
|
||||||
# Supported update types: all security
|
|
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: all
|
||||||
|
|
||||||
|
- package-ecosystem: bundler
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
allow:
|
||||||
|
- dependency-type: all
|
10
Gemfile
10
Gemfile
|
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
||||||
gem 'pghero', '~> 2.5'
|
gem 'pghero', '~> 2.5'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.66', require: false
|
gem 'aws-sdk-s3', '~> 1.67', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
|
@ -50,6 +50,7 @@ gem 'omniauth', '~> 1.9'
|
||||||
|
|
||||||
gem 'discard', '~> 1.2'
|
gem 'discard', '~> 1.2'
|
||||||
gem 'doorkeeper', '~> 5.4'
|
gem 'doorkeeper', '~> 5.4'
|
||||||
|
gem 'ed25519', '~> 1.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'goldfinger', '~> 2.1'
|
gem 'goldfinger', '~> 2.1'
|
||||||
|
@ -83,7 +84,7 @@ gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 1.1'
|
gem 'rqrcode', '~> 1.1'
|
||||||
gem 'ruby-progressbar', '~> 1.10'
|
gem 'ruby-progressbar', '~> 1.10'
|
||||||
gem 'sanitize', '~> 5.1'
|
gem 'sanitize', '~> 5.2'
|
||||||
gem 'sidekiq', '~> 6.0'
|
gem 'sidekiq', '~> 6.0'
|
||||||
gem 'sidekiq-scheduler', '~> 3.0'
|
gem 'sidekiq-scheduler', '~> 3.0'
|
||||||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||||
|
@ -93,7 +94,6 @@ gem 'simple_form', '~> 5.0'
|
||||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.0'
|
gem 'stoplight', '~> 2.2.0'
|
||||||
gem 'strong_migrations', '~> 0.6'
|
gem 'strong_migrations', '~> 0.6'
|
||||||
gem 'tty-command', '~> 0.9', require: false
|
|
||||||
gem 'tty-prompt', '~> 0.21', require: false
|
gem 'tty-prompt', '~> 0.21', require: false
|
||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2020'
|
gem 'tzinfo-data', '~> 1.2020'
|
||||||
|
@ -122,7 +122,7 @@ end
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.32'
|
gem 'capybara', '~> 3.32'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.11'
|
gem 'faker', '~> 2.12'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
|
@ -141,7 +141,7 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 1.4'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.84', require: false
|
gem 'rubocop', '~> 0.85', require: false
|
||||||
gem 'rubocop-rails', '~> 2.5', require: false
|
gem 'rubocop-rails', '~> 2.5', require: false
|
||||||
gem 'brakeman', '~> 4.8', require: false
|
gem 'brakeman', '~> 4.8', require: false
|
||||||
gem 'bundler-audit', '~> 0.6', require: false
|
gem 'bundler-audit', '~> 0.6', require: false
|
||||||
|
|
58
Gemfile.lock
58
Gemfile.lock
|
@ -92,20 +92,20 @@ GEM
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.320.0)
|
aws-partitions (1.326.0)
|
||||||
aws-sdk-core (3.96.1)
|
aws-sdk-core (3.98.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.31.0)
|
aws-sdk-kms (1.33.0)
|
||||||
aws-sdk-core (~> 3, >= 3.71.0)
|
aws-sdk-core (~> 3, >= 3.71.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.66.0)
|
aws-sdk-s3 (1.67.1)
|
||||||
aws-sdk-core (~> 3, >= 3.96.1)
|
aws-sdk-core (~> 3, >= 3.96.1)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.3)
|
aws-sigv4 (1.1.4)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
better_errors (2.7.1)
|
better_errors (2.7.1)
|
||||||
|
@ -119,7 +119,7 @@ GEM
|
||||||
bootsnap (1.4.6)
|
bootsnap (1.4.6)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.8.2)
|
brakeman (4.8.2)
|
||||||
browser (4.1.0)
|
browser (4.2.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.0)
|
bullet (6.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -164,9 +164,9 @@ GEM
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
coderay (1.1.2)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.1.6)
|
concurrent-ruby (1.1.6)
|
||||||
connection_pool (2.2.2)
|
connection_pool (2.2.3)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
|
@ -201,6 +201,7 @@ GEM
|
||||||
dotenv (= 2.7.5)
|
dotenv (= 2.7.5)
|
||||||
railties (>= 3.2, < 6.1)
|
railties (>= 3.2, < 6.1)
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
|
ed25519 (1.2.4)
|
||||||
elasticsearch (7.7.0)
|
elasticsearch (7.7.0)
|
||||||
elasticsearch-api (= 7.7.0)
|
elasticsearch-api (= 7.7.0)
|
||||||
elasticsearch-transport (= 7.7.0)
|
elasticsearch-transport (= 7.7.0)
|
||||||
|
@ -217,7 +218,7 @@ GEM
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.73.0)
|
excon (0.73.0)
|
||||||
fabrication (2.21.1)
|
fabrication (2.21.1)
|
||||||
faker (2.11.0)
|
faker (2.12.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
@ -235,14 +236,14 @@ GEM
|
||||||
fog-json (1.2.0)
|
fog-json (1.2.0)
|
||||||
fog-core
|
fog-core
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
fog-openstack (0.3.7)
|
fog-openstack (0.3.10)
|
||||||
fog-core (>= 1.45, <= 2.1.0)
|
fog-core (>= 1.45, <= 2.1.0)
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.3.5)
|
fugit (1.3.6)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.1)
|
raabro (~> 1.3)
|
||||||
fuubar (2.5.0)
|
fuubar (2.5.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
|
@ -284,7 +285,7 @@ GEM
|
||||||
httplog (1.4.2)
|
httplog (1.4.2)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.2)
|
i18n (1.8.3)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.31)
|
i18n-tasks (0.9.31)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
|
@ -309,7 +310,7 @@ GEM
|
||||||
multi_json (~> 1.14)
|
multi_json (~> 1.14)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.1)
|
||||||
json-ld-preloaded (3.1.2)
|
json-ld-preloaded (3.1.3)
|
||||||
json-ld (~> 3.1)
|
json-ld (~> 3.1)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.1)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
|
@ -406,7 +407,7 @@ GEM
|
||||||
parallel (1.19.1)
|
parallel (1.19.1)
|
||||||
parallel_tests (2.32.0)
|
parallel_tests (2.32.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.7.1.2)
|
parser (2.7.1.3)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.0)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.7.4)
|
pastel (0.7.4)
|
||||||
|
@ -484,7 +485,7 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.1)
|
rake (13.0.1)
|
||||||
rdf (3.1.1)
|
rdf (3.1.2)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
|
@ -509,10 +510,10 @@ GEM
|
||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.8.2)
|
redis-store (1.8.2)
|
||||||
redis (>= 4, < 5)
|
redis (>= 4, < 5)
|
||||||
regexp_parser (1.7.0)
|
regexp_parser (1.7.1)
|
||||||
request_store (1.5.0)
|
request_store (1.5.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.0)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
rexml (3.2.4)
|
rexml (3.2.4)
|
||||||
|
@ -544,10 +545,11 @@ GEM
|
||||||
rspec-support (3.9.3)
|
rspec-support (3.9.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (0.84.0)
|
rubocop (0.85.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.0.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.0.3)
|
rubocop-ast (>= 0.0.3)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
|
@ -564,7 +566,7 @@ GEM
|
||||||
rufus-scheduler (3.6.0)
|
rufus-scheduler (3.6.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safe_yaml (1.0.5)
|
safe_yaml (1.0.5)
|
||||||
sanitize (5.1.0)
|
sanitize (5.2.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
|
@ -623,8 +625,6 @@ GEM
|
||||||
thwait (0.1.0)
|
thwait (0.1.0)
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
tty-color (0.5.1)
|
tty-color (0.5.1)
|
||||||
tty-command (0.9.0)
|
|
||||||
pastel (~> 0.7.0)
|
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-prompt (0.21.0)
|
tty-prompt (0.21.0)
|
||||||
necromancer (~> 0.5.0)
|
necromancer (~> 0.5.0)
|
||||||
|
@ -634,7 +634,7 @@ GEM
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
tty-screen (~> 0.7)
|
tty-screen (~> 0.7)
|
||||||
wisper (~> 2.0.0)
|
wisper (~> 2.0.0)
|
||||||
tty-screen (0.7.1)
|
tty-screen (0.8.0)
|
||||||
twitter-text (1.14.7)
|
twitter-text (1.14.7)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.7)
|
tzinfo (1.2.7)
|
||||||
|
@ -662,7 +662,7 @@ GEM
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
websocket-driver (0.7.2)
|
websocket-driver (0.7.2)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.4)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.1)
|
wisper (2.0.1)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
@ -675,7 +675,7 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.7)
|
active_record_query_trace (~> 1.7)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.66)
|
aws-sdk-s3 (~> 1.67)
|
||||||
better_errors (~> 2.7)
|
better_errors (~> 2.7)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
@ -702,8 +702,9 @@ DEPENDENCIES
|
||||||
doorkeeper (~> 5.4)
|
doorkeeper (~> 5.4)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
e2mmap (~> 0.1.0)
|
e2mmap (~> 0.1.0)
|
||||||
|
ed25519 (~> 1.2)
|
||||||
fabrication (~> 2.21)
|
fabrication (~> 2.21)
|
||||||
faker (~> 2.11)
|
faker (~> 2.12)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
|
@ -773,10 +774,10 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 4.0)
|
rspec-rails (~> 4.0)
|
||||||
rspec-sidekiq (~> 3.0)
|
rspec-sidekiq (~> 3.0)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.4)
|
||||||
rubocop (~> 0.84)
|
rubocop (~> 0.85)
|
||||||
rubocop-rails (~> 2.5)
|
rubocop-rails (~> 2.5)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.1)
|
sanitize (~> 5.2)
|
||||||
sidekiq (~> 6.0)
|
sidekiq (~> 6.0)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.0)
|
sidekiq-scheduler (~> 3.0)
|
||||||
|
@ -792,7 +793,6 @@ DEPENDENCIES
|
||||||
strong_migrations (~> 0.6)
|
strong_migrations (~> 0.6)
|
||||||
thor (~> 0.20)
|
thor (~> 0.20)
|
||||||
thwait (~> 0.1.0)
|
thwait (~> 0.1.0)
|
||||||
tty-command (~> 0.9)
|
|
||||||
tty-prompt (~> 0.21)
|
tty-prompt (~> 0.21)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2020)
|
tzinfo-data (~> 1.2020)
|
||||||
|
|
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 3.1.x | :white_check_mark: |
|
||||||
|
| < 3.1 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
hello@joinmastodon.org
|
|
@ -1,7 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountsController < ApplicationController
|
class AccountsController < ApplicationController
|
||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
|
PAGE_SIZE_MAX = 200
|
||||||
|
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
|
@ -41,7 +42,8 @@ class AccountsController < ApplicationController
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 1.minute, public: true
|
expires_in 1.minute, public: true
|
||||||
|
|
||||||
@statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE)
|
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)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
||||||
end
|
end
|
||||||
|
|
21
app/controllers/activitypub/claims_controller.rb
Normal file
21
app/controllers/activitypub/claims_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ClaimsController < ActivityPub::BaseController
|
||||||
|
include SignatureVerification
|
||||||
|
include AccountOwnedConcern
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
|
||||||
|
before_action :require_signature!
|
||||||
|
before_action :set_claim_result
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_claim_result
|
||||||
|
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,8 +5,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
||||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||||
|
before_action :set_items
|
||||||
before_action :set_size
|
before_action :set_size
|
||||||
before_action :set_statuses
|
before_action :set_type
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -16,40 +17,53 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_statuses
|
def set_items
|
||||||
@statuses = scope_for_collection
|
|
||||||
@statuses = cache_collection(@statuses, Status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_size
|
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@size = @account.pinned_statuses.not_local_only.count
|
@items = begin
|
||||||
|
# Because in public fetch mode we cache the response, there would be no
|
||||||
|
# benefit from performing the check below, since a blocked account or domain
|
||||||
|
# would likely be served the cache from the reverse proxy anyway
|
||||||
|
|
||||||
|
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
cache_collection(@account.pinned_statuses.not_local_only, Status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
when 'devices'
|
||||||
|
@items = @account.devices
|
||||||
else
|
else
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def scope_for_collection
|
def set_size
|
||||||
|
case params[:id]
|
||||||
|
when 'featured', 'devices'
|
||||||
|
@size = @items.size
|
||||||
|
else
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_type
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
# Because in public fetch mode we cache the response, there would be no
|
@type = :ordered
|
||||||
# benefit from performing the check below, since a blocked account or domain
|
when 'devices'
|
||||||
# would likely be served the cache from the reverse proxy anyway
|
@type = :unordered
|
||||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
else
|
||||||
Status.none
|
not_found
|
||||||
else
|
|
||||||
@account.pinned_statuses.not_local_only
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def collection_presenter
|
def collection_presenter
|
||||||
ActivityPub::CollectionPresenter.new(
|
ActivityPub::CollectionPresenter.new(
|
||||||
id: account_collection_url(@account, params[:id]),
|
id: account_collection_url(@account, params[:id]),
|
||||||
type: :ordered,
|
type: @type,
|
||||||
size: @size,
|
size: @size,
|
||||||
items: @statuses
|
items: @items
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,6 +33,8 @@ module Admin
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
||||||
ensure
|
ensure
|
||||||
redirect_to admin_custom_emojis_path(filter_params)
|
redirect_to admin_custom_emojis_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
30
app/controllers/api/v1/crypto/deliveries_controller.rb
Normal file
30
app/controllers/api/v1/crypto/deliveries_controller.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::DeliveriesController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
def create
|
||||||
|
devices.each do |device_params|
|
||||||
|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:device)
|
||||||
|
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
|
||||||
|
end
|
||||||
|
|
||||||
|
def devices
|
||||||
|
Array(resource_params[:device])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,59 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
||||||
|
LIMIT = 80
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
before_action :set_encrypted_messages, only: :index
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_encrypted_messages
|
||||||
|
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@encrypted_messages.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@encrypted_messages.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@encrypted_messages.size == limit_param(LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(:limit).permit(:limit).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
25
app/controllers/api/v1/crypto/keys/claims_controller.rb
Normal file
25
app/controllers/api/v1/crypto/keys/claims_controller.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_claim_results
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_claim_results
|
||||||
|
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(device: [:account_id, :device_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def devices
|
||||||
|
Array(resource_params[:device])
|
||||||
|
end
|
||||||
|
end
|
17
app/controllers/api/v1/crypto/keys/counts_controller.rb
Normal file
17
app/controllers/api/v1/crypto/keys/counts_controller.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_current_device
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: { one_time_keys: @current_device.one_time_keys.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_current_device
|
||||||
|
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||||
|
end
|
||||||
|
end
|
26
app/controllers/api/v1/crypto/keys/queries_controller.rb
Normal file
26
app/controllers/api/v1/crypto/keys/queries_controller.rb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_accounts
|
||||||
|
before_action :set_query_results
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = Account.where(id: account_ids).includes(:devices)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_query_results
|
||||||
|
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_ids
|
||||||
|
Array(params[:id]).map(&:to_i)
|
||||||
|
end
|
||||||
|
end
|
29
app/controllers/api/v1/crypto/keys/uploads_controller.rb
Normal file
29
app/controllers/api/v1/crypto/keys/uploads_controller.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :crypto }
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
|
||||||
|
|
||||||
|
device.transaction do
|
||||||
|
device.account = current_account
|
||||||
|
device.update!(resource_params[:device])
|
||||||
|
|
||||||
|
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
|
||||||
|
resource_params[:one_time_keys].each do |one_time_key_params|
|
||||||
|
device.one_time_keys.create!(one_time_key_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: device, serializer: REST::Keys::DeviceSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,9 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
prepend_before_action :set_pack
|
prepend_before_action :set_pack
|
||||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
|
||||||
|
include TwoFactorAuthenticationConcern
|
||||||
|
include SignInTokenAuthenticationConcern
|
||||||
|
|
||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
@ -40,8 +42,8 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def find_user
|
def find_user
|
||||||
if session[:otp_user_id]
|
if session[:attempt_user_id]
|
||||||
User.find(session[:otp_user_id])
|
User.find(session[:attempt_user_id])
|
||||||
else
|
else
|
||||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||||
|
@ -50,7 +52,7 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :password, :otp_attempt)
|
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_in_path_for(resource)
|
def after_sign_in_path_for(resource)
|
||||||
|
@ -71,48 +73,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def two_factor_enabled?
|
|
||||||
find_user&.otp_required_for_login?
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_otp_attempt?(user)
|
|
||||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
|
||||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
|
||||||
rescue OpenSSL::Cipher::CipherError
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_with_two_factor
|
|
||||||
user = self.resource = find_user
|
|
||||||
|
|
||||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
|
||||||
authenticate_with_two_factor_via_otp(user)
|
|
||||||
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
|
|
||||||
# If encrypted_password is blank, we got the user from LDAP or PAM,
|
|
||||||
# so credentials are already valid
|
|
||||||
|
|
||||||
prompt_for_two_factor(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate_with_two_factor_via_otp(user)
|
|
||||||
if valid_otp_attempt?(user)
|
|
||||||
session.delete(:otp_user_id)
|
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
|
||||||
else
|
|
||||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
|
||||||
prompt_for_two_factor(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def prompt_for_two_factor(user)
|
|
||||||
session[:otp_user_id] = user.id
|
|
||||||
use_pack 'auth'
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
render :two_factor
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
# Delete flash message that isn't entirely useful and may be confusing in
|
# Delete flash message that isn't entirely useful and may be confusing in
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SignInTokenAuthenticationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_in_token_required?
|
||||||
|
find_user&.suspicious_sign_in?(request.remote_ip)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_sign_in_token_attempt?(user)
|
||||||
|
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_sign_in_token
|
||||||
|
user = self.resource = find_user
|
||||||
|
|
||||||
|
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
|
||||||
|
authenticate_with_sign_in_token_attempt(user)
|
||||||
|
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||||
|
prompt_for_sign_in_token(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_sign_in_token_attempt(user)
|
||||||
|
if valid_sign_in_token_attempt?(user)
|
||||||
|
session.delete(:attempt_user_id)
|
||||||
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
|
else
|
||||||
|
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||||
|
prompt_for_sign_in_token(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prompt_for_sign_in_token(user)
|
||||||
|
if user.sign_in_token_expired?
|
||||||
|
user.generate_sign_in_token && user.save
|
||||||
|
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
session[:attempt_user_id] = user.id
|
||||||
|
use_pack 'auth'
|
||||||
|
@body_classes = 'lighter'
|
||||||
|
render :sign_in_token
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TwoFactorAuthenticationConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||||
|
end
|
||||||
|
|
||||||
|
def two_factor_enabled?
|
||||||
|
find_user&.otp_required_for_login?
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_otp_attempt?(user)
|
||||||
|
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||||
|
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||||
|
rescue OpenSSL::Cipher::CipherError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_two_factor
|
||||||
|
user = self.resource = find_user
|
||||||
|
|
||||||
|
if user_params[:otp_attempt].present? && session[:attempt_user_id]
|
||||||
|
authenticate_with_two_factor_attempt(user)
|
||||||
|
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||||
|
prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_with_two_factor_attempt(user)
|
||||||
|
if valid_otp_attempt?(user)
|
||||||
|
session.delete(:attempt_user_id)
|
||||||
|
remember_me(user)
|
||||||
|
sign_in(user)
|
||||||
|
else
|
||||||
|
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||||
|
prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prompt_for_two_factor(user)
|
||||||
|
session[:attempt_user_id] = user.id
|
||||||
|
use_pack 'auth'
|
||||||
|
@body_classes = 'lighter'
|
||||||
|
render :two_factor
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,7 +18,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
||||||
if @redirect.valid_with_challenge?(current_user)
|
if @redirect.valid_with_challenge?(current_user)
|
||||||
current_account.update!(moved_to_account: @redirect.target_account)
|
current_account.update!(moved_to_account: @redirect.target_account)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||||
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
|
redirect_to settings_migration_path, notice: I18n.t('migrations.redirected_msg', acct: current_account.moved_to_account.acct)
|
||||||
else
|
else
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ class StatusesController < ApplicationController
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
|
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
|
||||||
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
|
render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
include SignatureVerification
|
include SignatureVerification
|
||||||
|
|
||||||
PAGE_SIZE = 20
|
PAGE_SIZE = 20
|
||||||
|
PAGE_SIZE_MAX = 200
|
||||||
|
|
||||||
layout 'public'
|
layout 'public'
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ class TagsController < ApplicationController
|
||||||
format.rss do
|
format.rss do
|
||||||
expires_in 0, public: true
|
expires_in 0, public: true
|
||||||
|
|
||||||
|
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
|
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,11 @@ module ApplicationHelper
|
||||||
text: [params[:title], params[:text], params[:url]].compact.join(' '),
|
text: [params[:title], params[:text], params[:url]].compact.join(' '),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
permit_visibilities = %w(public unlisted private direct)
|
||||||
|
default_privacy = current_account&.user&.setting_default_privacy
|
||||||
|
permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
|
||||||
|
state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
|
||||||
|
|
||||||
if user_signed_in?
|
if user_signed_in?
|
||||||
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
|
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
|
||||||
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
|
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Monkey-patch on monkey-patch.
|
||||||
|
# Because it conflicts with the request.rb patch.
|
||||||
|
class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation
|
||||||
|
def connect(socket_class, host, port, nodelay = false)
|
||||||
|
::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do
|
||||||
|
@socket = socket_class.open(host, port)
|
||||||
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
module WebfingerHelper
|
module WebfingerHelper
|
||||||
def webfinger!(uri)
|
def webfinger!(uri)
|
||||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
||||||
|
@ -12,6 +23,14 @@ module WebfingerHelper
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': Mastodon::Version.user_agent,
|
'User-Agent': Mastodon::Version.user_agent,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
timeout_class: HTTP::Timeout::PerOperationOriginal,
|
||||||
|
|
||||||
|
timeout_options: {
|
||||||
|
write_timeout: 10,
|
||||||
|
connect_timeout: 5,
|
||||||
|
read_timeout: 10,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
|
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
|
||||||
|
|
|
@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
||||||
|
|
||||||
export function searchTextFromRawStatus (status) {
|
export function searchTextFromRawStatus (status) {
|
||||||
const spoilerText = status.spoiler_text || '';
|
const spoilerText = status.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
inputRef={this.setTextarea}
|
ref={this.setTextarea}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -401,6 +401,7 @@ class Status extends ImmutablePureComponent {
|
||||||
compact
|
compact
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ describe('emoji', () => {
|
||||||
|
|
||||||
it('skips the textual presentation VS15 character', () => {
|
it('skips the textual presentation VS15 character', () => {
|
||||||
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
||||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />');
|
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,15 @@ const trie = new Trie(Object.keys(unicodeMapping));
|
||||||
|
|
||||||
const assetHost = process.env.CDN_HOST || '';
|
const assetHost = process.env.CDN_HOST || '';
|
||||||
|
|
||||||
|
// Emoji requiring extra borders depending on theme
|
||||||
|
const darkEmoji = '🎱🐜⚫🖤⬛◼️◾◼️✒️▪️💣🎳📷📸♣️🕶️✴️🔌💂♀️📽️🍳🦍💂🔪🕳️🕹️🕋🖊️🖋️💂♂️🎤🎓🎥🎼♠️🎩🦃📼📹🎮🐃🏴';
|
||||||
|
const lightEmoji = '👽⚾🐔☁️💨🕊️👀🍥👻🐐❕❔⛸️🌩️🔊🔇📃🌧️🐏🍚🍙🐓🐑💀☠️🌨️🔉🔈💬💭🏐🏳️⚪⬜◽◻️▫️';
|
||||||
|
|
||||||
|
const emojiFilename = (filename, match) => {
|
||||||
|
const borderedEmoji = document.body.classList.contains('theme-mastodon-light') ? lightEmoji : darkEmoji;
|
||||||
|
return borderedEmoji.includes(match) ? (filename + '_border') : filename;
|
||||||
|
};
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}) => {
|
const emojify = (str, customEmojis = {}) => {
|
||||||
const tagCharsWithoutEmojis = '<&';
|
const tagCharsWithoutEmojis = '<&';
|
||||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||||
|
@ -60,7 +69,7 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
} else { // matched to unicode emoji
|
} else { // matched to unicode emoji
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
const { filename, shortCode } = unicodeMapping[match];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
|
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${emojiFilename(filename, match)}.svg" />`;
|
||||||
rend = i + match.length;
|
rend = i + match.length;
|
||||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||||
if (str.codePointAt(rend) === 65038) {
|
if (str.codePointAt(rend) === 65038) {
|
||||||
|
|
|
@ -2,9 +2,13 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import punycode from 'punycode';
|
import punycode from 'punycode';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useBlurhash } from 'mastodon/initial_state';
|
||||||
|
import { decode } from 'blurhash';
|
||||||
|
|
||||||
const IDNA_PREFIX = 'xn--';
|
const IDNA_PREFIX = 'xn--';
|
||||||
|
|
||||||
|
@ -63,6 +67,7 @@ export default class Card extends React.PureComponent {
|
||||||
compact: PropTypes.bool,
|
compact: PropTypes.bool,
|
||||||
defaultWidth: PropTypes.number,
|
defaultWidth: PropTypes.number,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
|
sensitive: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -72,12 +77,44 @@ export default class Card extends React.PureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
width: this.props.defaultWidth || 280,
|
width: this.props.defaultWidth || 280,
|
||||||
|
previewLoaded: false,
|
||||||
embedded: false,
|
embedded: false,
|
||||||
|
revealed: !this.props.sensitive,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (!Immutable.is(this.props.card, nextProps.card)) {
|
if (!Immutable.is(this.props.card, nextProps.card)) {
|
||||||
this.setState({ embedded: false });
|
this.setState({ embedded: false, previewLoaded: false });
|
||||||
|
}
|
||||||
|
if (this.props.sensitive !== nextProps.sensitive) {
|
||||||
|
this.setState({ revealed: !nextProps.sensitive });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
if (this.props.card && this.props.card.get('blurhash')) {
|
||||||
|
this._decode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
const { card } = this.props;
|
||||||
|
if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash'))) {
|
||||||
|
this._decode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_decode () {
|
||||||
|
if (!useBlurhash) return;
|
||||||
|
|
||||||
|
const hash = this.props.card.get('blurhash');
|
||||||
|
const pixels = decode(hash, 32, 32);
|
||||||
|
|
||||||
|
if (pixels) {
|
||||||
|
const ctx = this.canvas.getContext('2d');
|
||||||
|
const imageData = new ImageData(pixels, 32, 32);
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +156,18 @@ export default class Card extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCanvasRef = c => {
|
||||||
|
this.canvas = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImageLoad = () => {
|
||||||
|
this.setState({ previewLoaded: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReveal = () => {
|
||||||
|
this.setState({ revealed: true });
|
||||||
|
}
|
||||||
|
|
||||||
renderVideo () {
|
renderVideo () {
|
||||||
const { card } = this.props;
|
const { card } = this.props;
|
||||||
const content = { __html: addAutoPlay(card.get('html')) };
|
const content = { __html: addAutoPlay(card.get('html')) };
|
||||||
|
@ -138,7 +187,7 @@ export default class Card extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { card, maxDescription, compact } = this.props;
|
const { card, maxDescription, compact } = this.props;
|
||||||
const { width, embedded } = this.state;
|
const { width, embedded, revealed } = this.state;
|
||||||
|
|
||||||
if (card === null) {
|
if (card === null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -153,7 +202,7 @@ export default class Card extends React.PureComponent {
|
||||||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<div className='status-card__content'>
|
<div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}>
|
||||||
{title}
|
{title}
|
||||||
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
||||||
<span className='status-card__host'>{provider}</span>
|
<span className='status-card__host'>{provider}</span>
|
||||||
|
@ -161,7 +210,18 @@ export default class Card extends React.PureComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
let embed = '';
|
let embed = '';
|
||||||
let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
|
let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
|
||||||
|
let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
||||||
|
let spoilerButton = (
|
||||||
|
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
|
||||||
|
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
spoilerButton = (
|
||||||
|
<div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
|
||||||
|
{spoilerButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
|
@ -175,14 +235,18 @@ export default class Card extends React.PureComponent {
|
||||||
|
|
||||||
embed = (
|
embed = (
|
||||||
<div className='status-card__image'>
|
<div className='status-card__image'>
|
||||||
|
{canvas}
|
||||||
{thumbnail}
|
{thumbnail}
|
||||||
|
|
||||||
<div className='status-card__actions'>
|
{revealed && (
|
||||||
<div>
|
<div className='status-card__actions'>
|
||||||
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
<div>
|
||||||
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
|
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
|
||||||
|
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{!revealed && spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -196,13 +260,16 @@ export default class Card extends React.PureComponent {
|
||||||
} else if (card.get('image')) {
|
} else if (card.get('image')) {
|
||||||
embed = (
|
embed = (
|
||||||
<div className='status-card__image'>
|
<div className='status-card__image'>
|
||||||
|
{canvas}
|
||||||
{thumbnail}
|
{thumbnail}
|
||||||
|
{!revealed && spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
embed = (
|
embed = (
|
||||||
<div className='status-card__image'>
|
<div className='status-card__image'>
|
||||||
<Icon id='file-text' />
|
<Icon id='file-text' />
|
||||||
|
{!revealed && spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else if (status.get('spoiler_text').length === 0) {
|
||||||
media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
|
|
|
@ -106,7 +106,7 @@
|
||||||
"confirmations.block.confirm": "Block",
|
"confirmations.block.confirm": "Block",
|
||||||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
"confirmations.block.message": "Are you sure you want to block {name}?",
|
||||||
"confirmations.delete.confirm": "Delete",
|
"confirmations.delete.confirm": "Delete",
|
||||||
"confirmations.delete.message": "Are you sure you want to delete this status?",
|
"confirmations.delete.message": "Are you sure you want to delete this toot?",
|
||||||
"confirmations.delete_list.confirm": "Delete",
|
"confirmations.delete_list.confirm": "Delete",
|
||||||
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
|
||||||
"confirmations.domain_block.confirm": "Block entire domain",
|
"confirmations.domain_block.confirm": "Block entire domain",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
|
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
|
||||||
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
"confirmations.mute.message": "Are you sure you want to mute {name}?",
|
||||||
"confirmations.redraft.confirm": "Delete & redraft",
|
"confirmations.redraft.confirm": "Delete & redraft",
|
||||||
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
"confirmations.redraft.message": "Are you sure you want to delete this toot and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
|
||||||
"confirmations.reply.confirm": "Reply",
|
"confirmations.reply.confirm": "Reply",
|
||||||
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||||
"confirmations.unfollow.confirm": "Unfollow",
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
"directory.local": "From {domain} only",
|
"directory.local": "From {domain} only",
|
||||||
"directory.new_arrivals": "New arrivals",
|
"directory.new_arrivals": "New arrivals",
|
||||||
"directory.recently_active": "Recently active",
|
"directory.recently_active": "Recently active",
|
||||||
"embed.instructions": "Embed this status on your website by copying the code below.",
|
"embed.instructions": "Embed this toot on your website by copying the code below.",
|
||||||
"embed.preview": "Here is what it will look like:",
|
"embed.preview": "Here is what it will look like:",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "Custom",
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
"empty_column.hashtag": "There is nothing in this hashtag yet.",
|
||||||
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
|
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
|
||||||
"empty_column.home.public_timeline": "the public timeline",
|
"empty_column.home.public_timeline": "the public timeline",
|
||||||
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
|
"empty_column.list": "There is nothing in this list yet. When members of this list post new toots, they will appear here.",
|
||||||
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
|
||||||
"empty_column.mutes": "You haven't muted any users yet.",
|
"empty_column.mutes": "You haven't muted any users yet.",
|
||||||
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
|
||||||
|
@ -216,12 +216,12 @@
|
||||||
"keyboard_shortcuts.back": "to navigate back",
|
"keyboard_shortcuts.back": "to navigate back",
|
||||||
"keyboard_shortcuts.blocked": "to open blocked users list",
|
"keyboard_shortcuts.blocked": "to open blocked users list",
|
||||||
"keyboard_shortcuts.boost": "to boost",
|
"keyboard_shortcuts.boost": "to boost",
|
||||||
"keyboard_shortcuts.column": "to focus a status in one of the columns",
|
"keyboard_shortcuts.column": "to focus a toot in one of the columns",
|
||||||
"keyboard_shortcuts.compose": "to focus the compose textarea",
|
"keyboard_shortcuts.compose": "to focus the compose textarea",
|
||||||
"keyboard_shortcuts.description": "Description",
|
"keyboard_shortcuts.description": "Description",
|
||||||
"keyboard_shortcuts.direct": "to open direct messages column",
|
"keyboard_shortcuts.direct": "to open direct messages column",
|
||||||
"keyboard_shortcuts.down": "to move down in the list",
|
"keyboard_shortcuts.down": "to move down in the list",
|
||||||
"keyboard_shortcuts.enter": "to open status",
|
"keyboard_shortcuts.enter": "to open toot",
|
||||||
"keyboard_shortcuts.favourite": "to favourite",
|
"keyboard_shortcuts.favourite": "to favourite",
|
||||||
"keyboard_shortcuts.favourites": "to open favourites list",
|
"keyboard_shortcuts.favourites": "to open favourites list",
|
||||||
"keyboard_shortcuts.federated": "to open federated timeline",
|
"keyboard_shortcuts.federated": "to open federated timeline",
|
||||||
|
@ -289,13 +289,13 @@
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
"notification.favourite": "{name} favourited your status",
|
"notification.favourite": "{name} favourited your toot",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
"notification.follow_request": "{name} has requested to follow you",
|
"notification.follow_request": "{name} has requested to follow you",
|
||||||
"notification.mention": "{name} mentioned you",
|
"notification.mention": "{name} mentioned you",
|
||||||
"notification.own_poll": "Your poll has ended",
|
"notification.own_poll": "Your poll has ended",
|
||||||
"notification.poll": "A poll you have voted in has ended",
|
"notification.poll": "A poll you have voted in has ended",
|
||||||
"notification.reblog": "{name} boosted your status",
|
"notification.reblog": "{name} boosted your toot",
|
||||||
"notifications.clear": "Clear notifications",
|
"notifications.clear": "Clear notifications",
|
||||||
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||||
"notifications.column_settings.alert": "Desktop notifications",
|
"notifications.column_settings.alert": "Desktop notifications",
|
||||||
|
@ -326,7 +326,7 @@
|
||||||
"poll.voted": "You voted for this answer",
|
"poll.voted": "You voted for this answer",
|
||||||
"poll_button.add_poll": "Add a poll",
|
"poll_button.add_poll": "Add a poll",
|
||||||
"poll_button.remove_poll": "Remove poll",
|
"poll_button.remove_poll": "Remove poll",
|
||||||
"privacy.change": "Adjust status privacy",
|
"privacy.change": "Adjust toot privacy",
|
||||||
"privacy.direct.long": "Visible for mentioned users only",
|
"privacy.direct.long": "Visible for mentioned users only",
|
||||||
"privacy.direct.short": "Direct",
|
"privacy.direct.short": "Direct",
|
||||||
"privacy.private.long": "Visible for followers only",
|
"privacy.private.long": "Visible for followers only",
|
||||||
|
@ -353,9 +353,9 @@
|
||||||
"report.target": "Reporting {target}",
|
"report.target": "Reporting {target}",
|
||||||
"search.placeholder": "Search",
|
"search.placeholder": "Search",
|
||||||
"search_popout.search_format": "Advanced search format",
|
"search_popout.search_format": "Advanced search format",
|
||||||
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
"search_popout.tips.full_text": "Simple text returns toots you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "hashtag",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "toot",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "user",
|
||||||
"search_results.accounts": "People",
|
"search_results.accounts": "People",
|
||||||
|
@ -364,12 +364,12 @@
|
||||||
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
|
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
|
||||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
"status.admin_status": "Open this status in the moderation interface",
|
"status.admin_status": "Open this toot in the moderation interface",
|
||||||
"status.block": "Block @{name}",
|
"status.block": "Block @{name}",
|
||||||
"status.bookmark": "Bookmark",
|
"status.bookmark": "Bookmark",
|
||||||
"status.cancel_reblog_private": "Unboost",
|
"status.cancel_reblog_private": "Unboost",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
"status.copy": "Copy link to status",
|
"status.copy": "Copy link to toot",
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
"status.detailed_status": "Detailed conversation view",
|
"status.detailed_status": "Detailed conversation view",
|
||||||
"status.direct": "Direct message @{name}",
|
"status.direct": "Direct message @{name}",
|
||||||
|
@ -382,7 +382,7 @@
|
||||||
"status.more": "More",
|
"status.more": "More",
|
||||||
"status.mute": "Mute @{name}",
|
"status.mute": "Mute @{name}",
|
||||||
"status.mute_conversation": "Mute conversation",
|
"status.mute_conversation": "Mute conversation",
|
||||||
"status.open": "Expand this status",
|
"status.open": "Expand this toot",
|
||||||
"status.pin": "Pin on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.pinned": "Pinned toot",
|
"status.pinned": "Pinned toot",
|
||||||
"status.read_more": "Read more",
|
"status.read_more": "Read more",
|
||||||
|
|
|
@ -39,3 +39,5 @@ $account-background-color: $white !default;
|
||||||
@function lighten($color, $amount) {
|
@function lighten($color, $amount) {
|
||||||
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
|
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$emojis-requiring-inversion: 'chains';
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
$black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash';
|
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
|
||||||
|
|
||||||
%white-emoji-outline {
|
%emoji-color-inversion {
|
||||||
filter: drop-shadow(1px 1px 0 $white) drop-shadow(-1px 1px 0 $white) drop-shadow(1px -1px 0 $white) drop-shadow(-1px -1px 0 $white);
|
filter: invert(1);
|
||||||
transform: scale(.71);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
@each $emoji in $black-emojis {
|
@each $emoji in $emojis-requiring-inversion {
|
||||||
&[title=':#{$emoji}:'] {
|
&[title=':#{$emoji}:'] {
|
||||||
@extend %white-emoji-outline;
|
@extend %emoji-color-inversion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3097,6 +3097,11 @@ a.status-card {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 14px 14px 14px 8px;
|
padding: 14px 14px 14px 8px;
|
||||||
|
|
||||||
|
&--blurred {
|
||||||
|
filter: blur(2px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card__description {
|
.status-card__description {
|
||||||
|
@ -3134,7 +3139,8 @@ a.status-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card__image-image {
|
.status-card__image-image,
|
||||||
|
.status-card__image-preview {
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3179,6 +3185,24 @@ a.status-card.compact:hover {
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-card__image-preview {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 0;
|
||||||
|
background: $base-overlay-background;
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
display: block;
|
display: block;
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
|
|
|
@ -2,6 +2,45 @@
|
||||||
|
|
||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
|
case @object['type']
|
||||||
|
when 'EncryptedMessage'
|
||||||
|
create_encrypted_message
|
||||||
|
else
|
||||||
|
create_status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_encrypted_message
|
||||||
|
return reject_payload! if invalid_origin?(@object['id']) || @options[:delivered_to_account_id].blank?
|
||||||
|
|
||||||
|
target_account = Account.find(@options[:delivered_to_account_id])
|
||||||
|
target_device = target_account.devices.find_by(device_id: @object.dig('to', 'deviceId'))
|
||||||
|
|
||||||
|
return if target_device.nil?
|
||||||
|
|
||||||
|
target_device.encrypted_messages.create!(
|
||||||
|
from_account: @account,
|
||||||
|
from_device_id: @object.dig('attributedTo', 'deviceId'),
|
||||||
|
type: @object['messageType'],
|
||||||
|
body: @object['cipherText'],
|
||||||
|
digest: @object.dig('digest', 'digestValue'),
|
||||||
|
message_franking: message_franking.to_token
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_franking
|
||||||
|
MessageFranking.new(
|
||||||
|
hmac: @object.dig('digest', 'digestValue'),
|
||||||
|
original_franking: @object['messageFranking'],
|
||||||
|
source_account_id: @account.id,
|
||||||
|
target_account_id: @options[:delivered_to_account_id],
|
||||||
|
timestamp: Time.now.utc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_status
|
||||||
return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
|
@ -23,8 +62,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
@status
|
@status
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def audience_to
|
def audience_to
|
||||||
@object['to'] || @json['to']
|
@object['to'] || @json['to']
|
||||||
end
|
end
|
||||||
|
@ -262,6 +299,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def poll_vote!
|
def poll_vote!
|
||||||
poll = replied_to_status.preloadable_poll
|
poll = replied_to_status.preloadable_poll
|
||||||
already_voted = true
|
already_voted = true
|
||||||
|
|
||||||
RedisLock.acquire(poll_lock_options) do |lock|
|
RedisLock.acquire(poll_lock_options) do |lock|
|
||||||
if lock.acquired?
|
if lock.acquired?
|
||||||
already_voted = poll.votes.where(account: @account).exists?
|
already_voted = poll.votes.where(account: @account).exists?
|
||||||
|
@ -270,20 +308,24 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
raise Mastodon::RaceConditionError
|
raise Mastodon::RaceConditionError
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
increment_voters_count! unless already_voted
|
increment_voters_count! unless already_voted
|
||||||
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals?
|
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals?
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_thread(status)
|
def resolve_thread(status)
|
||||||
return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
|
return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
|
||||||
|
|
||||||
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
|
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_replies(status)
|
def fetch_replies(status)
|
||||||
collection = @object['replies']
|
collection = @object['replies']
|
||||||
return if collection.nil?
|
return if collection.nil?
|
||||||
|
|
||||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, false)
|
replies = ActivityPub::FetchRepliesService.new.call(status, collection, false)
|
||||||
return unless replies.nil?
|
return unless replies.nil?
|
||||||
|
|
||||||
uri = value_or_id(collection)
|
uri = value_or_id(collection)
|
||||||
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
|
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
|
||||||
end
|
end
|
||||||
|
@ -291,6 +333,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def conversation_from_uri(uri)
|
def conversation_from_uri(uri)
|
||||||
return nil if uri.nil?
|
return nil if uri.nil?
|
||||||
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
|
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Conversation.find_or_create_by!(uri: uri)
|
Conversation.find_or_create_by!(uri: uri)
|
||||||
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
||||||
|
@ -404,6 +447,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
def skip_download?
|
def skip_download?
|
||||||
return @skip_download if defined?(@skip_download)
|
return @skip_download if defined?(@skip_download)
|
||||||
|
|
||||||
@skip_download ||= DomainBlock.reject_media?(@account.domain)
|
@skip_download ||= DomainBlock.reject_media?(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -436,11 +480,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
|
|
||||||
def forward_for_reply
|
def forward_for_reply
|
||||||
return unless @json['signature'].present? && reply_to_local?
|
return unless @json['signature'].present? && reply_to_local?
|
||||||
|
|
||||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_voters_count!
|
def increment_voters_count!
|
||||||
poll = replied_to_status.preloadable_poll
|
poll = replied_to_status.preloadable_poll
|
||||||
|
|
||||||
unless poll.voters_count.nil?
|
unless poll.voters_count.nil?
|
||||||
poll.voters_count = poll.voters_count + 1
|
poll.voters_count = poll.voters_count + 1
|
||||||
poll.save
|
poll.save
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
||||||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||||
|
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def self.default_key_transform
|
def self.default_key_transform
|
||||||
|
|
|
@ -287,9 +287,14 @@ class FeedManager
|
||||||
combined_regex = active_filters.reduce { |memo, obj| Regexp.union(memo, obj) }
|
combined_regex = active_filters.reduce { |memo, obj| Regexp.union(memo, obj) }
|
||||||
status = status.reblog if status.reblog?
|
status = status.reblog if status.reblog?
|
||||||
|
|
||||||
!combined_regex.match(Formatter.instance.plaintext(status)).nil? ||
|
combined_text = [
|
||||||
(status.spoiler_text.present? && !combined_regex.match(status.spoiler_text).nil?) ||
|
Formatter.instance.plaintext(status),
|
||||||
(status.preloadable_poll && !combined_regex.match(status.preloadable_poll.options.join("\n\n")).nil?)
|
status.spoiler_text,
|
||||||
|
status.preloadable_poll ? status.preloadable_poll.options.join("\n\n") : nil,
|
||||||
|
status.media_attachments.map(&:description).join("\n\n"),
|
||||||
|
].compact.join("\n\n")
|
||||||
|
|
||||||
|
!combined_regex.match(combined_text).nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a status to an account's feed, returning true if a status was
|
# Adds a status to an account's feed, returning true if a status was
|
||||||
|
|
|
@ -19,6 +19,8 @@ class InlineRenderer
|
||||||
serializer = REST::AnnouncementSerializer
|
serializer = REST::AnnouncementSerializer
|
||||||
when :reaction
|
when :reaction
|
||||||
serializer = REST::ReactionSerializer
|
serializer = REST::ReactionSerializer
|
||||||
|
when :encrypted_message
|
||||||
|
serializer = REST::EncryptedMessageSerializer
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
@ -126,4 +126,21 @@ class UserMailer < Devise::Mailer
|
||||||
reply_to: Setting.site_contact_email
|
reply_to: Setting.site_contact_email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_in_token(user, remote_ip, user_agent, timestamp)
|
||||||
|
@resource = user
|
||||||
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
@remote_ip = remote_ip
|
||||||
|
@user_agent = user_agent
|
||||||
|
@detection = Browser.new(user_agent)
|
||||||
|
@timestamp = timestamp.to_time.utc
|
||||||
|
|
||||||
|
return if @resource.disabled?
|
||||||
|
|
||||||
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
|
mail to: @resource.email,
|
||||||
|
subject: I18n.t('user_mailer.sign_in_token.subject'),
|
||||||
|
reply_to: Setting.site_contact_email
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
# hide_collections :boolean
|
# hide_collections :boolean
|
||||||
# avatar_storage_schema_version :integer
|
# avatar_storage_schema_version :integer
|
||||||
# header_storage_schema_version :integer
|
# header_storage_schema_version :integer
|
||||||
|
# devices_url :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
|
|
|
@ -9,6 +9,7 @@ module AccountAssociations
|
||||||
|
|
||||||
# Identity proofs
|
# Identity proofs
|
||||||
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
||||||
|
has_many :devices, dependent: :destroy, inverse_of: :account
|
||||||
|
|
||||||
# Timelines
|
# Timelines
|
||||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||||
|
|
35
app/models/device.rb
Normal file
35
app/models/device.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: devices
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# access_token_id :bigint(8)
|
||||||
|
# account_id :bigint(8)
|
||||||
|
# device_id :string default(""), not null
|
||||||
|
# name :string default(""), not null
|
||||||
|
# fingerprint_key :text default(""), not null
|
||||||
|
# identity_key :text default(""), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class Device < ApplicationRecord
|
||||||
|
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
has_many :one_time_keys, dependent: :destroy, inverse_of: :device
|
||||||
|
has_many :encrypted_messages, dependent: :destroy, inverse_of: :device
|
||||||
|
|
||||||
|
validates :name, :fingerprint_key, :identity_key, presence: true
|
||||||
|
validates :fingerprint_key, :identity_key, ed25519_key: true
|
||||||
|
|
||||||
|
before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invalidate_associations
|
||||||
|
one_time_keys.destroy_all
|
||||||
|
encrypted_messages.destroy_all
|
||||||
|
end
|
||||||
|
end
|
50
app/models/encrypted_message.rb
Normal file
50
app/models/encrypted_message.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: encrypted_messages
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# device_id :bigint(8)
|
||||||
|
# from_account_id :bigint(8)
|
||||||
|
# from_device_id :string default(""), not null
|
||||||
|
# type :integer default(0), not null
|
||||||
|
# body :text default(""), not null
|
||||||
|
# digest :text default(""), not null
|
||||||
|
# message_franking :text default(""), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class EncryptedMessage < ApplicationRecord
|
||||||
|
self.inheritance_column = nil
|
||||||
|
|
||||||
|
include Paginable
|
||||||
|
|
||||||
|
scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
|
||||||
|
|
||||||
|
belongs_to :device
|
||||||
|
belongs_to :from_account, class_name: 'Account'
|
||||||
|
|
||||||
|
around_create Mastodon::Snowflake::Callbacks
|
||||||
|
|
||||||
|
after_commit :push_to_streaming_api
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def push_to_streaming_api
|
||||||
|
Rails.logger.info(streaming_channel)
|
||||||
|
Rails.logger.info(subscribed_to_timeline?)
|
||||||
|
|
||||||
|
return if destroyed? || !subscribed_to_timeline?
|
||||||
|
|
||||||
|
PushEncryptedMessageWorker.perform_async(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribed_to_timeline?
|
||||||
|
Redis.current.exists("subscribed:#{streaming_channel}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def streaming_channel
|
||||||
|
"timeline:#{device.account_id}:#{device.device_id}"
|
||||||
|
end
|
||||||
|
end
|
19
app/models/message_franking.rb
Normal file
19
app/models/message_franking.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MessageFranking
|
||||||
|
attr_reader :hmac, :source_account_id, :target_account_id,
|
||||||
|
:timestamp, :original_franking
|
||||||
|
|
||||||
|
def initialize(attributes = {})
|
||||||
|
@hmac = attributes[:hmac]
|
||||||
|
@source_account_id = attributes[:source_account_id]
|
||||||
|
@target_account_id = attributes[:target_account_id]
|
||||||
|
@timestamp = attributes[:timestamp]
|
||||||
|
@original_franking = attributes[:original_franking]
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_token
|
||||||
|
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
|
||||||
|
crypt.encrypt_and_sign(self)
|
||||||
|
end
|
||||||
|
end
|
21
app/models/one_time_key.rb
Normal file
21
app/models/one_time_key.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: one_time_keys
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# device_id :bigint(8)
|
||||||
|
# key_id :string default(""), not null
|
||||||
|
# key :text default(""), not null
|
||||||
|
# signature :text default(""), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class OneTimeKey < ApplicationRecord
|
||||||
|
belongs_to :device
|
||||||
|
|
||||||
|
validates :key_id, :key, :signature, presence: true
|
||||||
|
validates :key, ed25519_key: true
|
||||||
|
validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
|
||||||
|
end
|
|
@ -23,19 +23,25 @@
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# embed_url :string default(""), not null
|
# embed_url :string default(""), not null
|
||||||
# image_storage_schema_version :integer
|
# image_storage_schema_version :integer
|
||||||
|
# blurhash :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class PreviewCard < ApplicationRecord
|
class PreviewCard < ApplicationRecord
|
||||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||||
LIMIT = 1.megabytes
|
LIMIT = 1.megabytes
|
||||||
|
|
||||||
|
BLURHASH_OPTIONS = {
|
||||||
|
x_comp: 4,
|
||||||
|
y_comp: 4,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
self.inheritance_column = false
|
self.inheritance_column = false
|
||||||
|
|
||||||
enum type: [:link, :photo, :video, :rich]
|
enum type: [:link, :photo, :video, :rich]
|
||||||
|
|
||||||
has_and_belongs_to_many :statuses
|
has_and_belongs_to_many :statuses
|
||||||
|
|
||||||
has_attached_file :image, styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }
|
||||||
|
|
||||||
include Attachmentable
|
include Attachmentable
|
||||||
|
|
||||||
|
@ -72,6 +78,7 @@ class PreviewCard < ApplicationRecord
|
||||||
geometry: '400x400>',
|
geometry: '400x400>',
|
||||||
file_geometry_parser: FastGeometryParser,
|
file_geometry_parser: FastGeometryParser,
|
||||||
convert_options: '-coalesce -strip',
|
convert_options: '-coalesce -strip',
|
||||||
|
blurhash: BLURHASH_OPTIONS,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
41
app/models/system_key.rb
Normal file
41
app/models/system_key.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: system_keys
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# key :binary
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class SystemKey < ApplicationRecord
|
||||||
|
ROTATION_PERIOD = 1.week.freeze
|
||||||
|
|
||||||
|
before_validation :set_key
|
||||||
|
|
||||||
|
scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - ROTATION_PERIOD * 3)) }
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def current_key
|
||||||
|
previous_key = order(id: :asc).last
|
||||||
|
|
||||||
|
if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
|
||||||
|
previous_key.key
|
||||||
|
else
|
||||||
|
create.key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_key
|
||||||
|
return if key.present?
|
||||||
|
|
||||||
|
cipher = OpenSSL::Cipher.new('AES-256-GCM')
|
||||||
|
cipher.encrypt
|
||||||
|
|
||||||
|
self.key = cipher.random_key
|
||||||
|
end
|
||||||
|
end
|
|
@ -38,6 +38,8 @@
|
||||||
# chosen_languages :string is an Array
|
# chosen_languages :string is an Array
|
||||||
# created_by_application_id :bigint(8)
|
# created_by_application_id :bigint(8)
|
||||||
# approved :boolean default(TRUE), not null
|
# approved :boolean default(TRUE), not null
|
||||||
|
# sign_in_token :string
|
||||||
|
# sign_in_token_sent_at :datetime
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
@ -114,7 +116,7 @@ class User < ApplicationRecord
|
||||||
:default_content_type, :system_emoji_font,
|
:default_content_type, :system_emoji_font,
|
||||||
to: :settings, prefix: :setting, allow_nil: false
|
to: :settings, prefix: :setting, allow_nil: false
|
||||||
|
|
||||||
attr_reader :invite_code
|
attr_reader :invite_code, :sign_in_token_attempt
|
||||||
attr_writer :external
|
attr_writer :external
|
||||||
|
|
||||||
def confirmed?
|
def confirmed?
|
||||||
|
@ -168,6 +170,10 @@ class User < ApplicationRecord
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def suspicious_sign_in?(ip)
|
||||||
|
!otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
|
||||||
|
end
|
||||||
|
|
||||||
def functional?
|
def functional?
|
||||||
confirmed? && approved? && !disabled? && !account.suspended?
|
confirmed? && approved? && !disabled? && !account.suspended?
|
||||||
end
|
end
|
||||||
|
@ -270,6 +276,13 @@ class User < ApplicationRecord
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def external_or_valid_password?(compare_password)
|
||||||
|
# If encrypted_password is blank, we got the user from LDAP or PAM,
|
||||||
|
# so credentials are already valid
|
||||||
|
|
||||||
|
encrypted_password.blank? || valid_password?(compare_password)
|
||||||
|
end
|
||||||
|
|
||||||
def send_reset_password_instructions
|
def send_reset_password_instructions
|
||||||
return false if encrypted_password.blank?
|
return false if encrypted_password.blank?
|
||||||
|
|
||||||
|
@ -305,6 +318,15 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sign_in_token_expired?
|
||||||
|
sign_in_token_sent_at.nil? || sign_in_token_sent_at < 5.minutes.ago
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_sign_in_token
|
||||||
|
self.sign_in_token = Devise.friendly_token(6)
|
||||||
|
self.sign_in_token_sent_at = Time.now.utc
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def send_devise_notification(notification, *args)
|
def send_devise_notification(notification, *args)
|
||||||
|
@ -321,6 +343,10 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def recent_ip?(ip)
|
||||||
|
recent_ips.any? { |(_, recent_ip)| recent_ip == ip }
|
||||||
|
end
|
||||||
|
|
||||||
def send_pending_devise_notifications
|
def send_pending_devise_notifications
|
||||||
pending_devise_notifications.each do |notification, args|
|
pending_devise_notifications.each do |notification, args|
|
||||||
render_and_send_devise_message(notification, *args)
|
render_and_send_devise_message(notification, *args)
|
||||||
|
|
41
app/presenters/activitypub/activity_presenter.rb
Normal file
41
app/presenters/activitypub/activity_presenter.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
|
||||||
|
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def from_status(status)
|
||||||
|
new.tap do |presenter|
|
||||||
|
presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status)
|
||||||
|
presenter.type = status.reblog? ? 'Announce' : 'Create'
|
||||||
|
presenter.actor = ActivityPub::TagManager.instance.uri_for(status.account)
|
||||||
|
presenter.published = status.created_at
|
||||||
|
presenter.to = ActivityPub::TagManager.instance.to(status)
|
||||||
|
presenter.cc = ActivityPub::TagManager.instance.cc(status)
|
||||||
|
|
||||||
|
presenter.virtual_object = begin
|
||||||
|
if status.reblog?
|
||||||
|
if status.account == status.proper.account && status.proper.private_visibility? && status.local?
|
||||||
|
status.proper
|
||||||
|
else
|
||||||
|
ActivityPub::TagManager.instance.uri_for(status.proper)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
status.proper
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_encrypted_message(encrypted_message)
|
||||||
|
new.tap do |presenter|
|
||||||
|
presenter.id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
||||||
|
presenter.type = 'Create'
|
||||||
|
presenter.actor = ActivityPub::TagManager.instance.uri_for(encrypted_message.source_account)
|
||||||
|
presenter.published = Time.now.utc
|
||||||
|
presenter.to = ActivityPub::TagManager.instance.uri_for(encrypted_message.target_account)
|
||||||
|
presenter.virtual_object = encrypted_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
class InitialStatePresenter < ActiveModelSerializers::Model
|
class InitialStatePresenter < ActiveModelSerializers::Model
|
||||||
attributes :settings, :push_subscription, :token,
|
attributes :settings, :push_subscription, :token,
|
||||||
:current_account, :admin, :text
|
:current_account, :admin, :text, :visibility
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,52 +1,22 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
class ActivityPub::ActivitySerializer < ActivityPub::Serializer
|
||||||
|
def self.serializer_for(model, options)
|
||||||
|
case model.class.name
|
||||||
|
when 'Status'
|
||||||
|
ActivityPub::NoteSerializer
|
||||||
|
when 'DeliverToDeviceService::EncryptedMessage'
|
||||||
|
ActivityPub::EncryptedMessageSerializer
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
attributes :id, :type, :actor, :published, :to, :cc
|
attributes :id, :type, :actor, :published, :to, :cc
|
||||||
|
|
||||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object?
|
has_one :virtual_object, key: :object
|
||||||
|
|
||||||
attribute :proper_uri, key: :object, unless: :serialize_object?
|
|
||||||
attribute :atom_uri, if: :announce?
|
|
||||||
|
|
||||||
def id
|
|
||||||
ActivityPub::TagManager.instance.activity_uri_for(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
|
||||||
announce? ? 'Announce' : 'Create'
|
|
||||||
end
|
|
||||||
|
|
||||||
def actor
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.account)
|
|
||||||
end
|
|
||||||
|
|
||||||
def published
|
def published
|
||||||
object.created_at.iso8601
|
object.published.iso8601
|
||||||
end
|
|
||||||
|
|
||||||
def to
|
|
||||||
ActivityPub::TagManager.instance.to(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def cc
|
|
||||||
ActivityPub::TagManager.instance.cc(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def proper_uri
|
|
||||||
ActivityPub::TagManager.instance.uri_for(object.proper)
|
|
||||||
end
|
|
||||||
|
|
||||||
def atom_uri
|
|
||||||
OStatus::TagManager.instance.uri_for(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
def announce?
|
|
||||||
object.reblog?
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialize_object?
|
|
||||||
return true unless announce?
|
|
||||||
# Serialize private self-boosts of local toots
|
|
||||||
object.account == object.proper.account && object.proper.private_visibility? && object.local?
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
|
|
||||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||||
:moved_to, :property_value, :identity_proof,
|
:moved_to, :property_value, :identity_proof,
|
||||||
:discoverable
|
:discoverable, :olm
|
||||||
|
|
||||||
attributes :id, :type, :following, :followers,
|
attributes :id, :type, :following, :followers,
|
||||||
:inbox, :outbox, :featured,
|
:inbox, :outbox, :featured,
|
||||||
|
@ -20,6 +20,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
has_many :virtual_tags, key: :tag
|
has_many :virtual_tags, key: :tag
|
||||||
has_many :virtual_attachments, key: :attachment
|
has_many :virtual_attachments, key: :attachment
|
||||||
|
|
||||||
|
attribute :devices, unless: :instance_actor?
|
||||||
attribute :moved_to, if: :moved?
|
attribute :moved_to, if: :moved?
|
||||||
attribute :also_known_as, if: :also_known_as?
|
attribute :also_known_as, if: :also_known_as?
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
|
has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists?
|
||||||
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
|
has_one :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
|
||||||
|
|
||||||
delegate :moved?, to: :object
|
delegate :moved?, :instance_actor?, to: :object
|
||||||
|
|
||||||
def id
|
def id
|
||||||
object.instance_actor? ? instance_actor_url : account_url(object)
|
object.instance_actor? ? instance_actor_url : account_url(object)
|
||||||
|
@ -68,6 +69,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||||
object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
|
object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def devices
|
||||||
|
account_collection_url(object, :devices)
|
||||||
|
end
|
||||||
|
|
||||||
def outbox
|
def outbox
|
||||||
account_outbox_url(object)
|
account_outbox_url(object)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ActivityPub::CollectionSerializer < ActivityPub::Serializer
|
class ActivityPub::CollectionSerializer < ActivityPub::Serializer
|
||||||
|
class StringSerializer < ActiveModel::Serializer
|
||||||
|
# Despite the name, it does not return a hash, but the same can be said of
|
||||||
|
# the ActiveModel::Serializer::CollectionSerializer class which handles
|
||||||
|
# arrays.
|
||||||
|
def serializable_hash(*_args)
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.serializer_for(model, options)
|
def self.serializer_for(model, options)
|
||||||
return ActivityPub::NoteSerializer if model.class.name == 'Status'
|
case model.class.name
|
||||||
return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter'
|
when 'Status'
|
||||||
super
|
ActivityPub::NoteSerializer
|
||||||
|
when 'Device'
|
||||||
|
ActivityPub::DeviceSerializer
|
||||||
|
when 'ActivityPub::CollectionPresenter'
|
||||||
|
ActivityPub::CollectionSerializer
|
||||||
|
when 'String'
|
||||||
|
StringSerializer
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :id, if: -> { object.id.present? }
|
attribute :id, if: -> { object.id.present? }
|
||||||
|
|
52
app/serializers/activitypub/device_serializer.rb
Normal file
52
app/serializers/activitypub/device_serializer.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::DeviceSerializer < ActivityPub::Serializer
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
class FingerprintKeySerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :public_key_base64
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Ed25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.fingerprint_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class IdentityKeySerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :public_key_base64
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Curve25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.identity_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :device_id, :type, :name, :claim
|
||||||
|
|
||||||
|
has_one :fingerprint_key, serializer: FingerprintKeySerializer
|
||||||
|
has_one :identity_key, serializer: IdentityKeySerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Device'
|
||||||
|
end
|
||||||
|
|
||||||
|
def claim
|
||||||
|
account_claim_url(object.account, id: object.device_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fingerprint_key
|
||||||
|
object
|
||||||
|
end
|
||||||
|
|
||||||
|
def identity_key
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
61
app/serializers/activitypub/encrypted_message_serializer.rb
Normal file
61
app/serializers/activitypub/encrypted_message_serializer.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::EncryptedMessageSerializer < ActivityPub::Serializer
|
||||||
|
context :security
|
||||||
|
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
class DeviceSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :device_id
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Device'
|
||||||
|
end
|
||||||
|
|
||||||
|
def device_id
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class DigestSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :digest_algorithm, :digest_value
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Digest'
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest_algorithm
|
||||||
|
'http://www.w3.org/2000/09/xmldsig#hmac-sha256'
|
||||||
|
end
|
||||||
|
|
||||||
|
def digest_value
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :type, :message_type, :cipher_text, :message_franking
|
||||||
|
|
||||||
|
has_one :attributed_to, serializer: DeviceSerializer
|
||||||
|
has_one :to, serializer: DeviceSerializer
|
||||||
|
has_one :digest, serializer: DigestSerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'EncryptedMessage'
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributed_to
|
||||||
|
object.source_device.device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def to
|
||||||
|
object.target_device_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_type
|
||||||
|
object.type
|
||||||
|
end
|
||||||
|
|
||||||
|
def cipher_text
|
||||||
|
object.body
|
||||||
|
end
|
||||||
|
end
|
35
app/serializers/activitypub/one_time_key_serializer.rb
Normal file
35
app/serializers/activitypub/one_time_key_serializer.rb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::OneTimeKeySerializer < ActivityPub::Serializer
|
||||||
|
context :security
|
||||||
|
|
||||||
|
context_extensions :olm
|
||||||
|
|
||||||
|
class SignatureSerializer < ActivityPub::Serializer
|
||||||
|
attributes :type, :signature_value
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Ed25519Signature'
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_value
|
||||||
|
object.signature
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attributes :key_id, :type, :public_key_base64
|
||||||
|
|
||||||
|
has_one :signature, serializer: SignatureSerializer
|
||||||
|
|
||||||
|
def type
|
||||||
|
'Curve25519Key'
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_key_base64
|
||||||
|
object.key
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature
|
||||||
|
object
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,14 @@
|
||||||
|
|
||||||
class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer
|
class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer
|
||||||
def self.serializer_for(model, options)
|
def self.serializer_for(model, options)
|
||||||
return ActivityPub::ActivitySerializer if model.is_a?(Status)
|
if model.class.name == 'ActivityPub::ActivityPresenter'
|
||||||
super
|
ActivityPub::ActivitySerializer
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def items
|
||||||
|
object.items.map { |status| ActivityPub::ActivityPresenter.from_status(status) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
||||||
attributes :id, :type, :actor, :to
|
attributes :id, :type, :actor, :to
|
||||||
|
|
||||||
has_one :object, serializer: ActivityPub::ActivitySerializer
|
has_one :virtual_object, key: :object, serializer: ActivityPub::ActivitySerializer
|
||||||
|
|
||||||
def id
|
def id
|
||||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#announces/', object.id, '/undo'].join
|
[ActivityPub::TagManager.instance.uri_for(object.account), '#announces/', object.id, '/undo'].join
|
||||||
|
@ -20,4 +20,8 @@ class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
||||||
def to
|
def to
|
||||||
[ActivityPub::TagManager::COLLECTIONS[:public]]
|
[ActivityPub::TagManager::COLLECTIONS[:public]]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def virtual_object
|
||||||
|
ActivityPub::ActivityPresenter.from_status(object)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,7 +72,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
if object.current_account
|
if object.current_account
|
||||||
store[:me] = object.current_account.id.to_s
|
store[:me] = object.current_account.id.to_s
|
||||||
store[:default_privacy] = object.current_account.user.setting_default_privacy
|
store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy
|
||||||
store[:default_sensitive] = object.current_account.user.setting_default_sensitive
|
store[:default_sensitive] = object.current_account.user.setting_default_sensitive
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
19
app/serializers/rest/encrypted_message_serializer.rb
Normal file
19
app/serializers/rest/encrypted_message_serializer.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::EncryptedMessageSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :account_id, :device_id,
|
||||||
|
:type, :body, :digest, :message_franking,
|
||||||
|
:created_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.from_account_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def device_id
|
||||||
|
object.from_device_id
|
||||||
|
end
|
||||||
|
end
|
9
app/serializers/rest/keys/claim_result_serializer.rb
Normal file
9
app/serializers/rest/keys/claim_result_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::ClaimResultSerializer < ActiveModel::Serializer
|
||||||
|
attributes :account_id, :device_id, :key_id, :key, :signature
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.account.id.to_s
|
||||||
|
end
|
||||||
|
end
|
6
app/serializers/rest/keys/device_serializer.rb
Normal file
6
app/serializers/rest/keys/device_serializer.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
|
||||||
|
attributes :device_id, :name, :identity_key,
|
||||||
|
:fingerprint_key
|
||||||
|
end
|
11
app/serializers/rest/keys/query_result_serializer.rb
Normal file
11
app/serializers/rest/keys/query_result_serializer.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::Keys::QueryResultSerializer < ActiveModel::Serializer
|
||||||
|
attributes :account_id
|
||||||
|
|
||||||
|
has_many :devices, serializer: REST::Keys::DeviceSerializer
|
||||||
|
|
||||||
|
def account_id
|
||||||
|
object.account.id.to_s
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
||||||
attributes :url, :title, :description, :type,
|
attributes :url, :title, :description, :type,
|
||||||
:author_name, :author_url, :provider_name,
|
:author_name, :author_url, :provider_name,
|
||||||
:provider_url, :html, :width, :height,
|
:provider_url, :html, :width, :height,
|
||||||
:image, :embed_url
|
:image, :embed_url, :blurhash
|
||||||
|
|
||||||
def image
|
def image
|
||||||
object.image? ? full_asset_url(object.image.url(:original)) : nil
|
object.image? ? full_asset_url(object.image.url(:original)) : nil
|
||||||
|
|
|
@ -76,6 +76,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
||||||
@account.followers_url = @json['followers'] || ''
|
@account.followers_url = @json['followers'] || ''
|
||||||
@account.featured_collection_url = @json['featured'] || ''
|
@account.featured_collection_url = @json['featured'] || ''
|
||||||
|
@account.devices_url = @json['devices'] || ''
|
||||||
@account.url = url || @uri
|
@account.url = url || @uri
|
||||||
@account.uri = @uri
|
@account.uri = @uri
|
||||||
@account.display_name = @json['name'] || ''
|
@account.display_name = @json['name'] || ''
|
||||||
|
|
|
@ -22,7 +22,7 @@ class BackupService < BaseService
|
||||||
|
|
||||||
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
|
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
|
||||||
statuses.each do |status|
|
statuses.each do |status|
|
||||||
item = serialize_payload(status, ActivityPub::ActivitySerializer, signer: @account, allow_local_only: true)
|
item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer, signer: @account, allow_local_only: true)
|
||||||
item.delete(:'@context')
|
item.delete(:'@context')
|
||||||
|
|
||||||
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
|
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
|
||||||
|
|
|
@ -26,59 +26,20 @@ class BlockDomainService < BaseService
|
||||||
suspend_accounts!
|
suspend_accounts!
|
||||||
end
|
end
|
||||||
|
|
||||||
clear_media! if domain_block.reject_media?
|
DomainClearMediaWorker.perform_async(domain_block.id) if domain_block.reject_media?
|
||||||
end
|
|
||||||
|
|
||||||
def invalidate_association_caches!
|
|
||||||
# Normally, associated models of a status are immutable (except for accounts)
|
|
||||||
# so they are aggressively cached. After updating the media attachments to no
|
|
||||||
# longer point to a local file, we need to clear the cache to make those
|
|
||||||
# changes appear in the API and UI
|
|
||||||
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def silence_accounts!
|
def silence_accounts!
|
||||||
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
|
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_media!
|
|
||||||
@affected_status_ids = []
|
|
||||||
|
|
||||||
clear_account_images!
|
|
||||||
clear_account_attachments!
|
|
||||||
clear_emojos!
|
|
||||||
|
|
||||||
invalidate_association_caches!
|
|
||||||
end
|
|
||||||
|
|
||||||
def suspend_accounts!
|
def suspend_accounts!
|
||||||
blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account|
|
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
|
||||||
|
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
|
||||||
SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_account_images!
|
|
||||||
blocked_domain_accounts.reorder(nil).find_each do |account|
|
|
||||||
account.avatar.destroy if account.avatar.exists?
|
|
||||||
account.header.destroy if account.header.exists?
|
|
||||||
account.save
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_account_attachments!
|
|
||||||
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
|
||||||
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
|
||||||
|
|
||||||
attachment.file.destroy if attachment.file.exists?
|
|
||||||
attachment.type = :unknown
|
|
||||||
attachment.save
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_emojos!
|
|
||||||
emojis_from_blocked_domains.destroy_all
|
|
||||||
end
|
|
||||||
|
|
||||||
def blocked_domain
|
def blocked_domain
|
||||||
domain_block.domain
|
domain_block.domain
|
||||||
end
|
end
|
||||||
|
@ -86,12 +47,4 @@ class BlockDomainService < BaseService
|
||||||
def blocked_domain_accounts
|
def blocked_domain_accounts
|
||||||
Account.by_domain_and_subdomains(blocked_domain)
|
Account.by_domain_and_subdomains(blocked_domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_from_blocked_domain
|
|
||||||
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
def emojis_from_blocked_domains
|
|
||||||
CustomEmoji.by_domain_and_subdomains(blocked_domain)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
70
app/services/clear_domain_media_service.rb
Normal file
70
app/services/clear_domain_media_service.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ClearDomainMediaService < BaseService
|
||||||
|
attr_reader :domain_block
|
||||||
|
|
||||||
|
def call(domain_block)
|
||||||
|
@domain_block = domain_block
|
||||||
|
clear_media! if domain_block.reject_media?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invalidate_association_caches!
|
||||||
|
# Normally, associated models of a status are immutable (except for accounts)
|
||||||
|
# so they are aggressively cached. After updating the media attachments to no
|
||||||
|
# longer point to a local file, we need to clear the cache to make those
|
||||||
|
# changes appear in the API and UI
|
||||||
|
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_media!
|
||||||
|
@affected_status_ids = []
|
||||||
|
|
||||||
|
begin
|
||||||
|
clear_account_images!
|
||||||
|
clear_account_attachments!
|
||||||
|
clear_emojos!
|
||||||
|
ensure
|
||||||
|
invalidate_association_caches!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_account_images!
|
||||||
|
blocked_domain_accounts.reorder(nil).find_each do |account|
|
||||||
|
account.avatar.destroy if account.avatar&.exists?
|
||||||
|
account.header.destroy if account.header&.exists?
|
||||||
|
account.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_account_attachments!
|
||||||
|
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
||||||
|
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||||
|
|
||||||
|
attachment.file.destroy if attachment.file&.exists?
|
||||||
|
attachment.type = :unknown
|
||||||
|
attachment.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_emojos!
|
||||||
|
emojis_from_blocked_domains.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_domain
|
||||||
|
domain_block.domain
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_domain_accounts
|
||||||
|
Account.by_domain_and_subdomains(blocked_domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_from_blocked_domain
|
||||||
|
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def emojis_from_blocked_domains
|
||||||
|
CustomEmoji.by_domain_and_subdomains(blocked_domain)
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,8 +5,9 @@ module Payloadable
|
||||||
signer = options.delete(:signer)
|
signer = options.delete(:signer)
|
||||||
sign_with = options.delete(:sign_with)
|
sign_with = options.delete(:sign_with)
|
||||||
payload = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json
|
payload = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json
|
||||||
|
object = record.respond_to?(:virtual_object) ? record.virtual_object : record
|
||||||
|
|
||||||
if (record.respond_to?(:sign?) && record.sign?) && signer && signing_enabled?
|
if (object.respond_to?(:sign?) && object.sign?) && signer && signing_enabled?
|
||||||
ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
|
ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with)
|
||||||
else
|
else
|
||||||
payload
|
payload
|
||||||
|
|
78
app/services/deliver_to_device_service.rb
Normal file
78
app/services/deliver_to_device_service.rb
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DeliverToDeviceService < BaseService
|
||||||
|
include Payloadable
|
||||||
|
|
||||||
|
class EncryptedMessage < ActiveModelSerializers::Model
|
||||||
|
attributes :source_account, :target_account, :source_device,
|
||||||
|
:target_device_id, :type, :body, :digest,
|
||||||
|
:message_franking
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(source_account, source_device, options = {})
|
||||||
|
@source_account = source_account
|
||||||
|
@source_device = source_device
|
||||||
|
@target_account = Account.find(options[:account_id])
|
||||||
|
@target_device_id = options[:device_id]
|
||||||
|
@body = options[:body]
|
||||||
|
@type = options[:type]
|
||||||
|
@hmac = options[:hmac]
|
||||||
|
|
||||||
|
set_message_franking!
|
||||||
|
|
||||||
|
if @target_account.local?
|
||||||
|
deliver_to_local!
|
||||||
|
else
|
||||||
|
deliver_to_remote!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_message_franking!
|
||||||
|
@message_franking = message_franking.to_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_to_local!
|
||||||
|
target_device = @target_account.devices.find_by!(device_id: @target_device_id)
|
||||||
|
|
||||||
|
target_device.encrypted_messages.create!(
|
||||||
|
from_account: @source_account,
|
||||||
|
from_device_id: @source_device.device_id,
|
||||||
|
type: @type,
|
||||||
|
body: @body,
|
||||||
|
digest: @hmac,
|
||||||
|
message_franking: @message_franking
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_to_remote!
|
||||||
|
ActivityPub::DeliveryWorker.perform_async(
|
||||||
|
Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_encrypted_message(encrypted_message), ActivityPub::ActivitySerializer)),
|
||||||
|
@source_account.id,
|
||||||
|
@target_account.inbox_url
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_franking
|
||||||
|
MessageFranking.new(
|
||||||
|
source_account_id: @source_account.id,
|
||||||
|
target_account_id: @target_account.id,
|
||||||
|
hmac: @hmac,
|
||||||
|
timestamp: Time.now.utc
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_message
|
||||||
|
EncryptedMessage.new(
|
||||||
|
source_account: @source_account,
|
||||||
|
target_account: @target_account,
|
||||||
|
source_device: @source_device,
|
||||||
|
target_device_id: @target_device_id,
|
||||||
|
type: @type,
|
||||||
|
body: @body,
|
||||||
|
digest: @hmac,
|
||||||
|
message_franking: @message_franking
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -81,7 +81,9 @@ class ImportService < BaseService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Import::RelationshipWorker.push_bulk(items) do |acct, extra|
|
head_items = items.uniq { |acct, _| acct.split('@')[1] }
|
||||||
|
tail_items = items - head_items
|
||||||
|
Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
|
||||||
[@account.id, acct, action, extra]
|
[@account.id, acct, action, extra]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
77
app/services/keys/claim_service.rb
Normal file
77
app/services/keys/claim_service.rb
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Keys::ClaimService < BaseService
|
||||||
|
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
||||||
|
|
||||||
|
class Result < ActiveModelSerializers::Model
|
||||||
|
attributes :account, :device_id, :key_id,
|
||||||
|
:key, :signature
|
||||||
|
|
||||||
|
def initialize(account, device_id, key_attributes = {})
|
||||||
|
@account = account
|
||||||
|
@device_id = device_id
|
||||||
|
@key_id = key_attributes[:key_id]
|
||||||
|
@key = key_attributes[:key]
|
||||||
|
@signature = key_attributes[:signature]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(source_account, target_account_id, device_id)
|
||||||
|
@source_account = source_account
|
||||||
|
@target_account = Account.find(target_account_id)
|
||||||
|
@device_id = device_id
|
||||||
|
|
||||||
|
if @target_account.local?
|
||||||
|
claim_local_key!
|
||||||
|
else
|
||||||
|
claim_remote_key!
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def claim_local_key!
|
||||||
|
device = @target_account.devices.find_by(device_id: @device_id)
|
||||||
|
key = nil
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
key = device.one_time_keys.order(Arel.sql('random()')).first!
|
||||||
|
key.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
@result = Result.new(@target_account, @device_id, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def claim_remote_key!
|
||||||
|
query_result = QueryService.new.call(@target_account)
|
||||||
|
device = query_result.find(@device_id)
|
||||||
|
|
||||||
|
return unless device.present? && device.valid_claim_url?
|
||||||
|
|
||||||
|
json = fetch_resource_with_post(device.claim_url)
|
||||||
|
|
||||||
|
return unless json.present? && json['publicKeyBase64'].present?
|
||||||
|
|
||||||
|
@result = Result.new(@target_account, @device_id, key_id: json['id'], key: json['publicKeyBase64'], signature: json.dig('signature', 'signatureValue'))
|
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||||
|
Rails.logger.debug "Claiming one-time key for #{@target_account.acct}:#{@device_id} failed: #{e}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_resource_with_post(uri)
|
||||||
|
build_post_request(uri).perform do |response|
|
||||||
|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
|
||||||
|
|
||||||
|
body_to_json(response.body_with_limit) if response.code == 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_post_request(uri)
|
||||||
|
Request.new(:post, uri).tap do |request|
|
||||||
|
request.on_behalf_of(@source_account, :uri)
|
||||||
|
request.add_headers(HEADERS)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
75
app/services/keys/query_service.rb
Normal file
75
app/services/keys/query_service.rb
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Keys::QueryService < BaseService
|
||||||
|
include JsonLdHelper
|
||||||
|
|
||||||
|
class Result < ActiveModelSerializers::Model
|
||||||
|
attributes :account, :devices
|
||||||
|
|
||||||
|
def initialize(account, devices)
|
||||||
|
@account = account
|
||||||
|
@devices = devices || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def find(device_id)
|
||||||
|
@devices.find { |device| device.device_id == device_id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Device < ActiveModelSerializers::Model
|
||||||
|
attributes :device_id, :name, :identity_key, :fingerprint_key
|
||||||
|
|
||||||
|
def initialize(attributes = {})
|
||||||
|
@device_id = attributes[:device_id]
|
||||||
|
@name = attributes[:name]
|
||||||
|
@identity_key = attributes[:identity_key]
|
||||||
|
@fingerprint_key = attributes[:fingerprint_key]
|
||||||
|
@claim_url = attributes[:claim_url]
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_claim_url?
|
||||||
|
return false if @claim_url.blank?
|
||||||
|
|
||||||
|
begin
|
||||||
|
parsed_url = Addressable::URI.parse(@claim_url).normalize
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(account)
|
||||||
|
@account = account
|
||||||
|
|
||||||
|
if @account.local?
|
||||||
|
query_local_devices!
|
||||||
|
else
|
||||||
|
query_remote_devices!
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.new(@account, @devices)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def query_local_devices!
|
||||||
|
@devices = @account.devices.map { |device| Device.new(device) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_remote_devices!
|
||||||
|
return if @account.devices_url.blank?
|
||||||
|
|
||||||
|
json = fetch_resource(@account.devices_url)
|
||||||
|
|
||||||
|
return if json['items'].blank?
|
||||||
|
|
||||||
|
@devices = json['items'].map do |device|
|
||||||
|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||||
|
end
|
||||||
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||||
|
Rails.logger.debug "Querying devices for #{@account.acct} failed: #{e}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -65,7 +65,7 @@ class ProcessMentionsService < BaseService
|
||||||
|
|
||||||
def activitypub_json
|
def activitypub_json
|
||||||
return @activitypub_json if defined?(@activitypub_json)
|
return @activitypub_json if defined?(@activitypub_json)
|
||||||
@activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
|
@activitypub_json = Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status), ActivityPub::ActivitySerializer, signer: @status.account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_account_service
|
def resolve_account_service
|
||||||
|
|
|
@ -60,6 +60,6 @@ class ReblogService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_json(reblog)
|
def build_json(reblog)
|
||||||
Oj.dump(serialize_payload(reblog, ActivityPub::ActivitySerializer, signer: reblog.account))
|
Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(reblog), ActivityPub::ActivitySerializer, signer: reblog.account))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -112,6 +112,8 @@ class ResolveAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger_update_due?
|
def webfinger_update_due?
|
||||||
|
return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
|
||||||
|
|
||||||
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
19
app/validators/ed25519_key_validator.rb
Normal file
19
app/validators/ed25519_key_validator.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ed25519KeyValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
key = Base64.decode64(value)
|
||||||
|
|
||||||
|
record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verified?(key)
|
||||||
|
Ed25519.validate_key_bytes(key)
|
||||||
|
rescue ArgumentError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
29
app/validators/ed25519_signature_validator.rb
Normal file
29
app/validators/ed25519_signature_validator.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Ed25519SignatureValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
verify_key = Ed25519::VerifyKey.new(Base64.decode64(option_to_value(record, :verify_key)))
|
||||||
|
signature = Base64.decode64(value)
|
||||||
|
message = option_to_value(record, :message)
|
||||||
|
|
||||||
|
record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def verified?(verify_key, signature, message)
|
||||||
|
verify_key.verify(signature, message)
|
||||||
|
rescue Ed25519::VerifyError, ArgumentError
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def option_to_value(record, key)
|
||||||
|
if options[key].is_a?(Proc)
|
||||||
|
options[key].call(record)
|
||||||
|
else
|
||||||
|
record.public_send(options[key])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -55,12 +55,15 @@
|
||||||
%p= t('about.unavailable_content_html')
|
%p= t('about.unavailable_content_html')
|
||||||
|
|
||||||
- if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty?
|
- if (blocks = @blocks.select(&:reject_media?)) && !blocks.empty?
|
||||||
|
%h3= t('about.unavailable_content_description.rejecting_media_title')
|
||||||
%p= t('about.unavailable_content_description.rejecting_media')
|
%p= t('about.unavailable_content_description.rejecting_media')
|
||||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||||
- if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
|
- if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
|
||||||
|
%h3= t('about.unavailable_content_description.silenced_title')
|
||||||
%p= t('about.unavailable_content_description.silenced')
|
%p= t('about.unavailable_content_description.silenced')
|
||||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||||
- if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
|
- if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
|
||||||
|
%h3= t('about.unavailable_content_description.suspended_title')
|
||||||
%p= t('about.unavailable_content_description.suspended')
|
%p= t('about.unavailable_content_description.suspended')
|
||||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('admin.custom_emojis.title')
|
= t('admin.custom_emojis.title')
|
||||||
|
|
||||||
- content_for :heading_actions do
|
- if can?(:create, :custom_emoji)
|
||||||
= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button'
|
- content_for :heading_actions do
|
||||||
|
= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button'
|
||||||
|
|
||||||
.filters
|
.filters
|
||||||
.filter-subset
|
.filter-subset
|
||||||
|
@ -55,9 +56,10 @@
|
||||||
|
|
||||||
= f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.disable')]), name: :disable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
= f.button safe_join([fa_icon('power-off'), t('admin.custom_emojis.disable')]), name: :disable, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
|
||||||
= f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
- if can?(:destroy, :custom_emoji)
|
||||||
|
= f.button safe_join([fa_icon('times'), t('admin.custom_emojis.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
|
||||||
- unless params[:local] == '1'
|
- if can?(:copy, :custom_emoji) && params[:local] != '1'
|
||||||
= f.button safe_join([fa_icon('copy'), t('admin.custom_emojis.copy')]), name: :copy, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
= f.button safe_join([fa_icon('copy'), t('admin.custom_emojis.copy')]), name: :copy, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||||
|
|
||||||
- if params[:local] == '1'
|
- if params[:local] == '1'
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('admin.instances.title')
|
= t('admin.instances.title')
|
||||||
|
|
||||||
|
- content_for :heading_actions do
|
||||||
|
- if whitelist_mode?
|
||||||
|
= link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
|
||||||
|
- else
|
||||||
|
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
|
||||||
|
|
||||||
.filters
|
.filters
|
||||||
.filter-subset
|
.filter-subset
|
||||||
%strong= t('admin.instances.moderation.title')
|
%strong= t('admin.instances.moderation.title')
|
||||||
|
@ -10,12 +16,6 @@
|
||||||
- unless whitelist_mode?
|
- unless whitelist_mode?
|
||||||
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
|
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
|
||||||
|
|
||||||
%div.special-action-button
|
|
||||||
- if whitelist_mode?
|
|
||||||
= link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
|
|
||||||
- else
|
|
||||||
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
|
|
||||||
|
|
||||||
- unless whitelist_mode?
|
- unless whitelist_mode?
|
||||||
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
|
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
|
||||||
.fields-group
|
.fields-group
|
||||||
|
|
14
app/views/auth/sessions/sign_in_token.html.haml
Normal file
14
app/views/auth/sessions/sign_in_token.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('auth.login')
|
||||||
|
|
||||||
|
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||||
|
%p.hint.otp-hint= t('users.suspicious_sign_in_confirmation')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :sign_in_token_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.sign_in_token_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.sign_in_token_attempt'), :autocomplete => 'off' }, autofocus: true
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('auth.login'), type: :submit
|
||||||
|
|
||||||
|
- if Setting.site_contact_email.present?
|
||||||
|
%p.hint.subtle-hint= t('users.generic_access_help_html', email: mail_to(Setting.site_contact_email, nil))
|
|
@ -39,7 +39,7 @@
|
||||||
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- elsif status.preview_card
|
- elsif status.preview_card
|
||||||
= react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
|
= react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
|
||||||
|
|
||||||
.detailed-status__meta
|
.detailed-status__meta
|
||||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- elsif status.preview_card
|
- elsif status.preview_card
|
||||||
= react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
|
= react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
|
||||||
|
|
||||||
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
|
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
|
||||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
|
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||||
|
|
105
app/views/user_mailer/sign_in_token.html.haml
Normal file
105
app/views/user_mailer/sign_in_token.html.haml
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.hero
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center.padded
|
||||||
|
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td
|
||||||
|
= image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
|
||||||
|
|
||||||
|
%h1= t 'user_mailer.sign_in_token.title'
|
||||||
|
%p.lead= t 'user_mailer.sign_in_token.explanation'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell.content-start
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.input-cell
|
||||||
|
%table.input{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td= @resource.sign_in_token
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.sign_in_token.details'
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p
|
||||||
|
%strong= "#{t('sessions.ip')}:"
|
||||||
|
= @remote_ip
|
||||||
|
%br/
|
||||||
|
%strong= "#{t('sessions.browser')}:"
|
||||||
|
%span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")
|
||||||
|
%br/
|
||||||
|
= l(@timestamp)
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
.email-row
|
||||||
|
.col-6
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.text-center
|
||||||
|
%p= t 'user_mailer.sign_in_token.further_actions'
|
||||||
|
|
||||||
|
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.email-body
|
||||||
|
.email-container
|
||||||
|
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.content-cell
|
||||||
|
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.column-cell.button-cell
|
||||||
|
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%td.button-primary
|
||||||
|
= link_to edit_user_registration_url do
|
||||||
|
%span= t 'settings.account_settings'
|
17
app/views/user_mailer/sign_in_token.text.erb
Normal file
17
app/views/user_mailer/sign_in_token.text.erb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<%= t 'user_mailer.sign_in_token.title' %>
|
||||||
|
|
||||||
|
===
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.explanation' %>
|
||||||
|
|
||||||
|
=> <%= @resource.sign_in_token %>
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.details' %>
|
||||||
|
|
||||||
|
<%= t('sessions.ip') %>: <%= @remote_ip %>
|
||||||
|
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
|
||||||
|
<%= l(@timestamp) %>
|
||||||
|
|
||||||
|
<%= t 'user_mailer.sign_in_token.further_actions' %>
|
||||||
|
|
||||||
|
=> <%= edit_user_registration_url %>
|
|
@ -43,7 +43,7 @@ class ActivityPub::DistributionWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @account))
|
@payload ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status), ActivityPub::ActivitySerializer, signer: @account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def relay!
|
def relay!
|
||||||
|
|
|
@ -29,6 +29,6 @@ class ActivityPub::ReplyDistributionWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def payload
|
def payload
|
||||||
@payload ||= Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account))
|
@payload ||= Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_status(@status), ActivityPub::ActivitySerializer, signer: @status.account))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,8 +4,9 @@ class DomainBlockWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
|
||||||
def perform(domain_block_id, update = false)
|
def perform(domain_block_id, update = false)
|
||||||
BlockDomainService.new.call(DomainBlock.find(domain_block_id), update)
|
domain_block = DomainBlock.find_by(id: domain_block_id)
|
||||||
rescue ActiveRecord::RecordNotFound
|
return true if domain_block.nil?
|
||||||
true
|
|
||||||
|
BlockDomainService.new.call(domain_block, update)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
14
app/workers/domain_clear_media_worker.rb
Normal file
14
app/workers/domain_clear_media_worker.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DomainClearMediaWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
|
def perform(domain_block_id)
|
||||||
|
domain_block = DomainBlock.find_by(id: domain_block_id)
|
||||||
|
return true if domain_block.nil?
|
||||||
|
|
||||||
|
ClearDomainMediaService.new.call(domain_block)
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,8 @@ class Import::RelationshipWorker
|
||||||
|
|
||||||
def perform(account_id, target_account_uri, relationship, options = {})
|
def perform(account_id, target_account_uri, relationship, options = {})
|
||||||
from_account = Account.find(account_id)
|
from_account = Account.find(account_id)
|
||||||
target_account = ResolveAccountService.new.call(target_account_uri)
|
target_domain = domain(target_account_uri)
|
||||||
|
target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) }
|
||||||
options.symbolize_keys!
|
options.symbolize_keys!
|
||||||
|
|
||||||
return if target_account.nil?
|
return if target_account.nil?
|
||||||
|
@ -29,4 +30,22 @@ class Import::RelationshipWorker
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def domain(uri)
|
||||||
|
domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
|
||||||
|
TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stoplight_wrap_request(domain, &block)
|
||||||
|
if domain.present?
|
||||||
|
Stoplight("source:#{domain}", &block)
|
||||||
|
.with_fallback { nil }
|
||||||
|
.with_threshold(1)
|
||||||
|
.with_cool_off_time(5.minutes.seconds)
|
||||||
|
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
||||||
|
.run
|
||||||
|
else
|
||||||
|
block.call
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
|
|
||||||
class PushConversationWorker
|
class PushConversationWorker
|
||||||
include Sidekiq::Worker
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
def perform(conversation_account_id)
|
def perform(conversation_account_id)
|
||||||
conversation = AccountConversation.find(conversation_account_id)
|
conversation = AccountConversation.find(conversation_account_id)
|
||||||
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
||||||
timeline_id = "timeline:direct:#{conversation.account_id}"
|
timeline_id = "timeline:direct:#{conversation.account_id}"
|
||||||
|
|
||||||
Redis.current.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
redis.publish(timeline_id, Oj.dump(event: :conversation, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
16
app/workers/push_encrypted_message_worker.rb
Normal file
16
app/workers/push_encrypted_message_worker.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PushEncryptedMessageWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
def perform(encrypted_message_id)
|
||||||
|
encrypted_message = EncryptedMessage.find(encrypted_message_id)
|
||||||
|
message = InlineRenderer.render(encrypted_message, nil, :encrypted_message)
|
||||||
|
timeline_id = "timeline:#{encrypted_message.device.account_id}:#{encrypted_message.device.device_id}"
|
||||||
|
|
||||||
|
redis.publish(timeline_id, Oj.dump(event: :encrypted_message, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,5 +8,6 @@ class Scheduler::DoorkeeperCleanupScheduler
|
||||||
def perform
|
def perform
|
||||||
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
Doorkeeper::AccessToken.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
||||||
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
Doorkeeper::AccessGrant.where('revoked_at IS NOT NULL').where('revoked_at < NOW()').delete_all
|
||||||
|
SystemKey.expired.delete_all
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,25 +1,5 @@
|
||||||
{
|
{
|
||||||
"ignored_warnings": [
|
"ignored_warnings": [
|
||||||
{
|
|
||||||
"warning_type": "Mass Assignment",
|
|
||||||
"warning_code": 105,
|
|
||||||
"fingerprint": "0117d2be5947ea4e4fbed9c15f23c6615b12c6892973411820c83d079808819d",
|
|
||||||
"check_name": "PermitAttributes",
|
|
||||||
"message": "Potentially dangerous key allowed for mass assignment",
|
|
||||||
"file": "app/controllers/api/v1/search_controller.rb",
|
|
||||||
"line": 30,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
|
||||||
"code": "params.permit(:type, :offset, :min_id, :max_id, :account_id)",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Api::V1::SearchController",
|
|
||||||
"method": "search_params"
|
|
||||||
},
|
|
||||||
"user_input": ":account_id",
|
|
||||||
"confidence": "High",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -27,7 +7,7 @@
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/report.rb",
|
"file": "app/models/report.rb",
|
||||||
"line": 90,
|
"line": 112,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
|
"code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
|
@ -47,7 +27,7 @@
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/status.rb",
|
"file": "app/models/status.rb",
|
||||||
"line": 87,
|
"line": 100,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
"code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
|
@ -61,39 +41,62 @@
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Dynamic Render Path",
|
||||||
"warning_code": 105,
|
"warning_code": 15,
|
||||||
"fingerprint": "28d81cc22580ef76e912b077b245f353499aa27b3826476667224c00227af2a9",
|
"fingerprint": "20a660939f2bbf8c665e69f2844031c0564524689a9570a0091ed94846212020",
|
||||||
"check_name": "PermitAttributes",
|
"check_name": "Render",
|
||||||
"message": "Potentially dangerous key allowed for mass assignment",
|
"message": "Render path contains parameter value",
|
||||||
"file": "app/controllers/admin/reports_controller.rb",
|
"file": "app/views/admin/action_logs/index.html.haml",
|
||||||
"line": 56,
|
"line": 26,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
"code": "params.permit(:account_id, :resolved, :target_account_id)",
|
"code": "render(action => Admin::ActionLogFilter.new(filter_params).results.page(params[:page]), {})",
|
||||||
"render_path": null,
|
"render_path": [
|
||||||
|
{
|
||||||
|
"type": "controller",
|
||||||
|
"class": "Admin::ActionLogsController",
|
||||||
|
"method": "index",
|
||||||
|
"line": 8,
|
||||||
|
"file": "app/controllers/admin/action_logs_controller.rb",
|
||||||
|
"rendered": {
|
||||||
|
"name": "admin/action_logs/index",
|
||||||
|
"file": "app/views/admin/action_logs/index.html.haml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "template",
|
||||||
"class": "Admin::ReportsController",
|
"template": "admin/action_logs/index"
|
||||||
"method": "filter_params"
|
|
||||||
},
|
},
|
||||||
"user_input": ":account_id",
|
"user_input": "params[:page]",
|
||||||
"confidence": "High",
|
"confidence": "Weak",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Dynamic Render Path",
|
"warning_type": "Dynamic Render Path",
|
||||||
"warning_code": 15,
|
"warning_code": 15,
|
||||||
"fingerprint": "4b6a895e2805578d03ceedbe1d469cc75a0c759eba093722523edb4b8683c873",
|
"fingerprint": "371fe16dc4c9d6ab08a20437d65be4825776107a67c38f6d4780a9c703cd44a5",
|
||||||
"check_name": "Render",
|
"check_name": "Render",
|
||||||
"message": "Render path contains parameter value",
|
"message": "Render path contains parameter value",
|
||||||
"file": "app/views/admin/action_logs/index.html.haml",
|
"file": "app/views/admin/email_domain_blocks/index.html.haml",
|
||||||
"line": 4,
|
"line": 17,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
"code": "render(action => Admin::ActionLog.page(params[:page]), {})",
|
"code": "render(action => EmailDomainBlock.where(:parent_id => nil).includes(:children).order(:id => :desc).page(params[:page]), {})",
|
||||||
"render_path": [{"type":"controller","class":"Admin::ActionLogsController","method":"index","line":7,"file":"app/controllers/admin/action_logs_controller.rb","rendered":{"name":"admin/action_logs/index","file":"/home/eugr/Projects/mastodon/app/views/admin/action_logs/index.html.haml"}}],
|
"render_path": [
|
||||||
|
{
|
||||||
|
"type": "controller",
|
||||||
|
"class": "Admin::EmailDomainBlocksController",
|
||||||
|
"method": "index",
|
||||||
|
"line": 10,
|
||||||
|
"file": "app/controllers/admin/email_domain_blocks_controller.rb",
|
||||||
|
"rendered": {
|
||||||
|
"name": "admin/email_domain_blocks/index",
|
||||||
|
"file": "app/views/admin/email_domain_blocks/index.html.haml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"location": {
|
"location": {
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"template": "admin/action_logs/index"
|
"template": "admin/email_domain_blocks/index"
|
||||||
},
|
},
|
||||||
"user_input": "params[:page]",
|
"user_input": "params[:page]",
|
||||||
"confidence": "Weak",
|
"confidence": "Weak",
|
||||||
|
@ -106,7 +109,7 @@
|
||||||
"check_name": "Redirect",
|
"check_name": "Redirect",
|
||||||
"message": "Possible unprotected redirect",
|
"message": "Possible unprotected redirect",
|
||||||
"file": "app/controllers/remote_interaction_controller.rb",
|
"file": "app/controllers/remote_interaction_controller.rb",
|
||||||
"line": 21,
|
"line": 24,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
||||||
"code": "redirect_to(RemoteFollow.new(resource_params).interact_address_for(Status.find(params[:id])))",
|
"code": "redirect_to(RemoteFollow.new(resource_params).interact_address_for(Status.find(params[:id])))",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
|
@ -119,25 +122,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "Dynamic Render Path",
|
|
||||||
"warning_code": 15,
|
|
||||||
"fingerprint": "67afc0d5f7775fa5bd91d1912e1b5505aeedef61876347546fa20f92fd6915e6",
|
|
||||||
"check_name": "Render",
|
|
||||||
"message": "Render path contains parameter value",
|
|
||||||
"file": "app/views/stream_entries/embed.html.haml",
|
|
||||||
"line": 3,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
|
||||||
"code": "render(action => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :centered => true, :autoplay => ActiveModel::Type::Boolean.new.cast(params[:autoplay]) })",
|
|
||||||
"render_path": [{"type":"controller","class":"StatusesController","method":"embed","line":63,"file":"app/controllers/statuses_controller.rb","rendered":{"name":"stream_entries/embed","file":"/home/eugr/Projects/mastodon/app/views/stream_entries/embed.html.haml"}}],
|
|
||||||
"location": {
|
|
||||||
"type": "template",
|
|
||||||
"template": "stream_entries/embed"
|
|
||||||
},
|
|
||||||
"user_input": "params[:id]",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
|
@ -145,7 +129,7 @@
|
||||||
"check_name": "SQL",
|
"check_name": "SQL",
|
||||||
"message": "Possible SQL injection",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/models/status.rb",
|
"file": "app/models/status.rb",
|
||||||
"line": 92,
|
"line": 105,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "result.joins(\"LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
"code": "result.joins(\"LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
|
@ -159,22 +143,43 @@
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Dynamic Render Path",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 15,
|
"warning_code": 105,
|
||||||
"fingerprint": "8d843713d99e8403f7992f3e72251b633817cf9076ffcbbad5613859d2bbc127",
|
"fingerprint": "7631e93d0099506e7c3e5c91ba8d88523b00a41a0834ae30031a5a4e8bb3020a",
|
||||||
"check_name": "Render",
|
"check_name": "PermitAttributes",
|
||||||
"message": "Render path contains parameter value",
|
"message": "Potentially dangerous key allowed for mass assignment",
|
||||||
"file": "app/views/admin/custom_emojis/index.html.haml",
|
"file": "app/controllers/api/v2/search_controller.rb",
|
||||||
"line": 45,
|
"line": 28,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||||
"code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})",
|
"code": "params.permit(:type, :offset, :min_id, :max_id, :account_id)",
|
||||||
"render_path": [{"type":"controller","class":"Admin::CustomEmojisController","method":"index","line":11,"file":"app/controllers/admin/custom_emojis_controller.rb","rendered":{"name":"admin/custom_emojis/index","file":"/home/eugr/Projects/mastodon/app/views/admin/custom_emojis/index.html.haml"}}],
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "template",
|
"type": "method",
|
||||||
"template": "admin/custom_emojis/index"
|
"class": "Api::V2::SearchController",
|
||||||
|
"method": "search_params"
|
||||||
},
|
},
|
||||||
"user_input": "params[:page]",
|
"user_input": ":account_id",
|
||||||
"confidence": "Weak",
|
"confidence": "High",
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "Mass Assignment",
|
||||||
|
"warning_code": 105,
|
||||||
|
"fingerprint": "8f63dec68951d9bcf7eddb15af9392b2e1333003089c41fb76688dfd3579f394",
|
||||||
|
"check_name": "PermitAttributes",
|
||||||
|
"message": "Potentially dangerous key allowed for mass assignment",
|
||||||
|
"file": "app/controllers/api/v1/crypto/deliveries_controller.rb",
|
||||||
|
"line": 23,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||||
|
"code": "params.require(:device).permit(:account_id, :device_id, :type, :body, :hmac)",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "Api::V1::Crypto::DeliveriesController",
|
||||||
|
"method": "resource_params"
|
||||||
|
},
|
||||||
|
"user_input": ":account_id",
|
||||||
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -204,10 +209,22 @@
|
||||||
"check_name": "Render",
|
"check_name": "Render",
|
||||||
"message": "Render path contains parameter value",
|
"message": "Render path contains parameter value",
|
||||||
"file": "app/views/admin/accounts/index.html.haml",
|
"file": "app/views/admin/accounts/index.html.haml",
|
||||||
"line": 47,
|
"line": 54,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
"code": "render(action => filtered_accounts.page(params[:page]), {})",
|
"code": "render(action => filtered_accounts.page(params[:page]), {})",
|
||||||
"render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":12,"file":"app/controllers/admin/accounts_controller.rb","rendered":{"name":"admin/accounts/index","file":"/home/eugr/Projects/mastodon/app/views/admin/accounts/index.html.haml"}}],
|
"render_path": [
|
||||||
|
{
|
||||||
|
"type": "controller",
|
||||||
|
"class": "Admin::AccountsController",
|
||||||
|
"method": "index",
|
||||||
|
"line": 12,
|
||||||
|
"file": "app/controllers/admin/accounts_controller.rb",
|
||||||
|
"rendered": {
|
||||||
|
"name": "admin/accounts/index",
|
||||||
|
"file": "app/views/admin/accounts/index.html.haml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"location": {
|
"location": {
|
||||||
"type": "template",
|
"type": "template",
|
||||||
"template": "admin/accounts/index"
|
"template": "admin/accounts/index"
|
||||||
|
@ -216,6 +233,26 @@
|
||||||
"confidence": "Weak",
|
"confidence": "Weak",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "Redirect",
|
||||||
|
"warning_code": 18,
|
||||||
|
"fingerprint": "ba568ac09683f98740f663f3d850c31785900215992e8c090497d359a2563d50",
|
||||||
|
"check_name": "Redirect",
|
||||||
|
"message": "Possible unprotected redirect",
|
||||||
|
"file": "app/controllers/remote_follow_controller.rb",
|
||||||
|
"line": 21,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
||||||
|
"code": "redirect_to(RemoteFollow.new(resource_params).subscribe_address_for(@account))",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "RemoteFollowController",
|
||||||
|
"method": "create"
|
||||||
|
},
|
||||||
|
"user_input": "RemoteFollow.new(resource_params).subscribe_address_for(@account)",
|
||||||
|
"confidence": "High",
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Redirect",
|
"warning_type": "Redirect",
|
||||||
"warning_code": 18,
|
"warning_code": 18,
|
||||||
|
@ -223,7 +260,7 @@
|
||||||
"check_name": "Redirect",
|
"check_name": "Redirect",
|
||||||
"message": "Possible unprotected redirect",
|
"message": "Possible unprotected redirect",
|
||||||
"file": "app/controllers/media_controller.rb",
|
"file": "app/controllers/media_controller.rb",
|
||||||
"line": 14,
|
"line": 20,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
||||||
"code": "redirect_to(MediaAttachment.attached.find_by!(:shortcode => ((params[:id] or params[:medium_id]))).file.url(:original))",
|
"code": "redirect_to(MediaAttachment.attached.find_by!(:shortcode => ((params[:id] or params[:medium_id]))).file.url(:original))",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
|
@ -236,26 +273,6 @@
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"warning_type": "Redirect",
|
|
||||||
"warning_code": 18,
|
|
||||||
"fingerprint": "bb7e94e60af41decb811bb32171f1b27e9bf3f4d01e9e511127362e22510eb11",
|
|
||||||
"check_name": "Redirect",
|
|
||||||
"message": "Possible unprotected redirect",
|
|
||||||
"file": "app/controllers/remote_follow_controller.rb",
|
|
||||||
"line": 19,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
|
||||||
"code": "redirect_to(RemoteFollow.new(resource_params).subscribe_address_for(Account.find_local!(params[:account_username])))",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "RemoteFollowController",
|
|
||||||
"method": "create"
|
|
||||||
},
|
|
||||||
"user_input": "RemoteFollow.new(resource_params).subscribe_address_for(Account.find_local!(params[:account_username]))",
|
|
||||||
"confidence": "High",
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 105,
|
"warning_code": 105,
|
||||||
|
@ -275,27 +292,8 @@
|
||||||
"user_input": ":account_id",
|
"user_input": ":account_id",
|
||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
|
||||||
{
|
|
||||||
"warning_type": "Dynamic Render Path",
|
|
||||||
"warning_code": 15,
|
|
||||||
"fingerprint": "fbd0fc59adb5c6d44b60e02debb31d3af11719f534c9881e21435bbff87404d6",
|
|
||||||
"check_name": "Render",
|
|
||||||
"message": "Render path contains parameter value",
|
|
||||||
"file": "app/views/stream_entries/show.html.haml",
|
|
||||||
"line": 23,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
|
||||||
"code": "render(partial => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { :locals => ({ Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :include_threads => true }) })",
|
|
||||||
"render_path": [{"type":"controller","class":"StatusesController","method":"show","line":34,"file":"app/controllers/statuses_controller.rb","rendered":{"name":"stream_entries/show","file":"/home/eugr/Projects/mastodon/app/views/stream_entries/show.html.haml"}}],
|
|
||||||
"location": {
|
|
||||||
"type": "template",
|
|
||||||
"template": "stream_entries/show"
|
|
||||||
},
|
|
||||||
"user_input": "params[:id]",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"note": ""
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2019-02-21 02:30:29 +0100",
|
"updated": "2020-06-01 18:18:02 +0200",
|
||||||
"brakeman_version": "4.4.0"
|
"brakeman_version": "4.8.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,8 @@ Doorkeeper.configure do
|
||||||
:'admin:read:reports',
|
:'admin:read:reports',
|
||||||
:'admin:write',
|
:'admin:write',
|
||||||
:'admin:write:accounts',
|
:'admin:write:accounts',
|
||||||
:'admin:write:reports'
|
:'admin:write:reports',
|
||||||
|
:crypto
|
||||||
|
|
||||||
# Change the way client credentials are retrieved from the request object.
|
# Change the way client credentials are retrieved from the request object.
|
||||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||||
|
|
|
@ -19,6 +19,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
inflect.acronym 'ActivityStreams'
|
inflect.acronym 'ActivityStreams'
|
||||||
inflect.acronym 'JsonLd'
|
inflect.acronym 'JsonLd'
|
||||||
inflect.acronym 'NodeInfo'
|
inflect.acronym 'NodeInfo'
|
||||||
|
inflect.acronym 'Ed25519'
|
||||||
|
|
||||||
inflect.singular 'data', 'data'
|
inflect.singular 'data', 'data'
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,13 +35,16 @@ en:
|
||||||
status_count_before: Who authored
|
status_count_before: Who authored
|
||||||
tagline: Follow friends and discover new ones
|
tagline: Follow friends and discover new ones
|
||||||
terms: Terms of service
|
terms: Terms of service
|
||||||
unavailable_content: Unavailable content
|
unavailable_content: Moderated servers
|
||||||
unavailable_content_description:
|
unavailable_content_description:
|
||||||
domain: Server
|
domain: Server
|
||||||
reason: Reason
|
reason: Reason
|
||||||
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
|
rejecting_media: 'Media files from these servers will not be processed or stored, and no thumbnails will be displayed, requiring manual click-through to the original file:'
|
||||||
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users'' interactions, unless you are following them:'
|
rejecting_media_title: Filtered media
|
||||||
|
silenced: 'Posts from these servers will be hidden in public timelines and conversations, and no notifications will be generated from their users interactions, unless you are following them:'
|
||||||
|
silenced_title: Silenced servers
|
||||||
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
|
suspended: 'No data from these servers will be processed, stored or exchanged, making any interaction or communication with users from these servers impossible:'
|
||||||
|
suspended_title: Suspended servers
|
||||||
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
|
unavailable_content_html: Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.
|
||||||
user_count_after:
|
user_count_after:
|
||||||
one: user
|
one: user
|
||||||
|
@ -309,6 +312,7 @@ en:
|
||||||
listed: Listed
|
listed: Listed
|
||||||
new:
|
new:
|
||||||
title: Add new custom emoji
|
title: Add new custom emoji
|
||||||
|
not_permitted: You are not permitted to perform this action
|
||||||
overwrite: Overwrite
|
overwrite: Overwrite
|
||||||
shortcode: Shortcode
|
shortcode: Shortcode
|
||||||
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
|
shortcode_hint: At least 2 characters, only alphanumeric characters and underscores
|
||||||
|
@ -721,6 +725,10 @@ en:
|
||||||
hint_html: "<strong>Tip:</strong> We won't ask you for your password again for the next hour."
|
hint_html: "<strong>Tip:</strong> We won't ask you for your password again for the next hour."
|
||||||
invalid_password: Invalid password
|
invalid_password: Invalid password
|
||||||
prompt: Confirm password to continue
|
prompt: Confirm password to continue
|
||||||
|
crypto:
|
||||||
|
errors:
|
||||||
|
invalid_key: is not a valid Ed25519 or Curve25519 key
|
||||||
|
invalid_signature: is not a valid Ed25519 signature
|
||||||
date:
|
date:
|
||||||
formats:
|
formats:
|
||||||
default: "%b %d, %Y"
|
default: "%b %d, %Y"
|
||||||
|
@ -918,6 +926,7 @@ en:
|
||||||
on_cooldown: You have recently migrated your account. This function will become available again in %{count} days.
|
on_cooldown: You have recently migrated your account. This function will become available again in %{count} days.
|
||||||
past_migrations: Past migrations
|
past_migrations: Past migrations
|
||||||
proceed_with_move: Move followers
|
proceed_with_move: Move followers
|
||||||
|
redirected_msg: Your account is now redirecting to %{acct}.
|
||||||
redirecting_to: Your account is redirecting to %{acct}.
|
redirecting_to: Your account is redirecting to %{acct}.
|
||||||
set_redirect: Set redirect
|
set_redirect: Set redirect
|
||||||
warning:
|
warning:
|
||||||
|
@ -1267,6 +1276,12 @@ en:
|
||||||
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
|
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
|
||||||
subject: Your archive is ready for download
|
subject: Your archive is ready for download
|
||||||
title: Archive takeout
|
title: Archive takeout
|
||||||
|
sign_in_token:
|
||||||
|
details: 'Here are details of the attempt:'
|
||||||
|
explanation: 'We detected an attempt to sign in to your account from an unrecognized IP address. If this is you, please enter the security code below on the sign in challenge page:'
|
||||||
|
further_actions: 'If this wasn''t you, please change your password and enable two-factor authentication on your account. You can do so here:'
|
||||||
|
subject: Please confirm attempted sign in
|
||||||
|
title: Sign in attempt
|
||||||
warning:
|
warning:
|
||||||
explanation:
|
explanation:
|
||||||
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
|
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
|
||||||
|
@ -1304,11 +1319,14 @@ en:
|
||||||
title: Welcome aboard, %{name}!
|
title: Welcome aboard, %{name}!
|
||||||
users:
|
users:
|
||||||
follow_limit_reached: You cannot follow more than %{limit} people
|
follow_limit_reached: You cannot follow more than %{limit} people
|
||||||
|
generic_access_help_html: Trouble accessing your account? You may get in touch with %{email} for assistance
|
||||||
invalid_email: The e-mail address is invalid
|
invalid_email: The e-mail address is invalid
|
||||||
invalid_otp_token: Invalid two-factor code
|
invalid_otp_token: Invalid two-factor code
|
||||||
|
invalid_sign_in_token: Invalid security code
|
||||||
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
||||||
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
|
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
|
||||||
signed_in_as: 'Signed in as:'
|
signed_in_as: 'Signed in as:'
|
||||||
|
suspicious_sign_in_confirmation: You appear to not have logged in from this device before, and you haven't logged in for a while, so we're sending a security code to your e-mail address to confirm that it's you.
|
||||||
verification:
|
verification:
|
||||||
explanation_html: 'You can <strong>verify yourself as the owner of the links in your profile metadata</strong>. For that, the linked website must contain a link back to your Mastodon profile. The link back <strong>must</strong> have a <code>rel="me"</code> attribute. The text content of the link does not matter. Here is an example:'
|
explanation_html: 'You can <strong>verify yourself as the owner of the links in your profile metadata</strong>. For that, the linked website must contain a link back to your Mastodon profile. The link back <strong>must</strong> have a <code>rel="me"</code> attribute. The text content of the link does not matter. Here is an example:'
|
||||||
verification: Verification
|
verification: Verification
|
||||||
|
|
|
@ -151,6 +151,7 @@ en:
|
||||||
setting_use_blurhash: Show colorful gradients for hidden media
|
setting_use_blurhash: Show colorful gradients for hidden media
|
||||||
setting_use_pending_items: Slow mode
|
setting_use_pending_items: Slow mode
|
||||||
severity: Severity
|
severity: Severity
|
||||||
|
sign_in_token_attempt: Security code
|
||||||
type: Import type
|
type: Import type
|
||||||
username: Username
|
username: Username
|
||||||
username_or_email: Username or Email
|
username_or_email: Username or Email
|
||||||
|
|
|
@ -79,6 +79,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resource :outbox, only: [:show], module: :activitypub
|
resource :outbox, only: [:show], module: :activitypub
|
||||||
resource :inbox, only: [:create], module: :activitypub
|
resource :inbox, only: [:create], module: :activitypub
|
||||||
|
resource :claim, only: [:create], module: :activitypub
|
||||||
resources :collections, only: [:show], module: :activitypub
|
resources :collections, only: [:show], module: :activitypub
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -342,6 +343,23 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :crypto do
|
||||||
|
resources :deliveries, only: :create
|
||||||
|
|
||||||
|
namespace :keys do
|
||||||
|
resource :upload, only: [:create]
|
||||||
|
resource :query, only: [:create]
|
||||||
|
resource :claim, only: [:create]
|
||||||
|
resource :count, only: [:show]
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :encrypted_messages, only: [:index] do
|
||||||
|
collection do
|
||||||
|
post :clear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resources :conversations, only: [:index, :destroy] do
|
resources :conversations, only: [:index, :destroy] do
|
||||||
member do
|
member do
|
||||||
post :read
|
post :read
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue