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.
lolsob-rspec
Thibaut Girka 2020-06-09 10:39:20 +02:00
commit 83dc54c487
246 changed files with 5027 additions and 1354 deletions

View File

@ -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
View 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
View File

@ -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

View File

@ -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
View 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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View 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

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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;
} }

View File

@ -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}

View File

@ -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')}
/> />
); );
} }

View File

@ -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" />');
}); });
}); });
}); });

View File

@ -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) {

View File

@ -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>
); );
} }

View File

@ -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')) {

View File

@ -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",

View File

@ -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';

View File

@ -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;
} }
} }
} }

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View 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

View 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

View 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

View File

@ -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
View 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

View File

@ -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)

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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? }

View 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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
attributes :device_id, :name, :identity_key,
:fingerprint_key
end

View 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

View File

@ -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

View File

@ -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'] || ''

View File

@ -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?

View File

@ -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

View 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

View File

@ -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

View 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

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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 }

View File

@ -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'

View File

@ -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

View 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))

View File

@ -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 }

View File

@ -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

View 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'

View 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 %>

View File

@ -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!

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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"
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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