Browse Source

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.
master
Thibaut Girka 1 month ago
parent
commit
12c8ac9e14
100 changed files with 1863 additions and 417 deletions
  1. 0
    28
      .dependabot/config.yml
  2. 22
    0
      .github/dependabot.yml
  3. 5
    5
      Gemfile
  4. 29
    29
      Gemfile.lock
  5. 12
    0
      SECURITY.md
  6. 4
    2
      app/controllers/accounts_controller.rb
  7. 21
    0
      app/controllers/activitypub/claims_controller.rb
  8. 31
    17
      app/controllers/activitypub/collections_controller.rb
  9. 2
    0
      app/controllers/admin/custom_emojis_controller.rb
  10. 30
    0
      app/controllers/api/v1/crypto/deliveries_controller.rb
  11. 59
    0
      app/controllers/api/v1/crypto/encrypted_messages_controller.rb
  12. 25
    0
      app/controllers/api/v1/crypto/keys/claims_controller.rb
  13. 17
    0
      app/controllers/api/v1/crypto/keys/counts_controller.rb
  14. 26
    0
      app/controllers/api/v1/crypto/keys/queries_controller.rb
  15. 29
    0
      app/controllers/api/v1/crypto/keys/uploads_controller.rb
  16. 6
    46
      app/controllers/auth/sessions_controller.rb
  17. 50
    0
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  18. 48
    0
      app/controllers/concerns/two_factor_authentication_concern.rb
  19. 1
    1
      app/controllers/settings/migration/redirects_controller.rb
  20. 1
    1
      app/controllers/statuses_controller.rb
  21. 3
    1
      app/controllers/tags_controller.rb
  22. 5
    0
      app/helpers/application_helper.rb
  23. 19
    0
      app/helpers/webfinger_helper.rb
  24. 1
    1
      app/javascript/mastodon/actions/importer/normalizer.js
  25. 1
    1
      app/javascript/mastodon/components/autosuggest_textarea.js
  26. 1
    0
      app/javascript/mastodon/components/status.js
  27. 1
    1
      app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
  28. 10
    1
      app/javascript/mastodon/features/emoji/emoji.js
  29. 76
    9
      app/javascript/mastodon/features/status/components/card.js
  30. 1
    1
      app/javascript/mastodon/features/status/components/detailed_status.js
  31. 14
    14
      app/javascript/mastodon/locales/en.json
  32. 2
    0
      app/javascript/styles/mastodon-light/variables.scss
  33. 5
    6
      app/javascript/styles/mastodon/accessibility.scss
  34. 25
    1
      app/javascript/styles/mastodon/components.scss
  35. 48
    2
      app/lib/activitypub/activity/create.rb
  36. 1
    0
      app/lib/activitypub/adapter.rb
  37. 8
    3
      app/lib/feed_manager.rb
  38. 2
    0
      app/lib/inline_renderer.rb
  39. 17
    0
      app/mailers/user_mailer.rb
  40. 1
    0
      app/models/account.rb
  41. 1
    0
      app/models/concerns/account_associations.rb
  42. 35
    0
      app/models/device.rb
  43. 50
    0
      app/models/encrypted_message.rb
  44. 19
    0
      app/models/message_franking.rb
  45. 21
    0
      app/models/one_time_key.rb
  46. 8
    1
      app/models/preview_card.rb
  47. 41
    0
      app/models/system_key.rb
  48. 27
    1
      app/models/user.rb
  49. 41
    0
      app/presenters/activitypub/activity_presenter.rb
  50. 1
    1
      app/presenters/initial_state_presenter.rb
  51. 12
    42
      app/serializers/activitypub/activity_serializer.rb
  52. 7
    2
      app/serializers/activitypub/actor_serializer.rb
  53. 21
    3
      app/serializers/activitypub/collection_serializer.rb
  54. 52
    0
      app/serializers/activitypub/device_serializer.rb
  55. 61
    0
      app/serializers/activitypub/encrypted_message_serializer.rb
  56. 35
    0
      app/serializers/activitypub/one_time_key_serializer.rb
  57. 9
    2
      app/serializers/activitypub/outbox_serializer.rb
  58. 5
    1
      app/serializers/activitypub/undo_announce_serializer.rb
  59. 1
    1
      app/serializers/initial_state_serializer.rb
  60. 19
    0
      app/serializers/rest/encrypted_message_serializer.rb
  61. 9
    0
      app/serializers/rest/keys/claim_result_serializer.rb
  62. 6
    0
      app/serializers/rest/keys/device_serializer.rb
  63. 11
    0
      app/serializers/rest/keys/query_result_serializer.rb
  64. 1
    1
      app/serializers/rest/preview_card_serializer.rb
  65. 1
    0
      app/services/activitypub/process_account_service.rb
  66. 1
    1
      app/services/backup_service.rb
  67. 3
    50
      app/services/block_domain_service.rb
  68. 70
    0
      app/services/clear_domain_media_service.rb
  69. 2
    1
      app/services/concerns/payloadable.rb
  70. 78
    0
      app/services/deliver_to_device_service.rb
  71. 3
    1
      app/services/import_service.rb
  72. 77
    0
      app/services/keys/claim_service.rb
  73. 75
    0
      app/services/keys/query_service.rb
  74. 1
    1
      app/services/process_mentions_service.rb
  75. 1
    1
      app/services/reblog_service.rb
  76. 2
    0
      app/services/resolve_account_service.rb
  77. 19
    0
      app/validators/ed25519_key_validator.rb
  78. 29
    0
      app/validators/ed25519_signature_validator.rb
  79. 3
    0
      app/views/about/more.html.haml
  80. 6
    4
      app/views/admin/custom_emojis/index.html.haml
  81. 6
    6
      app/views/admin/instances/index.html.haml
  82. 14
    0
      app/views/auth/sessions/sign_in_token.html.haml
  83. 1
    1
      app/views/statuses/_detailed_status.html.haml
  84. 1
    1
      app/views/statuses/_simple_status.html.haml
  85. 105
    0
      app/views/user_mailer/sign_in_token.html.haml
  86. 17
    0
      app/views/user_mailer/sign_in_token.text.erb
  87. 1
    1
      app/workers/activitypub/distribution_worker.rb
  88. 1
    1
      app/workers/activitypub/reply_distribution_worker.rb
  89. 4
    3
      app/workers/domain_block_worker.rb
  90. 14
    0
      app/workers/domain_clear_media_worker.rb
  91. 20
    1
      app/workers/import/relationship_worker.rb
  92. 2
    1
      app/workers/push_conversation_worker.rb
  93. 16
    0
      app/workers/push_encrypted_message_worker.rb
  94. 1
    0
      app/workers/scheduler/doorkeeper_cleanup_scheduler.rb
  95. 113
    115
      config/brakeman.ignore
  96. 2
    1
      config/initializers/doorkeeper.rb
  97. 1
    0
      config/initializers/inflections.rb
  98. 20
    2
      config/locales/en.yml
  99. 1
    0
      config/locales/simple_form.en.yml
  100. 0
    0
      config/routes.rb

+ 0
- 28
.dependabot/config.yml View File

@@ -1,28 +0,0 @@
version: 1

update_configs:
- package_manager: "ruby:bundler"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security

- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security

+ 22
- 0
.github/dependabot.yml View File

@@ -0,0 +1,22 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: all

- package-ecosystem: bundler
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: all

+ 5
- 5
Gemfile View File

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

+ 29
- 29
Gemfile.lock View File

@@ -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
- 0
SECURITY.md View File

@@ -0,0 +1,12 @@
# Security Policy

## Supported Versions

| Version | Supported |
| ------- | ------------------ |
| 3.1.x | :white_check_mark: |
| < 3.1 | :x: |

## Reporting a Vulnerability

hello@joinmastodon.org

+ 4
- 2
app/controllers/accounts_controller.rb View File

@@ -1,7 +1,8 @@
# frozen_string_literal: true

class AccountsController < ApplicationController
PAGE_SIZE = 20
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
- 0
app/controllers/activitypub/claims_controller.rb View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true

class ActivityPub::ClaimsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern

skip_before_action :authenticate_user!

before_action :require_signature!
before_action :set_claim_result

def create
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
end

private

def set_claim_result
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
end
end

+ 31
- 17
app/controllers/activitypub/collections_controller.rb View File

@@ -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)
def set_items
case params[:id]
when 'featured'
@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 set_size
case params[:id]
when 'featured'
@size = @account.pinned_statuses.not_local_only.count
when 'featured', 'devices'
@size = @items.size
else
not_found
end
end

def scope_for_collection
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
else
@account.pinned_statuses.not_local_only
end
@type = :ordered
when 'devices'
@type = :unordered
else
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

+ 2
- 0
app/controllers/admin/custom_emojis_controller.rb View File

@@ -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
- 0
app/controllers/api/v1/crypto/deliveries_controller.rb View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true

class Api::V1::Crypto::DeliveriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device

def create
devices.each do |device_params|
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
end

render_empty
end

private

def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end

def resource_params
params.require(:device)
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
end

def devices
Array(resource_params[:device])
end
end

+ 59
- 0
app/controllers/api/v1/crypto/encrypted_messages_controller.rb View File

@@ -0,0 +1,59 @@
# frozen_string_literal: true

class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
LIMIT = 80

before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device

before_action :set_encrypted_messages, only: :index
after_action :insert_pagination_headers, only: :index

def index
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
end

def clear
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
render_empty
end

private

def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end

def set_encrypted_messages
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end

def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end

def next_path
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
end

def prev_path
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end

def pagination_max_id
@encrypted_messages.last.id
end

def pagination_since_id
@encrypted_messages.first.id
end

def records_continue?
@encrypted_messages.size == limit_param(LIMIT)
end

def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end

+ 25
- 0
app/controllers/api/v1/crypto/keys/claims_controller.rb View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true

class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_claim_results

def create
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
end

private

def set_claim_results
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
end

def resource_params
params.permit(device: [:account_id, :device_id])
end

def devices
Array(resource_params[:device])
end
end

+ 17
- 0
app/controllers/api/v1/crypto/keys/counts_controller.rb View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true

class Api::V1::Crypto::Keys::CountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_current_device

def show
render json: { one_time_keys: @current_device.one_time_keys.count }
end

private

def set_current_device
@current_device = Device.find_by!(access_token: doorkeeper_token)
end
end

+ 26
- 0
app/controllers/api/v1/crypto/keys/queries_controller.rb View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true

class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!
before_action :set_accounts
before_action :set_query_results

def create
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
end

private

def set_accounts
@accounts = Account.where(id: account_ids).includes(:devices)
end

def set_query_results
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
end

def account_ids
Array(params[:id]).map(&:to_i)
end
end

+ 29
- 0
app/controllers/api/v1/crypto/keys/uploads_controller.rb View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true

class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
before_action -> { doorkeeper_authorize! :crypto }
before_action :require_user!

def create
device = Device.find_or_initialize_by(access_token: doorkeeper_token)

device.transaction do
device.account = current_account
device.update!(resource_params[:device])

if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
resource_params[:one_time_keys].each do |one_time_key_params|
device.one_time_keys.create!(one_time_key_params)
end
end
end

render json: device, serializer: REST::Keys::DeviceSerializer
end

private

def resource_params
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
end
end

+ 6
- 46
app/controllers/auth/sessions_controller.rb View File

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

+ 50
- 0
app/controllers/concerns/sign_in_token_authentication_concern.rb View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true

module SignInTokenAuthenticationConcern
extend ActiveSupport::Concern

included do
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
end

def sign_in_token_required?
find_user&.suspicious_sign_in?(request.remote_ip)
end

def valid_sign_in_token_attempt?(user)
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
end

def authenticate_with_sign_in_token
user = self.resource = find_user

if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
authenticate_with_sign_in_token_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user)
end
end

def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user)
session.delete(:attempt_user_id)
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
prompt_for_sign_in_token(user)
end
end

def prompt_for_sign_in_token(user)
if user.sign_in_token_expired?
user.generate_sign_in_token && user.save
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
end

session[:attempt_user_id] = user.id
use_pack 'auth'
@body_classes = 'lighter'
render :sign_in_token
end
end

+ 48
- 0
app/controllers/concerns/two_factor_authentication_concern.rb View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true

module TwoFactorAuthenticationConcern
extend ActiveSupport::Concern

included do
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
end

def two_factor_enabled?
find_user&.otp_required_for_login?
end

def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
rescue OpenSSL::Cipher::CipherError
false
end

def authenticate_with_two_factor
user = self.resource = find_user

if user_params[:otp_attempt].present? && session[:attempt_user_id]
authenticate_with_two_factor_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end

def authenticate_with_two_factor_attempt(user)
if valid_otp_attempt?(user)
session.delete(:attempt_user_id)
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_otp_token')
prompt_for_two_factor(user)
end
end

def prompt_for_two_factor(user)
session[:attempt_user_id] = user.id
use_pack 'auth'
@body_classes = 'lighter'
render :two_factor
end
end

+ 1
- 1
app/controllers/settings/migration/redirects_controller.rb View File

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

+ 1
- 1
app/controllers/statuses_controller.rb View File

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

+ 3
- 1
app/controllers/tags_controller.rb View File

@@ -3,7 +3,8 @@
class TagsController < ApplicationController
include SignatureVerification

PAGE_SIZE = 20
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)


+ 5
- 0
app/helpers/application_helper.rb View File

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

+ 19
- 0
app/helpers/webfinger_helper.rb View File

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

+ 1
- 1
app/javascript/mastodon/actions/importer/normalizer.js View File

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


+ 1
- 1
app/javascript/mastodon/components/autosuggest_textarea.js View File

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

+ 1
- 0
app/javascript/mastodon/components/status.js View File

@@ -401,6 +401,7 @@ class Status extends ImmutablePureComponent {
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/>
);
}

+ 1
- 1
app/javascript/mastodon/features/emoji/__tests__/emoji-test.js View File

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

+ 10
- 1
app/javascript/mastodon/features/emoji/emoji.js View File

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

+ 76
- 9
app/javascript/mastodon/features/status/components/card.js View File

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

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

+ 1
- 1
app/javascript/mastodon/features/status/components/detailed_status.js View File

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

+ 14
- 14
app/javascript/mastodon/locales/en.json View File

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

+ 2
- 0
app/javascript/styles/mastodon-light/variables.scss View File

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

+ 5
- 6
app/javascript/styles/mastodon/accessibility.scss View File

@@ -1,14 +1,13 @@
$black-emojis: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash';
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange' 'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on' 'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;

%white-emoji-outline {
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;
}
}
}

+ 25
- 1
app/javascript/styles/mastodon/components.scss View File

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

+ 48
- 2
app/lib/activitypub/activity/create.rb View File

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

+ 1
- 0
app/lib/activitypub/adapter.rb View File

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

+ 8
- 3
app/lib/feed_manager.rb View File

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

+ 2
- 0
app/lib/inline_renderer.rb View File

@@ -19,6 +19,8 @@ class InlineRenderer
serializer = REST::AnnouncementSerializer
when :reaction
serializer = REST::ReactionSerializer
when :encrypted_message
serializer = REST::EncryptedMessageSerializer
else
return
end

+ 17
- 0
app/mailers/user_mailer.rb View File

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

+ 1
- 0
app/models/account.rb View File

@@ -49,6 +49,7 @@
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
#

class Account < ApplicationRecord

+ 1
- 0
app/models/concerns/account_associations.rb View File

@@ -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
- 0
app/models/device.rb View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: devices
#
# id :bigint(8) not null, primary key
# access_token_id :bigint(8)
# account_id :bigint(8)
# device_id :string default(""), not null
# name :string default(""), not null
# fingerprint_key :text default(""), not null
# identity_key :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#

class Device < ApplicationRecord
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken'
belongs_to :account

has_many :one_time_keys, dependent: :destroy, inverse_of: :device
has_many :encrypted_messages, dependent: :destroy, inverse_of: :device

validates :name, :fingerprint_key, :identity_key, presence: true
validates :fingerprint_key, :identity_key, ed25519_key: true

before_save :invalidate_associations, if: -> { device_id_changed? || fingerprint_key_changed? || identity_key_changed? }

private

def invalidate_associations
one_time_keys.destroy_all
encrypted_messages.destroy_all
end
end

+ 50
- 0
app/models/encrypted_message.rb View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: encrypted_messages
#
# id :bigint(8) not null, primary key
# device_id :bigint(8)
# from_account_id :bigint(8)
# from_device_id :string default(""), not null
# type :integer default(0), not null
# body :text default(""), not null
# digest :text default(""), not null
# message_franking :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#

class EncryptedMessage < ApplicationRecord
self.inheritance_column = nil

include Paginable

scope :up_to, ->(id) { where(arel_table[:id].lteq(id)) }

belongs_to :device
belongs_to :from_account, class_name: 'Account'

around_create Mastodon::Snowflake::Callbacks

after_commit :push_to_streaming_api

private

def push_to_streaming_api
Rails.logger.info(streaming_channel)
Rails.logger.info(subscribed_to_timeline?)

