Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/activitypub/collections_controller.rb`: Conflict due to glitch-soc having to take care of local-only pinned toots in that controller. Took upstream's changes and restored the local-only special handling. - `app/controllers/auth/sessions_controller.rb`: Minor conflicts due to the theming system, applied upstream changes, adapted the following two files for glitch-soc's theming system: - `app/controllers/concerns/sign_in_token_authentication_concern.rb` - `app/controllers/concerns/two_factor_authentication_concern.rb` - `app/services/backup_service.rb`: Minor conflict due to glitch-soc having to handle local-only toots specially. Applied upstream changes and restored the local-only special handling. - `app/views/admin/custom_emojis/index.html.haml`: Minor conflict due to the theming system. - `package.json`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file. - `yarn.lock`: Upstream dependency updated, too close to a glitch-soc-only dependency in the file.
This commit is contained in:
commit
12c8ac9e14
246 changed files with 5027 additions and 1354 deletions
|
@ -1,28 +0,0 @@
|
|||
version: 1
|
||||
|
||||
update_configs:
|
||||
- package_manager: "ruby:bundler"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
||||
|
||||
- package_manager: "javascript"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: all
|
||||
|
||||
- package-ecosystem: bundler
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: all
|
10
Gemfile
10
Gemfile
|
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
|||
gem 'pghero', '~> 2.5'
|
||||
gem '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-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -50,6 +50,7 @@ gem 'omniauth', '~> 1.9'
|
|||
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.4'
|
||||
gem 'ed25519', '~> 1.2'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
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 'rqrcode', '~> 1.1'
|
||||
gem 'ruby-progressbar', '~> 1.10'
|
||||
gem 'sanitize', '~> 5.1'
|
||||
gem 'sanitize', '~> 5.2'
|
||||
gem 'sidekiq', '~> 6.0'
|
||||
gem 'sidekiq-scheduler', '~> 3.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 'stoplight', '~> 2.2.0'
|
||||
gem 'strong_migrations', '~> 0.6'
|
||||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.21', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2020'
|
||||
|
@ -122,7 +122,7 @@ end
|
|||
group :test do
|
||||
gem 'capybara', '~> 3.32'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.11'
|
||||
gem 'faker', '~> 2.12'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
|
@ -141,7 +141,7 @@ group :development do
|
|||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.84', require: false
|
||||
gem 'rubocop', '~> 0.85', require: false
|
||||
gem 'rubocop-rails', '~> 2.5', require: false
|
||||
gem 'brakeman', '~> 4.8', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
|
|
58
Gemfile.lock
58
Gemfile.lock
|
@ -92,20 +92,20 @@ GEM
|
|||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.320.0)
|
||||
aws-sdk-core (3.96.1)
|
||||
aws-partitions (1.326.0)
|
||||
aws-sdk-core (3.98.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.31.0)
|
||||
aws-sdk-kms (1.33.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.3)
|
||||
aws-sigv4 (1.1.4)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
bcrypt (3.1.13)
|
||||
better_errors (2.7.1)
|
||||
|
@ -119,7 +119,7 @@ GEM
|
|||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.2)
|
||||
browser (4.1.0)
|
||||
browser (4.2.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -164,9 +164,9 @@ GEM
|
|||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.2)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.1.6)
|
||||
connection_pool (2.2.2)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.6)
|
||||
|
@ -201,6 +201,7 @@ GEM
|
|||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
e2mmap (0.1.0)
|
||||
ed25519 (1.2.4)
|
||||
elasticsearch (7.7.0)
|
||||
elasticsearch-api (= 7.7.0)
|
||||
elasticsearch-transport (= 7.7.0)
|
||||
|
@ -217,7 +218,7 @@ GEM
|
|||
tzinfo
|
||||
excon (0.73.0)
|
||||
fabrication (2.21.1)
|
||||
faker (2.11.0)
|
||||
faker (2.12.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
|
@ -235,14 +236,14 @@ GEM
|
|||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (0.3.7)
|
||||
fog-openstack (0.3.10)
|
||||
fog-core (>= 1.45, <= 2.1.0)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.2.5)
|
||||
fugit (1.3.5)
|
||||
fugit (1.3.6)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.1)
|
||||
raabro (~> 1.3)
|
||||
fuubar (2.5.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
|
@ -284,7 +285,7 @@ GEM
|
|||
httplog (1.4.2)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.8.2)
|
||||
i18n (1.8.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.31)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -309,7 +310,7 @@ GEM
|
|||
multi_json (~> 1.14)
|
||||
rack (~> 2.0)
|
||||
rdf (~> 3.1)
|
||||
json-ld-preloaded (3.1.2)
|
||||
json-ld-preloaded (3.1.3)
|
||||
json-ld (~> 3.1)
|
||||
rdf (~> 3.1)
|
||||
jsonapi-renderer (0.2.2)
|
||||
|
@ -406,7 +407,7 @@ GEM
|
|||
parallel (1.19.1)
|
||||
parallel_tests (2.32.0)
|
||||
parallel
|
||||
parser (2.7.1.2)
|
||||
parser (2.7.1.3)
|
||||
ast (~> 2.4.0)
|
||||
parslet (2.0.0)
|
||||
pastel (0.7.4)
|
||||
|
@ -484,7 +485,7 @@ GEM
|
|||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rdf (3.1.1)
|
||||
rdf (3.1.2)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.4.0)
|
||||
|
@ -509,10 +510,10 @@ GEM
|
|||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.8.2)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.0)
|
||||
regexp_parser (1.7.1)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.4)
|
||||
|
@ -544,10 +545,11 @@ GEM
|
|||
rspec-support (3.9.3)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (0.84.0)
|
||||
rubocop (0.85.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
rexml
|
||||
rubocop-ast (>= 0.0.3)
|
||||
ruby-progressbar (~> 1.7)
|
||||
|
@ -564,7 +566,7 @@ GEM
|
|||
rufus-scheduler (3.6.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safe_yaml (1.0.5)
|
||||
sanitize (5.1.0)
|
||||
sanitize (5.2.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
|
@ -623,8 +625,6 @@ GEM
|
|||
thwait (0.1.0)
|
||||
tilt (2.0.10)
|
||||
tty-color (0.5.1)
|
||||
tty-command (0.9.0)
|
||||
pastel (~> 0.7.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-prompt (0.21.0)
|
||||
necromancer (~> 0.5.0)
|
||||
|
@ -634,7 +634,7 @@ GEM
|
|||
tty-cursor (~> 0.7)
|
||||
tty-screen (~> 0.7)
|
||||
wisper (~> 2.0.0)
|
||||
tty-screen (0.7.1)
|
||||
tty-screen (0.8.0)
|
||||
twitter-text (1.14.7)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.7)
|
||||
|
@ -662,7 +662,7 @@ GEM
|
|||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.2)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.4)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.1)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -675,7 +675,7 @@ DEPENDENCIES
|
|||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.66)
|
||||
aws-sdk-s3 (~> 1.67)
|
||||
better_errors (~> 2.7)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
|
@ -702,8 +702,9 @@ DEPENDENCIES
|
|||
doorkeeper (~> 5.4)
|
||||
dotenv-rails (~> 2.7)
|
||||
e2mmap (~> 0.1.0)
|
||||
ed25519 (~> 1.2)
|
||||
fabrication (~> 2.21)
|
||||
faker (~> 2.11)
|
||||
faker (~> 2.12)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
|
@ -773,10 +774,10 @@ DEPENDENCIES
|
|||
rspec-rails (~> 4.0)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 0.84)
|
||||
rubocop (~> 0.85)
|
||||
rubocop-rails (~> 2.5)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
sanitize (~> 5.2)
|
||||
sidekiq (~> 6.0)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.0)
|
||||
|
@ -792,7 +793,6 @@ DEPENDENCIES
|
|||
strong_migrations (~> 0.6)
|
||||
thor (~> 0.20)
|
||||
thwait (~> 0.1.0)
|
||||
tty-command (~> 0.9)
|
||||
tty-prompt (~> 0.21)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2020)
|
||||
|
|
12
SECURITY.md
Normal file
12
SECURITY.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.1.x | :white_check_mark: |
|
||||
| < 3.1 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
hello@joinmastodon.org
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class AccountsController < ApplicationController
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE_MAX = 200
|
||||
|
||||
include AccountControllerConcern
|
||||
include SignatureAuthentication
|
||||
|
@ -41,7 +42,8 @@ class AccountsController < ApplicationController
|
|||
format.rss do
|
||||
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)
|
||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
||||
end
|
||||
|
|
21
app/controllers/activitypub/claims_controller.rb
Normal file
21
app/controllers/activitypub/claims_controller.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ClaimsController < ActivityPub::BaseController
|
||||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :require_signature!
|
||||
before_action :set_claim_result
|
||||
|
||||
def create
|
||||
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_claim_result
|
||||
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
|
||||
end
|
||||
end
|
|
@ -5,8 +5,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
include AccountOwnedConcern
|
||||
|
||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_items
|
||||
before_action :set_size
|
||||
before_action :set_statuses
|
||||
before_action :set_type
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
|
@ -16,40 +17,53 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
|
||||
private
|
||||
|
||||
def set_statuses
|
||||
@statuses = scope_for_collection
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
end
|
||||
|
||||
def set_size
|
||||
def set_items
|
||||
case params[:id]
|
||||
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
|
||||
not_found
|
||||
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]
|
||||
when 'featured'
|
||||
# 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)))
|
||||
Status.none
|
||||
@type = :ordered
|
||||
when 'devices'
|
||||
@type = :unordered
|
||||
else
|
||||
@account.pinned_statuses.not_local_only
|
||||
end
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_collection_url(@account, params[:id]),
|
||||
type: :ordered,
|
||||
type: @type,
|
||||
size: @size,
|
||||
items: @statuses
|
||||
items: @items
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,6 +33,8 @@ module Admin
|
|||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
rescue Mastodon::NotPermittedError
|
||||
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
||||
ensure
|
||||
redirect_to admin_custom_emojis_path(filter_params)
|
||||
end
|
||||
|
|
30
app/controllers/api/v1/crypto/deliveries_controller.rb
Normal file
30
app/controllers/api/v1/crypto/deliveries_controller.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::DeliveriesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
def create
|
||||
devices.each do |device_params|
|
||||
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
|
||||
end
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:device)
|
||||
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
|
||||
end
|
||||
|
||||
def devices
|
||||
Array(resource_params[:device])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
||||
LIMIT = 80
|
||||
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
before_action :set_encrypted_messages, only: :index
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
def index
|
||||
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
|
||||
end
|
||||
|
||||
def clear
|
||||
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
|
||||
def set_encrypted_messages
|
||||
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@encrypted_messages.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@encrypted_messages.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@encrypted_messages.size == limit_param(LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
end
|
25
app/controllers/api/v1/crypto/keys/claims_controller.rb
Normal file
25
app/controllers/api/v1/crypto/keys/claims_controller.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_claim_results
|
||||
|
||||
def create
|
||||
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_claim_results
|
||||
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.permit(device: [:account_id, :device_id])
|
||||
end
|
||||
|
||||
def devices
|
||||
Array(resource_params[:device])
|
||||
end
|
||||
end
|
17
app/controllers/api/v1/crypto/keys/counts_controller.rb
Normal file
17
app/controllers/api/v1/crypto/keys/counts_controller.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
def show
|
||||
render json: { one_time_keys: @current_device.one_time_keys.count }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
end
|
26
app/controllers/api/v1/crypto/keys/queries_controller.rb
Normal file
26
app/controllers/api/v1/crypto/keys/queries_controller.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_accounts
|
||||
before_action :set_query_results
|
||||
|
||||
def create
|
||||
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.where(id: account_ids).includes(:devices)
|
||||
end
|
||||
|
||||
def set_query_results
|
||||
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
|
||||
end
|
||||
|
||||
def account_ids
|
||||
Array(params[:id]).map(&:to_i)
|
||||
end
|
||||
end
|
29
app/controllers/api/v1/crypto/keys/uploads_controller.rb
Normal file
29
app/controllers/api/v1/crypto/keys/uploads_controller.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
|
||||
def create
|
||||
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
|
||||
|
||||
device.transaction do
|
||||
device.account = current_account
|
||||
device.update!(resource_params[:device])
|
||||
|
||||
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
|
||||
resource_params[:one_time_keys].each do |one_time_key_params|
|
||||
device.one_time_keys.create!(one_time_key_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render json: device, serializer: REST::Keys::DeviceSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
|
||||
end
|
||||
end
|
|
@ -9,7 +9,9 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
skip_before_action :require_functional!
|
||||
|
||||
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_body_classes
|
||||
|
@ -40,8 +42,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
protected
|
||||
|
||||
def find_user
|
||||
if session[:otp_user_id]
|
||||
User.find(session[:otp_user_id])
|
||||
if session[:attempt_user_id]
|
||||
User.find(session[:attempt_user_id])
|
||||
else
|
||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||
|
@ -50,7 +52,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password, :otp_attempt)
|
||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
|
@ -71,48 +73,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
super
|
||||
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
|
||||
super
|
||||
# Delete flash message that isn't entirely useful and may be confusing in
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SignInTokenAuthenticationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
|
||||
end
|
||||
|
||||
def sign_in_token_required?
|
||||
find_user&.suspicious_sign_in?(request.remote_ip)
|
||||
end
|
||||
|
||||
def valid_sign_in_token_attempt?(user)
|
||||
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
|
||||
end
|
||||
|
||||
def authenticate_with_sign_in_token
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
|
||||
authenticate_with_sign_in_token_attempt(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_sign_in_token(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_sign_in_token_attempt(user)
|
||||
if valid_sign_in_token_attempt?(user)
|
||||
session.delete(:attempt_user_id)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||
prompt_for_sign_in_token(user)
|
||||
end
|
||||
end
|
||||
|
||||
def prompt_for_sign_in_token(user)
|
||||
if user.sign_in_token_expired?
|
||||
user.generate_sign_in_token && user.save
|
||||
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
|
||||
end
|
||||
|
||||
session[:attempt_user_id] = user.id
|
||||
use_pack 'auth'
|
||||
@body_classes = 'lighter'
|
||||
render :sign_in_token
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TwoFactorAuthenticationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
find_user&.otp_required_for_login?
|
||||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
rescue OpenSSL::Cipher::CipherError
|
||||
false
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:attempt_user_id]
|
||||
authenticate_with_two_factor_attempt(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor_attempt(user)
|
||||
if valid_otp_attempt?(user)
|
||||
session.delete(:attempt_user_id)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def prompt_for_two_factor(user)
|
||||
session[:attempt_user_id] = user.id
|
||||
use_pack 'auth'
|
||||
@body_classes = 'lighter'
|
||||
render :two_factor
|
||||
end
|
||||
end
|
|
@ -18,7 +18,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||
if @redirect.valid_with_challenge?(current_user)
|
||||
current_account.update!(moved_to_account: @redirect.target_account)
|
||||
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
|
||||
render :new
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ class StatusesController < ApplicationController
|
|||
|
||||
def activity
|
||||
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
|
||||
|
||||
def embed
|
||||
|
|
|
@ -4,6 +4,7 @@ class TagsController < ApplicationController
|
|||
include SignatureVerification
|
||||
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE_MAX = 200
|
||||
|
||||
layout 'public'
|
||||
|
||||
|
@ -26,6 +27,7 @@ class TagsController < ApplicationController
|
|||
format.rss do
|
||||
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 = cache_collection(@statuses, Status)
|
||||
|
||||
|
|
|
@ -137,6 +137,11 @@ module ApplicationHelper
|
|||
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?
|
||||
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)
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
# 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
|
||||
def webfinger!(uri)
|
||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
||||
|
@ -12,6 +23,14 @@ module WebfingerHelper
|
|||
headers: {
|
||||
'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
|
||||
|
|
|
@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
|||
|
||||
export function searchTextFromRawStatus (status) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -208,7 +208,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
|
||||
<Textarea
|
||||
inputRef={this.setTextarea}
|
||||
ref={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -401,6 +401,7 @@ class Status extends ImmutablePureComponent {
|
|||
compact
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('emoji', () => {
|
|||
|
||||
it('skips the textual presentation VS15 character', () => {
|
||||
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />');
|
||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,15 @@ const trie = new Trie(Object.keys(unicodeMapping));
|
|||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
|
||||
// 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 tagCharsWithoutEmojis = '<&';
|
||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||
|
@ -60,7 +69,7 @@ const emojify = (str, customEmojis = {}) => {
|
|||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
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;
|
||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||
if (str.codePointAt(rend) === 65038) {
|
||||
|
|
|
@ -2,9 +2,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import Immutable from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import punycode from 'punycode';
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import classNames from 'classnames';
|
||||
import { useBlurhash } from 'mastodon/initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
const IDNA_PREFIX = 'xn--';
|
||||
|
||||
|
@ -63,6 +67,7 @@ export default class Card extends React.PureComponent {
|
|||
compact: PropTypes.bool,
|
||||
defaultWidth: PropTypes.number,
|
||||
cacheWidth: PropTypes.func,
|
||||
sensitive: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -72,12 +77,44 @@ export default class Card extends React.PureComponent {
|
|||
|
||||
state = {
|
||||
width: this.props.defaultWidth || 280,
|
||||
previewLoaded: false,
|
||||
embedded: false,
|
||||
revealed: !this.props.sensitive,
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
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 () {
|
||||
const { card } = this.props;
|
||||
const content = { __html: addAutoPlay(card.get('html')) };
|
||||
|
@ -138,7 +187,7 @@ export default class Card extends React.PureComponent {
|
|||
|
||||
render () {
|
||||
const { card, maxDescription, compact } = this.props;
|
||||
const { width, embedded } = this.state;
|
||||
const { width, embedded, revealed } = this.state;
|
||||
|
||||
if (card === null) {
|
||||
return null;
|
||||
|
@ -153,7 +202,7 @@ export default class Card extends React.PureComponent {
|
|||
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
|
||||
|
||||
const description = (
|
||||
<div className='status-card__content'>
|
||||
<div className={classNames('status-card__content', { 'status-card__content--blurred': !revealed })}>
|
||||
{title}
|
||||
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
|
||||
<span className='status-card__host'>{provider}</span>
|
||||
|
@ -161,7 +210,18 @@ export default class Card extends React.PureComponent {
|
|||
);
|
||||
|
||||
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 (embedded) {
|
||||
|
@ -175,14 +235,18 @@ export default class Card extends React.PureComponent {
|
|||
|
||||
embed = (
|
||||
<div className='status-card__image'>
|
||||
{canvas}
|
||||
{thumbnail}
|
||||
|
||||
{revealed && (
|
||||
<div className='status-card__actions'>
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
{!revealed && spoilerButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -196,13 +260,16 @@ export default class Card extends React.PureComponent {
|
|||
} else if (card.get('image')) {
|
||||
embed = (
|
||||
<div className='status-card__image'>
|
||||
{canvas}
|
||||
{thumbnail}
|
||||
{!revealed && spoilerButton}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
embed = (
|
||||
<div className='status-card__image'>
|
||||
<Icon id='file-text' />
|
||||
{!revealed && spoilerButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
} 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')) {
|
||||
|
|
|
@ -106,7 +106,7 @@
|
|||
"confirmations.block.confirm": "Block",
|
||||
"confirmations.block.message": "Are you sure you want to block {name}?",
|
||||
"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.message": "Are you sure you want to permanently delete this list?",
|
||||
"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.message": "Are you sure you want to mute {name}?",
|
||||
"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.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
|
@ -130,7 +130,7 @@
|
|||
"directory.local": "From {domain} only",
|
||||
"directory.new_arrivals": "New arrivals",
|
||||
"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:",
|
||||
"emoji_button.activity": "Activity",
|
||||
"emoji_button.custom": "Custom",
|
||||
|
@ -159,7 +159,7 @@
|
|||
"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.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.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.",
|
||||
|
@ -216,12 +216,12 @@
|
|||
"keyboard_shortcuts.back": "to navigate back",
|
||||
"keyboard_shortcuts.blocked": "to open blocked users list",
|
||||
"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.description": "Description",
|
||||
"keyboard_shortcuts.direct": "to open direct messages column",
|
||||
"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.favourites": "to open favourites list",
|
||||
"keyboard_shortcuts.federated": "to open federated timeline",
|
||||
|
@ -289,13 +289,13 @@
|
|||
"navigation_bar.preferences": "Preferences",
|
||||
"navigation_bar.public_timeline": "Federated timeline",
|
||||
"navigation_bar.security": "Security",
|
||||
"notification.favourite": "{name} favourited your status",
|
||||
"notification.favourite": "{name} favourited your toot",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
"notification.mention": "{name} mentioned you",
|
||||
"notification.own_poll": "Your poll 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_confirmation": "Are you sure you want to permanently clear all your notifications?",
|
||||
"notifications.column_settings.alert": "Desktop notifications",
|
||||
|
@ -326,7 +326,7 @@
|
|||
"poll.voted": "You voted for this answer",
|
||||
"poll_button.add_poll": "Add a 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.short": "Direct",
|
||||
"privacy.private.long": "Visible for followers only",
|
||||
|
@ -353,9 +353,9 @@
|
|||
"report.target": "Reporting {target}",
|
||||
"search.placeholder": "Search",
|
||||
"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.status": "status",
|
||||
"search_popout.tips.status": "toot",
|
||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
||||
"search_popout.tips.user": "user",
|
||||
"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.total": "{count, number} {count, plural, one {result} other {results}}",
|
||||
"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.bookmark": "Bookmark",
|
||||
"status.cancel_reblog_private": "Unboost",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
"status.copy": "Copy link to status",
|
||||
"status.copy": "Copy link to toot",
|
||||
"status.delete": "Delete",
|
||||
"status.detailed_status": "Detailed conversation view",
|
||||
"status.direct": "Direct message @{name}",
|
||||
|
@ -382,7 +382,7 @@
|
|||
"status.more": "More",
|
||||
"status.mute": "Mute @{name}",
|
||||
"status.mute_conversation": "Mute conversation",
|
||||
"status.open": "Expand this status",
|
||||
"status.open": "Expand this toot",
|
||||
"status.pin": "Pin on profile",
|
||||
"status.pinned": "Pinned toot",
|
||||
"status.read_more": "Read more",
|
||||
|
|
|
@ -39,3 +39,5 @@ $account-background-color: $white !default;
|
|||
@function lighten($color, $amount) {
|
||||
@return hsl(hue($color), saturation($color), lightness($color) - $amount);
|
||||
}
|
||||
|
||||
$emojis-requiring-inversion: 'chains';
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
$black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash';
|
||||
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
|
||||
|
||||
%white-emoji-outline {
|
||||
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);
|
||||
transform: scale(.71);
|
||||
%emoji-color-inversion {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.emojione {
|
||||
@each $emoji in $black-emojis {
|
||||
@each $emoji in $emojis-requiring-inversion {
|
||||
&[title=':#{$emoji}:'] {
|
||||
@extend %white-emoji-outline;
|
||||
@extend %emoji-color-inversion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3097,6 +3097,11 @@ a.status-card {
|
|||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
padding: 14px 14px 14px 8px;
|
||||
|
||||
&--blurred {
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.status-card__description {
|
||||
|
@ -3134,7 +3139,8 @@ a.status-card {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.status-card__image-image {
|
||||
.status-card__image-image,
|
||||
.status-card__image-preview {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
|
@ -3179,6 +3185,24 @@ a.status-card.compact:hover {
|
|||
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 {
|
||||
display: block;
|
||||
color: $dark-text-color;
|
||||
|
|
|
@ -2,6 +2,45 @@
|
|||
|
||||
class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
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?
|
||||
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
|
@ -23,8 +62,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
@status
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def audience_to
|
||||
@object['to'] || @json['to']
|
||||
end
|
||||
|
@ -262,6 +299,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def poll_vote!
|
||||
poll = replied_to_status.preloadable_poll
|
||||
already_voted = true
|
||||
|
||||
RedisLock.acquire(poll_lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
already_voted = poll.votes.where(account: @account).exists?
|
||||
|
@ -270,20 +308,24 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
end
|
||||
|
||||
increment_voters_count! unless already_voted
|
||||
ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals?
|
||||
end
|
||||
|
||||
def resolve_thread(status)
|
||||
return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri)
|
||||
|
||||
ThreadResolveWorker.perform_async(status.id, in_reply_to_uri)
|
||||
end
|
||||
|
||||
def fetch_replies(status)
|
||||
collection = @object['replies']
|
||||
return if collection.nil?
|
||||
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, false)
|
||||
return unless replies.nil?
|
||||
|
||||
uri = value_or_id(collection)
|
||||
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
|
||||
end
|
||||
|
@ -291,6 +333,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def conversation_from_uri(uri)
|
||||
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)
|
||||
|
||||
begin
|
||||
Conversation.find_or_create_by!(uri: uri)
|
||||
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
||||
|
@ -404,6 +447,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
def skip_download?
|
||||
return @skip_download if defined?(@skip_download)
|
||||
|
||||
@skip_download ||= DomainBlock.reject_media?(@account.domain)
|
||||
end
|
||||
|
||||
|
@ -436,11 +480,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
def forward_for_reply
|
||||
return unless @json['signature'].present? && reply_to_local?
|
||||
|
||||
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
|
||||
end
|
||||
|
||||
def increment_voters_count!
|
||||
poll = replied_to_status.preloadable_poll
|
||||
|
||||
unless poll.voters_count.nil?
|
||||
poll.voters_count = poll.voters_count + 1
|
||||
poll.save
|
||||
|
|
|
@ -22,6 +22,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||
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
|
||||
|
||||
def self.default_key_transform
|
||||
|
|
|
@ -287,9 +287,14 @@ class FeedManager
|
|||
combined_regex = active_filters.reduce { |memo, obj| Regexp.union(memo, obj) }
|
||||
status = status.reblog if status.reblog?
|
||||
|
||||
!combined_regex.match(Formatter.instance.plaintext(status)).nil? ||
|
||||
(status.spoiler_text.present? && !combined_regex.match(status.spoiler_text).nil?) ||
|
||||
(status.preloadable_poll && !combined_regex.match(status.preloadable_poll.options.join("\n\n")).nil?)
|
||||
combined_text = [
|
||||
Formatter.instance.plaintext(status),
|
||||
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
|
||||
|
||||
# Adds a status to an account's feed, returning true if a status was
|
||||
|
|
|
@ -19,6 +19,8 @@ class InlineRenderer
|
|||
serializer = REST::AnnouncementSerializer
|
||||
when :reaction
|
||||
serializer = REST::ReactionSerializer
|
||||
when :encrypted_message
|
||||
serializer = REST::EncryptedMessageSerializer
|
||||
else
|
||||
return
|
||||
end
|
||||
|
|
|
@ -126,4 +126,21 @@ class UserMailer < Devise::Mailer
|
|||
reply_to: Setting.site_contact_email
|
||||
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
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
# hide_collections :boolean
|
||||
# avatar_storage_schema_version :integer
|
||||
# header_storage_schema_version :integer
|
||||
# devices_url :string
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
|
|
@ -9,6 +9,7 @@ module AccountAssociations
|
|||
|
||||
# Identity proofs
|
||||
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
||||
has_many :devices, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Timelines
|
||||
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||
|
|
35
app/models/device.rb
Normal file
35
app/models/device.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: devices
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# access_token_id :bigint(8)
|
||||
# account_id :bigint(8)
|
||||
# device_id :string default(""), not null
|
||||
# name :string default(""), not null
|
||||
# fingerprint_key :text default(""), not null
|
||||
# identity_key :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class Device < ApplicationRecord
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
|
||||
belongs_to :account
|
||||
|
||||
has_many :one_time_keys, dependent: :destroy, inverse_of: :device
|
||||
has_many :encrypted_messages, dependent: :destroy, inverse_of: :device
|
||||
|
||||
validates :name, :fingerprint_key, :identity_key, presence: true
|
||||
validates :fingerprint_key, :identity_key, ed25519_key: true
|
||||
|
||||
before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }
|
||||
|
||||
private
|
||||
|
||||
def invalidate_associations
|
||||
one_time_keys.destroy_all
|
||||
encrypted_messages.destroy_all
|
||||
end
|
||||
end
|
50
app/models/encrypted_message.rb
Normal file
50
app/models/encrypted_message.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: encrypted_messages
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# device_id :bigint(8)
|
||||
# from_account_id :bigint(8)
|
||||
# from_device_id :string default(""), not null
|
||||
# type :integer default(0), not null
|
||||
# body :text default(""), not null
|
||||
# digest :text default(""), not null
|
||||
# message_franking :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class EncryptedMessage < ApplicationRecord
|
||||
self.inheritance_column = nil
|
||||
|
||||
include Paginable
|
||||
|
||||
scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }
|
||||
|
||||
belongs_to :device
|
||||
belongs_to :from_account, class_name: 'Account'
|
||||
|
||||
around_create Mastodon::Snowflake::Callbacks
|
||||
|
||||
after_commit :push_to_streaming_api
|
||||
|
||||
private
|
||||
|
||||
def push_to_streaming_api
|
||||
Rails.logger.info(streaming_channel)
|
||||
Rails.logger.info(subscribed_to_timeline?)
|
||||
|
||||
return if destroyed? || !subscribed_to_timeline?
|
||||
|
||||
PushEncryptedMessageWorker.perform_async(id)
|
||||
end
|
||||
|
||||
def subscribed_to_timeline?
|
||||
Redis.current.exists("subscribed:#{streaming_channel}")
|
||||
end
|
||||
|
||||
def streaming_channel
|
||||
"timeline:#{device.account_id}:#{device.device_id}"
|
||||
end
|
||||
end
|
19
app/models/message_franking.rb
Normal file
19
app/models/message_franking.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MessageFranking
|
||||
attr_reader :hmac, :source_account_id, :target_account_id,
|
||||
:timestamp, :original_franking
|
||||
|
||||
def initialize(attributes = {})
|
||||
@hmac = attributes[:hmac]
|
||||
@source_account_id = attributes[:source_account_id]
|
||||
@target_account_id = attributes[:target_account_id]
|
||||
@timestamp = attributes[:timestamp]
|
||||
@original_franking = attributes[:original_franking]
|
||||
end
|
||||
|
||||
def to_token
|
||||
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
|
||||
crypt.encrypt_and_sign(self)
|
||||
end
|
||||
end
|
21
app/models/one_time_key.rb
Normal file
21
app/models/one_time_key.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: one_time_keys
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# device_id :bigint(8)
|
||||
# key_id :string default(""), not null
|
||||
# key :text default(""), not null
|
||||
# signature :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class OneTimeKey < ApplicationRecord
|
||||
belongs_to :device
|
||||
|
||||
validates :key_id, :key, :signature, presence: true
|
||||
validates :key, ed25519_key: true
|
||||
validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
|
||||
end
|
|
@ -23,19 +23,25 @@
|
|||
# updated_at :datetime not null
|
||||
# embed_url :string default(""), not null
|
||||
# image_storage_schema_version :integer
|
||||
# blurhash :string
|
||||
#
|
||||
|
||||
class PreviewCard < ApplicationRecord
|
||||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
LIMIT = 1.megabytes
|
||||
|
||||
BLURHASH_OPTIONS = {
|
||||
x_comp: 4,
|
||||
y_comp: 4,
|
||||
}.freeze
|
||||
|
||||
self.inheritance_column = false
|
||||
|
||||
enum type: [:link, :photo, :video, :rich]
|
||||
|
||||
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
|
||||
|
||||
|
@ -72,6 +78,7 @@ class PreviewCard < ApplicationRecord
|
|||
geometry: '400x400>',
|
||||
file_geometry_parser: FastGeometryParser,
|
||||
convert_options: '-coalesce -strip',
|
||||
blurhash: BLURHASH_OPTIONS,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
41
app/models/system_key.rb
Normal file
41
app/models/system_key.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: system_keys
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# key :binary
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class SystemKey < ApplicationRecord
|
||||
ROTATION_PERIOD = 1.week.freeze
|
||||
|
||||
before_validation :set_key
|
||||
|
||||
scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - ROTATION_PERIOD * 3)) }
|
||||
|
||||
class << self
|
||||
def current_key
|
||||
previous_key = order(id: :asc).last
|
||||
|
||||
if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
|
||||
previous_key.key
|
||||
else
|
||||
create.key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_key
|
||||
return if key.present?
|
||||
|
||||
cipher = OpenSSL::Cipher.new('AES-256-GCM')
|
||||
cipher.encrypt
|
||||
|
||||
self.key = cipher.random_key
|
||||
end
|
||||
end
|
|
@ -38,6 +38,8 @@
|
|||
# chosen_languages :string is an Array
|
||||
# created_by_application_id :bigint(8)
|
||||
# approved :boolean default(TRUE), not null
|
||||
# sign_in_token :string
|
||||
# sign_in_token_sent_at :datetime
|
||||
#
|
||||
|
||||
class User < ApplicationRecord
|
||||
|
@ -114,7 +116,7 @@ class User < ApplicationRecord
|
|||
:default_content_type, :system_emoji_font,
|
||||
to: :settings, prefix: :setting, allow_nil: false
|
||||
|
||||
attr_reader :invite_code
|
||||
attr_reader :invite_code, :sign_in_token_attempt
|
||||
attr_writer :external
|
||||
|
||||
def confirmed?
|
||||
|
@ -168,6 +170,10 @@ class User < ApplicationRecord
|
|||
true
|
||||
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?
|
||||
confirmed? && approved? && !disabled? && !account.suspended?
|
||||
end
|
||||
|
@ -270,6 +276,13 @@ class User < ApplicationRecord
|
|||
super
|
||||
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
|
||||
return false if encrypted_password.blank?
|
||||
|
||||
|
@ -305,6 +318,15 @@ class User < ApplicationRecord
|
|||
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
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
|
@ -321,6 +343,10 @@ class User < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def recent_ip?(ip)
|
||||
recent_ips.any? { |(_, recent_ip)| recent_ip == ip }
|
||||
end
|
||||
|
||||
def send_pending_devise_notifications
|
||||
pending_devise_notifications.each do |notification, args|
|
||||
render_and_send_devise_message(notification, *args)
|
||||
|
|
41
app/presenters/activitypub/activity_presenter.rb
Normal file
41
app/presenters/activitypub/activity_presenter.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
|
||||
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object
|
||||
|
||||
class << self
|
||||
def from_status(status)
|
||||
new.tap do |presenter|
|
||||
presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status)
|
||||
presenter.type = status.reblog? ? 'Announce' : 'Create'
|
||||
presenter.actor = ActivityPub::TagManager.instance.uri_for(status.account)
|
||||
presenter.published = status.created_at
|
||||
presenter.to = ActivityPub::TagManager.instance.to(status)
|
||||
presenter.cc = ActivityPub::TagManager.instance.cc(status)
|
||||
|
||||
presenter.virtual_object = begin
|
||||
if status.reblog?
|
||||
if status.account == status.proper.account && status.proper.private_visibility? && status.local?
|
||||
status.proper
|
||||
else
|
||||
ActivityPub::TagManager.instance.uri_for(status.proper)
|
||||
end
|
||||
else
|
||||
status.proper
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def from_encrypted_message(encrypted_message)
|
||||
new.tap do |presenter|
|
||||
presenter.id = ActivityPub::TagManager.instance.generate_uri_for(nil)
|
||||
presenter.type = 'Create'
|
||||
presenter.actor = ActivityPub::TagManager.instance.uri_for(encrypted_message.source_account)
|
||||
presenter.published = Time.now.utc
|
||||
presenter.to = ActivityPub::TagManager.instance.uri_for(encrypted_message.target_account)
|
||||
presenter.virtual_object = encrypted_message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
class InitialStatePresenter < ActiveModelSerializers::Model
|
||||
attributes :settings, :push_subscription, :token,
|
||||
:current_account, :admin, :text
|
||||
:current_account, :admin, :text, :visibility
|
||||
end
|
||||
|
|
|
@ -1,52 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_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
|
||||
has_one :virtual_object, key: :object
|
||||
|
||||
def published
|
||||
object.created_at.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?
|
||||
object.published.iso8601
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :identity_proof,
|
||||
:discoverable
|
||||
:discoverable, :olm
|
||||
|
||||
attributes :id, :type, :following, :followers,
|
||||
:inbox, :outbox, :featured,
|
||||
|
@ -20,6 +20,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
has_many :virtual_tags, key: :tag
|
||||
has_many :virtual_attachments, key: :attachment
|
||||
|
||||
attribute :devices, unless: :instance_actor?
|
||||
attribute :moved_to, if: :moved?
|
||||
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 :image, serializer: ActivityPub::ImageSerializer, if: :header_exists?
|
||||
|
||||
delegate :moved?, to: :object
|
||||
delegate :moved?, :instance_actor?, to: :object
|
||||
|
||||
def id
|
||||
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)
|
||||
end
|
||||
|
||||
def devices
|
||||
account_collection_url(object, :devices)
|
||||
end
|
||||
|
||||
def outbox
|
||||
account_outbox_url(object)
|
||||
end
|
||||
|
|
|
@ -1,11 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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)
|
||||
return ActivityPub::NoteSerializer if model.class.name == 'Status'
|
||||
return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter'
|
||||
case model.class.name
|
||||
when 'Status'
|
||||
ActivityPub::NoteSerializer
|
||||
when 'Device'
|
||||
ActivityPub::DeviceSerializer
|
||||
when 'ActivityPub::CollectionPresenter'
|
||||
ActivityPub::CollectionSerializer
|
||||
when 'String'
|
||||
StringSerializer
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
attribute :id, if: -> { object.id.present? }
|
||||
attribute :type
|
||||
|
|
52
app/serializers/activitypub/device_serializer.rb
Normal file
52
app/serializers/activitypub/device_serializer.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::DeviceSerializer < ActivityPub::Serializer
|
||||
context_extensions :olm
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
class FingerprintKeySerializer < ActivityPub::Serializer
|
||||
attributes :type, :public_key_base64
|
||||
|
||||
def type
|
||||
'Ed25519Key'
|
||||
end
|
||||
|
||||
def public_key_base64
|
||||
object.fingerprint_key
|
||||
end
|
||||
end
|
||||
|
||||
class IdentityKeySerializer < ActivityPub::Serializer
|
||||
attributes :type, :public_key_base64
|
||||
|
||||
def type
|
||||
'Curve25519Key'
|
||||
end
|
||||
|
||||
def public_key_base64
|
||||
object.identity_key
|
||||
end
|
||||
end
|
||||
|
||||
attributes :device_id, :type, :name, :claim
|
||||
|
||||
has_one :fingerprint_key, serializer: FingerprintKeySerializer
|
||||
has_one :identity_key, serializer: IdentityKeySerializer
|
||||
|
||||
def type
|
||||
'Device'
|
||||
end
|
||||
|
||||
def claim
|
||||
account_claim_url(object.account, id: object.device_id)
|
||||
end
|
||||
|
||||
def fingerprint_key
|
||||
object
|
||||
end
|
||||
|
||||
def identity_key
|
||||
object
|
||||
end
|
||||
end
|
61
app/serializers/activitypub/encrypted_message_serializer.rb
Normal file
61
app/serializers/activitypub/encrypted_message_serializer.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::EncryptedMessageSerializer < ActivityPub::Serializer
|
||||
context :security
|
||||
|
||||
context_extensions :olm
|
||||
|
||||
class DeviceSerializer < ActivityPub::Serializer
|
||||
attributes :type, :device_id
|
||||
|
||||
def type
|
||||
'Device'
|
||||
end
|
||||
|
||||
def device_id
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
class DigestSerializer < ActivityPub::Serializer
|
||||
attributes :type, :digest_algorithm, :digest_value
|
||||
|
||||
def type
|
||||
'Digest'
|
||||
end
|
||||
|
||||
def digest_algorithm
|
||||
'http://www.w3.org/2000/09/xmldsig#hmac-sha256'
|
||||
end
|
||||
|
||||
def digest_value
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
attributes :type, :message_type, :cipher_text, :message_franking
|
||||
|
||||
has_one :attributed_to, serializer: DeviceSerializer
|
||||
has_one :to, serializer: DeviceSerializer
|
||||
has_one :digest, serializer: DigestSerializer
|
||||
|
||||
def type
|
||||
'EncryptedMessage'
|
||||
end
|
||||
|
||||
def attributed_to
|
||||
object.source_device.device_id
|
||||
end
|
||||
|
||||
def to
|
||||
object.target_device_id
|
||||
end
|
||||
|
||||
def message_type
|
||||
object.type
|
||||
end
|
||||
|
||||
def cipher_text
|
||||
object.body
|
||||
end
|
||||
end
|
35
app/serializers/activitypub/one_time_key_serializer.rb
Normal file
35
app/serializers/activitypub/one_time_key_serializer.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::OneTimeKeySerializer < ActivityPub::Serializer
|
||||
context :security
|
||||
|
||||
context_extensions :olm
|
||||
|
||||
class SignatureSerializer < ActivityPub::Serializer
|
||||
attributes :type, :signature_value
|
||||
|
||||
def type
|
||||
'Ed25519Signature'
|
||||
end
|
||||
|
||||
def signature_value
|
||||
object.signature
|
||||
end
|
||||
end
|
||||
|
||||
attributes :key_id, :type, :public_key_base64
|
||||
|
||||
has_one :signature, serializer: SignatureSerializer
|
||||
|
||||
def type
|
||||
'Curve25519Key'
|
||||
end
|
||||
|
||||
def public_key_base64
|
||||
object.key
|
||||
end
|
||||
|
||||
def signature
|
||||
object
|
||||
end
|
||||
end
|
|
@ -2,7 +2,14 @@
|
|||
|
||||
class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer
|
||||
def self.serializer_for(model, options)
|
||||
return ActivityPub::ActivitySerializer if model.is_a?(Status)
|
||||
if model.class.name == 'ActivityPub::ActivityPresenter'
|
||||
ActivityPub::ActivitySerializer
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def items
|
||||
object.items.map { |status| ActivityPub::ActivityPresenter.from_status(status) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
||||
attributes :id, :type, :actor, :to
|
||||
|
||||
has_one :object, serializer: ActivityPub::ActivitySerializer
|
||||
has_one :virtual_object, key: :object, serializer: ActivityPub::ActivitySerializer
|
||||
|
||||
def id
|
||||
[ActivityPub::TagManager.instance.uri_for(object.account), '#announces/', object.id, '/undo'].join
|
||||
|
@ -20,4 +20,8 @@ class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer
|
|||
def to
|
||||
[ActivityPub::TagManager::COLLECTIONS[:public]]
|
||||
end
|
||||
|
||||
def virtual_object
|
||||
ActivityPub::ActivityPresenter.from_status(object)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,7 +72,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
|||
|
||||
if object.current_account
|
||||
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
|
||||
end
|
||||
|
||||
|
|
19
app/serializers/rest/encrypted_message_serializer.rb
Normal file
19
app/serializers/rest/encrypted_message_serializer.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::EncryptedMessageSerializer < ActiveModel::Serializer
|
||||
attributes :id, :account_id, :device_id,
|
||||
:type, :body, :digest, :message_franking,
|
||||
:created_at
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def account_id
|
||||
object.from_account_id.to_s
|
||||
end
|
||||
|
||||
def device_id
|
||||
object.from_device_id
|
||||
end
|
||||
end
|
9
app/serializers/rest/keys/claim_result_serializer.rb
Normal file
9
app/serializers/rest/keys/claim_result_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::Keys::ClaimResultSerializer < ActiveModel::Serializer
|
||||
attributes :account_id, :device_id, :key_id, :key, :signature
|
||||
|
||||
def account_id
|
||||
object.account.id.to_s
|
||||
end
|
||||
end
|
6
app/serializers/rest/keys/device_serializer.rb
Normal file
6
app/serializers/rest/keys/device_serializer.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::Keys::DeviceSerializer < ActiveModel::Serializer
|
||||
attributes :device_id, :name, :identity_key,
|
||||
:fingerprint_key
|
||||
end
|
11
app/serializers/rest/keys/query_result_serializer.rb
Normal file
11
app/serializers/rest/keys/query_result_serializer.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::Keys::QueryResultSerializer < ActiveModel::Serializer
|
||||
attributes :account_id
|
||||
|
||||
has_many :devices, serializer: REST::Keys::DeviceSerializer
|
||||
|
||||
def account_id
|
||||
object.account.id.to_s
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
|||
attributes :url, :title, :description, :type,
|
||||
:author_name, :author_url, :provider_name,
|
||||
:provider_url, :html, :width, :height,
|
||||
:image, :embed_url
|
||||
:image, :embed_url, :blurhash
|
||||
|
||||
def image
|
||||
object.image? ? full_asset_url(object.image.url(:original)) : nil
|
||||
|
|
|
@ -76,6 +76,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
|
||||
@account.followers_url = @json['followers'] || ''
|
||||
@account.featured_collection_url = @json['featured'] || ''
|
||||
@account.devices_url = @json['devices'] || ''
|
||||
@account.url = url || @uri
|
||||
@account.uri = @uri
|
||||
@account.display_name = @json['name'] || ''
|
||||
|
|
|
@ -22,7 +22,7 @@ class BackupService < BaseService
|
|||
|
||||
account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
|
||||
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')
|
||||
|
||||
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
|
||||
|
|
|
@ -26,59 +26,20 @@ class BlockDomainService < BaseService
|
|||
suspend_accounts!
|
||||
end
|
||||
|
||||
clear_media! 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}-*") }
|
||||
DomainClearMediaWorker.perform_async(domain_block.id) if domain_block.reject_media?
|
||||
end
|
||||
|
||||
def silence_accounts!
|
||||
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
|
||||
end
|
||||
|
||||
def clear_media!
|
||||
@affected_status_ids = []
|
||||
|
||||
clear_account_images!
|
||||
clear_account_attachments!
|
||||
clear_emojos!
|
||||
|
||||
invalidate_association_caches!
|
||||
end
|
||||
|
||||
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)
|
||||
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
|
||||
|
@ -86,12 +47,4 @@ class BlockDomainService < BaseService
|
|||
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
|
||||
|
|
70
app/services/clear_domain_media_service.rb
Normal file
70
app/services/clear_domain_media_service.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ClearDomainMediaService < BaseService
|
||||
attr_reader :domain_block
|
||||
|
||||
def call(domain_block)
|
||||
@domain_block = domain_block
|
||||
clear_media! if domain_block.reject_media?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invalidate_association_caches!
|
||||
# Normally, associated models of a status are immutable (except for accounts)
|
||||
# so they are aggressively cached. After updating the media attachments to no
|
||||
# longer point to a local file, we need to clear the cache to make those
|
||||
# changes appear in the API and UI
|
||||
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
|
||||
end
|
||||
|
||||
def clear_media!
|
||||
@affected_status_ids = []
|
||||
|
||||
begin
|
||||
clear_account_images!
|
||||
clear_account_attachments!
|
||||
clear_emojos!
|
||||
ensure
|
||||
invalidate_association_caches!
|
||||
end
|
||||
end
|
||||
|
||||
def clear_account_images!
|
||||
blocked_domain_accounts.reorder(nil).find_each do |account|
|
||||
account.avatar.destroy if account.avatar&.exists?
|
||||
account.header.destroy if account.header&.exists?
|
||||
account.save
|
||||
end
|
||||
end
|
||||
|
||||
def clear_account_attachments!
|
||||
media_from_blocked_domain.reorder(nil).find_each do |attachment|
|
||||
@affected_status_ids << attachment.status_id if attachment.status_id.present?
|
||||
|
||||
attachment.file.destroy if attachment.file&.exists?
|
||||
attachment.type = :unknown
|
||||
attachment.save
|
||||
end
|
||||
end
|
||||
|
||||
def clear_emojos!
|
||||
emojis_from_blocked_domains.destroy_all
|
||||
end
|
||||
|
||||
def blocked_domain
|
||||
domain_block.domain
|
||||
end
|
||||
|
||||
def blocked_domain_accounts
|
||||
Account.by_domain_and_subdomains(blocked_domain)
|
||||
end
|
||||
|
||||
def media_from_blocked_domain
|
||||
MediaAttachment.joins(:account).merge(blocked_domain_accounts).reorder(nil)
|
||||
end
|
||||
|
||||
def emojis_from_blocked_domains
|
||||
CustomEmoji.by_domain_and_subdomains(blocked_domain)
|
||||
end
|
||||
end
|
|
@ -5,8 +5,9 @@ module Payloadable
|
|||
signer = options.delete(:signer)
|
||||
sign_with = options.delete(:sign_with)
|
||||
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)
|
||||
else
|
||||
payload
|
||||
|
|
78
app/services/deliver_to_device_service.rb
Normal file
78
app/services/deliver_to_device_service.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeliverToDeviceService < BaseService
|
||||
include Payloadable
|
||||
|
||||
class EncryptedMessage < ActiveModelSerializers::Model
|
||||
attributes :source_account, :target_account, :source_device,
|
||||
:target_device_id, :type, :body, :digest,
|
||||
:message_franking
|
||||
end
|
||||
|
||||
def call(source_account, source_device, options = {})
|
||||
@source_account = source_account
|
||||
@source_device = source_device
|
||||
@target_account = Account.find(options[:account_id])
|
||||
@target_device_id = options[:device_id]
|
||||
@body = options[:body]
|
||||
@type = options[:type]
|
||||
@hmac = options[:hmac]
|
||||
|
||||
set_message_franking!
|
||||
|
||||
if @target_account.local?
|
||||
deliver_to_local!
|
||||
else
|
||||
deliver_to_remote!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_message_franking!
|
||||
@message_franking = message_franking.to_token
|
||||
end
|
||||
|
||||
def deliver_to_local!
|
||||
target_device = @target_account.devices.find_by!(device_id: @target_device_id)
|
||||
|
||||
target_device.encrypted_messages.create!(
|
||||
from_account: @source_account,
|
||||
from_device_id: @source_device.device_id,
|
||||
type: @type,
|
||||
body: @body,
|
||||
digest: @hmac,
|
||||
message_franking: @message_franking
|
||||
)
|
||||
end
|
||||
|
||||
def deliver_to_remote!
|
||||
ActivityPub::DeliveryWorker.perform_async(
|
||||
Oj.dump(serialize_payload(ActivityPub::ActivityPresenter.from_encrypted_message(encrypted_message), ActivityPub::ActivitySerializer)),
|
||||
@source_account.id,
|
||||
@target_account.inbox_url
|
||||
)
|
||||
end
|
||||
|
||||
def message_franking
|
||||
MessageFranking.new(
|
||||
source_account_id: @source_account.id,
|
||||
target_account_id: @target_account.id,
|
||||
hmac: @hmac,
|
||||
timestamp: Time.now.utc
|
||||
)
|
||||
end
|
||||
|
||||
def encrypted_message
|
||||
EncryptedMessage.new(
|
||||
source_account: @source_account,
|
||||
target_account: @target_account,
|
||||
source_device: @source_device,
|
||||
target_device_id: @target_device_id,
|
||||
type: @type,
|
||||
body: @body,
|
||||
digest: @hmac,
|
||||
message_franking: @message_franking
|
||||
)
|
||||
end
|
||||
end
|
|
@ -81,7 +81,9 @@ class ImportService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
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]
|
||||
end
|
||||
end
|
||||
|
|
77
app/services/keys/claim_service.rb
Normal file
77
app/services/keys/claim_service.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Keys::ClaimService < BaseService
|
||||
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
||||
|
||||
class Result < ActiveModelSerializers::Model
|
||||
attributes :account, :device_id, :key_id,
|
||||
:key, :signature
|
||||
|
||||
def initialize(account, device_id, key_attributes = {})
|
||||
@account = account
|
||||
@device_id = device_id
|
||||
@key_id = key_attributes[:key_id]
|
||||
@key = key_attributes[:key]
|
||||
@signature = key_attributes[:signature]
|
||||
end
|
||||
end
|
||||
|
||||
def call(source_account, target_account_id, device_id)
|
||||
@source_account = source_account
|
||||
@target_account = Account.find(target_account_id)
|
||||
@device_id = device_id
|
||||
|
||||
if @target_account.local?
|
||||
claim_local_key!
|
||||
else
|
||||
claim_remote_key!
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def claim_local_key!
|
||||
device = @target_account.devices.find_by(device_id: @device_id)
|
||||
key = nil
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
key = device.one_time_keys.order(Arel.sql('random()')).first!
|
||||
key.destroy!
|
||||
end
|
||||
|
||||
@result = Result.new(@target_account, @device_id, key)
|
||||
end
|
||||
|
||||
def claim_remote_key!
|
||||
query_result = QueryService.new.call(@target_account)
|
||||
device = query_result.find(@device_id)
|
||||
|
||||
return unless device.present? && device.valid_claim_url?
|
||||
|
||||
json = fetch_resource_with_post(device.claim_url)
|
||||
|
||||
return unless json.present? && json['publicKeyBase64'].present?
|
||||
|
||||
@result = Result.new(@target_account, @device_id, key_id: json['id'], key: json['publicKeyBase64'], signature: json.dig('signature', 'signatureValue'))
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||
Rails.logger.debug "Claiming one-time key for #{@target_account.acct}:#{@device_id} failed: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
def fetch_resource_with_post(uri)
|
||||
build_post_request(uri).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
|
||||
|
||||
body_to_json(response.body_with_limit) if response.code == 200
|
||||
end
|
||||
end
|
||||
|
||||
def build_post_request(uri)
|
||||
Request.new(:post, uri).tap do |request|
|
||||
request.on_behalf_of(@source_account, :uri)
|
||||
request.add_headers(HEADERS)
|
||||
end
|
||||
end
|
||||
end
|
75
app/services/keys/query_service.rb
Normal file
75
app/services/keys/query_service.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Keys::QueryService < BaseService
|
||||
include JsonLdHelper
|
||||
|
||||
class Result < ActiveModelSerializers::Model
|
||||
attributes :account, :devices
|
||||
|
||||
def initialize(account, devices)
|
||||
@account = account
|
||||
@devices = devices || []
|
||||
end
|
||||
|
||||
def find(device_id)
|
||||
@devices.find { |device| device.device_id == device_id }
|
||||
end
|
||||
end
|
||||
|
||||
class Device < ActiveModelSerializers::Model
|
||||
attributes :device_id, :name, :identity_key, :fingerprint_key
|
||||
|
||||
def initialize(attributes = {})
|
||||
@device_id = attributes[:device_id]
|
||||
@name = attributes[:name]
|
||||
@identity_key = attributes[:identity_key]
|
||||
@fingerprint_key = attributes[:fingerprint_key]
|
||||
@claim_url = attributes[:claim_url]
|
||||
end
|
||||
|
||||
def valid_claim_url?
|
||||
return false if @claim_url.blank?
|
||||
|
||||
begin
|
||||
parsed_url = Addressable::URI.parse(@claim_url).normalize
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
return false
|
||||
end
|
||||
|
||||
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
|
||||
end
|
||||
end
|
||||
|
||||
def call(account)
|
||||
@account = account
|
||||
|
||||
if @account.local?
|
||||
query_local_devices!
|
||||
else
|
||||
query_remote_devices!
|
||||
end
|
||||
|
||||
Result.new(@account, @devices)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_local_devices!
|
||||
@devices = @account.devices.map { |device| Device.new(device) }
|
||||
end
|
||||
|
||||
def query_remote_devices!
|
||||
return if @account.devices_url.blank?
|
||||
|
||||
json = fetch_resource(@account.devices_url)
|
||||
|
||||
return if json['items'].blank?
|
||||
|
||||
@devices = json['items'].map do |device|
|
||||
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||
end
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||
Rails.logger.debug "Querying devices for #{@account.acct} failed: #{e}"
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -65,7 +65,7 @@ class ProcessMentionsService < BaseService
|
|||
|
||||
def activitypub_json
|
||||
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
|
||||
|
||||
def resolve_account_service
|
||||
|
|
|
@ -60,6 +60,6 @@ class ReblogService < BaseService
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -112,6 +112,8 @@ class ResolveAccountService < BaseService
|
|||
end
|
||||
|
||||
def webfinger_update_due?
|
||||
return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
|
||||
|
||||
@account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
|
||||
end
|
||||
|
||||
|
|
19
app/validators/ed25519_key_validator.rb
Normal file
19
app/validators/ed25519_key_validator.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ed25519KeyValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.blank?
|
||||
|
||||
key = Base64.decode64(value)
|
||||
|
||||
record.errors[attribute] << I18n.t('crypto.errors.invalid_key') unless verified?(key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verified?(key)
|
||||
Ed25519.validate_key_bytes(key)
|
||||
rescue ArgumentError
|
||||
false
|
||||
end
|
||||
end
|
29
app/validators/ed25519_signature_validator.rb
Normal file
29
app/validators/ed25519_signature_validator.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Ed25519SignatureValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.blank?
|
||||
|
||||
verify_key = Ed25519::VerifyKey.new(Base64.decode64(option_to_value(record, :verify_key)))
|
||||
signature = Base64.decode64(value)
|
||||
message = option_to_value(record, :message)
|
||||
|
||||
record.errors[attribute] << I18n.t('crypto.errors.invalid_signature') unless verified?(verify_key, signature, message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verified?(verify_key, signature, message)
|
||||
verify_key.verify(signature, message)
|
||||
rescue Ed25519::VerifyError, ArgumentError
|
||||
false
|
||||
end
|
||||
|
||||
def option_to_value(record, key)
|
||||
if options[key].is_a?(Proc)
|
||||
options[key].call(record)
|
||||
else
|
||||
record.public_send(options[key])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -55,12 +55,15 @@
|
|||
%p= t('about.unavailable_content_html')
|
||||
|
||||
- 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')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
- if (blocks = @blocks.select(&:silence?)) && !blocks.empty?
|
||||
%h3= t('about.unavailable_content_description.silenced_title')
|
||||
%p= t('about.unavailable_content_description.silenced')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
- if (blocks = @blocks.select(&:suspend?)) && !blocks.empty?
|
||||
%h3= t('about.unavailable_content_description.suspended_title')
|
||||
%p= t('about.unavailable_content_description.suspended')
|
||||
= render partial: 'domain_blocks', locals: { domain_blocks: blocks }
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- content_for :page_title do
|
||||
= t('admin.custom_emojis.title')
|
||||
|
||||
- if can?(:create, :custom_emoji)
|
||||
- content_for :heading_actions do
|
||||
= link_to t('admin.custom_emojis.upload'), new_admin_custom_emoji_path, class: 'button'
|
||||
|
||||
|
@ -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') }
|
||||
|
||||
- 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') }
|
||||
|
||||
- if params[:local] == '1'
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
- content_for :page_title do
|
||||
= 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
|
||||
.filter-subset
|
||||
%strong= t('admin.instances.moderation.title')
|
||||
|
@ -10,12 +16,6 @@
|
|||
- unless whitelist_mode?
|
||||
%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?
|
||||
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
|
||||
.fields-group
|
||||
|
|
14
app/views/auth/sessions/sign_in_token.html.haml
Normal file
14
app/views/auth/sessions/sign_in_token.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
|||
- content_for :page_title do
|
||||
= t('auth.login')
|
||||
|
||||
= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
|
||||
%p.hint.otp-hint= t('users.suspicious_sign_in_confirmation')
|
||||
|
||||
.fields-group
|
||||
= f.input :sign_in_token_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.sign_in_token_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.sign_in_token_attempt'), :autocomplete => 'off' }, autofocus: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('auth.login'), type: :submit
|
||||
|
||||
- if Setting.site_contact_email.present?
|
||||
%p.hint.subtle-hint= t('users.generic_access_help_html', email: mail_to(Setting.site_contact_email, nil))
|
|
@ -39,7 +39,7 @@
|
|||
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
- 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
|
||||
%data.dt-published{ value: status.created_at.to_time.iso8601 }
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||
- 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
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
|
|
105
app/views/user_mailer/sign_in_token.html.haml
Normal file
105
app/views/user_mailer/sign_in_token.html.haml
Normal file
|
@ -0,0 +1,105 @@
|
|||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.email-body
|
||||
.email-container
|
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.content-cell.hero
|
||||
.email-row
|
||||
.col-6
|
||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.column-cell.text-center.padded
|
||||
%table.hero-icon.alert-icon{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td
|
||||
= image_tag full_pack_url('media/images/mailer/icon_email.png'), alt: ''
|
||||
|
||||
%h1= t 'user_mailer.sign_in_token.title'
|
||||
%p.lead= t 'user_mailer.sign_in_token.explanation'
|
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.email-body
|
||||
.email-container
|
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.content-cell.content-start
|
||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.column-cell.input-cell
|
||||
%table.input{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td= @resource.sign_in_token
|
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.email-body
|
||||
.email-container
|
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.content-cell
|
||||
.email-row
|
||||
.col-6
|
||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.column-cell.text-center
|
||||
%p= t 'user_mailer.sign_in_token.details'
|
||||
%tr
|
||||
%td.column-cell.text-center
|
||||
%p
|
||||
%strong= "#{t('sessions.ip')}:"
|
||||
= @remote_ip
|
||||
%br/
|
||||
%strong= "#{t('sessions.browser')}:"
|
||||
%span{ title: @user_agent }= t 'sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")
|
||||
%br/
|
||||
= l(@timestamp)
|
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.email-body
|
||||
.email-container
|
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.content-cell
|
||||
.email-row
|
||||
.col-6
|
||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.column-cell.text-center
|
||||
%p= t 'user_mailer.sign_in_token.further_actions'
|
||||
|
||||
%table.email-table{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.email-body
|
||||
.email-container
|
||||
%table.content-section{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.content-cell
|
||||
%table.column{ cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.column-cell.button-cell
|
||||
%table.button{ align: 'center', cellspacing: 0, cellpadding: 0 }
|
||||
%tbody
|
||||
%tr
|
||||
%td.button-primary
|
||||
= link_to edit_user_registration_url do
|
||||
%span= t 'settings.account_settings'
|
17
app/views/user_mailer/sign_in_token.text.erb
Normal file
17
app/views/user_mailer/sign_in_token.text.erb
Normal file
|
@ -0,0 +1,17 @@
|
|||
<%= t 'user_mailer.sign_in_token.title' %>
|
||||
|
||||
===
|
||||
|
||||
<%= t 'user_mailer.sign_in_token.explanation' %>
|
||||
|
||||
=> <%= @resource.sign_in_token %>
|
||||
|
||||
<%= t 'user_mailer.sign_in_token.details' %>
|
||||
|
||||
<%= t('sessions.ip') %>: <%= @remote_ip %>
|
||||
<%= t('sessions.browser') %>: <%= t('sessions.description', browser: t("sessions.browsers.#{@detection.id}", default: "#{@detection.id}"), platform: t("sessions.platforms.#{@detection.platform.id}", default: "#{@detection.platform.id}")) %>
|
||||
<%= l(@timestamp) %>
|
||||
|
||||
<%= t 'user_mailer.sign_in_token.further_actions' %>
|
||||
|
||||
=> <%= edit_user_registration_url %>
|
|
@ -43,7 +43,7 @@ class ActivityPub::DistributionWorker
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def relay!
|
||||
|
|
|
@ -29,6 +29,6 @@ class ActivityPub::ReplyDistributionWorker
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -4,8 +4,9 @@ class DomainBlockWorker
|
|||
include Sidekiq::Worker
|
||||
|
||||
def perform(domain_block_id, update = false)
|
||||
BlockDomainService.new.call(DomainBlock.find(domain_block_id), update)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
domain_block = DomainBlock.find_by(id: domain_block_id)
|
||||
return true if domain_block.nil?
|
||||
|
||||
BlockDomainService.new.call(domain_block, update)
|
||||
end
|
||||
end
|
||||
|
|
14
app/workers/domain_clear_media_worker.rb
Normal file
14
app/workers/domain_clear_media_worker.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DomainClearMediaWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(domain_block_id)
|
||||
domain_block = DomainBlock.find_by(id: domain_block_id)
|
||||
return true if domain_block.nil?
|
||||
|
||||
ClearDomainMediaService.new.call(domain_block)
|
||||
end
|
||||
end
|
|
@ -7,7 +7,8 @@ class Import::RelationshipWorker
|
|||
|
||||
def perform(account_id, target_account_uri, relationship, options = {})
|
||||
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!
|
||||
|
||||
return if target_account.nil?
|
||||
|
@ -29,4 +30,22 @@ class Import::RelationshipWorker
|
|||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
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
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
class PushConversationWorker
|
||||
include Sidekiq::Worker
|
||||
include Redisable
|
||||
|
||||
def perform(conversation_account_id)
|
||||
conversation = AccountConversation.find(conversation_account_id)
|
||||
message = InlineRenderer.render(conversation, conversation.account, :conversation)
|
||||
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
|
||||
true
|
||||
end
|
||||
|
|
16
app/workers/push_encrypted_message_worker.rb
Normal file
16
app/workers/push_encrypted_message_worker.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PushEncryptedMessageWorker
|
||||
include Sidekiq::Worker
|
||||
include Redisable
|
||||
|
||||
def perform(encrypted_message_id)
|
||||
encrypted_message = EncryptedMessage.find(encrypted_message_id)
|
||||
message = InlineRenderer.render(encrypted_message, nil, :encrypted_message)
|
||||
timeline_id = "timeline:#{encrypted_message.device.account_id}:#{encrypted_message.device.device_id}"
|
||||
|
||||
redis.publish(timeline_id, Oj.dump(event: :encrypted_message, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -8,5 +8,6 @@ class Scheduler::DoorkeeperCleanupScheduler
|
|||
def perform
|
||||
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
|
||||
SystemKey.expired.delete_all
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,25 +1,5 @@
|
|||
{
|
||||
"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_code": 0,
|
||||
|
@ -27,7 +7,7 @@
|
|||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/report.rb",
|
||||
"line": 90,
|
||||
"line": 112,
|
||||
"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\")",
|
||||
"render_path": null,
|
||||
|
@ -47,7 +27,7 @@
|
|||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/status.rb",
|
||||
"line": 87,
|
||||
"line": 100,
|
||||
"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}\")",
|
||||
"render_path": null,
|
||||
|
@ -61,39 +41,62 @@
|
|||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Mass Assignment",
|
||||
"warning_code": 105,
|
||||
"fingerprint": "28d81cc22580ef76e912b077b245f353499aa27b3826476667224c00227af2a9",
|
||||
"check_name": "PermitAttributes",
|
||||
"message": "Potentially dangerous key allowed for mass assignment",
|
||||
"file": "app/controllers/admin/reports_controller.rb",
|
||||
"line": 56,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||
"code": "params.permit(:account_id, :resolved, :target_account_id)",
|
||||
"render_path": null,
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "20a660939f2bbf8c665e69f2844031c0564524689a9570a0091ed94846212020",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/admin/action_logs/index.html.haml",
|
||||
"line": 26,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => Admin::ActionLogFilter.new(filter_params).results.page(params[:page]), {})",
|
||||
"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": {
|
||||
"type": "method",
|
||||
"class": "Admin::ReportsController",
|
||||
"method": "filter_params"
|
||||
"type": "template",
|
||||
"template": "admin/action_logs/index"
|
||||
},
|
||||
"user_input": ":account_id",
|
||||
"confidence": "High",
|
||||
"user_input": "params[:page]",
|
||||
"confidence": "Weak",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "4b6a895e2805578d03ceedbe1d469cc75a0c759eba093722523edb4b8683c873",
|
||||
"fingerprint": "371fe16dc4c9d6ab08a20437d65be4825776107a67c38f6d4780a9c703cd44a5",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/admin/action_logs/index.html.haml",
|
||||
"line": 4,
|
||||
"file": "app/views/admin/email_domain_blocks/index.html.haml",
|
||||
"line": 17,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => Admin::ActionLog.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"}}],
|
||||
"code": "render(action => EmailDomainBlock.where(:parent_id => nil).includes(:children).order(:id => :desc).page(params[:page]), {})",
|
||||
"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": {
|
||||
"type": "template",
|
||||
"template": "admin/action_logs/index"
|
||||
"template": "admin/email_domain_blocks/index"
|
||||
},
|
||||
"user_input": "params[:page]",
|
||||
"confidence": "Weak",
|
||||
|
@ -106,7 +109,7 @@
|
|||
"check_name": "Redirect",
|
||||
"message": "Possible unprotected redirect",
|
||||
"file": "app/controllers/remote_interaction_controller.rb",
|
||||
"line": 21,
|
||||
"line": 24,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
|
||||
"code": "redirect_to(RemoteFollow.new(resource_params).interact_address_for(Status.find(params[:id])))",
|
||||
"render_path": null,
|
||||
|
@ -119,25 +122,6 @@
|
|||
"confidence": "High",
|
||||
"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_code": 0,
|
||||
|
@ -145,7 +129,7 @@
|
|||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/status.rb",
|
||||
"line": 92,
|
||||
"line": 105,
|
||||
"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}\")",
|
||||
"render_path": null,
|
||||
|
@ -159,22 +143,43 @@
|
|||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Dynamic Render Path",
|
||||
"warning_code": 15,
|
||||
"fingerprint": "8d843713d99e8403f7992f3e72251b633817cf9076ffcbbad5613859d2bbc127",
|
||||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/admin/custom_emojis/index.html.haml",
|
||||
"line": 45,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"code": "render(action => filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page]), {})",
|
||||
"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"}}],
|
||||
"warning_type": "Mass Assignment",
|
||||
"warning_code": 105,
|
||||
"fingerprint": "7631e93d0099506e7c3e5c91ba8d88523b00a41a0834ae30031a5a4e8bb3020a",
|
||||
"check_name": "PermitAttributes",
|
||||
"message": "Potentially dangerous key allowed for mass assignment",
|
||||
"file": "app/controllers/api/v2/search_controller.rb",
|
||||
"line": 28,
|
||||
"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": "template",
|
||||
"template": "admin/custom_emojis/index"
|
||||
"type": "method",
|
||||
"class": "Api::V2::SearchController",
|
||||
"method": "search_params"
|
||||
},
|
||||
"user_input": "params[:page]",
|
||||
"confidence": "Weak",
|
||||
"user_input": ":account_id",
|
||||
"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": ""
|
||||
},
|
||||
{
|
||||
|
@ -204,10 +209,22 @@
|
|||
"check_name": "Render",
|
||||
"message": "Render path contains parameter value",
|
||||
"file": "app/views/admin/accounts/index.html.haml",
|
||||
"line": 47,
|
||||
"line": 54,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||
"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": {
|
||||
"type": "template",
|
||||
"template": "admin/accounts/index"
|
||||
|
@ -216,6 +233,26 @@
|
|||
"confidence": "Weak",
|
||||
"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_code": 18,
|
||||
|
@ -223,7 +260,7 @@
|
|||
"check_name": "Redirect",
|
||||
"message": "Possible unprotected redirect",
|
||||
"file": "app/controllers/media_controller.rb",
|
||||
"line": 14,
|
||||
"line": 20,
|
||||
"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))",
|
||||
"render_path": null,
|
||||
|
@ -236,26 +273,6 @@
|
|||
"confidence": "High",
|
||||
"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_code": 105,
|
||||
|
@ -275,27 +292,8 @@
|
|||
"user_input": ":account_id",
|
||||
"confidence": "High",
|
||||
"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",
|
||||
"brakeman_version": "4.4.0"
|
||||
"updated": "2020-06-01 18:18:02 +0200",
|
||||
"brakeman_version": "4.8.0"
|
||||
}
|
||||
|
|
|
@ -95,7 +95,8 @@ Doorkeeper.configure do
|
|||
:'admin:read:reports',
|
||||
:'admin:write',
|
||||
:'admin:write:accounts',
|
||||
:'admin:write:reports'
|
||||
:'admin:write:reports',
|
||||
:crypto
|
||||
|
||||
# Change the way client credentials are retrieved from the request object.
|
||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||
|
|
|
@ -19,6 +19,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
inflect.acronym 'ActivityStreams'
|
||||
inflect.acronym 'JsonLd'
|
||||
inflect.acronym 'NodeInfo'
|
||||
inflect.acronym 'Ed25519'
|
||||
|
||||
inflect.singular 'data', 'data'
|
||||
end
|
||||
|
|
|
@ -35,13 +35,16 @@ en:
|
|||
status_count_before: Who authored
|
||||
tagline: Follow friends and discover new ones
|
||||
terms: Terms of service
|
||||
unavailable_content: Unavailable content
|
||||
unavailable_content: Moderated servers
|
||||
unavailable_content_description:
|
||||
domain: Server
|
||||
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:'
|
||||
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_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.
|
||||
user_count_after:
|
||||
one: user
|
||||
|
@ -309,6 +312,7 @@ en:
|
|||
listed: Listed
|
||||
new:
|
||||
title: Add new custom emoji
|
||||
not_permitted: You are not permitted to perform this action
|
||||
overwrite: Overwrite
|
||||
shortcode: Shortcode
|
||||
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."
|
||||
invalid_password: Invalid password
|
||||
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:
|
||||
formats:
|
||||
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.
|
||||
past_migrations: Past migrations
|
||||
proceed_with_move: Move followers
|
||||
redirected_msg: Your account is now redirecting to %{acct}.
|
||||
redirecting_to: Your account is redirecting to %{acct}.
|
||||
set_redirect: Set redirect
|
||||
warning:
|
||||
|
@ -1267,6 +1276,12 @@ en:
|
|||
explanation: You requested a full backup of your Mastodon account. It's now ready for download!
|
||||
subject: Your archive is ready for download
|
||||
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:
|
||||
explanation:
|
||||
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}!
|
||||
users:
|
||||
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_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}
|
||||
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:'
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -151,6 +151,7 @@ en:
|
|||
setting_use_blurhash: Show colorful gradients for hidden media
|
||||
setting_use_pending_items: Slow mode
|
||||
severity: Severity
|
||||
sign_in_token_attempt: Security code
|
||||
type: Import type
|
||||
username: Username
|
||||
username_or_email: Username or Email
|
||||
|
|
|
@ -79,6 +79,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resource :outbox, only: [:show], module: :activitypub
|
||||
resource :inbox, only: [:create], module: :activitypub
|
||||
resource :claim, only: [:create], module: :activitypub
|
||||
resources :collections, only: [:show], module: :activitypub
|
||||
end
|
||||
|
||||
|
@ -342,6 +343,23 @@ Rails.application.routes.draw do
|
|||
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
|
||||
member do
|
||||
post :read
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue