Merge tag 'v3.2.1' into instance_only_statuses
This commit is contained in:
commit
92c4d909a0
29
CHANGELOG.md
29
CHANGELOG.md
@ -3,6 +3,35 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.2.1] - 2020-10-19
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add support for latest HTTP Signatures spec draft ([ThibG](https://github.com/tootsuite/mastodon/pull/14556))
|
||||||
|
- Add support for inlined objects in ActivityPub `to`/`cc` ([ThibG](https://github.com/tootsuite/mastodon/pull/14514))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change actors to not be served at all without authentication in limited federation mode ([ThibG](https://github.com/tootsuite/mastodon/pull/14800))
|
||||||
|
- Previously, a bare version of an actor was served when not authenticated, i.e. username and public key
|
||||||
|
- Because all actor fetch requests are signed using a separate system actor, that is no longer required
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `tootctl media` commands not recognizing very large IDs ([ThibG](https://github.com/tootsuite/mastodon/pull/14536))
|
||||||
|
- Fix crash when failing to load emoji picker in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14525))
|
||||||
|
- Fix contrast requirements in thumbnail color extraction ([ThibG](https://github.com/tootsuite/mastodon/pull/14464))
|
||||||
|
- Fix audio/video player not using `CDN_HOST` on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/14486))
|
||||||
|
- Fix private boost icon not being used on public pages ([OmmyZhang](https://github.com/tootsuite/mastodon/pull/14471))
|
||||||
|
- Fix audio player on Safari in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14485), [ThibG](https://github.com/tootsuite/mastodon/pull/14465))
|
||||||
|
- Fix dereferencing remote statuses not using the correct account for signature when receiving a targeted inbox delivery ([ThibG](https://github.com/tootsuite/mastodon/pull/14656))
|
||||||
|
- Fix nil error in `tootctl media remove` ([noellabo](https://github.com/tootsuite/mastodon/pull/14657))
|
||||||
|
- Fix videos with near-60 fps being rejected ([Gargron](https://github.com/tootsuite/mastodon/pull/14684))
|
||||||
|
- Fix reported statuses not being included in warning e-mail ([Gargron](https://github.com/tootsuite/mastodon/pull/14778))
|
||||||
|
- Fix `Reject` activities of `Follow` objects not correctly destroying a follow relationship ([ThibG](https://github.com/tootsuite/mastodon/pull/14479))
|
||||||
|
- Fix inefficiencies in fan-out-on-write service ([Gargron](https://github.com/tootsuite/mastodon/pull/14682), [noellabo](https://github.com/tootsuite/mastodon/pull/14709))
|
||||||
|
- Fix timeout errors when trying to webfinger some IPv6 configurations ([Gargron](https://github.com/tootsuite/mastodon/pull/14919))
|
||||||
|
- Fix files served as `application/octet-stream` being rejected without attempting mime type detection ([ThibG](https://github.com/tootsuite/mastodon/pull/14452))
|
||||||
|
|
||||||
## [3.2.0] - 2020-07-27
|
## [3.2.0] - 2020-07-27
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
1
Gemfile
1
Gemfile
@ -54,7 +54,6 @@ gem 'doorkeeper', '~> 5.4'
|
|||||||
gem 'ed25519', '~> 1.2'
|
gem 'ed25519', '~> 1.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'goldfinger', '~> 2.1'
|
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.7'
|
gem 'redis-namespace', '~> 1.7'
|
||||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||||
|
@ -250,11 +250,6 @@ GEM
|
|||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.4.2)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
goldfinger (2.1.1)
|
|
||||||
addressable (~> 2.5)
|
|
||||||
http (~> 4.0)
|
|
||||||
nokogiri (~> 1.8)
|
|
||||||
oj (~> 3.0)
|
|
||||||
hamlit (2.11.0)
|
hamlit (2.11.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
@ -708,7 +703,6 @@ DEPENDENCIES
|
|||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
fog-openstack (~> 0.3)
|
fog-openstack (~> 0.3)
|
||||||
fuubar (~> 2.5)
|
fuubar (~> 2.5)
|
||||||
goldfinger (~> 2.1)
|
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
health_check!
|
health_check!
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
|
@ -7,6 +7,7 @@ class AccountsController < ApplicationController
|
|||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
|
|
||||||
|
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ class AccountsController < ApplicationController
|
|||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
||||||
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
|
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -153,12 +154,4 @@ class AccountsController < ApplicationController
|
|||||||
def params_slice(*keys)
|
def params_slice(*keys)
|
||||||
params.slice(*keys).permit(*keys)
|
params.slice(*keys).permit(*keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
|
||||||
if signed_request_account.present? || public_fetch_mode?
|
|
||||||
# Return all fields
|
|
||||||
else
|
|
||||||
%i(id type preferred_username inbox public_key endpoints)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -7,6 +7,44 @@ module SignatureVerification
|
|||||||
|
|
||||||
include DomainControlHelper
|
include DomainControlHelper
|
||||||
|
|
||||||
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
||||||
|
CLOCK_SKEW_MARGIN = 1.hour
|
||||||
|
|
||||||
|
class SignatureVerificationError < StandardError; end
|
||||||
|
|
||||||
|
class SignatureParamsParser < Parslet::Parser
|
||||||
|
rule(:token) { match("[0-9a-zA-Z!#$%&'*+.^_`|~-]").repeat(1).as(:token) }
|
||||||
|
rule(:quoted_string) { str('"') >> (qdtext | quoted_pair).repeat.as(:quoted_string) >> str('"') }
|
||||||
|
# qdtext and quoted_pair are not exactly according to spec but meh
|
||||||
|
rule(:qdtext) { match('[^\\\\"]') }
|
||||||
|
rule(:quoted_pair) { str('\\') >> any }
|
||||||
|
rule(:bws) { match('\s').repeat }
|
||||||
|
rule(:param) { (token.as(:key) >> bws >> str('=') >> bws >> (token | quoted_string).as(:value)).as(:param) }
|
||||||
|
rule(:comma) { bws >> str(',') >> bws }
|
||||||
|
# Old versions of node-http-signature add an incorrect "Signature " prefix to the header
|
||||||
|
rule(:buggy_prefix) { str('Signature ') }
|
||||||
|
rule(:params) { buggy_prefix.maybe >> (param >> (comma >> param).repeat).as(:params) }
|
||||||
|
root(:params)
|
||||||
|
end
|
||||||
|
|
||||||
|
class SignatureParamsTransformer < Parslet::Transform
|
||||||
|
rule(params: subtree(:p)) do
|
||||||
|
(p.is_a?(Array) ? p : [p]).each_with_object({}) { |(key, val), h| h[key] = val }
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(param: { key: simple(:key), value: simple(:val) }) do
|
||||||
|
[key, val]
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(quoted_string: simple(:string)) do
|
||||||
|
string.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
rule(token: simple(:string)) do
|
||||||
|
string.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def require_signature!
|
def require_signature!
|
||||||
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||||
end
|
end
|
||||||
@ -24,72 +62,40 @@ module SignatureVerification
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signature_key_id
|
def signature_key_id
|
||||||
raw_signature = request.headers['Signature']
|
|
||||||
signature_params = {}
|
|
||||||
|
|
||||||
raw_signature.split(',').each do |part|
|
|
||||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
|
||||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
|
||||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
|
||||||
end
|
|
||||||
|
|
||||||
signature_params['keyId']
|
signature_params['keyId']
|
||||||
|
rescue SignatureVerificationError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_request_account
|
def signed_request_account
|
||||||
return @signed_request_account if defined?(@signed_request_account)
|
return @signed_request_account if defined?(@signed_request_account)
|
||||||
|
|
||||||
unless signed_request?
|
raise SignatureVerificationError, 'Request not signed' unless signed_request?
|
||||||
@signature_verification_failure_reason = 'Request not signed'
|
raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters?
|
||||||
@signed_request_account = nil
|
raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm)
|
||||||
return
|
raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window?
|
||||||
end
|
|
||||||
|
|
||||||
if request.headers['Date'].present? && !matches_time_window?
|
verify_signature_strength!
|
||||||
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
raw_signature = request.headers['Signature']
|
|
||||||
signature_params = {}
|
|
||||||
|
|
||||||
raw_signature.split(',').each do |part|
|
|
||||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
|
||||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
|
||||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
|
||||||
end
|
|
||||||
|
|
||||||
if incompatible_signature?(signature_params)
|
|
||||||
@signature_verification_failure_reason = 'Incompatible request signature'
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
account = account_from_key_id(signature_params['keyId'])
|
account = account_from_key_id(signature_params['keyId'])
|
||||||
|
|
||||||
if account.nil?
|
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
signature = Base64.decode64(signature_params['signature'])
|
signature = Base64.decode64(signature_params['signature'])
|
||||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
compare_signed_string = build_signed_string
|
||||||
|
|
||||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||||
|
|
||||||
if account.nil?
|
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if account.nil?
|
||||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
|
||||||
@signed_request_account = nil
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||||
|
|
||||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)"
|
||||||
|
@signed_request_account = nil
|
||||||
|
rescue SignatureVerificationError => e
|
||||||
|
@signature_verification_failure_reason = e.message
|
||||||
@signed_request_account = nil
|
@signed_request_account = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -99,6 +105,31 @@ module SignatureVerification
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def signature_params
|
||||||
|
@signature_params ||= begin
|
||||||
|
raw_signature = request.headers['Signature']
|
||||||
|
tree = SignatureParamsParser.new.parse(raw_signature)
|
||||||
|
SignatureParamsTransformer.new.apply(tree)
|
||||||
|
end
|
||||||
|
rescue Parslet::ParseFailed
|
||||||
|
raise SignatureVerificationError, 'Error parsing signature parameters'
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_algorithm
|
||||||
|
signature_params.fetch('algorithm', 'hs2019')
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_headers
|
||||||
|
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_signature_strength!
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed' unless signed_headers.include?('host')
|
||||||
|
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||||
|
end
|
||||||
|
|
||||||
def verify_signature(account, signature, compare_signed_string)
|
def verify_signature(account, signature, compare_signed_string)
|
||||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||||
@signed_request_account = account
|
@signed_request_account = account
|
||||||
@ -108,12 +139,20 @@ module SignatureVerification
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_signed_string(signed_headers)
|
def build_signed_string
|
||||||
signed_headers = 'date' if signed_headers.blank?
|
signed_headers.map do |signed_header|
|
||||||
|
|
||||||
signed_headers.downcase.split(' ').map do |signed_header|
|
|
||||||
if signed_header == Request::REQUEST_TARGET
|
if signed_header == Request::REQUEST_TARGET
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
|
elsif signed_header == '(created)'
|
||||||
|
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||||
|
|
||||||
|
"(created): #{signature_params['created']}"
|
||||||
|
elsif signed_header == '(expires)'
|
||||||
|
raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
|
||||||
|
|
||||||
|
"(expires): #{signature_params['expires']}"
|
||||||
elsif signed_header == 'digest'
|
elsif signed_header == 'digest'
|
||||||
"digest: #{body_digest}"
|
"digest: #{body_digest}"
|
||||||
else
|
else
|
||||||
@ -123,13 +162,28 @@ module SignatureVerification
|
|||||||
end
|
end
|
||||||
|
|
||||||
def matches_time_window?
|
def matches_time_window?
|
||||||
|
created_time = nil
|
||||||
|
expires_time = nil
|
||||||
|
|
||||||
begin
|
begin
|
||||||
time_sent = Time.httpdate(request.headers['Date'])
|
if signature_algorithm == 'hs2019' && signature_params['created'].present?
|
||||||
|
created_time = Time.at(signature_params['created'].to_i).utc
|
||||||
|
elsif request.headers['Date'].present?
|
||||||
|
created_time = Time.httpdate(request.headers['Date']).utc
|
||||||
|
end
|
||||||
|
|
||||||
|
expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present?
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
(Time.now.utc - time_sent).abs <= 12.hours
|
expires_time ||= created_time + 5.minutes unless created_time.nil?
|
||||||
|
expires_time = [expires_time, created_time + EXPIRATION_WINDOW_LIMIT].min unless created_time.nil?
|
||||||
|
|
||||||
|
return false if created_time.present? && created_time > Time.now.utc + CLOCK_SKEW_MARGIN
|
||||||
|
return false if expires_time.present? && Time.now.utc > expires_time + CLOCK_SKEW_MARGIN
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def body_digest
|
def body_digest
|
||||||
@ -140,9 +194,8 @@ module SignatureVerification
|
|||||||
name.split(/-/).map(&:capitalize).join('-')
|
name.split(/-/).map(&:capitalize).join('-')
|
||||||
end
|
end
|
||||||
|
|
||||||
def incompatible_signature?(signature_params)
|
def missing_required_signature_parameters?
|
||||||
signature_params['keyId'].blank? ||
|
signature_params['keyId'].blank? || signature_params['signature'].blank?
|
||||||
signature_params['signature'].blank?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_from_key_id(key_id)
|
def account_from_key_id(key_id)
|
||||||
|
@ -1,38 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Monkey-patch on monkey-patch.
|
|
||||||
# Because it conflicts with the request.rb patch.
|
|
||||||
class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation
|
|
||||||
def connect(socket_class, host, port, nodelay = false)
|
|
||||||
::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do
|
|
||||||
@socket = socket_class.open(host, port)
|
|
||||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module WebfingerHelper
|
module WebfingerHelper
|
||||||
def webfinger!(uri)
|
def webfinger!(uri)
|
||||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
Webfinger.new(uri).perform
|
||||||
|
|
||||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
|
|
||||||
|
|
||||||
opts = {
|
|
||||||
ssl: !hidden_service_uri,
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,6 +7,7 @@ import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { me, isStaff } from '../initial_state';
|
import { me, isStaff } from '../initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
@ -331,7 +332,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||||
<IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
@ -115,6 +115,10 @@ class Audio extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
togglePlay = () => {
|
togglePlay = () => {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
this._initAudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.paused) {
|
if (this.state.paused) {
|
||||||
this.setState({ paused: false }, () => this.audio.play());
|
this.setState({ paused: false }, () => this.audio.play());
|
||||||
} else {
|
} else {
|
||||||
@ -133,10 +137,6 @@ class Audio extends React.PureComponent {
|
|||||||
handlePlay = () => {
|
handlePlay = () => {
|
||||||
this.setState({ paused: false });
|
this.setState({ paused: false });
|
||||||
|
|
||||||
if (this.canvas && !this.audioContext) {
|
|
||||||
this._initAudioContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||||
this.audioContext.resume();
|
this.audioContext.resume();
|
||||||
}
|
}
|
||||||
@ -269,8 +269,9 @@ class Audio extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_initAudioContext () {
|
_initAudioContext () {
|
||||||
const context = new AudioContext();
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
const source = context.createMediaElementSource(this.audio);
|
const context = new AudioContext();
|
||||||
|
const source = context.createMediaElementSource(this.audio);
|
||||||
|
|
||||||
this.visualizer.setAudioContext(context, source);
|
this.visualizer.setAudioContext(context, source);
|
||||||
source.connect(context.destination);
|
source.connect(context.destination);
|
||||||
|
@ -315,7 +315,7 @@ class EmojiPickerDropdown extends React.PureComponent {
|
|||||||
|
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false, active: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { me, isStaff } from '../../../initial_state';
|
import { me, isStaff } from '../../../initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
@ -273,7 +274,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
|
||||||
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -168,11 +168,13 @@ class ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def signed_fetch_account
|
def signed_fetch_account
|
||||||
|
return Account.find(@options[:delivered_to_account_id]) if @options[:delivered_to_account_id].present?
|
||||||
|
|
||||||
first_mentioned_local_account || first_local_follower
|
first_mentioned_local_account || first_local_follower
|
||||||
end
|
end
|
||||||
|
|
||||||
def first_mentioned_local_account
|
def first_mentioned_local_account
|
||||||
audience = (as_array(@json['to']) + as_array(@json['cc'])).uniq
|
audience = (as_array(@json['to']) + as_array(@json['cc'])).map { |x| value_or_id(x) }.uniq
|
||||||
local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
|
local_usernames = audience.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
|
||||||
.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||||
|
|
||||||
|
@ -34,12 +34,20 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def audience_to
|
||||||
|
as_array(@json['to']).map { |x| value_or_id(x) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def audience_cc
|
||||||
|
as_array(@json['cc']).map { |x| value_or_id(x) }
|
||||||
|
end
|
||||||
|
|
||||||
def visibility_from_audience
|
def visibility_from_audience
|
||||||
if equals_or_includes?(@json['to'], ActivityPub::TagManager::COLLECTIONS[:public])
|
if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
||||||
:public
|
:public
|
||||||
elsif equals_or_includes?(@json['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
|
elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
||||||
:unlisted
|
:unlisted
|
||||||
elsif equals_or_includes?(@json['to'], @account.followers_url)
|
elsif audience_to.include?(@account.followers_url)
|
||||||
:private
|
:private
|
||||||
else
|
else
|
||||||
:direct
|
:direct
|
||||||
|
@ -65,11 +65,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def audience_to
|
def audience_to
|
||||||
@object['to'] || @json['to']
|
as_array(@object['to'] || @json['to']).map { |x| value_or_id(x) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def audience_cc
|
def audience_cc
|
||||||
@object['cc'] || @json['cc']
|
as_array(@object['cc'] || @json['cc']).map { |x| value_or_id(x) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_status
|
def process_status
|
||||||
@ -122,7 +122,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_audience
|
def process_audience
|
||||||
(as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience|
|
(audience_to + audience_cc).uniq.each do |audience|
|
||||||
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
|
||||||
|
|
||||||
# Unlike with tags, there is no point in resolving accounts we don't already
|
# Unlike with tags, there is no point in resolving accounts we don't already
|
||||||
@ -352,11 +352,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def visibility_from_audience
|
def visibility_from_audience
|
||||||
if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public])
|
if audience_to.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
||||||
:public
|
:public
|
||||||
elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public])
|
elsif audience_cc.include?(ActivityPub::TagManager::COLLECTIONS[:public])
|
||||||
:unlisted
|
:unlisted
|
||||||
elsif equals_or_includes?(audience_to, @account.followers_url)
|
elsif audience_to.include?(@account.followers_url)
|
||||||
:private
|
:private
|
||||||
else
|
else
|
||||||
:direct
|
:direct
|
||||||
@ -365,7 +365,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
|
|
||||||
def audience_includes?(account)
|
def audience_includes?(account)
|
||||||
uri = ActivityPub::TagManager.instance.uri_for(account)
|
uri = ActivityPub::TagManager.instance.uri_for(account)
|
||||||
equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri)
|
audience_to.include?(uri) || audience_cc.include?(uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
def replied_to_status
|
def replied_to_status
|
||||||
@ -477,7 +477,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
def addresses_local_accounts?
|
def addresses_local_accounts?
|
||||||
return true if @options[:delivered_to_account_id]
|
return true if @options[:delivered_to_account_id]
|
||||||
|
|
||||||
local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
local_usernames = (audience_to + audience_cc).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
|
||||||
|
|
||||||
return false if local_usernames.empty?
|
return false if local_usernames.empty?
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
|||||||
def perform
|
def perform
|
||||||
return reject_follow_for_relay if relay_follow?
|
return reject_follow_for_relay if relay_follow?
|
||||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
||||||
return UnfollowService.new.call(follow_from_object.target_account, @account) unless follow_from_object.nil?
|
return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil?
|
||||||
|
|
||||||
case @object['type']
|
case @object['type']
|
||||||
when 'Follow'
|
when 'Follow'
|
||||||
|
93
app/lib/webfinger.rb
Normal file
93
app/lib/webfinger.rb
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Webfinger
|
||||||
|
class Error < StandardError; end
|
||||||
|
|
||||||
|
class Response
|
||||||
|
def initialize(body)
|
||||||
|
@json = Oj.load(body, mode: :strict)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subject
|
||||||
|
@json['subject']
|
||||||
|
end
|
||||||
|
|
||||||
|
def link(rel, attribute)
|
||||||
|
links.dig(rel, attribute)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def links
|
||||||
|
@links ||= @json['links'].map { |link| [link['rel'], link] }.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(uri)
|
||||||
|
_, @domain = uri.split('@')
|
||||||
|
|
||||||
|
raise ArgumentError, 'Webfinger requested for local account' if @domain.nil?
|
||||||
|
|
||||||
|
@uri = uri
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Response.new(body_from_webfinger)
|
||||||
|
rescue Oj::ParseError
|
||||||
|
raise Webfinger::Error, "Invalid JSON in response for #{@uri}"
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
raise Webfinger::Error, "Invalid URI for #{@uri}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def body_from_webfinger(url = standard_url, use_fallback = true)
|
||||||
|
webfinger_request(url).perform do |res|
|
||||||
|
if res.code == 200
|
||||||
|
res.body_with_limit
|
||||||
|
elsif res.code == 404 && use_fallback
|
||||||
|
body_from_host_meta
|
||||||
|
else
|
||||||
|
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_from_host_meta
|
||||||
|
host_meta_request.perform do |res|
|
||||||
|
if res.code == 200
|
||||||
|
body_from_webfinger(url_from_template(res.body_with_limit), false)
|
||||||
|
else
|
||||||
|
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def url_from_template(str)
|
||||||
|
link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]')
|
||||||
|
|
||||||
|
if link.present?
|
||||||
|
link['template'].gsub('{uri}', @uri)
|
||||||
|
else
|
||||||
|
raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger"
|
||||||
|
end
|
||||||
|
rescue Nokogiri::XML::XPath::SyntaxError
|
||||||
|
raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def host_meta_request
|
||||||
|
Request.new(:get, host_meta_url).add_headers('Accept' => 'application/xrd+xml, application/xml, text/xml')
|
||||||
|
end
|
||||||
|
|
||||||
|
def webfinger_request(url)
|
||||||
|
Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json')
|
||||||
|
end
|
||||||
|
|
||||||
|
def standard_url
|
||||||
|
"https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def host_meta_url
|
||||||
|
"https://#{@domain}/.well-known/host-meta"
|
||||||
|
end
|
||||||
|
end
|
@ -33,7 +33,7 @@ class AccountAlias < ApplicationRecord
|
|||||||
def set_uri
|
def set_uri
|
||||||
target_account = ResolveAccountService.new.call(acct)
|
target_account = ResolveAccountService.new.call(acct)
|
||||||
self.uri = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
|
self.uri = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil?
|
||||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
||||||
# Validation will take care of it
|
# Validation will take care of it
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class AccountMigration < ApplicationRecord
|
|||||||
|
|
||||||
def set_target_account
|
def set_target_account
|
||||||
self.target_account = ResolveAccountService.new.call(acct)
|
self.target_account = ResolveAccountService.new.call(acct)
|
||||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
||||||
# Validation will take care of it
|
# Validation will take care of it
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ class Admin::AccountAction
|
|||||||
end
|
end
|
||||||
|
|
||||||
def status_ids
|
def status_ids
|
||||||
@report.status_ids if @report && include_statuses
|
report.status_ids if report && include_statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
def reports
|
def reports
|
||||||
|
@ -32,7 +32,7 @@ class Form::Redirect
|
|||||||
|
|
||||||
def set_target_account
|
def set_target_account
|
||||||
@target_account = ResolveAccountService.new.call(acct)
|
@target_account = ResolveAccountService.new.call(acct)
|
||||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error
|
||||||
# Validation will take care of it
|
# Validation will take care of it
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -338,7 +338,7 @@ class MediaAttachment < ApplicationRecord
|
|||||||
|
|
||||||
raise Mastodon::StreamValidationError, 'Video has no video stream' if movie.width.nil? || movie.frame_rate.nil?
|
raise Mastodon::StreamValidationError, 'Video has no video stream' if movie.width.nil? || movie.frame_rate.nil?
|
||||||
raise Mastodon::DimensionsValidationError, "#{movie.width}x#{movie.height} videos are not supported" if movie.width * movie.height > MAX_VIDEO_MATRIX_LIMIT
|
raise Mastodon::DimensionsValidationError, "#{movie.width}x#{movie.height} videos are not supported" if movie.width * movie.height > MAX_VIDEO_MATRIX_LIMIT
|
||||||
raise Mastodon::DimensionsValidationError, "#{movie.frame_rate.to_i}fps videos are not supported" if movie.frame_rate > MAX_VIDEO_FRAME_RATE
|
raise Mastodon::DimensionsValidationError, "#{movie.frame_rate.floor}fps videos are not supported" if movie.frame_rate.floor > MAX_VIDEO_FRAME_RATE
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_meta
|
def set_meta
|
||||||
|
@ -56,7 +56,7 @@ class RemoteFollow
|
|||||||
|
|
||||||
if domain.nil?
|
if domain.nil?
|
||||||
@addressable_template = Addressable::Template.new("#{authorize_interaction_url}?uri={uri}")
|
@addressable_template = Addressable::Template.new("#{authorize_interaction_url}?uri={uri}")
|
||||||
elsif redirect_url_link.nil? || redirect_url_link.template.nil?
|
elsif redirect_uri_template.nil?
|
||||||
missing_resource_error
|
missing_resource_error
|
||||||
else
|
else
|
||||||
@addressable_template = Addressable::Template.new(redirect_uri_template)
|
@addressable_template = Addressable::Template.new(redirect_uri_template)
|
||||||
@ -64,16 +64,12 @@ class RemoteFollow
|
|||||||
end
|
end
|
||||||
|
|
||||||
def redirect_uri_template
|
def redirect_uri_template
|
||||||
redirect_url_link.template
|
acct_resource&.link('http://ostatus.org/schema/1.0/subscribe', 'template')
|
||||||
end
|
|
||||||
|
|
||||||
def redirect_url_link
|
|
||||||
acct_resource&.link('http://ostatus.org/schema/1.0/subscribe')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def acct_resource
|
def acct_resource
|
||||||
@acct_resource ||= webfinger!("acct:#{acct}")
|
@acct_resource ||= webfinger!("acct:#{acct}")
|
||||||
rescue Goldfinger::Error, HTTP::ConnectionError
|
rescue Webfinger::Error, HTTP::ConnectionError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,17 +39,16 @@ class ActivityPub::FetchRemoteAccountService < BaseService
|
|||||||
webfinger = webfinger!("acct:#{@username}@#{@domain}")
|
webfinger = webfinger!("acct:#{@username}@#{@domain}")
|
||||||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||||
|
|
||||||
return webfinger.link('self')&.href == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
return webfinger.link('self', 'href') == @uri if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
||||||
|
|
||||||
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
|
webfinger = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
|
||||||
@username, @domain = split_acct(webfinger.subject)
|
@username, @domain = split_acct(webfinger.subject)
|
||||||
self_reference = webfinger.link('self')
|
|
||||||
|
|
||||||
return false unless @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
return false unless @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
||||||
return false if self_reference&.href != @uri
|
return false if webfinger.link('self', 'href') != @uri
|
||||||
|
|
||||||
true
|
true
|
||||||
rescue Goldfinger::Error
|
rescue Webfinger::Error
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ class FanOutOnWriteService < BaseService
|
|||||||
def call(status)
|
def call(status)
|
||||||
raise Mastodon::RaceConditionError if status.visibility.nil?
|
raise Mastodon::RaceConditionError if status.visibility.nil?
|
||||||
|
|
||||||
render_anonymous_payload(status)
|
|
||||||
|
|
||||||
if status.direct_visibility?
|
if status.direct_visibility?
|
||||||
deliver_to_own_conversation(status)
|
deliver_to_own_conversation(status)
|
||||||
elsif status.limited_visibility?
|
elsif status.limited_visibility?
|
||||||
@ -20,6 +18,8 @@ class FanOutOnWriteService < BaseService
|
|||||||
|
|
||||||
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
return if status.account.silenced? || !status.public_visibility? || status.reblog?
|
||||||
|
|
||||||
|
render_anonymous_payload(status)
|
||||||
|
|
||||||
deliver_to_hashtags(status)
|
deliver_to_hashtags(status)
|
||||||
|
|
||||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||||
@ -58,8 +58,10 @@ class FanOutOnWriteService < BaseService
|
|||||||
def deliver_to_mentioned_followers(status)
|
def deliver_to_mentioned_followers(status)
|
||||||
Rails.logger.debug "Delivering status #{status.id} to limited followers"
|
Rails.logger.debug "Delivering status #{status.id} to limited followers"
|
||||||
|
|
||||||
FeedInsertWorker.push_bulk(status.mentions.includes(:account).map(&:account).select { |mentioned_account| mentioned_account.local? && mentioned_account.following?(status.account) }) do |follower|
|
status.mentions.joins(:account).merge(status.account.followers_for_local_distribution).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
|
||||||
[status.id, follower.id, :home]
|
FeedInsertWorker.push_bulk(mentions) do |mention|
|
||||||
|
[status.id, mention.account_id, :home]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class ProcessMentionsService < BaseService
|
|||||||
if mention_undeliverable?(mentioned_account)
|
if mention_undeliverable?(mentioned_account)
|
||||||
begin
|
begin
|
||||||
mentioned_account = resolve_account_service.call(Regexp.last_match(1))
|
mentioned_account = resolve_account_service.call(Regexp.last_match(1))
|
||||||
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
|
rescue Webfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::UnexpectedResponseError
|
||||||
mentioned_account = nil
|
mentioned_account = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -26,11 +26,10 @@ class ResolveAccountService < BaseService
|
|||||||
|
|
||||||
@account ||= Account.find_remote(@username, @domain)
|
@account ||= Account.find_remote(@username, @domain)
|
||||||
|
|
||||||
return @account if @account&.local? || !webfinger_update_due?
|
return @account if @account&.local? || @domain.nil? || !webfinger_update_due?
|
||||||
|
|
||||||
# At this point we are in need of a Webfinger query, which may
|
# At this point we are in need of a Webfinger query, which may
|
||||||
# yield us a different username/domain through a redirect
|
# yield us a different username/domain through a redirect
|
||||||
|
|
||||||
process_webfinger!(@uri)
|
process_webfinger!(@uri)
|
||||||
|
|
||||||
# Because the username/domain pair may be different than what
|
# Because the username/domain pair may be different than what
|
||||||
@ -47,7 +46,7 @@ class ResolveAccountService < BaseService
|
|||||||
# either needs to be created, or updated from fresh data
|
# either needs to be created, or updated from fresh data
|
||||||
|
|
||||||
process_account!
|
process_account!
|
||||||
rescue Goldfinger::Error, WebfingerRedirectError, Oj::ParseError => e
|
rescue Webfinger::Error, WebfingerRedirectError, Oj::ParseError => e
|
||||||
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
|
Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}"
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@ -118,11 +117,11 @@ class ResolveAccountService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def activitypub_ready?
|
def activitypub_ready?
|
||||||
!@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type)
|
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def actor_url
|
def actor_url
|
||||||
@actor_url ||= @webfinger.link('self').href
|
@actor_url ||= @webfinger.link('self', 'href')
|
||||||
end
|
end
|
||||||
|
|
||||||
def actor_json
|
def actor_json
|
||||||
|
@ -29,11 +29,11 @@
|
|||||||
- if !status.media_attachments.empty?
|
- if !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
= react_component :video, src: video.file.url(:original), preview: video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
|
= react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- elsif status.media_attachments.first.audio?
|
- elsif status.media_attachments.first.audio?
|
||||||
- audio = status.media_attachments.first
|
- audio = status.media_attachments.first
|
||||||
= react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
|
= react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- else
|
- else
|
||||||
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
|
@ -35,11 +35,11 @@
|
|||||||
- if !status.media_attachments.empty?
|
- if !status.media_attachments.empty?
|
||||||
- if status.media_attachments.first.video?
|
- if status.media_attachments.first.video?
|
||||||
- video = status.media_attachments.first
|
- video = status.media_attachments.first
|
||||||
= react_component :video, src: video.file.url(:original), preview: video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do
|
= react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- elsif status.media_attachments.first.audio?
|
- elsif status.media_attachments.first.audio?
|
||||||
- audio = status.media_attachments.first
|
- audio = status.media_attachments.first
|
||||||
= react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
|
= react_component :audio, src: full_asset_url(audio.file.url(:original)), poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
|
||||||
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
|
||||||
- else
|
- else
|
||||||
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
|
||||||
|
@ -5,7 +5,6 @@ doc << Ox::Element.new('XRD').tap do |xrd|
|
|||||||
|
|
||||||
xrd << Ox::Element.new('Link').tap do |link|
|
xrd << Ox::Element.new('Link').tap do |link|
|
||||||
link['rel'] = 'lrdd'
|
link['rel'] = 'lrdd'
|
||||||
link['type'] = 'application/xrd+xml'
|
|
||||||
link['template'] = @webfinger_template
|
link['template'] = @webfinger_template
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -31,7 +31,7 @@ module Mastodon
|
|||||||
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment|
|
processed, aggregate = parallelize_with_progress(MediaAttachment.cached.where.not(remote_url: '').where('created_at < ?', time_ago)) do |media_attachment|
|
||||||
next if media_attachment.file.blank?
|
next if media_attachment.file.blank?
|
||||||
|
|
||||||
size = media_attachment.file_file_size + (media_attachment.thumbnail_file_size || 0)
|
size = (media_attachment.file_file_size || 0) + (media_attachment.thumbnail_file_size || 0)
|
||||||
|
|
||||||
unless options[:dry_run]
|
unless options[:dry_run]
|
||||||
media_attachment.file.destroy
|
media_attachment.file.destroy
|
||||||
@ -89,7 +89,7 @@ module Mastodon
|
|||||||
path_segments = object.key.split('/')
|
path_segments = object.key.split('/')
|
||||||
path_segments.delete('cache')
|
path_segments.delete('cache')
|
||||||
|
|
||||||
if path_segments.size != 7
|
unless [7, 10].include?(path_segments.size)
|
||||||
progress.log(pastel.yellow("Unrecognized file found: #{object.key}"))
|
progress.log(pastel.yellow("Unrecognized file found: #{object.key}"))
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
@ -133,7 +133,7 @@ module Mastodon
|
|||||||
path_segments = key.split(File::SEPARATOR)
|
path_segments = key.split(File::SEPARATOR)
|
||||||
path_segments.delete('cache')
|
path_segments.delete('cache')
|
||||||
|
|
||||||
if path_segments.size != 7
|
unless [7, 10].include?(path_segments.size)
|
||||||
progress.log(pastel.yellow("Unrecognized file found: #{key}"))
|
progress.log(pastel.yellow("Unrecognized file found: #{key}"))
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
@ -258,7 +258,7 @@ module Mastodon
|
|||||||
path_segments = path.split('/')[2..-1]
|
path_segments = path.split('/')[2..-1]
|
||||||
path_segments.delete('cache')
|
path_segments.delete('cache')
|
||||||
|
|
||||||
if path_segments.size != 7
|
unless [7, 10].include?(path_segments.size)
|
||||||
say('Not a media URL', :red)
|
say('Not a media URL', :red)
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
@ -311,7 +311,7 @@ module Mastodon
|
|||||||
segments = object.key.split('/')
|
segments = object.key.split('/')
|
||||||
segments.delete('cache')
|
segments.delete('cache')
|
||||||
|
|
||||||
next if segments.size != 7
|
next unless [7, 10].include?(segments.size)
|
||||||
|
|
||||||
model_name = segments.first.classify
|
model_name = segments.first.classify
|
||||||
record_id = segments[2..-2].join.to_i
|
record_id = segments[2..-2].join.to_i
|
||||||
|
@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
0
|
1
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
@ -5,6 +5,7 @@ require 'mime/types/columnar'
|
|||||||
module Paperclip
|
module Paperclip
|
||||||
class ColorExtractor < Paperclip::Processor
|
class ColorExtractor < Paperclip::Processor
|
||||||
MIN_CONTRAST = 3.0
|
MIN_CONTRAST = 3.0
|
||||||
|
ACCENT_MIN_CONTRAST = 2.0
|
||||||
FREQUENCY_THRESHOLD = 0.01
|
FREQUENCY_THRESHOLD = 0.01
|
||||||
|
|
||||||
def make
|
def make
|
||||||
@ -26,8 +27,9 @@ module Paperclip
|
|||||||
|
|
||||||
foreground_palette.each do |color|
|
foreground_palette.each do |color|
|
||||||
distance = ColorDiff.between(background_color, color)
|
distance = ColorDiff.between(background_color, color)
|
||||||
|
contrast = w3c_contrast(background_color, color)
|
||||||
|
|
||||||
if distance > max_distance
|
if distance > max_distance && contrast >= ACCENT_MIN_CONTRAST
|
||||||
max_distance = distance
|
max_distance = distance
|
||||||
max_distance_color = color
|
max_distance_color = color
|
||||||
end
|
end
|
||||||
@ -77,8 +79,8 @@ module Paperclip
|
|||||||
private
|
private
|
||||||
|
|
||||||
def w3c_contrast(color1, color2)
|
def w3c_contrast(color1, color2)
|
||||||
luminance1 = (0.2126 * color1.r + 0.7152 * color1.g + 0.0722 * color1.b) + 0.05
|
luminance1 = color1.to_xyz.y * 0.01 + 0.05
|
||||||
luminance2 = (0.2126 * color2.r + 0.7152 * color2.g + 0.0722 * color2.b) + 0.05
|
luminance2 = color2.to_xyz.y * 0.01 + 0.05
|
||||||
|
|
||||||
if luminance1 > luminance2
|
if luminance1 > luminance2
|
||||||
luminance1 / luminance2
|
luminance1 / luminance2
|
||||||
|
@ -19,7 +19,7 @@ module Paperclip
|
|||||||
@original_filename = filename_from_content_disposition || filename_from_path || 'data'
|
@original_filename = filename_from_content_disposition || filename_from_path || 'data'
|
||||||
@size = @target.response.content_length
|
@size = @target.response.content_length
|
||||||
@tempfile = copy_to_tempfile(@target)
|
@tempfile = copy_to_tempfile(@target)
|
||||||
@content_type = @target.response.mime_type || ContentTypeDetector.new(@tempfile.path).detect
|
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy_to_tempfile(source)
|
def copy_to_tempfile(source)
|
||||||
|
@ -348,24 +348,8 @@ RSpec.describe AccountsController, type: :controller do
|
|||||||
context 'in authorized fetch mode' do
|
context 'in authorized fetch mode' do
|
||||||
let(:authorized_fetch_mode) { true }
|
let(:authorized_fetch_mode) { true }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http unauthorized' do
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(401)
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns application/activity+json' do
|
|
||||||
expect(response.content_type).to eq 'application/activity+json'
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'cachable response'
|
|
||||||
|
|
||||||
it 'returns Vary header with Signature' do
|
|
||||||
expect(response.headers['Vary']).to include 'Signature'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders bare minimum account' do
|
|
||||||
json = body_as_json
|
|
||||||
expect(json).to include(:id, :type, :preferredUsername, :inbox, :publicKey)
|
|
||||||
expect(json).to_not include(:name, :summary)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -43,8 +43,7 @@ describe RemoteFollowController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'renders new when template is nil' do
|
it 'renders new when template is nil' do
|
||||||
link_with_nil_template = double(template: nil)
|
resource_with_link = double(link: nil)
|
||||||
resource_with_link = double(link: link_with_nil_template)
|
|
||||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||||
|
|
||||||
@ -55,8 +54,7 @@ describe RemoteFollowController do
|
|||||||
|
|
||||||
context 'when webfinger values are good' do
|
context 'when webfinger values are good' do
|
||||||
before do
|
before do
|
||||||
link_with_template = double(template: 'http://example.com/follow_me?acct={uri}')
|
resource_with_link = double(link: 'http://example.com/follow_me?acct={uri}')
|
||||||
resource_with_link = double(link: link_with_template)
|
|
||||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_return(resource_with_link)
|
||||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||||
end
|
end
|
||||||
@ -78,8 +76,8 @@ describe RemoteFollowController do
|
|||||||
expect(response).to render_template(:new)
|
expect(response).to render_template(:new)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders new with error when goldfinger fails' do
|
it 'renders new with error when webfinger fails' do
|
||||||
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Goldfinger::Error)
|
allow_any_instance_of(WebfingerHelper).to receive(:webfinger!).with('acct:user@example.com').and_raise(Webfinger::Error)
|
||||||
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
post :create, params: { account_username: @account.to_param, remote_follow: { acct: 'user@example.com' } }
|
||||||
|
|
||||||
expect(response).to render_template(:new)
|
expect(response).to render_template(:new)
|
||||||
|
@ -12,7 +12,7 @@ describe WellKnown::HostMetaController, type: :controller do
|
|||||||
expect(response.body).to eq <<XML
|
expect(response.body).to eq <<XML
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
<Link rel="lrdd" type="application/xrd+xml" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
|
<Link rel="lrdd" template="https://cb6e6126.ngrok.io/.well-known/webfinger?resource={uri}"/>
|
||||||
</XRD>
|
</XRD>
|
||||||
XML
|
XML
|
||||||
end
|
end
|
||||||
|
@ -73,6 +73,26 @@ RSpec.describe ActivityPub::Activity::Announce do
|
|||||||
expect(sender.reblogged?(sender.statuses.first)).to be true
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'self-boost of a previously unknown status with correct attributedTo, inlined Collection in audience' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'https://example.com/actor#bar',
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
attributedTo: 'https://example.com/actor',
|
||||||
|
to: {
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'id': 'http://example.com/followers',
|
||||||
|
'first': 'http://example.com/followers?page=true',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a reblog by sender of status' do
|
||||||
|
expect(sender.reblogged?(sender.statuses.first)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the status belongs to a local user' do
|
context 'when the status belongs to a local user' do
|
||||||
|
@ -18,6 +18,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
|
|
||||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||||
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
||||||
|
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
@ -120,6 +121,28 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'private with inlined Collection in audience' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
to: {
|
||||||
|
'type': 'OrderedCollection',
|
||||||
|
'id': 'http://example.com/followers',
|
||||||
|
'first': 'http://example.com/followers?page=true',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.visibility).to eq 'private'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'limited' do
|
context 'limited' do
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
|
||||||
@ -451,6 +474,32 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with emojis served with invalid content-type' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum :tinkong:',
|
||||||
|
tag: [
|
||||||
|
{
|
||||||
|
type: 'Emoji',
|
||||||
|
icon: {
|
||||||
|
url: 'http://example.com/emojib.png',
|
||||||
|
},
|
||||||
|
name: 'tinkong',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.emojis.map(&:shortcode)).to include('tinkong')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with emojis missing name' do
|
context 'with emojis missing name' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,14 @@ require 'rails_helper'
|
|||||||
RSpec.describe ActivityPub::Activity::Reject do
|
RSpec.describe ActivityPub::Activity::Reject do
|
||||||
let(:sender) { Fabricate(:account) }
|
let(:sender) { Fabricate(:account) }
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: 'bar',
|
||||||
|
type: 'Follow',
|
||||||
|
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||||
|
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
@ -10,29 +18,105 @@ RSpec.describe ActivityPub::Activity::Reject do
|
|||||||
id: 'foo',
|
id: 'foo',
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||||
object: {
|
object: object_json,
|
||||||
id: 'bar',
|
|
||||||
type: 'Follow',
|
|
||||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
|
||||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
|
||||||
},
|
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
before do
|
context 'rejecting a pending follow request by target' do
|
||||||
Fabricate(:follow_request, account: recipient, target_account: sender)
|
before do
|
||||||
subject.perform
|
Fabricate(:follow_request, account: recipient, target_account: sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create a follow relationship' do
|
context 'rejecting a pending follow request by uri' do
|
||||||
expect(recipient.following?(sender)).to be false
|
before do
|
||||||
|
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the follow request' do
|
context 'rejecting a pending follow request by uri only' do
|
||||||
expect(recipient.requested?(sender)).to be false
|
let(:object_json) { 'bar' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:follow_request, account: recipient, target_account: sender, uri: 'bar')
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'rejecting an existing follow relationship by target' do
|
||||||
|
before do
|
||||||
|
Fabricate(:follow, account: recipient, target_account: sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'rejecting an existing follow relationship by uri' do
|
||||||
|
before do
|
||||||
|
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'rejecting an existing follow relationship by uri only' do
|
||||||
|
let(:object_json) { 'bar' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Fabricate(:follow, account: recipient, target_account: sender, uri: 'bar')
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the follow relationship' do
|
||||||
|
expect(recipient.following?(sender)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(recipient.requested?(sender)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ RSpec.describe FeedManager do
|
|||||||
|
|
||||||
it 'returns false for status by followee mentioning another account' do
|
it 'returns false for status by followee mentioning another account' do
|
||||||
bob.follow!(alice)
|
bob.follow!(alice)
|
||||||
|
jeff.follow!(alice)
|
||||||
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
status = PostStatusService.new.call(alice, text: 'Hey @jeff')
|
||||||
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
expect(FeedManager.instance.filter?(:home, status, bob.id)).to be false
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user