return if destroyed? || !subscribed_to_timeline?

PushEncryptedMessageWorker.perform_async(id)
end

def subscribed_to_timeline?
Redis.current.exists("subscribed:#{streaming_channel}")
end

def streaming_channel
"timeline:#{device.account_id}:#{device.device_id}"
end
end

+ 19
- 0
app/models/message_franking.rb View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true

class MessageFranking
attr_reader :hmac, :source_account_id, :target_account_id,
:timestamp, :original_franking

def initialize(attributes = {})
@hmac = attributes[:hmac]
@source_account_id = attributes[:source_account_id]
@target_account_id = attributes[:target_account_id]
@timestamp = attributes[:timestamp]
@original_franking = attributes[:original_franking]
end

def to_token
crypt = ActiveSupport::MessageEncryptor.new(SystemKey.current_key, serializer: Oj)
crypt.encrypt_and_sign(self)
end
end

+ 21
- 0
app/models/one_time_key.rb View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: one_time_keys
#
# id :bigint(8) not null, primary key
# device_id :bigint(8)
# key_id :string default(""), not null
# key :text default(""), not null
# signature :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#

class OneTimeKey < ApplicationRecord
belongs_to :device

validates :key_id, :key, :signature, presence: true
validates :key, ed25519_key: true
validates :signature, ed25519_signature: { message: :key, verify_key: ->(one_time_key) { one_time_key.device.fingerprint_key } }
end

+ 8
- 1
app/models/preview_card.rb View File

@@ -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
- 0
app/models/system_key.rb View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: system_keys
#
# id :bigint(8) not null, primary key
# key :binary
# created_at :datetime not null
# updated_at :datetime not null
#
class SystemKey < ApplicationRecord
ROTATION_PERIOD = 1.week.freeze

before_validation :set_key

scope :expired, ->(now = Time.now.utc) { where(arel_table[:created_at].lt(now - ROTATION_PERIOD * 3)) }

class << self
def current_key
previous_key = order(id: :asc).last

if previous_key && previous_key.created_at >= ROTATION_PERIOD.ago
previous_key.key
else
create.key
end
end
end

private

def set_key
return if key.present?

cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt

self.key = cipher.random_key
end
end

+ 27
- 1
app/models/user.rb View File

@@ -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
- 0
app/presenters/activitypub/activity_presenter.rb View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true

class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object

class << self
def from_status(status)
new.tap do |presenter|
presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status)
presenter.type = status.reblog? ? 'Announce' : 'Create'
presenter.actor = ActivityPub::TagManager.instance.uri_for(status.account)
presenter.published = status.created_at
presenter.to = ActivityPub::TagManager.instance.to(status)
presenter.cc = ActivityPub::TagManager.instance.cc(status)

presenter.virtual_object = begin
if status.reblog?
if status.account == status.proper.account && status.proper.private_visibility? && status.local?
status.proper
else
ActivityPub::TagManager.instance.uri_for(status.proper)
end
else
status.proper
end
end
end
end

def from_encrypted_message(encrypted_message)
new.tap do |presenter|
presenter.id = ActivityPub::TagManager.instance.generate_uri_for(nil)
presenter.type = 'Create'
presenter.actor = ActivityPub::TagManager.instance.uri_for(encrypted_message.source_account)
presenter.published = Time.now.utc
presenter.to = ActivityPub::TagManager.instance.uri_for(encrypted_message.target_account)
presenter.virtual_object = encrypted_message
end
end
end
end

+ 1
- 1
app/presenters/initial_state_presenter.rb View File

@@ -2,5 +2,5 @@

class InitialStatePresenter < ActiveModelSerializers::Model
attributes :settings, :push_subscription, :token,
:current_account, :admin, :text
:current_account, :admin, :text, :visibility
end

+ 12
- 42
app/serializers/activitypub/activity_serializer.rb View File

@@ -1,52 +1,22 @@
# frozen_string_literal: true

class ActivityPub::ActivitySerializer < ActivityPub::Serializer
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)
def self.serializer_for(model, options)
case model.class.name
when 'Status'
ActivityPub::NoteSerializer
when 'DeliverToDeviceService::EncryptedMessage'
ActivityPub::EncryptedMessageSerializer
else
super
end
end

def type
announce? ? 'Announce' : 'Create'
end
attributes :id, :type, :actor, :published, :to, :cc

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?