Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1c8c318281 | ||
|
d722222fe1 | ||
|
03f0e98b32 | ||
|
2c83b9076d | ||
|
c8301bcfc3 | ||
|
0ae91e45de | ||
|
2363b026e6 | ||
|
959234c1e4 | ||
|
0dc103ea11 | ||
|
b782f86b51 | ||
|
fd868f8ca0 | ||
|
4cd33a2c71 | ||
|
f264cca1d2 | ||
|
5e4b04de88 | ||
|
3c18311d86 | ||
|
e5113a8cad | ||
|
22cd1e6ab5 | ||
|
e65ede1ac5 | ||
|
1bcb3daf7e | ||
|
9c610ca0a4 | ||
|
77d0297313 |
@@ -167,8 +167,45 @@ jobs:
|
|||||||
name: Create database
|
name: Create database
|
||||||
command: ./bin/rails db:create
|
command: ./bin/rails db:create
|
||||||
- run:
|
- run:
|
||||||
name: Run migrations
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining migrations
|
||||||
|
|
||||||
|
test-two-step-migrations:
|
||||||
|
<<: *defaults
|
||||||
|
docker:
|
||||||
|
- image: circleci/ruby:2.7-buster-node
|
||||||
|
environment: *ruby_environment
|
||||||
|
- image: circleci/postgres:12.2
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
- image: circleci/redis:5-alpine
|
||||||
|
steps:
|
||||||
|
- *attach_workspace
|
||||||
|
- *install_system_dependencies
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:create
|
||||||
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all pre-deployment migrations
|
||||||
|
evironment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all post-deployment remaining migrations
|
||||||
|
|
||||||
test-ruby2.7:
|
test-ruby2.7:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
@@ -238,6 +275,9 @@ workflows:
|
|||||||
- test-migrations:
|
- test-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
- install-ruby2.7
|
||||||
|
- test-two-step-migrations:
|
||||||
|
requires:
|
||||||
|
- install-ruby2.7
|
||||||
- test-ruby2.7:
|
- test-ruby2.7:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
- install-ruby2.7
|
||||||
|
34
.github/workflows/build-image.yml
vendored
Normal file
34
.github/workflows/build-image.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- uses: docker/metadata-action@v3
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: tootsuite/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=semver,pattern={{ raw }}
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||||
|
cache-to: type=inline
|
29
CHANGELOG.md
29
CHANGELOG.md
@@ -3,6 +3,35 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.4.5] - 2022-01-31
|
||||||
|
### Added
|
||||||
|
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))
|
||||||
|
- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394))
|
||||||
|
- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398))
|
||||||
|
- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188))
|
||||||
|
- Fix Ruby 3.0 dependencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16723))
|
||||||
|
- Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510))
|
||||||
|
|
||||||
|
## [3.4.4] - 2021-11-26
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036))
|
||||||
|
- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010))
|
||||||
|
- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986))
|
||||||
|
- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998))
|
||||||
|
- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574))
|
||||||
|
- Fix “open” link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524))
|
||||||
|
- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516))
|
||||||
|
- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042))
|
||||||
|
- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041))
|
||||||
|
|
||||||
## [3.4.3] - 2021-11-06
|
## [3.4.3] - 2021-11-06
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@@ -54,8 +54,8 @@ RUN npm install -g yarn && \
|
|||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
RUN cd /opt/mastodon && \
|
RUN cd /opt/mastodon && \
|
||||||
bundle config set deployment 'true' && \
|
bundle config set --local deployment 'true' && \
|
||||||
bundle config set without 'development test' && \
|
bundle config set --local without 'development test' && \
|
||||||
bundle install -j"$(nproc)" && \
|
bundle install -j"$(nproc)" && \
|
||||||
yarn install --pure-lockfile
|
yarn install --pure-lockfile
|
||||||
|
|
||||||
|
@@ -545,8 +545,9 @@ GEM
|
|||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-saml (1.11.0)
|
ruby-saml (1.13.0)
|
||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.10.5)
|
||||||
|
rexml
|
||||||
ruby2_keywords (0.0.4)
|
ruby2_keywords (0.0.4)
|
||||||
rufus-scheduler (3.6.0)
|
rufus-scheduler (3.6.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
|
@@ -6,6 +6,10 @@ import { multiply } from 'color-blend';
|
|||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export default class ModalRoot extends React.PureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
@@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@@ -74,6 +74,7 @@ class Compose extends React.PureComponent {
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -114,7 +114,11 @@ class Footer extends ImmutablePureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status } = this.props;
|
const { status, onClose } = this.props;
|
||||||
|
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
router.history.push(`/statuses/${status.get('id')}`);
|
router.history.push(`/statuses/${status.get('id')}`);
|
||||||
}
|
}
|
||||||
|
@@ -83,7 +83,7 @@ const makeMapStateToProps = () => {
|
|||||||
ancestorsIds = ancestorsIds.withMutations(mutable => {
|
ancestorsIds = ancestorsIds.withMutations(mutable => {
|
||||||
let id = statusId;
|
let id = statusId;
|
||||||
|
|
||||||
while (id) {
|
while (id && !mutable.includes(id)) {
|
||||||
mutable.unshift(id);
|
mutable.unshift(id);
|
||||||
id = inReplyTos.get(id);
|
id = inReplyTos.get(id);
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ const makeMapStateToProps = () => {
|
|||||||
const ids = [statusId];
|
const ids = [statusId];
|
||||||
|
|
||||||
while (ids.length > 0) {
|
while (ids.length > 0) {
|
||||||
let id = ids.shift();
|
let id = ids.pop();
|
||||||
const replies = contextReplies.get(id);
|
const replies = contextReplies.get(id);
|
||||||
|
|
||||||
if (statusId !== id) {
|
if (statusId !== id) {
|
||||||
@@ -110,7 +110,7 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
if (replies) {
|
if (replies) {
|
||||||
replies.reverse().forEach(reply => {
|
replies.reverse().forEach(reply => {
|
||||||
ids.unshift(reply);
|
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent {
|
|||||||
onConfirm: PropTypes.func.isRequired,
|
onConfirm: PropTypes.func.isRequired,
|
||||||
secondary: PropTypes.string,
|
secondary: PropTypes.string,
|
||||||
onSecondary: PropTypes.func,
|
onSecondary: PropTypes.func,
|
||||||
|
closeWhenConfirm: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
closeWhenConfirm: true,
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.button.focus();
|
this.button.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
this.props.onClose();
|
if (this.props.closeWhenConfirm) {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
this.props.onConfirm();
|
this.props.onConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
message: intl.formatMessage(messages.logoutMessage),
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||||
|
closeWhenConfirm: false,
|
||||||
onConfirm: () => logOut(),
|
onConfirm: () => logOut(),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@@ -829,6 +829,7 @@ a.name-tag,
|
|||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
flex: 1 0 50%;
|
flex: 1 0 50%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__header__fields,
|
.account__header__fields,
|
||||||
|
@@ -46,7 +46,9 @@ class Webfinger
|
|||||||
def body_from_webfinger(url = standard_url, use_fallback = true)
|
def body_from_webfinger(url = standard_url, use_fallback = true)
|
||||||
webfinger_request(url).perform do |res|
|
webfinger_request(url).perform do |res|
|
||||||
if res.code == 200
|
if res.code == 200
|
||||||
res.body_with_limit
|
body = res.body_with_limit
|
||||||
|
raise Webfinger::Error, "Request for #{@uri} returned empty response" if body.empty?
|
||||||
|
body
|
||||||
elsif res.code == 404 && use_fallback
|
elsif res.code == 404 && use_fallback
|
||||||
body_from_host_meta
|
body_from_host_meta
|
||||||
elsif res.code == 410
|
elsif res.code == 410
|
||||||
|
@@ -60,6 +60,7 @@ class Account < ApplicationRecord
|
|||||||
|
|
||||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
||||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
|
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
|
||||||
|
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
|
||||||
|
|
||||||
include AccountAssociations
|
include AccountAssociations
|
||||||
include AccountAvatar
|
include AccountAvatar
|
||||||
@@ -379,7 +380,7 @@ class Account < ApplicationRecord
|
|||||||
def synchronization_uri_prefix
|
def synchronization_uri_prefix
|
||||||
return 'local' if local?
|
return 'local' if local?
|
||||||
|
|
||||||
@synchronization_uri_prefix ||= uri[/http(s?):\/\/[^\/]+\//]
|
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
|
||||||
end
|
end
|
||||||
|
|
||||||
class Field < ActiveModelSerializers::Model
|
class Field < ActiveModelSerializers::Model
|
||||||
|
@@ -15,7 +15,7 @@ class CanonicalEmailBlock < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :reference_account, class_name: 'Account'
|
belongs_to :reference_account, class_name: 'Account'
|
||||||
|
|
||||||
validates :canonical_email_hash, presence: true
|
validates :canonical_email_hash, presence: true, uniqueness: true
|
||||||
|
|
||||||
def email=(email)
|
def email=(email)
|
||||||
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
self.canonical_email_hash = email_to_canonical_email_hash(email)
|
||||||
|
@@ -251,10 +251,13 @@ module AccountInteractions
|
|||||||
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_followers_hash(url_prefix)
|
def remote_followers_hash(url)
|
||||||
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}") do
|
url_prefix = url[Account::URL_PREFIX_RE]
|
||||||
|
return if url_prefix.blank?
|
||||||
|
|
||||||
|
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
|
||||||
digest = "\x00" * 32
|
digest = "\x00" * 32
|
||||||
followers.where(Account.arel_table[:uri].matches(url_prefix + '%', false, true)).pluck_each(:uri) do |uri|
|
followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
|
||||||
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
||||||
end
|
end
|
||||||
digest.unpack('H*')[0]
|
digest.unpack('H*')[0]
|
||||||
|
@@ -67,8 +67,49 @@ class NotifyService < BaseService
|
|||||||
message? && @notification.target_status.direct_visibility?
|
message? && @notification.target_status.direct_visibility?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns true if the sender has been mentionned by the recipient up the thread
|
||||||
def response_to_recipient?
|
def response_to_recipient?
|
||||||
@notification.target_status.in_reply_to_account_id == @recipient.id && @notification.target_status.thread&.direct_visibility?
|
return false if @notification.target_status.in_reply_to_id.nil?
|
||||||
|
|
||||||
|
# Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
|
||||||
|
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
|
||||||
|
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender) AS (
|
||||||
|
SELECT
|
||||||
|
s.id, s.in_reply_to_id, (CASE
|
||||||
|
WHEN s.account_id = :recipient_id THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT *
|
||||||
|
FROM mentions m
|
||||||
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||||
|
)
|
||||||
|
ELSE
|
||||||
|
FALSE
|
||||||
|
END)
|
||||||
|
FROM statuses s
|
||||||
|
WHERE s.id = :id
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.in_reply_to_id,
|
||||||
|
(CASE
|
||||||
|
WHEN s.account_id = :recipient_id THEN
|
||||||
|
EXISTS (
|
||||||
|
SELECT *
|
||||||
|
FROM mentions m
|
||||||
|
WHERE m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
|
||||||
|
)
|
||||||
|
ELSE
|
||||||
|
FALSE
|
||||||
|
END)
|
||||||
|
FROM ancestors st
|
||||||
|
JOIN statuses s ON s.id = st.in_reply_to_id
|
||||||
|
WHERE st.replying_to_sender IS FALSE
|
||||||
|
)
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM ancestors st
|
||||||
|
JOIN statuses s ON s.id = st.id
|
||||||
|
WHERE st.replying_to_sender IS TRUE AND s.visibility = 3
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_staff?
|
def from_staff?
|
||||||
|
@@ -44,11 +44,7 @@ class ActivityPub::DeliveryWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def synchronization_header
|
def synchronization_header
|
||||||
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(inbox_url_prefix)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
"collectionId=\"#{account_followers_url(@source_account)}\", digest=\"#{@source_account.remote_followers_hash(@inbox_url)}\", url=\"#{account_followers_synchronization_url(@source_account)}\""
|
||||||
end
|
|
||||||
|
|
||||||
def inbox_url_prefix
|
|
||||||
@inbox_url[/http(s?):\/\/[^\/]+\//]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform_request
|
def perform_request
|
||||||
|
@@ -1,6 +1,46 @@
|
|||||||
class RemoveFauxRemoteAccountDuplicates < ActiveRecord::Migration[5.2]
|
class RemoveFauxRemoteAccountDuplicates < ActiveRecord::Migration[5.2]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class StreamEntry < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :stream_entries
|
||||||
|
end
|
||||||
|
|
||||||
|
class Status < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :statuses
|
||||||
|
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||||
|
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||||
|
end
|
||||||
|
|
||||||
|
class Favourite < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :favourites
|
||||||
|
belongs_to :status, inverse_of: :favourites
|
||||||
|
end
|
||||||
|
|
||||||
|
class Mention < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, inverse_of: :mentions
|
||||||
|
belongs_to :status
|
||||||
|
end
|
||||||
|
|
||||||
|
class Notification < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
belongs_to :account, optional: true
|
||||||
|
belongs_to :from_account, class_name: 'Account', optional: true
|
||||||
|
belongs_to :activity, polymorphic: true, optional: true
|
||||||
|
end
|
||||||
|
|
||||||
|
class Account < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
has_many :stream_entries, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :statuses, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :favourites, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :mentions, inverse_of: :account, dependent: :destroy
|
||||||
|
has_many :notifications, inverse_of: :account, dependent: :destroy
|
||||||
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
local_domain = Rails.configuration.x.local_domain
|
local_domain = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
@@ -1,4 +1,9 @@
|
|||||||
class AddInstanceActor < ActiveRecord::Migration[5.2]
|
class AddInstanceActor < ActiveRecord::Migration[5.2]
|
||||||
|
class Account < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
validates :username, uniqueness: { scope: :domain, case_sensitive: false }
|
||||||
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
||||||
end
|
end
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
class UpdatePtLocales < ActiveRecord::Migration[5.2]
|
class UpdatePtLocales < ActiveRecord::Migration[5.2]
|
||||||
|
class User < ApplicationRecord
|
||||||
|
# Dummy class, to make migration possible across version changes
|
||||||
|
end
|
||||||
|
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
@@ -20,7 +20,7 @@ FROM (
|
|||||||
HAVING count(follows.id) >= 5
|
HAVING count(follows.id) >= 5
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT accounts.id AS account_id,
|
SELECT accounts.id AS account_id,
|
||||||
sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
|
sum(status_stats.reblogs_count + status_stats.favourites_count) / (1.0 + sum(status_stats.reblogs_count + status_stats.favourites_count)) AS rank,
|
||||||
'most_interactions' AS reason
|
'most_interactions' AS reason
|
||||||
FROM status_stats
|
FROM status_stats
|
||||||
INNER JOIN statuses ON statuses.id = status_stats.status_id
|
INNER JOIN statuses ON statuses.id = status_stats.status_id
|
||||||
@@ -32,7 +32,7 @@ FROM (
|
|||||||
AND accounts.locked = 'f'
|
AND accounts.locked = 'f'
|
||||||
AND accounts.discoverable = 't'
|
AND accounts.discoverable = 't'
|
||||||
GROUP BY accounts.id
|
GROUP BY accounts.id
|
||||||
HAVING sum(reblogs_count + favourites_count) >= 5
|
HAVING sum(status_stats.reblogs_count + status_stats.favourites_count) >= 5
|
||||||
) t0
|
) t0
|
||||||
GROUP BY account_id
|
GROUP BY account_id
|
||||||
ORDER BY rank DESC
|
ORDER BY rank DESC
|
||||||
|
@@ -18,7 +18,7 @@ FROM (
|
|||||||
HAVING count(follows.id) >= 5
|
HAVING count(follows.id) >= 5
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT account_summaries.account_id AS account_id,
|
SELECT account_summaries.account_id AS account_id,
|
||||||
sum(reblogs_count + favourites_count) / (1.0 + sum(reblogs_count + favourites_count)) AS rank,
|
sum(status_stats.reblogs_count + status_stats.favourites_count) / (1.0 + sum(status_stats.reblogs_count + status_stats.favourites_count)) AS rank,
|
||||||
'most_interactions' AS reason
|
'most_interactions' AS reason
|
||||||
FROM status_stats
|
FROM status_stats
|
||||||
INNER JOIN statuses ON statuses.id = status_stats.status_id
|
INNER JOIN statuses ON statuses.id = status_stats.status_id
|
||||||
@@ -28,7 +28,7 @@ FROM (
|
|||||||
AND account_summaries.sensitive = 'f'
|
AND account_summaries.sensitive = 'f'
|
||||||
AND follow_recommendation_suppressions.id IS NULL
|
AND follow_recommendation_suppressions.id IS NULL
|
||||||
GROUP BY account_summaries.account_id
|
GROUP BY account_summaries.account_id
|
||||||
HAVING sum(reblogs_count + favourites_count) >= 5
|
HAVING sum(status_stats.reblogs_count + status_stats.favourites_count) >= 5
|
||||||
) t0
|
) t0
|
||||||
GROUP BY account_id
|
GROUP BY account_id
|
||||||
ORDER BY rank DESC
|
ORDER BY rank DESC
|
||||||
|
@@ -43,7 +43,7 @@ services:
|
|||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.4.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||||
@@ -63,7 +63,7 @@ services:
|
|||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.4.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
@@ -80,7 +80,7 @@ services:
|
|||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: tootsuite/mastodon
|
image: tootsuite/mastodon:v3.4.5
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
@@ -295,7 +295,7 @@ module Mastodon
|
|||||||
table = Arel::Table.new(table_name)
|
table = Arel::Table.new(table_name)
|
||||||
|
|
||||||
total = estimate_rows_in_table(table_name).to_i
|
total = estimate_rows_in_table(table_name).to_i
|
||||||
if total == 0
|
if total < 1
|
||||||
count_arel = table.project(Arel.star.count.as('count'))
|
count_arel = table.project(Arel.star.count.as('count'))
|
||||||
count_arel = yield table, count_arel if block_given?
|
count_arel = yield table, count_arel if block_given?
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
3
|
5
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
@@ -17,9 +17,9 @@ module Paperclip
|
|||||||
|
|
||||||
def cache_current_values
|
def cache_current_values
|
||||||
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
@original_filename = filename_from_content_disposition.presence || filename_from_path.presence || 'data'
|
||||||
@size = @target.response.content_length
|
|
||||||
@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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy_to_tempfile(source)
|
def copy_to_tempfile(source)
|
||||||
|
@@ -350,11 +350,11 @@ namespace :mastodon do
|
|||||||
end
|
end
|
||||||
end.join("\n")
|
end.join("\n")
|
||||||
|
|
||||||
generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n"
|
generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup
|
||||||
|
|
||||||
if incompatible_syntax
|
if incompatible_syntax
|
||||||
generated_header << "Some variables in this file will be interpreted differently whether you are\n"
|
generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
|
||||||
generated_header << "using docker-compose or not.\n\n"
|
generated_header << "# using docker-compose or not.\n\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
|
File.write(Rails.root.join('.env.production'), "#{generated_header}#{env_contents}\n")
|
||||||
|
181
lib/tasks/tests.rake
Normal file
181
lib/tasks/tests.rake
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :tests do
|
||||||
|
namespace :migrations do
|
||||||
|
desc 'Populate the database with test data for 2.0.0'
|
||||||
|
task populate_v2: :environment do
|
||||||
|
admin_key = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
user_key = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
remote_key = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
remote_key2 = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
remote_key3 = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
admin_private_key = ActiveRecord::Base.connection.quote(admin_key.to_pem)
|
||||||
|
admin_public_key = ActiveRecord::Base.connection.quote(admin_key.public_key.to_pem)
|
||||||
|
user_private_key = ActiveRecord::Base.connection.quote(user_key.to_pem)
|
||||||
|
user_public_key = ActiveRecord::Base.connection.quote(user_key.public_key.to_pem)
|
||||||
|
remote_public_key = ActiveRecord::Base.connection.quote(remote_key.public_key.to_pem)
|
||||||
|
remote_public_key2 = ActiveRecord::Base.connection.quote(remote_key2.public_key.to_pem)
|
||||||
|
remote_public_key_ap = ActiveRecord::Base.connection.quote(remote_key3.public_key.to_pem)
|
||||||
|
local_domain = ActiveRecord::Base.connection.quote(Rails.configuration.x.local_domain)
|
||||||
|
|
||||||
|
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||||
|
-- accounts
|
||||||
|
|
||||||
|
INSERT INTO "accounts"
|
||||||
|
(id, username, domain, private_key, public_key, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 'admin', NULL, #{admin_private_key}, #{admin_public_key}, now(), now()),
|
||||||
|
(2, 'user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
|
||||||
|
|
||||||
|
INSERT INTO "accounts"
|
||||||
|
(id, username, domain, private_key, public_key, created_at, updated_at, remote_url, salmon_url)
|
||||||
|
VALUES
|
||||||
|
(3, 'remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
|
||||||
|
'https://remote.com/@remote', 'https://remote.com/salmon/1'),
|
||||||
|
(4, 'Remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
|
||||||
|
'https://remote.com/@Remote', 'https://remote.com/salmon/1'),
|
||||||
|
(5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now(), now(),
|
||||||
|
'https://remote.com/stale/@REMOTE', 'https://remote.com/stale/salmon/1');
|
||||||
|
|
||||||
|
INSERT INTO "accounts"
|
||||||
|
(id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url)
|
||||||
|
VALUES
|
||||||
|
(6, 'bob', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(),
|
||||||
|
1, 'https://activitypub.com/users/bob/inbox', 'https://activitypub.com/users/bob/outbox', 'https://activitypub.com/users/bob/followers');
|
||||||
|
|
||||||
|
INSERT INTO "accounts"
|
||||||
|
(id, username, domain, private_key, public_key, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(7, 'user', #{local_domain}, #{user_private_key}, #{user_public_key}, now(), now()),
|
||||||
|
(8, 'pt_user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
|
||||||
|
|
||||||
|
-- users
|
||||||
|
|
||||||
|
INSERT INTO "users"
|
||||||
|
(id, account_id, email, created_at, updated_at, admin)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 'admin@localhost', now(), now(), true),
|
||||||
|
(2, 2, 'user@localhost', now(), now(), false);
|
||||||
|
|
||||||
|
INSERT INTO "users"
|
||||||
|
(id, account_id, email, created_at, updated_at, admin, locale)
|
||||||
|
VALUES
|
||||||
|
(3, 7, 'ptuser@localhost', now(), now(), false, 'pt');
|
||||||
|
|
||||||
|
-- statuses
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, text, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 'test', now(), now()),
|
||||||
|
(2, 1, '@remote@remote.com hello', now(), now()),
|
||||||
|
(3, 1, '@Remote@remote.com hello', now(), now()),
|
||||||
|
(4, 1, '@REMOTE@remote.com hello', now(), now());
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, text, created_at, updated_at, uri, local)
|
||||||
|
VALUES
|
||||||
|
(5, 1, 'activitypub status', now(), now(), 'https://localhost/users/admin/statuses/4', true);
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, text, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(6, 3, 'test', now(), now());
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, text, created_at, updated_at, in_reply_to_id, in_reply_to_account_id)
|
||||||
|
VALUES
|
||||||
|
(7, 4, '@admin hello', now(), now(), 3, 1);
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, text, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(8, 5, 'test', now(), now());
|
||||||
|
|
||||||
|
INSERT INTO "statuses"
|
||||||
|
(id, account_id, reblog_of_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(9, 1, 2, now(), now());
|
||||||
|
|
||||||
|
-- mentions (from previous statuses)
|
||||||
|
|
||||||
|
INSERT INTO "mentions"
|
||||||
|
(status_id, account_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(2, 3, now(), now()),
|
||||||
|
(3, 4, now(), now()),
|
||||||
|
(4, 5, now(), now());
|
||||||
|
|
||||||
|
-- stream entries
|
||||||
|
|
||||||
|
INSERT INTO "stream_entries"
|
||||||
|
(activity_id, account_id, activity_type, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 'status', now(), now()),
|
||||||
|
(2, 1, 'status', now(), now()),
|
||||||
|
(3, 1, 'status', now(), now()),
|
||||||
|
(4, 1, 'status', now(), now()),
|
||||||
|
(5, 1, 'status', now(), now()),
|
||||||
|
(6, 3, 'status', now(), now()),
|
||||||
|
(7, 4, 'status', now(), now()),
|
||||||
|
(8, 5, 'status', now(), now()),
|
||||||
|
(9, 1, 'status', now(), now());
|
||||||
|
|
||||||
|
|
||||||
|
-- custom emoji
|
||||||
|
|
||||||
|
INSERT INTO "custom_emojis"
|
||||||
|
(shortcode, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('test', now(), now()),
|
||||||
|
('Test', now(), now()),
|
||||||
|
('blobcat', now(), now());
|
||||||
|
|
||||||
|
INSERT INTO "custom_emojis"
|
||||||
|
(shortcode, domain, uri, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('blobcat', 'remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
|
||||||
|
('blobcat', 'Remote.org', 'https://remote.org/emoji/blobcat', now(), now()),
|
||||||
|
('Blobcat', 'remote.org', 'https://remote.org/emoji/Blobcat', now(), now());
|
||||||
|
|
||||||
|
-- favourites
|
||||||
|
|
||||||
|
INSERT INTO "favourites"
|
||||||
|
(account_id, status_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 1, now(), now()),
|
||||||
|
(1, 7, now(), now()),
|
||||||
|
(4, 1, now(), now()),
|
||||||
|
(3, 1, now(), now()),
|
||||||
|
(5, 1, now(), now());
|
||||||
|
|
||||||
|
-- pinned statuses
|
||||||
|
|
||||||
|
INSERT INTO "status_pins"
|
||||||
|
(account_id, status_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 1, now(), now()),
|
||||||
|
(3, 6, now(), now()),
|
||||||
|
(4, 7, now(), now());
|
||||||
|
|
||||||
|
-- follows
|
||||||
|
|
||||||
|
INSERT INTO "follows"
|
||||||
|
(account_id, target_account_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, 5, now(), now()),
|
||||||
|
(6, 2, now(), now()),
|
||||||
|
(5, 2, now(), now()),
|
||||||
|
(6, 1, now(), now());
|
||||||
|
|
||||||
|
-- follow requests
|
||||||
|
|
||||||
|
INSERT INTO "follow_requests"
|
||||||
|
(account_id, target_account_id, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(2, 5, now(), now()),
|
||||||
|
(5, 1, now(), now());
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@@ -5,6 +5,37 @@ RSpec.describe Account, type: :model do
|
|||||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||||
subject { Fabricate(:account) }
|
subject { Fabricate(:account) }
|
||||||
|
|
||||||
|
describe '#suspend!' do
|
||||||
|
it 'marks the account as suspended' do
|
||||||
|
subject.suspend!
|
||||||
|
expect(subject.suspended?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a deletion request' do
|
||||||
|
subject.suspend!
|
||||||
|
expect(AccountDeletionRequest.where(account: subject).exists?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the account is of a local user' do
|
||||||
|
let!(:subject) { Fabricate(:account, user: Fabricate(:user, email: 'foo+bar@domain.org')) }
|
||||||
|
|
||||||
|
it 'creates a canonical domain block' do
|
||||||
|
subject.suspend!
|
||||||
|
expect(CanonicalEmailBlock.block?(subject.user_email)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a canonical domain block already exists for that email' do
|
||||||
|
before do
|
||||||
|
Fabricate(:canonical_email_block, email: subject.user_email)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not raise an error' do
|
||||||
|
expect { subject.suspend! }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#follow!' do
|
describe '#follow!' do
|
||||||
it 'creates a follow' do
|
it 'creates a follow' do
|
||||||
follow = subject.follow!(bob)
|
follow = subject.follow!(bob)
|
||||||
|
@@ -539,46 +539,57 @@ describe AccountInteractions do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#followers_hash' do
|
describe '#remote_followers_hash' do
|
||||||
let(:me) { Fabricate(:account, username: 'Me') }
|
let(:me) { Fabricate(:account, username: 'Me') }
|
||||||
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
||||||
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
|
let(:remote_2) { Fabricate(:account, username: 'bob', domain: 'example.org', uri: 'https://example.org/users/bob') }
|
||||||
let(:remote_3) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
|
let(:remote_3) { Fabricate(:account, username: 'instance-actor', domain: 'example.org', uri: 'https://example.org') }
|
||||||
|
let(:remote_4) { Fabricate(:account, username: 'eve', domain: 'foo.org', uri: 'https://foo.org/users/eve') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
remote_1.follow!(me)
|
remote_1.follow!(me)
|
||||||
remote_2.follow!(me)
|
remote_2.follow!(me)
|
||||||
remote_3.follow!(me)
|
remote_3.follow!(me)
|
||||||
|
remote_4.follow!(me)
|
||||||
me.follow!(remote_1)
|
me.follow!(remote_1)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on a local user' do
|
it 'returns correct hash for remote domains' do
|
||||||
it 'returns correct hash for remote domains' do
|
expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
|
||||||
expect(me.remote_followers_hash('https://foo.org/')).to eq 'ccb9c18a67134cfff9d62c7f7e7eb88e6b803446c244b84265565f4eba29df0e'
|
expect(me.remote_followers_hash('https://foo.org.evil.com/')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
end
|
expect(me.remote_followers_hash('https://foo')).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
|
||||||
it 'invalidates cache as needed when removing or adding followers' do
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
|
||||||
remote_1.unfollow!(me)
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
|
|
||||||
remote_1.follow!(me)
|
|
||||||
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'on a remote user' do
|
it 'invalidates cache as needed when removing or adding followers' do
|
||||||
it 'returns correct hash for remote domains' do
|
expect(me.remote_followers_hash('https://example.org/')).to eq '20aecbe774b3d61c25094370baf370012b9271c5b172ecedb05caff8d79ef0c7'
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
remote_3.unfollow!(me)
|
||||||
end
|
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||||
|
remote_1.unfollow!(me)
|
||||||
|
expect(me.remote_followers_hash('https://example.org/')).to eq '241b00794ce9b46aa864f3220afadef128318da2659782985bac5ed5bd436bff'
|
||||||
|
remote_1.follow!(me)
|
||||||
|
expect(me.remote_followers_hash('https://example.org/')).to eq '707962e297b7bd94468a21bc8e506a1bcea607a9142cd64e27c9b106b2a5f6ec'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'invalidates cache as needed when removing or adding followers' do
|
describe '#local_followers_hash' do
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
let(:me) { Fabricate(:account, username: 'Me') }
|
||||||
me.unfollow!(remote_1)
|
let(:remote_1) { Fabricate(:account, username: 'alice', domain: 'example.org', uri: 'https://example.org/users/alice') }
|
||||||
expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
|
||||||
me.follow!(remote_1)
|
before do
|
||||||
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
me.follow!(remote_1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns correct hash for local users' do
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'invalidates cache as needed when removing or adding followers' do
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
|
me.unfollow!(remote_1)
|
||||||
|
expect(remote_1.local_followers_hash).to eq '0000000000000000000000000000000000000000000000000000000000000000'
|
||||||
|
me.follow!(remote_1)
|
||||||
|
expect(remote_1.local_followers_hash).to eq Digest::SHA256.hexdigest(ActivityPub::TagManager.instance.uri_for(me))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@@ -64,8 +64,9 @@ RSpec.describe NotifyService, type: :service do
|
|||||||
is_expected.to_not change(Notification, :count)
|
is_expected.to_not change(Notification, :count)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'if the message chain initiated by recipient, but is not direct message' do
|
context 'if the message chain is initiated by recipient, but is not direct message' do
|
||||||
let(:reply_to) { Fabricate(:status, account: recipient) }
|
let(:reply_to) { Fabricate(:status, account: recipient) }
|
||||||
|
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||||
|
|
||||||
it 'does not notify' do
|
it 'does not notify' do
|
||||||
@@ -73,8 +74,20 @@ RSpec.describe NotifyService, type: :service do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'if the message chain initiated by recipient and is direct message' do
|
context 'if the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
|
||||||
|
let(:reply_to) { Fabricate(:status, account: recipient) }
|
||||||
|
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||||
|
let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }
|
||||||
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }
|
||||||
|
|
||||||
|
it 'does not notify' do
|
||||||
|
is_expected.to_not change(Notification, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'if the message chain is initiated by the recipient with a mention to the sender' do
|
||||||
let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
|
let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
|
||||||
|
let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
|
||||||
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }
|
||||||
|
|
||||||
it 'does notify' do
|
it 'does notify' do
|
||||||
|
@@ -11,7 +11,7 @@ describe ActivityPub::DeliveryWorker do
|
|||||||
let(:payload) { 'test' }
|
let(:payload) { 'test' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/').and_return('somehash')
|
allow_any_instance_of(Account).to receive(:remote_followers_hash).with('https://example.com/api').and_return('somehash')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'perform' do
|
describe 'perform' do
|
||||||
|
Reference in New Issue
Block a user