Compare commits
49 commits
kb_develop
...
releases/1
Author | SHA1 | Date | |
---|---|---|---|
|
a60fa38b80 | ||
|
4f64ba26dc | ||
|
e20c9ad106 | ||
|
ed59271078 | ||
|
5d2f763f47 | ||
|
eb1094143c | ||
|
fd72d4bed7 | ||
|
8ce87662b0 | ||
|
ceff4265e1 | ||
|
d9abcc61ff | ||
|
8658079f0d | ||
|
a6997fab01 | ||
|
d5e14b2865 | ||
|
1d546f4388 | ||
|
7c3c2d2444 | ||
|
6a57868e89 | ||
|
a9f7667900 | ||
|
6f66145f9d | ||
|
8b0fd35955 | ||
|
2e7e260ead | ||
|
76cf23dfd6 | ||
|
23faeafe42 | ||
|
e082a8f4a0 | ||
|
d468b94158 | ||
|
1282a45c54 | ||
|
eac1d8ad5b | ||
|
48d091dd18 | ||
|
018eb174e8 | ||
|
48f860945d | ||
|
72367f6848 | ||
|
fcb0ebdb5b | ||
|
1ac58c3858 | ||
|
58267dcfe9 | ||
|
1225c22810 | ||
|
8c23a8aa2b | ||
|
eec533e8cd | ||
|
6bc4219f59 | ||
|
2338fc4aec | ||
|
f40e951d29 | ||
|
e83c4c5604 | ||
|
7d9d2e2e86 | ||
|
d87a11bc7d | ||
|
201fd37bc3 | ||
|
65cc1273aa | ||
|
ef3eb48932 | ||
|
0fcf61ee0e | ||
|
cc408a5e7d | ||
|
8cd1d7e5d0 | ||
|
5bf543ca24 |
88 changed files with 1028 additions and 290 deletions
6
.bundler-audit.yml
Normal file
6
.bundler-audit.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
ignore:
|
||||||
|
# devise-two-factor advisory about brute-forcing TOTP
|
||||||
|
# We have rate-limits on authentication endpoints in place (including second
|
||||||
|
# factor verification) since Mastodon v3.2.0
|
||||||
|
- CVE-2024-0227
|
|
@ -1 +1 @@
|
||||||
3.2.2
|
3.2.3
|
||||||
|
|
95
CHANGELOG.md
95
CHANGELOG.md
|
@ -2,6 +2,101 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.2.7] - 2024-02-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
|
||||||
|
- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
|
||||||
|
|
||||||
|
## [4.2.6] - 2024-02-14
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
|
||||||
|
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
|
||||||
|
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
|
||||||
|
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
|
||||||
|
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
|
||||||
|
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
|
||||||
|
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
|
||||||
|
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
|
||||||
|
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
|
||||||
|
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
|
||||||
|
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
|
||||||
|
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
|
||||||
|
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
|
||||||
|
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
|
||||||
|
|
||||||
|
## [4.2.5] - 2024-02-01
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
|
||||||
|
|
||||||
|
## [4.2.4] - 2024-01-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
|
||||||
|
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
|
||||||
|
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
|
||||||
|
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
|
||||||
|
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
|
||||||
|
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
|
||||||
|
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
|
||||||
|
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
|
||||||
|
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
|
||||||
|
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
|
||||||
|
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
|
||||||
|
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
|
||||||
|
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
|
||||||
|
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
|
||||||
|
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
|
||||||
|
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
|
||||||
|
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
|
||||||
|
|
||||||
|
## [4.2.3] - 2023-12-05
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
|
||||||
|
|
||||||
|
## [4.2.2] - 2023-12-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
|
||||||
|
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
||||||
|
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
||||||
|
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
|
||||||
|
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
||||||
|
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
|
||||||
|
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
||||||
|
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
|
||||||
|
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
||||||
|
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
|
||||||
|
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
|
||||||
|
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
||||||
|
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
|
||||||
|
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
|
||||||
|
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
|
||||||
|
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
|
||||||
|
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
|
||||||
|
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
|
||||||
|
|
||||||
## [4.2.1] - 2023-10-10
|
## [4.2.1] - 2023-10-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
||||||
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"]
|
||||||
ARG RUBY_VERSION="3.2.2"
|
ARG RUBY_VERSION="3.2.3"
|
||||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
ARG NODE_MAJOR_VERSION="20"
|
ARG NODE_MAJOR_VERSION="20"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||||
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
||||||
# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm)
|
# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm)
|
||||||
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
||||||
|
|
||||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -481,8 +481,9 @@ GEM
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
net-ssh (7.1.0)
|
||||||
nokogiri (1.16.0)
|
nio4r (2.7.0)
|
||||||
|
nokogiri (1.16.2)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.3)
|
oj (3.16.3)
|
||||||
|
@ -523,8 +524,8 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.4)
|
pg (1.5.5)
|
||||||
pghero (3.4.0)
|
pghero (3.4.1)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.21.0)
|
premailer (1.21.0)
|
||||||
|
@ -544,7 +545,7 @@ GEM
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.4)
|
public_suffix (5.0.4)
|
||||||
puma (6.4.1)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.1)
|
pundit (2.3.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
@ -726,7 +727,7 @@ GEM
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 6, < 8)
|
sidekiq (>= 6, < 8)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.30)
|
sidekiq-unique-jobs (7.1.33)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
redis (< 5.0)
|
redis (< 5.0)
|
||||||
|
|
|
@ -5,8 +5,6 @@ class ActivityPub::ReferencesController < ActivityPub::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountOwnedConcern
|
include AccountOwnedConcern
|
||||||
|
|
||||||
REFERENCES_LIMIT = 5
|
|
||||||
|
|
||||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
|
|
||||||
|
@ -40,17 +38,21 @@ class ActivityPub::ReferencesController < ActivityPub::BaseController
|
||||||
@results ||= begin
|
@results ||= begin
|
||||||
references = @status.reference_objects.order(target_status_id: :asc)
|
references = @status.reference_objects.order(target_status_id: :asc)
|
||||||
references = references.where('target_status_id > ?', page_params[:min_id]) if page_params[:min_id].present?
|
references = references.where('target_status_id > ?', page_params[:min_id]) if page_params[:min_id].present?
|
||||||
references = references.limit(limit_param(REFERENCES_LIMIT))
|
references = references.limit(limit_param(references_limit))
|
||||||
references.pluck(:target_status_id)
|
references.pluck(:target_status_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def references_limit
|
||||||
|
StatusReference::REFERENCES_LIMIT
|
||||||
|
end
|
||||||
|
|
||||||
def pagination_min_id
|
def pagination_min_id
|
||||||
results.last
|
results.last
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
results.size == limit_param(REFERENCES_LIMIT)
|
results.size == limit_param(references_limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
def references_collection_presenter
|
def references_collection_presenter
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class Api::V1::StreamingController < Api::BaseController
|
class Api::V1::StreamingController < Api::BaseController
|
||||||
def index
|
def index
|
||||||
if Rails.configuration.x.streaming_api_base_url == request.host
|
if same_host?
|
||||||
not_found
|
not_found
|
||||||
else
|
else
|
||||||
redirect_to streaming_api_url, status: 301, allow_other_host: true
|
redirect_to streaming_api_url, status: 301, allow_other_host: true
|
||||||
|
@ -11,6 +11,11 @@ class Api::V1::StreamingController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def same_host?
|
||||||
|
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||||
|
request.host == base_url.host && request.port == (base_url.port || 80)
|
||||||
|
end
|
||||||
|
|
||||||
def streaming_api_url
|
def streaming_api_url
|
||||||
Addressable::URI.parse(request.url).tap do |uri|
|
Addressable::URI.parse(request.url).tap do |uri|
|
||||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
def self.provides_callback_for(provider)
|
def self.provides_callback_for(provider)
|
||||||
define_method provider do
|
define_method provider do
|
||||||
@provider = provider
|
@provider = provider
|
||||||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
|
||||||
|
|
||||||
if @user.persisted?
|
if @user.persisted?
|
||||||
record_login_activity
|
record_login_activity
|
||||||
|
@ -17,6 +17,9 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||||
redirect_to new_user_registration_url
|
redirect_to new_user_registration_url
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
|
||||||
|
redirect_to new_user_session_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
|
include Redisable
|
||||||
|
|
||||||
|
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :check_self_destruct!
|
skip_before_action :check_self_destruct!
|
||||||
|
@ -130,9 +134,23 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
session.delete(:attempt_user_updated_at)
|
session.delete(:attempt_user_updated_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clear_2fa_attempt_from_user(user)
|
||||||
|
redis.del(second_factor_attempts_key(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_second_factor_rate_limits(user)
|
||||||
|
attempts, = redis.multi do |multi|
|
||||||
|
multi.incr(second_factor_attempts_key(user))
|
||||||
|
multi.expire(second_factor_attempts_key(user), 1.hour)
|
||||||
|
end
|
||||||
|
|
||||||
|
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
||||||
|
end
|
||||||
|
|
||||||
def on_authentication_success(user, security_measure)
|
def on_authentication_success(user, security_measure)
|
||||||
@on_authentication_success_called = true
|
@on_authentication_success_called = true
|
||||||
|
|
||||||
|
clear_2fa_attempt_from_user(user)
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
|
|
||||||
user.update_sign_in!(new_sign_in: true)
|
user.update_sign_in!(new_sign_in: true)
|
||||||
|
@ -164,4 +182,8 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
user_agent: request.user_agent
|
user_agent: request.user_agent
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def second_factor_attempts_key(user)
|
||||||
|
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,6 +66,11 @@ module Auth::TwoFactorAuthenticationConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_two_factor_via_otp(user)
|
def authenticate_with_two_factor_via_otp(user)
|
||||||
|
if check_second_factor_rate_limits(user)
|
||||||
|
flash.now[:alert] = I18n.t('users.rate_limited')
|
||||||
|
return prompt_for_two_factor(user)
|
||||||
|
end
|
||||||
|
|
||||||
if valid_otp_attempt?(user)
|
if valid_otp_attempt?(user)
|
||||||
on_authentication_success(user, :otp)
|
on_authentication_success(user, :otp)
|
||||||
else
|
else
|
||||||
|
|
|
@ -266,7 +266,7 @@ module SignatureVerification
|
||||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
||||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
||||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
|
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
||||||
account
|
account
|
||||||
end
|
end
|
||||||
rescue Mastodon::PrivateNetworkAddressError => e
|
rescue Mastodon::PrivateNetworkAddressError => e
|
||||||
|
|
|
@ -163,8 +163,8 @@ module JsonLdHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
|
||||||
unless id
|
unless id_is_known
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||||
|
|
||||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||||
|
@ -172,17 +172,29 @@ module JsonLdHelper
|
||||||
uri = json['id']
|
uri = json['id']
|
||||||
end
|
end
|
||||||
|
|
||||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
||||||
json.present? && json['id'] == uri ? json : nil
|
json.present? && json['id'] == uri ? json : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
||||||
on_behalf_of ||= Account.representative
|
on_behalf_of ||= Account.representative
|
||||||
|
|
||||||
build_request(uri, on_behalf_of).perform do |response|
|
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||||
|
|
||||||
body_to_json(response.body_with_limit) if response.code == 200
|
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_activitypub_content_type?(response)
|
||||||
|
return true if response.mime_type == 'application/activity+json'
|
||||||
|
|
||||||
|
# When the mime type is `application/ld+json`, we need to check the profile,
|
||||||
|
# but `http.rb` does not parse it for us.
|
||||||
|
return false unless response.mime_type == 'application/ld+json'
|
||||||
|
|
||||||
|
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
|
||||||
|
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -212,8 +224,8 @@ module JsonLdHelper
|
||||||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_request(uri, on_behalf_of = nil)
|
def build_request(uri, on_behalf_of = nil, options: {})
|
||||||
Request.new(:get, uri).tap do |request|
|
Request.new(:get, uri, **options).tap do |request|
|
||||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||||
end
|
end
|
||||||
|
|
|
@ -169,6 +169,7 @@ class PrivacyDropdown extends PureComponent {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
noDirect: PropTypes.bool,
|
noDirect: PropTypes.bool,
|
||||||
|
noLimited: PropTypes.bool,
|
||||||
replyToLimited: PropTypes.bool,
|
replyToLimited: PropTypes.bool,
|
||||||
container: PropTypes.func,
|
container: PropTypes.func,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
@ -260,6 +261,10 @@ class PrivacyDropdown extends PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.noLimited) {
|
||||||
|
this.options = this.options.filter((opt) => !['mutual', 'circle'].includes(opt.value));
|
||||||
|
}
|
||||||
|
|
||||||
this.selectableOptions = [...this.options];
|
this.selectableOptions = [...this.options];
|
||||||
|
|
||||||
if (!enableLoginPrivacy) {
|
if (!enableLoginPrivacy) {
|
||||||
|
|
|
@ -110,6 +110,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
|
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
|
||||||
<PrivacyDropdown
|
<PrivacyDropdown
|
||||||
noDirect
|
noDirect
|
||||||
|
noLimited
|
||||||
value={privacy}
|
value={privacy}
|
||||||
container={this._findContainer}
|
container={this._findContainer}
|
||||||
onChange={this.props.onChangeBoostPrivacy}
|
onChange={this.props.onChangeBoostPrivacy}
|
||||||
|
|
|
@ -33,7 +33,8 @@ class AccountStatusesFilter
|
||||||
available_visibilities -= [:unlisted] if (domain_block&.detect_invalid_subscription || misskey_software?) && @account.user&.setting_reject_unlisted_subscription
|
available_visibilities -= [:unlisted] if (domain_block&.detect_invalid_subscription || misskey_software?) && @account.user&.setting_reject_unlisted_subscription
|
||||||
available_visibilities -= [:login] if current_account.nil?
|
available_visibilities -= [:login] if current_account.nil?
|
||||||
|
|
||||||
scope.merge!(scope.where(spoiler_text: ['', nil])) if domain_block&.reject_send_sensitive
|
scope.merge!(scope.where(sensitive: false)) if domain_block&.reject_send_sensitive
|
||||||
|
|
||||||
scope.merge!(scope.where(searchability: available_searchabilities))
|
scope.merge!(scope.where(searchability: available_searchabilities))
|
||||||
scope.merge!(scope.where(visibility: available_visibilities))
|
scope.merge!(scope.where(visibility: available_visibilities))
|
||||||
|
|
||||||
|
@ -153,9 +154,9 @@ class AccountStatusesFilter
|
||||||
end
|
end
|
||||||
|
|
||||||
def domain_block
|
def domain_block
|
||||||
return nil if @account.nil? || @account.local?
|
return nil if @current_account.nil? || @current_account.local?
|
||||||
|
|
||||||
@domain_block = DomainBlock.find_by(domain: @account.domain)
|
@domain_block = DomainBlock.find_by(domain: @current_account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def misskey_software?
|
def misskey_software?
|
||||||
|
|
|
@ -154,7 +154,7 @@ class ActivityPub::Activity
|
||||||
if object_uri.start_with?('http')
|
if object_uri.start_with?('http')
|
||||||
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
return if ActivityPub::TagManager.instance.local_uri?(object_uri)
|
||||||
|
|
||||||
ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
|
ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
|
||||||
elsif @object['url'].present?
|
elsif @object['url'].present?
|
||||||
::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id])
|
::FetchRemoteStatusService.new.call(@object['url'], request_id: @options[:request_id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,7 +19,7 @@ class ActivityPub::LinkedDataSignature
|
||||||
return unless type == 'RsaSignature2017'
|
return unless type == 'RsaSignature2017'
|
||||||
|
|
||||||
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
|
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
|
||||||
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
|
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?
|
||||||
|
|
||||||
return if creator.nil?
|
return if creator.nil?
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,34 @@ module ApplicationExtension
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
include Redisable
|
||||||
|
|
||||||
has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application
|
has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application
|
||||||
|
|
||||||
validates :name, length: { maximum: 60 }
|
validates :name, length: { maximum: 60 }
|
||||||
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
||||||
validates :redirect_uri, length: { maximum: 2_000 }
|
validates :redirect_uri, length: { maximum: 2_000 }
|
||||||
|
|
||||||
|
# The relationship used between Applications and AccessTokens is using
|
||||||
|
# dependent: delete_all, which means the ActiveRecord callback in
|
||||||
|
# AccessTokenExtension is not run, so instead we manually announce to
|
||||||
|
# streaming that these tokens are being deleted.
|
||||||
|
before_destroy :push_to_streaming_api, prepend: true
|
||||||
end
|
end
|
||||||
|
|
||||||
def confirmation_redirect_uri
|
def confirmation_redirect_uri
|
||||||
redirect_uri.lines.first.strip
|
redirect_uri.lines.first.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def push_to_streaming_api
|
||||||
|
# TODO: #28793 Combine into a single topic
|
||||||
|
payload = Oj.dump(event: :kill)
|
||||||
|
access_tokens.in_batches do |tokens|
|
||||||
|
redis.pipelined do |pipeline|
|
||||||
|
tokens.ids.each do |id|
|
||||||
|
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,47 +38,39 @@ class StatusReachFinder
|
||||||
private
|
private
|
||||||
|
|
||||||
def reached_account_inboxes
|
def reached_account_inboxes
|
||||||
|
Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def reached_account_inboxes_for_misskey
|
||||||
|
Account.where(id: reached_account_ids, domain: banned_domains_for_misskey - friend_domains).inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def reached_account_inboxes_for_friend
|
||||||
|
Account.where(id: reached_account_ids, domain: friend_domains).inboxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def reached_account_ids
|
||||||
# When the status is a reblog, there are no interactions with it
|
# When the status is a reblog, there are no interactions with it
|
||||||
# directly, we assume all interactions are with the original one
|
# directly, we assume all interactions are with the original one
|
||||||
|
|
||||||
if @status.reblog?
|
if @status.reblog?
|
||||||
[]
|
[reblog_of_account_id]
|
||||||
elsif @status.limited_visibility?
|
elsif @status.limited_visibility?
|
||||||
Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes
|
[mentioned_account_ids]
|
||||||
else
|
else
|
||||||
Account.where(id: reached_account_ids).where.not(domain: banned_domains + friend_domains).inboxes
|
[
|
||||||
end
|
replied_to_account_id,
|
||||||
end
|
reblog_of_account_id,
|
||||||
|
mentioned_account_ids,
|
||||||
def reached_account_inboxes_for_misskey
|
reblogs_account_ids,
|
||||||
if @status.reblog? || @status.limited_visibility?
|
favourites_account_ids,
|
||||||
[]
|
replies_account_ids,
|
||||||
else
|
quoted_account_id,
|
||||||
Account.where(id: reached_account_ids, domain: banned_domains_for_misskey - friend_domains).inboxes
|
].tap do |arr|
|
||||||
end
|
arr.flatten!
|
||||||
end
|
arr.compact!
|
||||||
|
arr.uniq!
|
||||||
def reached_account_inboxes_for_friend
|
end
|
||||||
if @status.reblog? || @status.limited_visibility?
|
|
||||||
[]
|
|
||||||
else
|
|
||||||
Account.where(id: reached_account_ids, domain: friend_domains).inboxes
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def reached_account_ids
|
|
||||||
[
|
|
||||||
replied_to_account_id,
|
|
||||||
reblog_of_account_id,
|
|
||||||
mentioned_account_ids,
|
|
||||||
reblogs_account_ids,
|
|
||||||
favourites_account_ids,
|
|
||||||
replies_account_ids,
|
|
||||||
quoted_account_id,
|
|
||||||
].tap do |arr|
|
|
||||||
arr.flatten!
|
|
||||||
arr.compact!
|
|
||||||
arr.uniq!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,6 @@ module Account::OtherSettings
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_preview?
|
|
||||||
return user.setting_link_preview if local? && user.present?
|
|
||||||
return settings['link_preview'] if settings.present? && settings.key?('link_preview')
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def allow_quote?
|
def allow_quote?
|
||||||
return user.setting_allow_quote if local? && user.present?
|
return user.setting_allow_quote if local? && user.present?
|
||||||
return settings['allow_quote'] if settings.present? && settings.key?('allow_quote')
|
return settings['allow_quote'] if settings.present? && settings.key?('allow_quote')
|
||||||
|
@ -95,7 +88,6 @@ module Account::OtherSettings
|
||||||
'hide_following_count' => hide_following_count?,
|
'hide_following_count' => hide_following_count?,
|
||||||
'hide_followers_count' => hide_followers_count?,
|
'hide_followers_count' => hide_followers_count?,
|
||||||
'translatable_private' => translatable_private?,
|
'translatable_private' => translatable_private?,
|
||||||
'link_preview' => link_preview?,
|
|
||||||
'allow_quote' => allow_quote?,
|
'allow_quote' => allow_quote?,
|
||||||
'emoji_reaction_policy' => Setting.enable_emoji_reaction ? emoji_reaction_policy.to_s : 'block',
|
'emoji_reaction_policy' => Setting.enable_emoji_reaction ? emoji_reaction_policy.to_s : 'block',
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,6 +127,10 @@ module User::HasSettings
|
||||||
settings['allow_quote']
|
settings['allow_quote']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def setting_reject_send_limited_to_suspects
|
||||||
|
settings['reject_send_limited_to_suspects']
|
||||||
|
end
|
||||||
|
|
||||||
def setting_noindex
|
def setting_noindex
|
||||||
settings['noindex']
|
settings['noindex']
|
||||||
end
|
end
|
||||||
|
@ -135,10 +139,6 @@ module User::HasSettings
|
||||||
settings['translatable_private']
|
settings['translatable_private']
|
||||||
end
|
end
|
||||||
|
|
||||||
def setting_link_preview
|
|
||||||
settings['link_preview']
|
|
||||||
end
|
|
||||||
|
|
||||||
def setting_dtl_force_visibility
|
def setting_dtl_force_visibility
|
||||||
settings['dtl_force_visibility']&.to_sym || :unchange
|
settings['dtl_force_visibility']&.to_sym || :unchange
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,17 +19,18 @@ module User::Omniauthable
|
||||||
end
|
end
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def find_for_oauth(auth, signed_in_resource = nil)
|
def find_for_omniauth(auth, signed_in_resource = nil)
|
||||||
# EOLE-SSO Patch
|
# EOLE-SSO Patch
|
||||||
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
|
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
|
||||||
identity = Identity.find_for_oauth(auth)
|
identity = Identity.find_for_omniauth(auth)
|
||||||
|
|
||||||
# If a signed_in_resource is provided it always overrides the existing user
|
# If a signed_in_resource is provided it always overrides the existing user
|
||||||
# to prevent the identity being locked with accidentally created accounts.
|
# to prevent the identity being locked with accidentally created accounts.
|
||||||
# Note that this may leave zombie accounts (with no associated identity) which
|
# Note that this may leave zombie accounts (with no associated identity) which
|
||||||
# can be cleaned up at a later date.
|
# can be cleaned up at a later date.
|
||||||
user = signed_in_resource || identity.user
|
user = signed_in_resource || identity.user
|
||||||
user ||= create_for_oauth(auth)
|
user ||= reattach_for_auth(auth)
|
||||||
|
user ||= create_for_auth(auth)
|
||||||
|
|
||||||
if identity.user.nil?
|
if identity.user.nil?
|
||||||
identity.user = user
|
identity.user = user
|
||||||
|
@ -39,19 +40,35 @@ module User::Omniauthable
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_for_oauth(auth)
|
private
|
||||||
# Check if the user exists with provided email. If no email was provided,
|
|
||||||
|
def reattach_for_auth(auth)
|
||||||
|
# If allowed, check if a user exists with the provided email address,
|
||||||
|
# and return it if they does not have an associated identity with the
|
||||||
|
# current authentication provider.
|
||||||
|
|
||||||
|
# This can be used to provide a choice of alternative auth providers
|
||||||
|
# or provide smooth gradual transition between multiple auth providers,
|
||||||
|
# but this is discouraged because any insecure provider will put *all*
|
||||||
|
# local users at risk, regardless of which provider they registered with.
|
||||||
|
|
||||||
|
return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true'
|
||||||
|
|
||||||
|
email, email_is_verified = email_from_auth(auth)
|
||||||
|
return unless email_is_verified
|
||||||
|
|
||||||
|
user = User.find_by(email: email)
|
||||||
|
return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id)
|
||||||
|
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_for_auth(auth)
|
||||||
|
# Create a user for the given auth params. If no email was provided,
|
||||||
# we assign a temporary email and ask the user to verify it on
|
# we assign a temporary email and ask the user to verify it on
|
||||||
# the next step via Auth::SetupController.show
|
# the next step via Auth::SetupController.show
|
||||||
|
|
||||||
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
|
email, email_is_verified = email_from_auth(auth)
|
||||||
assume_verified = strategy&.security&.assume_email_is_verified
|
|
||||||
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
|
|
||||||
email = auth.info.verified_email || auth.info.email
|
|
||||||
|
|
||||||
user = User.find_by(email: email) if email_is_verified
|
|
||||||
|
|
||||||
return user unless user.nil?
|
|
||||||
|
|
||||||
user = User.new(user_params_from_auth(email, auth))
|
user = User.new(user_params_from_auth(email, auth))
|
||||||
|
|
||||||
|
@ -66,7 +83,14 @@ module User::Omniauthable
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def email_from_auth(auth)
|
||||||
|
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
|
||||||
|
assume_verified = strategy&.security&.assume_email_is_verified
|
||||||
|
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
|
||||||
|
email = auth.info.verified_email || auth.info.email
|
||||||
|
|
||||||
|
[email, email_is_verified]
|
||||||
|
end
|
||||||
|
|
||||||
def user_params_from_auth(email, auth)
|
def user_params_from_auth(email, auth)
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Identity < ApplicationRecord
|
||||||
validates :uid, presence: true, uniqueness: { scope: :provider }
|
validates :uid, presence: true, uniqueness: { scope: :provider }
|
||||||
validates :provider, presence: true
|
validates :provider, presence: true
|
||||||
|
|
||||||
def self.find_for_oauth(auth)
|
def self.find_for_omniauth(auth)
|
||||||
find_or_create_by(uid: auth.uid, provider: auth.provider)
|
find_or_create_by(uid: auth.uid, provider: auth.provider)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,17 +17,17 @@ class InstanceInfo < ApplicationRecord
|
||||||
after_commit :reset_cache
|
after_commit :reset_cache
|
||||||
|
|
||||||
EMOJI_REACTION_AVAILABLE_SOFTWARES = %w(
|
EMOJI_REACTION_AVAILABLE_SOFTWARES = %w(
|
||||||
misskey
|
|
||||||
calckey
|
|
||||||
cherrypick
|
|
||||||
meisskey
|
|
||||||
sharkey
|
|
||||||
firefish
|
|
||||||
catodon
|
|
||||||
renedon
|
|
||||||
fedibird
|
|
||||||
pleroma
|
|
||||||
akkoma
|
akkoma
|
||||||
|
calckey
|
||||||
|
catodon
|
||||||
|
cherrypick
|
||||||
|
fedibird
|
||||||
|
firefish
|
||||||
|
iceshrimp
|
||||||
|
meisskey
|
||||||
|
misskey
|
||||||
|
pleroma
|
||||||
|
sharkey
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
def self.emoji_reaction_available?(domain)
|
def self.emoji_reaction_available?(domain)
|
||||||
|
|
|
@ -618,7 +618,7 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def distributable_friend?
|
def distributable_friend?
|
||||||
public_visibility? || public_unlisted_visibility? || (unlisted_visibility? && (public_searchability? || public_unlisted_searchability?))
|
public_visibility? || public_unlisted_visibility? || login_visibility? || (unlisted_visibility? && (public_searchability? || public_unlisted_searchability?))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class StatusReference < ApplicationRecord
|
class StatusReference < ApplicationRecord
|
||||||
|
REFERENCES_LIMIT = 5
|
||||||
|
|
||||||
belongs_to :status
|
belongs_to :status
|
||||||
belongs_to :target_status, class_name: 'Status'
|
belongs_to :target_status, class_name: 'Status'
|
||||||
|
|
||||||
|
|
|
@ -361,6 +361,16 @@ class User < ApplicationRecord
|
||||||
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
|
||||||
batch.update_all(revoked_at: Time.now.utc)
|
batch.update_all(revoked_at: Time.now.utc)
|
||||||
Web::PushSubscription.where(access_token_id: batch).delete_all
|
Web::PushSubscription.where(access_token_id: batch).delete_all
|
||||||
|
|
||||||
|
# Revoke each access token for the Streaming API, since `update_all``
|
||||||
|
# doesn't trigger ActiveRecord Callbacks:
|
||||||
|
# TODO: #28793 Combine into a single topic
|
||||||
|
payload = Oj.dump(event: :kill)
|
||||||
|
redis.pipelined do |pipeline|
|
||||||
|
batch.ids.each do |id|
|
||||||
|
pipeline.publish("timeline:access_token:#{id}", payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ class UserSettings
|
||||||
setting :theme, default: -> { ::Setting.theme }
|
setting :theme, default: -> { ::Setting.theme }
|
||||||
setting :noindex, default: -> { ::Setting.noindex }
|
setting :noindex, default: -> { ::Setting.noindex }
|
||||||
setting :translatable_private, default: false
|
setting :translatable_private, default: false
|
||||||
setting :link_preview, default: true
|
|
||||||
setting :bio_markdown, default: false
|
setting :bio_markdown, default: false
|
||||||
setting :discoverable_local, default: false
|
setting :discoverable_local, default: false
|
||||||
setting :hide_statuses_count, default: false
|
setting :hide_statuses_count, default: false
|
||||||
|
@ -42,6 +41,7 @@ class UserSettings
|
||||||
setting :dtl_force_subscribable, default: false
|
setting :dtl_force_subscribable, default: false
|
||||||
setting :lock_follow_from_bot, default: false
|
setting :lock_follow_from_bot, default: false
|
||||||
setting :allow_quote, default: true
|
setting :allow_quote, default: true
|
||||||
|
setting :reject_send_limited_to_suspects, default: false
|
||||||
|
|
||||||
setting_inverse_alias :indexable, :noindex
|
setting_inverse_alias :indexable, :noindex
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
||||||
|
|
||||||
case collection['type']
|
case collection['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
collection['items']
|
as_array(collection['items'])
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
collection['orderedItems']
|
as_array(collection['orderedItems'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ class ActivityPub::FetchReferencesService < BaseService
|
||||||
def call(status, collection_or_uri)
|
def call(status, collection_or_uri)
|
||||||
@account = status.account
|
@account = status.account
|
||||||
|
|
||||||
collection_items(collection_or_uri)&.map { |item| value_or_id(item) }
|
collection_items(collection_or_uri)&.take(8)&.map { |item| value_or_id(item) }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -20,9 +20,9 @@ class ActivityPub::FetchReferencesService < BaseService
|
||||||
|
|
||||||
case collection['type']
|
case collection['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
collection['items']
|
as_array(collection['items'])
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
collection['orderedItems']
|
as_array(collection['orderedItems'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,6 +31,19 @@ class ActivityPub::FetchReferencesService < BaseService
|
||||||
return if unsupported_uri_scheme?(collection_or_uri)
|
return if unsupported_uri_scheme?(collection_or_uri)
|
||||||
return if ActivityPub::TagManager.instance.local_uri?(collection_or_uri)
|
return if ActivityPub::TagManager.instance.local_uri?(collection_or_uri)
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
|
||||||
|
# queries incorrectly by default.
|
||||||
|
#
|
||||||
|
# While this is relevant for all URLs with query strings, this is
|
||||||
|
# the only code path where this happens in practice.
|
||||||
|
#
|
||||||
|
# Therefore, retry with correct signatures if this fails.
|
||||||
|
begin
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||||
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
|
||||||
|
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
|
class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
|
||||||
# Does a WebFinger roundtrip on each call, unless `only_key` is true
|
# Does a WebFinger roundtrip on each call, unless `only_key` is true
|
||||||
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
|
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
|
||||||
actor = super
|
actor = super
|
||||||
return actor if actor.nil? || actor.is_a?(Account)
|
return actor if actor.nil? || actor.is_a?(Account)
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,15 @@ class ActivityPub::FetchRemoteActorService < BaseService
|
||||||
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
|
SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze
|
||||||
|
|
||||||
# Does a WebFinger roundtrip on each call, unless `only_key` is true
|
# Does a WebFinger roundtrip on each call, unless `only_key` is true
|
||||||
def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
|
def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
|
||||||
return if domain_not_allowed?(uri)
|
return if domain_not_allowed?(uri)
|
||||||
return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
|
return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)
|
||||||
|
|
||||||
@json = begin
|
@json = begin
|
||||||
if prefetched_body.nil?
|
if prefetched_body.nil?
|
||||||
fetch_resource(uri, id)
|
fetch_resource(uri, true)
|
||||||
else
|
else
|
||||||
body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
body_to_json(prefetched_body, compare_id: uri)
|
||||||
end
|
end
|
||||||
rescue Oj::ParseError
|
rescue Oj::ParseError
|
||||||
raise Error, "Error parsing JSON-LD document #{uri}"
|
raise Error, "Error parsing JSON-LD document #{uri}"
|
||||||
|
|
|
@ -6,23 +6,10 @@ class ActivityPub::FetchRemoteKeyService < BaseService
|
||||||
class Error < StandardError; end
|
class Error < StandardError; end
|
||||||
|
|
||||||
# Returns actor that owns the key
|
# Returns actor that owns the key
|
||||||
def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
|
def call(uri, suppress_errors: true)
|
||||||
raise Error, 'No key URI given' if uri.blank?
|
raise Error, 'No key URI given' if uri.blank?
|
||||||
|
|
||||||
if prefetched_body.nil?
|
@json = fetch_resource(uri, false)
|
||||||
if id
|
|
||||||
@json = fetch_resource_without_id_validation(uri)
|
|
||||||
if actor_type?
|
|
||||||
@json = fetch_resource(@json['id'], true)
|
|
||||||
elsif uri != @json['id']
|
|
||||||
raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@json = fetch_resource(uri, id)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
|
raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
|
||||||
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
|
raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
|
||||||
|
|
|
@ -8,14 +8,14 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
||||||
DISCOVERIES_PER_REQUEST = 1000
|
DISCOVERIES_PER_REQUEST = 1000
|
||||||
|
|
||||||
# Should be called when uri has already been checked for locality
|
# Should be called when uri has already been checked for locality
|
||||||
def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
|
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
|
||||||
return if domain_not_allowed?(uri)
|
return if domain_not_allowed?(uri)
|
||||||
|
|
||||||
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
|
||||||
@json = if prefetched_body.nil?
|
@json = if prefetched_body.nil?
|
||||||
fetch_resource(uri, id, on_behalf_of)
|
fetch_resource(uri, true, on_behalf_of)
|
||||||
else
|
else
|
||||||
body_to_json(prefetched_body, compare_id: id ? uri : nil)
|
body_to_json(prefetched_body, compare_id: uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless supported_context?
|
return unless supported_context?
|
||||||
|
@ -65,7 +65,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
|
||||||
|
|
||||||
def account_from_uri(uri)
|
def account_from_uri(uri)
|
||||||
actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
|
actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
|
||||||
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale?
|
actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale?
|
||||||
actor
|
actor
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService
|
||||||
|
|
||||||
case collection['type']
|
case collection['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
collection['items']
|
as_array(collection['items'])
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
collection['orderedItems']
|
as_array(collection['orderedItems'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService
|
||||||
return unless @allow_synchronous_requests
|
return unless @allow_synchronous_requests
|
||||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||||
|
|
||||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
|
||||||
|
# queries incorrectly by default.
|
||||||
|
#
|
||||||
|
# While this is relevant for all URLs with query strings, this is
|
||||||
|
# the only code path where this happens in practice.
|
||||||
|
#
|
||||||
|
# Therefore, retry with correct signatures if this fails.
|
||||||
|
begin
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||||
|
rescue Mastodon::UnexpectedResponseError => e
|
||||||
|
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
|
||||||
|
|
||||||
|
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_replies
|
def filtered_replies
|
||||||
|
|
|
@ -131,8 +131,8 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_account?
|
def valid_account?
|
||||||
display_name = @json['name']
|
display_name = @json['name'] || ''
|
||||||
note = @json['summary']
|
note = @json['summary'] || ''
|
||||||
!Admin::NgWord.reject?(display_name) && !Admin::NgWord.reject?(note)
|
!Admin::NgWord.reject?(display_name) && !Admin::NgWord.reject?(note)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
|
|
||||||
def moved_account
|
def moved_account
|
||||||
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
|
account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
|
||||||
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id])
|
account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id])
|
||||||
account
|
account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
||||||
|
|
||||||
case collection['type']
|
case collection['type']
|
||||||
when 'Collection', 'CollectionPage'
|
when 'Collection', 'CollectionPage'
|
||||||
collection['items']
|
as_array(collection['items'])
|
||||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||||
collection['orderedItems']
|
as_array(collection['orderedItems'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ module AccountScope
|
||||||
scope_local
|
scope_local
|
||||||
when :private
|
when :private
|
||||||
scope_account_local_followers(status.account)
|
scope_account_local_followers(status.account)
|
||||||
|
when :limited
|
||||||
|
scope_status_all_mentioned(status)
|
||||||
else
|
else
|
||||||
scope_status_mentioned(status)
|
scope_status_mentioned(status)
|
||||||
end
|
end
|
||||||
|
@ -24,6 +26,10 @@ module AccountScope
|
||||||
Account.local.where(id: status.active_mentions.select(:account_id)).reorder(nil)
|
Account.local.where(id: status.active_mentions.select(:account_id)).reorder(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def scope_status_all_mentioned(status)
|
||||||
|
Account.local.where(id: status.mentions.select(:account_id)).reorder(nil)
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: not work
|
# TODO: not work
|
||||||
def scope_list_following_account(account)
|
def scope_list_following_account(account)
|
||||||
account.lists_for_local_distribution.select(:id).reorder(nil)
|
account.lists_for_local_distribution.select(:id).reorder(nil)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class FetchLinkCardService < BaseService
|
||||||
@status = status
|
@status = status
|
||||||
@original_url = parse_urls
|
@original_url = parse_urls
|
||||||
|
|
||||||
return if @original_url.nil? || @status.with_preview_card? || !@status.account.link_preview?
|
return if @original_url.nil? || @status.with_preview_card?
|
||||||
|
|
||||||
@url = @original_url.to_s
|
@url = @original_url.to_s
|
||||||
|
|
||||||
|
@ -88,6 +88,10 @@ class FetchLinkCardService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def referenced_urls
|
def referenced_urls
|
||||||
|
referenced_urls_raw.filter { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Status, url: true).present? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def referenced_urls_raw
|
||||||
unless @status.local?
|
unless @status.local?
|
||||||
document = Nokogiri::HTML(@status.text)
|
document = Nokogiri::HTML(@status.text)
|
||||||
document.search('a[href^="http://"]', 'a[href^="https://"]').each do |link|
|
document.search('a[href^="http://"]', 'a[href^="https://"]').each do |link|
|
||||||
|
|
|
@ -44,11 +44,19 @@ class FetchResourceService < BaseService
|
||||||
@response_code = response.code
|
@response_code = response.code
|
||||||
return nil if response.code != 200
|
return nil if response.code != 200
|
||||||
|
|
||||||
if ['application/activity+json', 'application/ld+json'].include?(response.mime_type)
|
if valid_activitypub_content_type?(response)
|
||||||
body = response.body_with_limit
|
body = response.body_with_limit
|
||||||
json = body_to_json(body)
|
json = body_to_json(body)
|
||||||
|
|
||||||
[json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
|
return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
|
||||||
|
|
||||||
|
if json['id'] != @url
|
||||||
|
return if terminal
|
||||||
|
|
||||||
|
return process(json['id'], terminal: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
[@url, { prefetched_body: body }]
|
||||||
elsif !terminal
|
elsif !terminal
|
||||||
link_header = response['Link'] && parse_link_header(response)
|
link_header = response['Link'] && parse_link_header(response)
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ class Keys::QueryService < BaseService
|
||||||
|
|
||||||
return if json['items'].blank?
|
return if json['items'].blank?
|
||||||
|
|
||||||
@devices = json['items'].map do |device|
|
@devices = as_array(json['items']).map do |device|
|
||||||
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||||
end
|
end
|
||||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||||
|
|
|
@ -105,7 +105,10 @@ class ProcessMentionsService < BaseService
|
||||||
def process_mutual!
|
def process_mutual!
|
||||||
mentioned_account_ids = @current_mentions.map(&:account_id)
|
mentioned_account_ids = @current_mentions.map(&:account_id)
|
||||||
|
|
||||||
@status.account.mutuals.reorder(nil).find_each do |target_account|
|
mutuals = @status.account.mutuals
|
||||||
|
mutuals = mutuals.where.not(domain: InstanceInfo.where(software: 'misskey').select(:domain)).or(mutuals.where(domain: nil)) if @status.account.user&.setting_reject_send_limited_to_suspects
|
||||||
|
|
||||||
|
mutuals.reorder(nil).find_each do |target_account|
|
||||||
@current_mentions << @status.mentions.new(silent: true, account: target_account) unless mentioned_account_ids.include?(target_account.id)
|
@current_mentions << @status.mentions.new(silent: true, account: target_account) unless mentioned_account_ids.include?(target_account.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,11 +44,7 @@ class ReblogService < BaseService
|
||||||
def create_notification(reblog)
|
def create_notification(reblog)
|
||||||
reblogged_status = reblog.reblog
|
reblogged_status = reblog.reblog
|
||||||
|
|
||||||
if reblogged_status.account.local?
|
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog') if reblogged_status.account.local?
|
||||||
LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name, 'reblog')
|
|
||||||
elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account)
|
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def increment_statistics
|
def increment_statistics
|
||||||
|
|
|
@ -19,10 +19,16 @@
|
||||||
= ff.input :translatable_private, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_translatable_private')
|
= ff.input :translatable_private, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_translatable_private')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :link_preview, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_link_preview'), hint: I18n.t('simple_form.hints.defaults.setting_link_preview')
|
= f.input :subscription_policy,
|
||||||
|
as: :radio_buttons,
|
||||||
.fields-group
|
collection: %w(allow followers_only block),
|
||||||
= f.input :subscription_policy, kmyblue: true, collection: %w(allow followers_only block), label_method: ->(item) { safe_join([t("simple_form.labels.subscription_policy.#{item}")]) }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: t('simple_form.labels.defaults.subscription_policy'), hint: t('simple_form.hints.defaults.subscription_policy')
|
collection_wrapper_tag: 'ul',
|
||||||
|
hint: t('simple_form.hints.defaults.subscription_policy'),
|
||||||
|
item_wrapper_tag: 'li',
|
||||||
|
kmyblue: true,
|
||||||
|
label: t('simple_form.labels.defaults.subscription_policy'),
|
||||||
|
label_method: ->(item) { safe_join([t("simple_form.labels.subscription_policy.#{item}")]) },
|
||||||
|
wrapper: :with_floating_label
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :allow_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_allow_quote'), hint: false
|
= ff.input :allow_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_allow_quote'), hint: false
|
||||||
|
@ -39,5 +45,8 @@
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :reject_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_unlisted_subscription'), hint: I18n.t('simple_form.hints.defaults.setting_reject_unlisted_subscription')
|
= ff.input :reject_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_unlisted_subscription'), hint: I18n.t('simple_form.hints.defaults.setting_reject_unlisted_subscription')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= ff.input :reject_send_limited_to_suspects, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_send_limited_to_suspects'), hint: I18n.t('simple_form.hints.defaults.setting_reject_send_limited_to_suspects')
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -7,7 +7,7 @@ class LinkCrawlWorker
|
||||||
|
|
||||||
def perform(status_id)
|
def perform(status_id)
|
||||||
FetchLinkCardService.new.call(Status.find(status_id))
|
FetchLinkCardService.new.call(Status.find(status_id))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,9 +21,14 @@ Doorkeeper.configure do
|
||||||
user unless user&.otp_required_for_login?
|
user unless user&.otp_required_for_login?
|
||||||
end
|
end
|
||||||
|
|
||||||
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
|
# Doorkeeper provides some administrative interfaces for managing OAuth
|
||||||
|
# Applications, allowing creation, edit, and deletion of applications from the
|
||||||
|
# server. At present, these administrative routes are not integrated into
|
||||||
|
# Mastodon, and as such, we've disabled them by always return a 403 forbidden
|
||||||
|
# response for them. This does not affect the ability for users to manage
|
||||||
|
# their own OAuth Applications.
|
||||||
admin_authenticator do
|
admin_authenticator do
|
||||||
current_user&.admin? || redirect_to(new_user_session_url)
|
head 403
|
||||||
end
|
end
|
||||||
|
|
||||||
# Authorization Code expiration time (default 10 minutes).
|
# Authorization Code expiration time (default 10 minutes).
|
||||||
|
|
|
@ -12,6 +12,7 @@ en:
|
||||||
last_attempt: You have one more attempt before your account is locked.
|
last_attempt: You have one more attempt before your account is locked.
|
||||||
locked: Your account is locked.
|
locked: Your account is locked.
|
||||||
not_found_in_database: Invalid %{authentication_keys} or password.
|
not_found_in_database: Invalid %{authentication_keys} or password.
|
||||||
|
omniauth_user_creation_failure: Error creating an account for this identity.
|
||||||
pending: Your account is still under review.
|
pending: Your account is still under review.
|
||||||
timeout: Your session expired. Please login again to continue.
|
timeout: Your session expired. Please login again to continue.
|
||||||
unauthenticated: You need to login or sign up before continuing.
|
unauthenticated: You need to login or sign up before continuing.
|
||||||
|
|
|
@ -2075,6 +2075,7 @@ en:
|
||||||
go_to_sso_account_settings: Go to your identity provider's account settings
|
go_to_sso_account_settings: Go to your identity provider's account settings
|
||||||
invalid_otp_token: Invalid two-factor code
|
invalid_otp_token: Invalid two-factor code
|
||||||
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
otp_lost_help_html: If you lost access to both, you may get in touch with %{email}
|
||||||
|
rate_limited: Too many authentication attempts, try again later.
|
||||||
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
|
seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available.
|
||||||
signed_in_as: 'Signed in as:'
|
signed_in_as: 'Signed in as:'
|
||||||
verification:
|
verification:
|
||||||
|
|
|
@ -278,11 +278,11 @@ en:
|
||||||
setting_hide_network: Hide your social graph
|
setting_hide_network: Hide your social graph
|
||||||
setting_hide_recent_emojis: Hide recent emojis
|
setting_hide_recent_emojis: Hide recent emojis
|
||||||
setting_hide_statuses_count: Hide statuses count
|
setting_hide_statuses_count: Hide statuses count
|
||||||
setting_link_preview: Generate post link preview card
|
|
||||||
setting_lock_follow_from_bot: Request approval about bot follow
|
setting_lock_follow_from_bot: Request approval about bot follow
|
||||||
setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app
|
setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app
|
||||||
setting_reduce_motion: Reduce motion in animations
|
setting_reduce_motion: Reduce motion in animations
|
||||||
setting_reject_public_unlisted_subscription: Reject sending public unlisted visibility/non-public searchability posts to Misskey, Calckey
|
setting_reject_public_unlisted_subscription: Reject sending public unlisted visibility/non-public searchability posts to Misskey, Calckey
|
||||||
|
setting_reject_send_limited_to_suspects: Reject sending mutual posts to Misskey
|
||||||
setting_reject_unlisted_subscription: Reject sending unlisted visibility/non-public searchability posts to Misskey, Calckey
|
setting_reject_unlisted_subscription: Reject sending unlisted visibility/non-public searchability posts to Misskey, Calckey
|
||||||
setting_send_without_domain_blocks: Send your post to all server with administrator set as rejecting-post-server for protect you [DEPRECATED]
|
setting_send_without_domain_blocks: Send your post to all server with administrator set as rejecting-post-server for protect you [DEPRECATED]
|
||||||
setting_show_application: Disclose application used to send posts
|
setting_show_application: Disclose application used to send posts
|
||||||
|
|
|
@ -79,8 +79,8 @@ ja:
|
||||||
setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、スタンプ機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります
|
setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、スタンプ機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります
|
||||||
setting_enable_emoji_reaction: この機能を無効にしても、他の人はあなたの投稿にスタンプをつけられます
|
setting_enable_emoji_reaction: この機能を無効にしても、他の人はあなたの投稿にスタンプをつけられます
|
||||||
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
|
||||||
setting_link_preview: プレビュー生成を停止することは、センシティブなサイトへのリンクを頻繁に投稿する人にも有効かもしれません
|
|
||||||
setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります
|
setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります
|
||||||
|
setting_reject_send_limited_to_suspects: これは「相互のみ」投稿に適用されます。サークル投稿は例外なく配送されます。一部のMisskeyサーバーが独自に限定投稿へ対応しましたが、相互のみ投稿を行うたびに相手に通知されるなど複数の問題があるため、気になる人向けの設定です
|
||||||
setting_reject_unlisted_subscription: Misskeyやそのフォークは、フォローしていないアカウントの「非収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに非収載として配信されること、ご理解ください
|
setting_reject_unlisted_subscription: Misskeyやそのフォークは、フォローしていないアカウントの「非収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに非収載として配信されること、ご理解ください
|
||||||
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
|
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
|
||||||
setting_single_ref_to_quote: 当サーバーがまだ対象投稿を取り込んでいない場合、引用が相手に正常に認識されない場合があります
|
setting_single_ref_to_quote: 当サーバーがまだ対象投稿を取り込んでいない場合、引用が相手に正常に認識されない場合があります
|
||||||
|
@ -287,7 +287,6 @@ ja:
|
||||||
setting_hide_network: 繋がりを隠す
|
setting_hide_network: 繋がりを隠す
|
||||||
setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(絵文字デッキのみを表示する)
|
setting_hide_recent_emojis: 絵文字ピッカーで最近使用した絵文字を隠す(絵文字デッキのみを表示する)
|
||||||
setting_hide_statuses_count: 投稿数を隠す
|
setting_hide_statuses_count: 投稿数を隠す
|
||||||
setting_link_preview: リンクのプレビューを生成する
|
|
||||||
setting_lock_follow_from_bot: botからのフォローを承認制にする
|
setting_lock_follow_from_bot: botからのフォローを承認制にする
|
||||||
setting_show_quote_in_home: ホーム・リスト・アンテナなどで引用を表示する
|
setting_show_quote_in_home: ホーム・リスト・アンテナなどで引用を表示する
|
||||||
setting_show_quote_in_public: 公開タイムライン(ローカル・連合)で引用を表示する
|
setting_show_quote_in_public: 公開タイムライン(ローカル・連合)で引用を表示する
|
||||||
|
@ -295,6 +294,7 @@ ja:
|
||||||
setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
|
setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
|
||||||
setting_reduce_motion: アニメーションの動きを減らす
|
setting_reduce_motion: アニメーションの動きを減らす
|
||||||
setting_reject_public_unlisted_subscription: Misskey系サーバーに「ローカル公開」かつ検索許可「誰でも以外」の投稿を「フォロワーのみ」に変換して配送する
|
setting_reject_public_unlisted_subscription: Misskey系サーバーに「ローカル公開」かつ検索許可「誰でも以外」の投稿を「フォロワーのみ」に変換して配送する
|
||||||
|
setting_reject_send_limited_to_suspects: Misskey系サーバーに「相互のみ」投稿を配送しない
|
||||||
setting_reject_unlisted_subscription: Misskey系サーバーに「非収載」かつ検索許可「誰でも以外」の投稿を「フォロワーのみ」に変換して配送する
|
setting_reject_unlisted_subscription: Misskey系サーバーに「非収載」かつ検索許可「誰でも以外」の投稿を「フォロワーのみ」に変換して配送する
|
||||||
setting_send_without_domain_blocks: 管理人の設定した配送停止設定を拒否する (非推奨)
|
setting_send_without_domain_blocks: 管理人の設定した配送停止設定を拒否する (非推奨)
|
||||||
setting_show_application: 送信したアプリを開示する
|
setting_show_application: 送信したアプリを開示する
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'sidekiq_unique_jobs/web'
|
require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true
|
||||||
require 'sidekiq-scheduler/web'
|
require 'sidekiq-scheduler/web'
|
||||||
|
|
||||||
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
|
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
|
||||||
|
|
|
@ -56,7 +56,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.1
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
|
@ -77,7 +77,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.1
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -95,7 +95,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.2.1
|
image: ghcr.io/mastodon/mastodon:v4.2.7
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -6,8 +6,8 @@ Install Ruby
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||||
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.2.2
|
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.2.3
|
||||||
rbenv global 3.2.2
|
rbenv global 3.2.3
|
||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ Install Ruby
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||||
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.2.2
|
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.2.3
|
||||||
rbenv global 3.2.2
|
rbenv global 3.2.3
|
||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def kmyblue_minor
|
def kmyblue_minor
|
||||||
0
|
5
|
||||||
end
|
end
|
||||||
|
|
||||||
def kmyblue_flag
|
def kmyblue_flag
|
||||||
|
@ -29,7 +29,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
'alpha.0'
|
'alpha.3'
|
||||||
end
|
end
|
||||||
|
|
||||||
def prerelease
|
def prerelease
|
||||||
|
|
|
@ -16,7 +16,7 @@ module Paperclip
|
||||||
private
|
private
|
||||||
|
|
||||||
def cache_current_values
|
def cache_current_values
|
||||||
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
@original_filename = truncated_filename
|
||||||
@tempfile = copy_to_tempfile(@target)
|
@tempfile = copy_to_tempfile(@target)
|
||||||
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
@content_type = ContentTypeDetector.new(@tempfile.path).detect
|
||||||
@size = File.size(@tempfile)
|
@size = File.size(@tempfile)
|
||||||
|
@ -43,6 +43,13 @@ module Paperclip
|
||||||
source.response.connection.close
|
source.response.connection.close
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def truncated_filename
|
||||||
|
filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
||||||
|
extension = File.extname(filename)
|
||||||
|
basename = File.basename(filename, extension)
|
||||||
|
[basename[...20], extension[..4]].compact_blank.join
|
||||||
|
end
|
||||||
|
|
||||||
def filename_from_content_disposition
|
def filename_from_content_disposition
|
||||||
disposition = @target.response.headers['content-disposition']
|
disposition = @target.response.headers['content-disposition']
|
||||||
disposition&.match(/filename="([^"]*)"/)&.captures&.first
|
disposition&.match(/filename="([^"]*)"/)&.captures&.first
|
||||||
|
|
|
@ -17,7 +17,7 @@ namespace :db do
|
||||||
|
|
||||||
task :pre_migration_check do
|
task :pre_migration_check do
|
||||||
version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
|
version = ActiveRecord::Base.connection.select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
|
||||||
abort 'This version of Mastodon requires PostgreSQL 9.5 or newer. Please update PostgreSQL before updating Mastodon' if version < 90_500
|
abort 'This version of Mastodon requires PostgreSQL 10.0 or newer. Please update PostgreSQL before updating Mastodon' if version < 100_000
|
||||||
end
|
end
|
||||||
|
|
||||||
Rake::Task['db:migrate'].enhance(['db:pre_migration_check'])
|
Rake::Task['db:migrate'].enhance(['db:pre_migration_check'])
|
||||||
|
|
11
lib/tasks/sidekiq_unique_jobs.rake
Normal file
11
lib/tasks/sidekiq_unique_jobs.rake
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :sidekiq_unique_jobs do
|
||||||
|
task delete_all_locks: :environment do
|
||||||
|
digests = SidekiqUniqueJobs::Digests.new
|
||||||
|
digests.delete_by_pattern('*', count: digests.count)
|
||||||
|
|
||||||
|
expiring_digests = SidekiqUniqueJobs::ExpiringDigests.new
|
||||||
|
expiring_digests.delete_by_pattern('*', count: expiring_digests.count)
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ require 'rails_helper'
|
||||||
describe Api::V1::StreamingController do
|
describe Api::V1::StreamingController do
|
||||||
around do |example|
|
around do |example|
|
||||||
before = Rails.configuration.x.streaming_api_base_url
|
before = Rails.configuration.x.streaming_api_base_url
|
||||||
Rails.configuration.x.streaming_api_base_url = Rails.configuration.x.web_domain
|
Rails.configuration.x.streaming_api_base_url = "wss://#{Rails.configuration.x.web_domain}"
|
||||||
example.run
|
example.run
|
||||||
Rails.configuration.x.streaming_api_base_url = before
|
Rails.configuration.x.streaming_api_base_url = before
|
||||||
end
|
end
|
||||||
|
|
|
@ -262,6 +262,26 @@ RSpec.describe Auth::SessionsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when repeatedly using an invalid TOTP code before using a valid code' do
|
||||||
|
before do
|
||||||
|
stub_const('Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not log the user in' do
|
||||||
|
# Travel to the beginning of an hour to avoid crossing rate-limit buckets
|
||||||
|
travel_to '2023-12-20T10:00:00Z'
|
||||||
|
|
||||||
|
Auth::SessionsController::MAX_2FA_ATTEMPTS_PER_HOUR.times do
|
||||||
|
post :create, params: { user: { otp_attempt: '1234' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||||
|
expect(controller.current_user).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||||
|
expect(controller.current_user).to be_nil
|
||||||
|
expect(flash[:alert]).to match I18n.t('users.rate_limited')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when using a valid OTP' do
|
context 'when using a valid OTP' do
|
||||||
before do
|
before do
|
||||||
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
post :create, params: { user: { otp_attempt: user.current_otp } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||||
|
|
|
@ -56,15 +56,15 @@ describe JsonLdHelper do
|
||||||
describe '#fetch_resource' do
|
describe '#fetch_resource' do
|
||||||
context 'when the second argument is false' do
|
context 'when the second argument is false' do
|
||||||
it 'returns resource even if the retrieved ID and the given URI does not match' do
|
it 'returns resource even if the retrieved ID and the given URI does not match' do
|
||||||
stub_request(:get, 'https://bob.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://bob.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://alice.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://alice.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
|
expect(fetch_resource('https://bob.test/', false)).to eq({ 'id' => 'https://alice.test/' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
|
it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do
|
||||||
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://marvin.test/"}'
|
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://marvin.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://marvin.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://marvin.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
expect(fetch_resource('https://mallory.test/', false)).to be_nil
|
expect(fetch_resource('https://mallory.test/', false)).to be_nil
|
||||||
end
|
end
|
||||||
|
@ -72,7 +72,7 @@ describe JsonLdHelper do
|
||||||
|
|
||||||
context 'when the second argument is true' do
|
context 'when the second argument is true' do
|
||||||
it 'returns nil if the retrieved ID and the given URI does not match' do
|
it 'returns nil if the retrieved ID and the given URI does not match' do
|
||||||
stub_request(:get, 'https://mallory.test/').to_return body: '{"id": "https://alice.test/"}'
|
stub_request(:get, 'https://mallory.test/').to_return(body: '{"id": "https://alice.test/"}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource('https://mallory.test/', true)).to be_nil
|
expect(fetch_resource('https://mallory.test/', true)).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -80,12 +80,12 @@ describe JsonLdHelper do
|
||||||
|
|
||||||
describe '#fetch_resource_without_id_validation' do
|
describe '#fetch_resource_without_id_validation' do
|
||||||
it 'returns nil if the status code is not 200' do
|
it 'returns nil if the status code is not 200' do
|
||||||
stub_request(:get, 'https://host.test/').to_return status: 400, body: '{}'
|
stub_request(:get, 'https://host.test/').to_return(status: 400, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
|
expect(fetch_resource_without_id_validation('https://host.test/')).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns hash' do
|
it 'returns hash' do
|
||||||
stub_request(:get, 'https://host.test/').to_return status: 200, body: '{}'
|
stub_request(:get, 'https://host.test/').to_return(status: 200, body: '{}', headers: { 'Content-Type': 'application/activity+json' })
|
||||||
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
|
expect(fetch_resource_without_id_validation('https://host.test/')).to eq({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -267,6 +267,80 @@ RSpec.describe AccountStatusesFilter do
|
||||||
it_behaves_like 'filter params'
|
it_behaves_like 'filter params'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when accessed by a remote account' do
|
||||||
|
let(:current_account) { Fabricate(:account, uri: 'https://example.com/', domain: 'example.com') }
|
||||||
|
let!(:sensitive_status_with_cw) { Fabricate(:status, account: account, visibility: :public, spoiler_text: 'CW', sensitive: true) }
|
||||||
|
let!(:sensitive_status_with_media) do
|
||||||
|
Fabricate(:status, account: account, visibility: :public, sensitive: true).tap do |status|
|
||||||
|
Fabricate(:media_attachment, account: account, status: status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'as_like_public_visibility' do
|
||||||
|
it 'returns private statuses, replies, and reblogs' do
|
||||||
|
expect(results_unique_visibilities).to match_array %w(login unlisted public_unlisted public)
|
||||||
|
|
||||||
|
expect(results_in_reply_to_ids).to_not be_empty
|
||||||
|
|
||||||
|
expect(results_reblog_of_ids).to_not be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a direct status mentioning the non-follower' do
|
||||||
|
let!(:direct_status) { status_with_mention!(:direct, current_account) }
|
||||||
|
|
||||||
|
it 'returns the direct status' do
|
||||||
|
expect(results_ids).to include(direct_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a direct status mentioning other user' do
|
||||||
|
let!(:direct_status) { status_with_mention!(:direct) }
|
||||||
|
|
||||||
|
it 'not returns the direct status' do
|
||||||
|
expect(results_ids).to_not include(direct_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a limited status mentioning the non-follower' do
|
||||||
|
let!(:limited_status) { status_with_mention!(:limited, current_account) }
|
||||||
|
|
||||||
|
it 'returns the limited status' do
|
||||||
|
expect(results_ids).to include(limited_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a limited status mentioning other user' do
|
||||||
|
let!(:limited_status) { status_with_mention!(:limited) }
|
||||||
|
|
||||||
|
it 'not returns the limited status' do
|
||||||
|
expect(results_ids).to_not include(limited_status.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'as_like_public_visibility'
|
||||||
|
it_behaves_like 'filter params'
|
||||||
|
|
||||||
|
it 'returns the sensitive status' do
|
||||||
|
expect(results_ids).to include(sensitive_status_with_cw.id)
|
||||||
|
expect(results_ids).to include(sensitive_status_with_media.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when domain-blocked reject_media' do
|
||||||
|
before do
|
||||||
|
Fabricate(:domain_block, domain: 'example.com', severity: :noop, reject_send_sensitive: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'as_like_public_visibility'
|
||||||
|
it_behaves_like 'filter params'
|
||||||
|
|
||||||
|
it 'does not return the sensitive status' do
|
||||||
|
expect(results_ids).to_not include(sensitive_status_with_cw.id)
|
||||||
|
expect(results_ids).to_not include(sensitive_status_with_media.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def results_unique_visibilities
|
def results_unique_visibilities
|
||||||
|
|
|
@ -35,7 +35,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
context 'when sender is followed by a local account' do
|
context 'when sender is followed by a local account' do
|
||||||
before do
|
before do
|
||||||
Fabricate(:account).follow!(sender)
|
Fabricate(:account).follow!(sender)
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Announce do
|
||||||
let(:object_json) { 'https://example.com/actor/hello-world' }
|
let(:object_json) { 'https://example.com/actor/hello-world' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json))
|
stub_request(:get, 'https://example.com/actor/hello-world').to_return(body: Oj.dump(unknown_object_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the relay is enabled' do
|
context 'when the relay is enabled' do
|
||||||
|
|
|
@ -30,7 +30,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' })
|
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
|
||||||
stub_request(:get, 'http://example.com/conversation').to_return(body: Oj.dump(conversation))
|
stub_request(:get, 'http://example.com/conversation').to_return(body: Oj.dump(conversation), headers: { 'Content-Type' => 'application/activity+json' })
|
||||||
stub_request(:get, 'http://example.com/invalid-conversation').to_return(status: 404)
|
stub_request(:get, 'http://example.com/invalid-conversation').to_return(status: 404)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1158,8 +1158,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://foo.test').to_return(status: 200, body: Oj.dump(actor_json))
|
stub_request(:get, 'https://foo.test').to_return(status: 200, body: Oj.dump(actor_json), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://foo.test/.well-known/webfinger?resource=acct:actor@foo.test').to_return(status: 200, body: Oj.dump(webfinger))
|
stub_request(:get, 'https://foo.test/.well-known/webfinger?resource=acct:actor@foo.test').to_return(status: 200, body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:post, 'https://foo.test/inbox').to_return(status: 200)
|
stub_request(:post, 'https://foo.test/inbox').to_return(status: 200)
|
||||||
stub_request(:get, 'https://foo.test/.well-known/nodeinfo').to_return(status: 200)
|
stub_request(:get, 'https://foo.test/.well-known/nodeinfo').to_return(status: 200)
|
||||||
subject.perform
|
subject.perform
|
||||||
|
|
|
@ -60,8 +60,8 @@ RSpec.describe ActivityPub::Activity::Like do
|
||||||
before do
|
before do
|
||||||
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://foo.bar/emoji2.png').to_return(body: attachment_fixture('emojo.png'))
|
stub_request(:get, 'http://foo.bar/emoji2.png').to_return(body: attachment_fixture('emojo.png'))
|
||||||
stub_request(:get, 'https://example.com/aaa').to_return(status: 200, body: Oj.dump(original_emoji))
|
stub_request(:get, 'https://example.com/aaa').to_return(status: 200, body: Oj.dump(original_emoji), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/invalid').to_return(status: 200, body: Oj.dump(original_invalid_emoji))
|
stub_request(:get, 'https://example.com/invalid').to_return(status: 200, body: Oj.dump(original_invalid_emoji), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
|
|
|
@ -56,7 +56,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
||||||
|
|
||||||
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
|
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
|
||||||
|
|
||||||
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
|
allow(service_stub).to receive(:call).with('http://example.com/alice') do
|
||||||
sender.update!(public_key: old_key)
|
sender.update!(public_key: old_key)
|
||||||
sender
|
sender
|
||||||
end
|
end
|
||||||
|
@ -64,7 +64,7 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
||||||
|
|
||||||
it 'fetches key and returns creator' do
|
it 'fetches key and returns creator' do
|
||||||
expect(subject.verify_actor!).to eq sender
|
expect(subject.verify_actor!).to eq sender
|
||||||
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
|
expect(service_stub).to have_received(:call).with('http://example.com/alice').once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,14 @@ RSpec.describe ActivityPub::TagManager do
|
||||||
expect(subject.cc(status)).to include(subject.uri_for(foo))
|
expect(subject.cc(status)).to include(subject.uri_for(foo))
|
||||||
expect(subject.cc(status)).to_not include(subject.uri_for(alice))
|
expect(subject.cc(status)).to_not include(subject.uri_for(alice))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns poster of reblogged post, if reblog' do
|
||||||
|
bob = Fabricate(:account, username: 'bob', domain: 'example.com', inbox_url: 'http://example.com/bob')
|
||||||
|
alice = Fabricate(:account, username: 'alice')
|
||||||
|
status = Fabricate(:status, visibility: :public, account: bob)
|
||||||
|
reblog = Fabricate(:status, visibility: :public, account: alice, reblog: status)
|
||||||
|
expect(subject.cc(reblog)).to include(subject.uri_for(bob))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#cc_for_misskey' do
|
describe '#cc_for_misskey' do
|
||||||
|
|
|
@ -397,13 +397,9 @@ RSpec.describe Account do
|
||||||
describe '#public_settings_for_local' do
|
describe '#public_settings_for_local' do
|
||||||
subject { account.public_settings_for_local }
|
subject { account.public_settings_for_local }
|
||||||
|
|
||||||
let(:account) { Fabricate(:user, settings: { link_preview: false, allow_quote: true, hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account }
|
let(:account) { Fabricate(:user, settings: { allow_quote: true, hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account }
|
||||||
|
|
||||||
shared_examples 'some settings' do |permitted, emoji_reaction_policy|
|
shared_examples 'some settings' do |permitted, emoji_reaction_policy|
|
||||||
it 'link_preview is disallowed' do
|
|
||||||
expect(subject['link_preview']).to be permitted.include?(:link_preview)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allow_quote is allowed' do
|
it 'allow_quote is allowed' do
|
||||||
expect(subject['allow_quote']).to be permitted.include?(:allow_quote)
|
expect(subject['allow_quote']).to be permitted.include?(:allow_quote)
|
||||||
end
|
end
|
||||||
|
@ -423,8 +419,14 @@ RSpec.describe Account do
|
||||||
|
|
||||||
it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only'
|
it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only'
|
||||||
|
|
||||||
|
context 'when default true setting is set false' do
|
||||||
|
let(:account) { Fabricate(:user, settings: { allow_quote: false, hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account }
|
||||||
|
|
||||||
|
it_behaves_like 'some settings', %i(hide_statuses_count), 'followers_only'
|
||||||
|
end
|
||||||
|
|
||||||
context 'when remote user' do
|
context 'when remote user' do
|
||||||
let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor', settings: { 'link_preview' => false, 'allow_quote' => true, 'hide_statuses_count' => true, 'emoji_reaction_policy' => 'followers_only' }) }
|
let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor', settings: { 'allow_quote' => true, 'hide_statuses_count' => true, 'emoji_reaction_policy' => 'followers_only' }) }
|
||||||
|
|
||||||
it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only'
|
it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only'
|
||||||
end
|
end
|
||||||
|
@ -432,7 +434,7 @@ RSpec.describe Account do
|
||||||
context 'when remote user by server other_settings is not supported' do
|
context 'when remote user by server other_settings is not supported' do
|
||||||
let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') }
|
let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') }
|
||||||
|
|
||||||
it_behaves_like 'some settings', %i(link_preview allow_quote), 'allow'
|
it_behaves_like 'some settings', %i(allow_quote), 'allow'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,19 +3,19 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Identity do
|
RSpec.describe Identity do
|
||||||
describe '.find_for_oauth' do
|
describe '.find_for_omniauth' do
|
||||||
let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }
|
let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }
|
||||||
|
|
||||||
it 'calls .find_or_create_by' do
|
it 'calls .find_or_create_by' do
|
||||||
allow(described_class).to receive(:find_or_create_by)
|
allow(described_class).to receive(:find_or_create_by)
|
||||||
|
|
||||||
described_class.find_for_oauth(auth)
|
described_class.find_for_omniauth(auth)
|
||||||
|
|
||||||
expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider)
|
expect(described_class).to have_received(:find_or_create_by).with(uid: auth.uid, provider: auth.provider)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an instance of Identity' do
|
it 'returns an instance of Identity' do
|
||||||
expect(described_class.find_for_oauth(auth)).to be_instance_of described_class
|
expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -412,7 +412,10 @@ RSpec.describe User do
|
||||||
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
|
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
|
||||||
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
|
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }
|
||||||
|
|
||||||
|
let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
|
||||||
user.reset_password!
|
user.reset_password!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -429,6 +432,10 @@ RSpec.describe User do
|
||||||
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
|
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'revokes streaming access for all access tokens' do
|
||||||
|
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once
|
||||||
|
end
|
||||||
|
|
||||||
it 'removes push subscriptions' do
|
it 'removes push subscriptions' do
|
||||||
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
|
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
|
||||||
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { web_push_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
|
83
spec/requests/disabled_oauth_endpoints_spec.rb
Normal file
83
spec/requests/disabled_oauth_endpoints_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Disabled OAuth routes' do
|
||||||
|
# These routes are disabled via the doorkeeper configuration for
|
||||||
|
# `admin_authenticator`, as these routes should only be accessible by server
|
||||||
|
# administrators. For now, these routes are not properly designed and
|
||||||
|
# integrated into Mastodon, so we're disabling them completely
|
||||||
|
describe 'GET /oauth/applications' do
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
get oauth_applications_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /oauth/applications' do
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
post oauth_applications_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /oauth/applications/new' do
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
get new_oauth_application_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /oauth/applications/:id' do
|
||||||
|
let(:application) { Fabricate(:application, scopes: 'read') }
|
||||||
|
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
get oauth_application_path(application)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PATCH /oauth/applications/:id' do
|
||||||
|
let(:application) { Fabricate(:application, scopes: 'read') }
|
||||||
|
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
patch oauth_application_path(application)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /oauth/applications/:id' do
|
||||||
|
let(:application) { Fabricate(:application, scopes: 'read') }
|
||||||
|
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
put oauth_application_path(application)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /oauth/applications/:id' do
|
||||||
|
let(:application) { Fabricate(:application, scopes: 'read') }
|
||||||
|
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
delete oauth_application_path(application)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /oauth/applications/:id/edit' do
|
||||||
|
let(:application) { Fabricate(:application, scopes: 'read') }
|
||||||
|
|
||||||
|
it 'returns 403 forbidden' do
|
||||||
|
get edit_oauth_application_path(application)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,16 +39,35 @@ describe 'OmniAuth callbacks' do
|
||||||
Fabricate(:user, email: 'user@host.example')
|
Fabricate(:user, email: 'user@host.example')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'matches the existing user, creates an identity, and redirects to root path' do
|
context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is set to true' do
|
||||||
expect { subject }
|
around do |example|
|
||||||
.to not_change(User, :count)
|
ClimateControl.modify ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH: 'true' do
|
||||||
.and change(Identity, :count)
|
example.run
|
||||||
.by(1)
|
end
|
||||||
.and change(LoginActivity, :count)
|
end
|
||||||
.by(1)
|
|
||||||
|
|
||||||
expect(Identity.find_by(user: User.last).uid).to eq('123')
|
it 'matches the existing user, creates an identity, and redirects to root path' do
|
||||||
expect(response).to redirect_to(root_path)
|
expect { subject }
|
||||||
|
.to not_change(User, :count)
|
||||||
|
.and change(Identity, :count)
|
||||||
|
.by(1)
|
||||||
|
.and change(LoginActivity, :count)
|
||||||
|
.by(1)
|
||||||
|
|
||||||
|
expect(Identity.find_by(user: User.last).uid).to eq('123')
|
||||||
|
expect(response).to redirect_to(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH is not set to true' do
|
||||||
|
it 'does not match the existing user or create an identity, and redirects to login page' do
|
||||||
|
expect { subject }
|
||||||
|
.to not_change(User, :count)
|
||||||
|
.and not_change(Identity, :count)
|
||||||
|
.and not_change(LoginActivity, :count)
|
||||||
|
|
||||||
|
expect(response).to redirect_to(new_user_session_url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -96,7 +115,7 @@ describe 'OmniAuth callbacks' do
|
||||||
|
|
||||||
context 'when a user cannot be built' do
|
context 'when a user cannot be built' do
|
||||||
before do
|
before do
|
||||||
allow(User).to receive(:find_for_oauth).and_return(User.new)
|
allow(User).to receive(:find_for_omniauth).and_return(User.new)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'redirects to the new user signup page' do
|
it 'redirects to the new user signup page' do
|
||||||
|
|
|
@ -31,7 +31,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:status_json_pinned_unknown_unreachable) do
|
let(:status_json_pinned_unknown_reachable) do
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
|
@ -72,11 +72,11 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
|
|
||||||
shared_examples 'sets pinned posts' do
|
shared_examples 'sets pinned posts' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known))
|
stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
|
stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
|
||||||
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_unreachable))
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null))
|
stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
subject.call(actor, note: true, hashtag: false)
|
subject.call(actor, note: true, hashtag: false)
|
||||||
end
|
end
|
||||||
|
@ -94,7 +94,7 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection' do
|
context 'when the endpoint is a Collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
@ -111,10 +111,25 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
|
||||||
|
context 'when there is a single item, with the array compacted away' do
|
||||||
|
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
subject.call(actor, note: true, hashtag: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets expected posts as pinned posts' do
|
||||||
|
expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
|
||||||
|
'https://example.com/account/pinned/unknown-reachable'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the endpoint is a paginated Collection' do
|
context 'when the endpoint is a paginated Collection' do
|
||||||
|
@ -132,10 +147,25 @@ RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets pinned posts'
|
it_behaves_like 'sets pinned posts'
|
||||||
|
|
||||||
|
context 'when there is a single item, with the array compacted away' do
|
||||||
|
let(:items) { 'https://example.com/account/pinned/unknown-reachable' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_reachable), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
subject.call(actor, note: true, hashtag: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets expected posts as pinned posts' do
|
||||||
|
expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
|
||||||
|
'https://example.com/account/pinned/unknown-reachable'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection' do
|
context 'when the endpoint is a Collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
@ -46,7 +46,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
|
|
||||||
context 'when the account already has featured tags' do
|
context 'when the account already has featured tags' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
|
||||||
actor.featured_tags.create!(name: 'FoO')
|
actor.featured_tags.create!(name: 'FoO')
|
||||||
actor.featured_tags.create!(name: 'baz')
|
actor.featured_tags.create!(name: 'baz')
|
||||||
|
@ -67,7 +67,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
@ -88,7 +88,7 @@ RSpec.describe ActivityPub::FetchFeaturedTagsCollectionService, type: :service d
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'sets featured tags'
|
it_behaves_like 'sets featured tags'
|
||||||
|
|
128
spec/services/activitypub/fetch_references_service_spec.rb
Normal file
128
spec/services/activitypub/fetch_references_service_spec.rb
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::FetchReferencesService, type: :service do
|
||||||
|
subject { described_class.new.call(status, payload) }
|
||||||
|
|
||||||
|
let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
|
||||||
|
let(:status) { Fabricate(:status, account: actor) }
|
||||||
|
let(:collection_uri) { 'http://example.com/references/1' }
|
||||||
|
|
||||||
|
let(:items) do
|
||||||
|
[
|
||||||
|
'http://example.com/self-references-1',
|
||||||
|
'http://example.com/self-references-2',
|
||||||
|
'http://example.com/self-references-3',
|
||||||
|
'http://other.com/other-references-1',
|
||||||
|
'http://other.com/other-references-2',
|
||||||
|
'http://other.com/other-references-3',
|
||||||
|
'http://example.com/self-references-4',
|
||||||
|
'http://example.com/self-references-5',
|
||||||
|
'http://example.com/self-references-6',
|
||||||
|
'http://example.com/self-references-7',
|
||||||
|
'http://example.com/self-references-8',
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'Collection',
|
||||||
|
id: collection_uri,
|
||||||
|
items: items,
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#call' do
|
||||||
|
context 'when the payload is a Collection with inlined replies' do
|
||||||
|
context 'when there is a single reference, with the array compacted away' do
|
||||||
|
let(:items) { 'http://example.com/self-references-1' }
|
||||||
|
|
||||||
|
it 'a item is returned' do
|
||||||
|
expect(subject).to eq ['http://example.com/self-references-1']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the collection itself' do
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the URL to the collection' do
|
||||||
|
subject { described_class.new.call(status, collection_uri) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the payload is an OrderedCollection with inlined references' do
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'OrderedCollection',
|
||||||
|
id: collection_uri,
|
||||||
|
orderedItems: items,
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the collection itself' do
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the URL to the collection' do
|
||||||
|
subject { described_class.new.call(status, collection_uri) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the payload is a paginated Collection with inlined references' do
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
type: 'Collection',
|
||||||
|
id: collection_uri,
|
||||||
|
first: {
|
||||||
|
type: 'CollectionPage',
|
||||||
|
partOf: collection_uri,
|
||||||
|
items: items,
|
||||||
|
},
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the collection itself' do
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing the URL to the collection' do
|
||||||
|
subject { described_class.new.call(status, collection_uri) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'first 8 items are returned' do
|
||||||
|
expect(subject).to eq items.take(8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
let(:account) { subject.call('https://example.com/alice', id: true) }
|
let(:account) { subject.call('https://example.com/alice') }
|
||||||
|
|
||||||
shared_examples 'sets profile data' do
|
shared_examples 'sets profile data' do
|
||||||
it 'returns an account' do
|
it 'returns an account' do
|
||||||
|
@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
before do
|
before do
|
||||||
actor[:inbox] = nil
|
actor[:inbox] = nil
|
||||||
|
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -68,7 +68,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -95,7 +95,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
|
@ -128,7 +128,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -152,7 +152,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
let(:account) { subject.call('https://example.com/alice', id: true) }
|
let(:account) { subject.call('https://example.com/alice') }
|
||||||
|
|
||||||
shared_examples 'sets profile data' do
|
shared_examples 'sets profile data' do
|
||||||
it 'returns an account' do
|
it 'returns an account' do
|
||||||
|
@ -44,7 +44,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
before do
|
before do
|
||||||
actor[:inbox] = nil
|
actor[:inbox] = nil
|
||||||
|
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -68,7 +68,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -95,7 +95,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
|
@ -128,7 +128,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
@ -152,7 +152,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
|
||||||
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://iscool.af/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
|
|
|
@ -50,17 +50,17 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
|
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
|
||||||
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(body: '{}')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
let(:account) { subject.call(public_key_id, id: false) }
|
let(:account) { subject.call(public_key_id) }
|
||||||
|
|
||||||
context 'when the key is a sub-object from the actor' do
|
context 'when the key is a sub-object from the actor' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(actor))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(actor), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the expected account' do
|
it 'returns the expected account' do
|
||||||
|
@ -72,7 +72,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
let(:public_key_id) { 'https://example.com/alice-public-key.json' }
|
let(:public_key_id) { 'https://example.com/alice-public-key.json' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the expected account' do
|
it 'returns the expected account' do
|
||||||
|
@ -85,7 +85,7 @@ RSpec.describe ActivityPub::FetchRemoteKeyService, type: :service do
|
||||||
let(:actor_public_key) { 'https://example.com/alice-public-key.json' }
|
let(:actor_public_key) { 'https://example.com/alice-public-key.json' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })))
|
stub_request(:get, public_key_id).to_return(body: Oj.dump(key_json.merge({ '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] })), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the nil' do
|
it 'returns the nil' do
|
||||||
|
|
|
@ -34,6 +34,18 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the payload is a Collection with inlined replies' do
|
context 'when the payload is a Collection with inlined replies' do
|
||||||
|
context 'when there is a single reply, with the array compacted away' do
|
||||||
|
let(:items) { 'http://example.com/self-reply-1' }
|
||||||
|
|
||||||
|
it 'queues the expected worker' do
|
||||||
|
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||||
|
|
||||||
|
subject.call(status, payload)
|
||||||
|
|
||||||
|
expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when passing the collection itself' do
|
context 'when passing the collection itself' do
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
allow(FetchReplyWorker).to receive(:push_bulk)
|
allow(FetchReplyWorker).to receive(:push_bulk)
|
||||||
|
@ -46,7 +58,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
@ -81,7 +93,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
@ -120,7 +132,7 @@ RSpec.describe ActivityPub::FetchRepliesService, type: :service do
|
||||||
|
|
||||||
context 'when passing the URL to the collection' do
|
context 'when passing the URL to the collection' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'spawns workers for up to 5 replies on the same server' do
|
it 'spawns workers for up to 5 replies on the same server' do
|
||||||
|
|
|
@ -282,6 +282,33 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when account is using note contains ng words' do
|
||||||
|
subject { described_class.new.call(account.username, account.domain, payload) }
|
||||||
|
|
||||||
|
let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }
|
||||||
|
|
||||||
|
let(:payload) do
|
||||||
|
{
|
||||||
|
id: 'https://foo.test',
|
||||||
|
type: 'Actor',
|
||||||
|
inbox: 'https://foo.test/inbox',
|
||||||
|
name: 'Ohagi',
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates account when ng word is not set' do
|
||||||
|
Setting.ng_words = ['Amazon']
|
||||||
|
subject
|
||||||
|
expect(account.reload.display_name).to eq 'Ohagi'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create account when ng word is set' do
|
||||||
|
Setting.ng_words = ['Ohagi']
|
||||||
|
subject
|
||||||
|
expect(account.reload.display_name).to_not eq 'Ohagi'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when account is not suspended' do
|
context 'when account is not suspended' do
|
||||||
subject { described_class.new.call(account.username, account.domain, payload) }
|
subject { described_class.new.call(account.username, account.domain, payload) }
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
context 'when the endpoint is a Collection of actor URIs' do
|
context 'when the endpoint is a Collection of actor URIs' do
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
@ -77,7 +77,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
@ -98,7 +98,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
|
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'synchronizes followers'
|
it_behaves_like 'synchronizes followers'
|
||||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
|
|
||||||
let(:html) { '<!doctype html><title>Hello world</title>' }
|
let(:html) { '<!doctype html><title>Hello world</title>' }
|
||||||
let(:oembed_cache) { nil }
|
let(:oembed_cache) { nil }
|
||||||
|
let(:custom_before) { false }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_request(:get, 'http://example.com/html').to_return(headers: { 'Content-Type' => 'text/html' }, body: html)
|
stub_request(:get, 'http://example.com/html').to_return(headers: { 'Content-Type' => 'text/html' }, body: html)
|
||||||
|
@ -30,7 +31,7 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
|
|
||||||
Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
|
Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
|
||||||
|
|
||||||
subject.call(status)
|
subject.call(status) unless custom_before
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a local status' do
|
context 'with a local status' do
|
||||||
|
@ -236,32 +237,53 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with URL of reference' do
|
context 'with URI of reference and normal page' do
|
||||||
let(:status) { Fabricate(:status, text: 'RT http://example.com/html') }
|
|
||||||
|
|
||||||
it 'creates preview card' do
|
|
||||||
expect(status.preview_card).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with URL of reference and normal page' do
|
|
||||||
let(:status) { Fabricate(:status, text: 'RT http://example.com/text http://example.com/html') }
|
let(:status) { Fabricate(:status, text: 'RT http://example.com/text http://example.com/html') }
|
||||||
|
let(:custom_before) { true }
|
||||||
|
|
||||||
|
before { Fabricate(:status, uri: 'http://example.com/text') }
|
||||||
|
|
||||||
it 'creates preview card' do
|
it 'creates preview card' do
|
||||||
|
subject.call(status)
|
||||||
expect(status.preview_card).to_not be_nil
|
expect(status.preview_card).to_not be_nil
|
||||||
expect(status.preview_card.url).to eq 'http://example.com/html'
|
expect(status.preview_card.url).to eq 'http://example.com/html'
|
||||||
expect(status.preview_card.title).to eq 'Hello world'
|
expect(status.preview_card.title).to eq 'Hello world'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with URL but author is not allow preview card' do
|
context 'with URI of reference' do
|
||||||
let(:account) { Fabricate(:user, settings: { link_preview: false }).account }
|
let(:status) { Fabricate(:status, text: 'RT http://example.com/text') }
|
||||||
let(:status) { Fabricate(:status, text: 'http://example.com/html', account: account) }
|
let(:custom_before) { true }
|
||||||
|
|
||||||
it 'not create preview card' do
|
before { Fabricate(:status, uri: 'http://example.com/text') }
|
||||||
|
|
||||||
|
it 'does not create preview card' do
|
||||||
|
subject.call(status)
|
||||||
expect(status.preview_card).to be_nil
|
expect(status.preview_card).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with URL of reference' do
|
||||||
|
let(:status) { Fabricate(:status, text: 'RT http://example.com/text') }
|
||||||
|
let(:custom_before) { true }
|
||||||
|
|
||||||
|
before { Fabricate(:status, uri: 'http://example.com/text/activity', url: 'http://example.com/text') }
|
||||||
|
|
||||||
|
it 'does not create preview card' do
|
||||||
|
subject.call(status)
|
||||||
|
expect(status.preview_card).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with reference normal URL' do
|
||||||
|
let(:status) { Fabricate(:status, text: 'RT http://example.com/html') }
|
||||||
|
|
||||||
|
it 'creates preview card' do
|
||||||
|
expect(status.preview_card).to_not be_nil
|
||||||
|
expect(status.preview_card.url).to eq 'http://example.com/html'
|
||||||
|
expect(status.preview_card.title).to eq 'Hello world'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a remote status' do
|
context 'with a remote status' do
|
||||||
|
@ -282,14 +304,6 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
it 'ignores URLs to hashtags' do
|
it 'ignores URLs to hashtags' do
|
||||||
expect(a_request(:get, 'https://quitter.se/tag/wannacry')).to_not have_been_made
|
expect(a_request(:get, 'https://quitter.se/tag/wannacry')).to_not have_been_made
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with URL but author is not allow preview card' do
|
|
||||||
let(:account) { Fabricate(:account, domain: 'example.com', settings: { link_preview: false }) }
|
|
||||||
|
|
||||||
it 'not create link preview' do
|
|
||||||
expect(status.preview_card).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a remote status of reference' do
|
context 'with a remote status of reference' do
|
||||||
|
@ -298,8 +312,12 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
RT <a href="http://example.com/html" target="_blank" rel="noopener noreferrer" title="http://example.com/html">Hello</a>
|
RT <a href="http://example.com/html" target="_blank" rel="noopener noreferrer" title="http://example.com/html">Hello</a>
|
||||||
TEXT
|
TEXT
|
||||||
end
|
end
|
||||||
|
let(:custom_before) { true }
|
||||||
|
|
||||||
|
before { Fabricate(:status, uri: 'http://example.com/html') }
|
||||||
|
|
||||||
it 'creates preview card' do
|
it 'creates preview card' do
|
||||||
|
subject.call(status)
|
||||||
expect(status.preview_card).to be_nil
|
expect(status.preview_card).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -311,8 +329,12 @@ RSpec.describe FetchLinkCardService, type: :service do
|
||||||
<a href="http://example.com/html_sub" target="_blank" rel="noopener noreferrer" title="http://example.com/html_sub">Hello</a>
|
<a href="http://example.com/html_sub" target="_blank" rel="noopener noreferrer" title="http://example.com/html_sub">Hello</a>
|
||||||
TEXT
|
TEXT
|
||||||
end
|
end
|
||||||
|
let(:custom_before) { true }
|
||||||
|
|
||||||
|
before { Fabricate(:status, uri: 'http://example.com/html') }
|
||||||
|
|
||||||
it 'creates preview card' do
|
it 'creates preview card' do
|
||||||
|
subject.call(status)
|
||||||
expect(status.preview_card).to_not be_nil
|
expect(status.preview_card).to_not be_nil
|
||||||
expect(status.preview_card.url).to eq 'http://example.com/html_sub'
|
expect(status.preview_card.url).to eq 'http://example.com/html_sub'
|
||||||
expect(status.preview_card.title).to eq 'Hello world'
|
expect(status.preview_card.title).to eq 'Hello world'
|
||||||
|
|
|
@ -57,7 +57,7 @@ RSpec.describe FetchResourceService, type: :service do
|
||||||
|
|
||||||
let(:json) do
|
let(:json) do
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 'http://example.com/foo',
|
||||||
'@context': ActivityPub::TagManager::CONTEXT,
|
'@context': ActivityPub::TagManager::CONTEXT,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
}.to_json
|
}.to_json
|
||||||
|
@ -83,27 +83,27 @@ RSpec.describe FetchResourceService, type: :service do
|
||||||
let(:content_type) { 'application/activity+json; charset=utf-8' }
|
let(:content_type) { 'application/activity+json; charset=utf-8' }
|
||||||
let(:body) { json }
|
let(:body) { json }
|
||||||
|
|
||||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
|
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when content type is ld+json with profile' do
|
context 'when content type is ld+json with profile' do
|
||||||
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
|
let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' }
|
||||||
let(:body) { json }
|
let(:body) { json }
|
||||||
|
|
||||||
it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
|
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when link header is present' do
|
context 'when link header is present' do
|
||||||
let(:headers) { { 'Link' => '<http://example.com/foo>; rel="alternate"; type="application/activity+json"' } }
|
let(:headers) { { 'Link' => '<http://example.com/foo>; rel="alternate"; type="application/activity+json"' } }
|
||||||
|
|
||||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
|
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when content type is text/html' do
|
context 'when content type is text/html' do
|
||||||
let(:content_type) { 'text/html' }
|
let(:content_type) { 'text/html' }
|
||||||
let(:body) { '<html><head><link rel="alternate" href="http://example.com/foo" type="application/activity+json"/></head></html>' }
|
let(:body) { '<html><head><link rel="alternate" href="http://example.com/foo" type="application/activity+json"/></head></html>' }
|
||||||
|
|
||||||
it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
|
it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -192,21 +192,50 @@ RSpec.describe PostStatusService, type: :service do
|
||||||
expect(mention_service).to have_received(:call).with(status, limited_type: '', circle: nil, save_records: false)
|
expect(mention_service).to have_received(:call).with(status, limited_type: '', circle: nil, save_records: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'mutual visibility' do
|
context 'with mutual visibility' do
|
||||||
account = Fabricate(:account)
|
let(:sender) { Fabricate(:user).account }
|
||||||
mutual_account = Fabricate(:account)
|
let(:io_account) { Fabricate(:account, domain: 'misskey.io', uri: 'https://misskey.io/actor', inbox_url: 'https://misskey.io/inbox') }
|
||||||
other_account = Fabricate(:account)
|
let(:local_account) { Fabricate(:account) }
|
||||||
text = 'This is an English text.'
|
let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor', inbox_url: 'https://example.com/inbox') }
|
||||||
|
let(:follower) { Fabricate(:account) }
|
||||||
|
let(:followee) { Fabricate(:account) }
|
||||||
|
|
||||||
mutual_account.follow!(account)
|
before do
|
||||||
account.follow!(mutual_account)
|
stub_request(:post, 'https://misskey.io/inbox').to_return(status: 200)
|
||||||
other_account.follow!(account)
|
stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
|
||||||
status = subject.call(account, text: text, visibility: 'mutual')
|
Fabricate(:instance_info, domain: 'misskey.io', software: 'misskey')
|
||||||
|
io_account.follow!(sender)
|
||||||
|
local_account.follow!(sender)
|
||||||
|
remote_account.follow!(sender)
|
||||||
|
follower.follow!(sender)
|
||||||
|
sender.follow!(io_account)
|
||||||
|
sender.follow!(local_account)
|
||||||
|
sender.follow!(remote_account)
|
||||||
|
sender.follow!(followee)
|
||||||
|
end
|
||||||
|
|
||||||
expect(status.visibility).to eq 'limited'
|
it 'visibility is set' do
|
||||||
expect(status.limited_scope).to eq 'mutual'
|
status = subject.call(sender, text: 'text', visibility: 'mutual')
|
||||||
expect(status.mentioned_accounts.count).to eq 1
|
|
||||||
expect(status.mentioned_accounts.first.id).to eq mutual_account.id
|
expect(status.visibility).to eq 'limited'
|
||||||
|
expect(status.limited_scope).to eq 'mutual'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sent to mutuals' do
|
||||||
|
status = subject.call(sender, text: 'text', visibility: 'mutual')
|
||||||
|
|
||||||
|
expect(status.mentioned_accounts.count).to eq 3
|
||||||
|
expect(status.mentioned_accounts.pluck(:id)).to contain_exactly(io_account.id, local_account.id, remote_account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sent to mutuals without misskey.io users' do
|
||||||
|
sender.user.update!(settings: { reject_send_limited_to_suspects: true })
|
||||||
|
|
||||||
|
status = subject.call(sender, text: 'text', visibility: 'mutual')
|
||||||
|
|
||||||
|
expect(status.mentioned_accounts.count).to eq 2
|
||||||
|
expect(status.mentioned_accounts.pluck(:id)).to contain_exactly(local_account.id, remote_account.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'limited visibility and direct searchability' do
|
it 'limited visibility and direct searchability' do
|
||||||
|
|
|
@ -86,9 +86,5 @@ RSpec.describe ReblogService, type: :service do
|
||||||
it 'distributes to followers' do
|
it 'distributes to followers' do
|
||||||
expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
|
expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends an announce activity to the author' do
|
|
||||||
expect(a_request(:post, bob.inbox_url)).to have_been_made.once
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -160,4 +160,22 @@ RSpec.describe RemoveStatusService, type: :service do
|
||||||
)).to have_been_made.once
|
)).to have_been_made.once
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when removed status is a reblog of a non-follower' do
|
||||||
|
let!(:original_status) { Fabricate(:status, account: bill, text: 'Hello ThisIsASecret', visibility: :public) }
|
||||||
|
let!(:status) { ReblogService.new.call(alice, original_status) }
|
||||||
|
|
||||||
|
it 'sends Undo activity to followers' do
|
||||||
|
subject.call(status)
|
||||||
|
expect(a_request(:post, bill.shared_inbox_url).with(
|
||||||
|
body: hash_including({
|
||||||
|
'type' => 'Undo',
|
||||||
|
'object' => hash_including({
|
||||||
|
'type' => 'Announce',
|
||||||
|
'object' => ActivityPub::TagManager.instance.uri_for(original_status),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)).to have_been_made.once
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -139,6 +139,7 @@ describe ResolveURLService, type: :service do
|
||||||
stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url })
|
stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url })
|
||||||
body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json
|
body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json
|
||||||
stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
|
stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
|
||||||
|
stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns status by url' do
|
it 'returns status by url' do
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe ActivityPub::FetchRepliesWorker do
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
it 'performs a request if the collection URI is from the same host' do
|
it 'performs a request if the collection URI is from the same host' do
|
||||||
stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json)
|
stub_request(:get, 'https://example.com/statuses_replies/1').to_return(status: 200, body: json, headers: { 'Content-Type': 'application/activity+json' })
|
||||||
subject.perform(status.id, 'https://example.com/statuses_replies/1')
|
subject.perform(status.id, 'https://example.com/statuses_replies/1')
|
||||||
expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
|
expect(a_request(:get, 'https://example.com/statuses_replies/1')).to have_been_made.once
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue