Merge commit '23e32a4b3031d1da8b911e0145d61b4dd47c4f96' into glitch-soc/merge-upstream
Conflicts: - `Gemfile.lock`: Conflict because we had updated the `json-ld` gem to fix a yanked dependency. Kept our version of `json-ld` while updating other dependencies.th-new
commit
bc33be0342
|
@ -57,7 +57,6 @@ RSpec/AnyInstance:
|
|||
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
|
||||
- 'spec/controllers/admin/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/admin/resets_controller_spec.rb'
|
||||
- 'spec/controllers/admin/settings/branding_controller_spec.rb'
|
||||
- 'spec/controllers/auth/sessions_controller_spec.rb'
|
||||
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
|
||||
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
|
|||
gem 'simple_form', '~> 5.2'
|
||||
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 3.0.1'
|
||||
gem 'strong_migrations', '1.3.0'
|
||||
gem 'strong_migrations', '1.6.4'
|
||||
gem 'tty-prompt', '~> 0.23', require: false
|
||||
gem 'twitter-text', '~> 3.1.0'
|
||||
gem 'tzinfo-data', '~> 1.2023'
|
||||
|
@ -195,7 +195,7 @@ gem 'xorcist', '~> 1.1'
|
|||
|
||||
gem 'cocoon', '~> 1.2'
|
||||
|
||||
gem 'net-http', '~> 0.3.2'
|
||||
gem 'net-http', '~> 0.4.0'
|
||||
gem 'rubyzip', '~> 2.3'
|
||||
|
||||
gem 'hcaptcha', '~> 7.1'
|
||||
|
|
34
Gemfile.lock
34
Gemfile.lock
|
@ -130,8 +130,8 @@ GEM
|
|||
attr_required (1.0.1)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.809.0)
|
||||
aws-sdk-core (3.181.0)
|
||||
aws-partitions (1.828.0)
|
||||
aws-sdk-core (3.183.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
|
@ -139,7 +139,7 @@ GEM
|
|||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.133.0)
|
||||
aws-sdk-s3 (1.136.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
|
@ -389,10 +389,10 @@ GEM
|
|||
multi_json (~> 1.15)
|
||||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.3)
|
||||
json-ld-preloaded (3.2.2)
|
||||
json-ld (~> 3.2)
|
||||
rdf (~> 3.2)
|
||||
json-schema (4.0.0)
|
||||
json-ld-preloaded (3.3.0)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (4.1.1)
|
||||
addressable (>= 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
|
@ -454,13 +454,13 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
mutex_m (0.1.2)
|
||||
net-http (0.3.2)
|
||||
net-http (0.4.0)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
|
@ -689,13 +689,13 @@ GEM
|
|||
fugit (~> 1.1, >= 1.1.6)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sanitize (6.0.2)
|
||||
sanitize (6.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.7.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
selenium-webdriver (4.13.1)
|
||||
selenium-webdriver (4.15.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
|
@ -710,7 +710,7 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (7.1.29)
|
||||
sidekiq-unique-jobs (7.1.30)
|
||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
redis (< 5.0)
|
||||
|
@ -718,7 +718,7 @@ GEM
|
|||
thor (>= 0.20, < 3.0)
|
||||
simple-navigation (4.4.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (5.2.0)
|
||||
simple_form (5.3.0)
|
||||
actionpack (>= 5.2)
|
||||
activemodel (>= 5.2)
|
||||
simplecov (0.22.0)
|
||||
|
@ -740,7 +740,7 @@ GEM
|
|||
stoplight (3.0.2)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.0.8)
|
||||
strong_migrations (1.3.0)
|
||||
strong_migrations (1.6.4)
|
||||
activerecord (>= 5.2)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
|
@ -753,7 +753,7 @@ GEM
|
|||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
test-prof (1.2.3)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.0)
|
||||
tpm-key_attestation (0.12.0)
|
||||
|
@ -883,7 +883,7 @@ DEPENDENCIES
|
|||
md-paperclip-azure (~> 2.2)
|
||||
memory_profiler
|
||||
mime-types (~> 3.5.0)
|
||||
net-http (~> 0.3.2)
|
||||
net-http (~> 0.4.0)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
nsa!
|
||||
|
@ -941,7 +941,7 @@ DEPENDENCIES
|
|||
sprockets-rails (~> 3.4)
|
||||
stackprof
|
||||
stoplight (~> 3.0.1)
|
||||
strong_migrations (= 1.3.0)
|
||||
strong_migrations (= 1.6.4)
|
||||
test-prof
|
||||
thor (~> 1.2)
|
||||
tty-prompt (~> 0.23)
|
||||
|
|
|
@ -6,7 +6,7 @@ module Admin::AccountModerationNotesHelper
|
|||
|
||||
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
||||
safe_join([
|
||||
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
|
||||
image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'),
|
||||
content_tag(:span, account.acct, class: 'username'),
|
||||
], ' ')
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ module SettingsHelper
|
|||
return if account.nil?
|
||||
|
||||
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
|
||||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
|||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
|
|||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/static/alice.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -42,7 +42,7 @@ export const Avatar: React.FC<Props> = ({
|
|||
onMouseLeave={handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
{src && <img src={src} alt={account?.get('acct')} />}
|
||||
{src && <img src={src} alt='' />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
|
||||
class Media extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
paused: props.paused || false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMediaClick = () => {
|
||||
const { onClick } = this.props;
|
||||
|
||||
this.setState(prevState => ({
|
||||
paused: !prevState.paused,
|
||||
}));
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
onClick();
|
||||
}
|
||||
|
||||
const { title } = this.props;
|
||||
const mediaElements = document.querySelectorAll(`div[title="${title}"]`);
|
||||
|
||||
setTimeout(() => {
|
||||
mediaElements.forEach(element => {
|
||||
if (element !== this && !element.classList.contains('paused')) {
|
||||
element.click();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title } = this.props;
|
||||
const { paused } = this.state;
|
||||
|
||||
return (
|
||||
<button title={title} onClick={this.handleMediaClick}>
|
||||
Media Component - {paused ? 'Paused' : 'Playing'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Media.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
paused: PropTypes.bool,
|
||||
};
|
||||
|
||||
describe('Media attachments test', () => {
|
||||
let currentMedia = null;
|
||||
const togglePlayMock = jest.fn();
|
||||
|
||||
it('plays a new media file and pauses others that were playing', () => {
|
||||
const container = render(
|
||||
<div>
|
||||
<Media title='firstMedia' paused onClick={togglePlayMock} />
|
||||
<Media title='secondMedia' paused onClick={togglePlayMock} />
|
||||
</div>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.getByTitle('firstMedia'));
|
||||
expect(togglePlayMock).toHaveBeenCalledTimes(1);
|
||||
currentMedia = container.getByTitle('firstMedia');
|
||||
expect(currentMedia.textContent).toMatch(/Playing/);
|
||||
|
||||
fireEvent.click(container.getByTitle('secondMedia'));
|
||||
expect(togglePlayMock).toHaveBeenCalledTimes(2);
|
||||
currentMedia = container.getByTitle('secondMedia');
|
||||
expect(currentMedia.textContent).toMatch(/Playing/);
|
||||
});
|
||||
});
|
|
@ -178,7 +178,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
modalType: 'IMAGE',
|
||||
modalProps: {
|
||||
src: account.get('avatar'),
|
||||
alt: account.get('acct'),
|
||||
alt: '',
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@ import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/featur
|
|||
|
||||
import { Blurhash } from '../../components/blurhash';
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
|
||||
|
||||
import Visualizer from './visualizer';
|
||||
|
||||
|
@ -165,15 +166,32 @@ class Audio extends PureComponent {
|
|||
}
|
||||
|
||||
togglePlay = () => {
|
||||
if (!this.audioContext) {
|
||||
this._initAudioContext();
|
||||
const audios = document.querySelectorAll('audio');
|
||||
|
||||
audios.forEach((audio) => {
|
||||
const button = audio.previousElementSibling;
|
||||
button.addEventListener('click', () => {
|
||||
if(audio.paused) {
|
||||
audios.forEach((e) => {
|
||||
if (e !== audio) {
|
||||
e.pause();
|
||||
}
|
||||
});
|
||||
audio.play();
|
||||
this.setState({ paused: false });
|
||||
} else {
|
||||
audio.pause();
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (currentMedia !== null) {
|
||||
currentMedia.pause();
|
||||
}
|
||||
|
||||
if (this.state.paused) {
|
||||
this.setState({ paused: false }, () => this.audio.play());
|
||||
} else {
|
||||
this.setState({ paused: true }, () => this.audio.pause());
|
||||
}
|
||||
this.audio.play();
|
||||
setCurrentMedia(this.audio);
|
||||
};
|
||||
|
||||
handleResize = debounce(() => {
|
||||
|
@ -195,6 +213,7 @@ class Audio extends PureComponent {
|
|||
};
|
||||
|
||||
handlePause = () => {
|
||||
this.audio.pause();
|
||||
this.setState({ paused: true });
|
||||
|
||||
if (this.audioContext) {
|
||||
|
|
|
@ -19,8 +19,10 @@ import { throttle } from 'lodash';
|
|||
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { playerSettings } from 'mastodon/settings';
|
||||
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -180,6 +182,7 @@ class Video extends PureComponent {
|
|||
};
|
||||
|
||||
handlePause = () => {
|
||||
this.video.pause();
|
||||
this.setState({ paused: true });
|
||||
};
|
||||
|
||||
|
@ -226,8 +229,8 @@ class Video extends PureComponent {
|
|||
|
||||
if(!isNaN(x)) {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
this.video.volume = x;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState(x);
|
||||
this._saveVolumeState(x);
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
|
@ -343,11 +346,32 @@ class Video extends PureComponent {
|
|||
};
|
||||
|
||||
togglePlay = () => {
|
||||
if (this.state.paused) {
|
||||
this.setState({ paused: false }, () => this.video.play());
|
||||
} else {
|
||||
this.setState({ paused: true }, () => this.video.pause());
|
||||
const videos = document.querySelectorAll('video');
|
||||
|
||||
videos.forEach((video) => {
|
||||
const button = video.nextElementSibling;
|
||||
button.addEventListener('click', () => {
|
||||
if (video.paused) {
|
||||
videos.forEach((e) => {
|
||||
if (e !== video) {
|
||||
e.pause();
|
||||
}
|
||||
});
|
||||
video.play();
|
||||
this.setState({ paused: false });
|
||||
} else {
|
||||
video.pause();
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (currentMedia !== null) {
|
||||
currentMedia.pause();
|
||||
}
|
||||
|
||||
this.video.play();
|
||||
setCurrentMedia(this.video);
|
||||
};
|
||||
|
||||
toggleFullscreen = () => {
|
||||
|
@ -365,6 +389,8 @@ class Video extends PureComponent {
|
|||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
|
||||
this._syncVideoFromLocalStorage();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -437,8 +463,24 @@ class Video extends PureComponent {
|
|||
const muted = !(this.video.muted || this.state.volume === 0);
|
||||
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
this.video.volume = this.state.volume;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState();
|
||||
this._saveVolumeState();
|
||||
});
|
||||
};
|
||||
|
||||
_syncVideoToVolumeState = (volume = null, muted = null) => {
|
||||
this.video.volume = volume ?? this.state.volume;
|
||||
this.video.muted = muted ?? this.state.muted;
|
||||
};
|
||||
|
||||
_saveVolumeState = (volume = null, muted = null) => {
|
||||
playerSettings.set('volume', volume ?? this.state.volume);
|
||||
playerSettings.set('muted', muted ?? this.state.muted);
|
||||
};
|
||||
|
||||
_syncVideoFromLocalStorage = () => {
|
||||
this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
|
||||
this._syncVideoToVolumeState();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -480,6 +522,7 @@ class Video extends PureComponent {
|
|||
|
||||
handleVolumeChange = () => {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
this._saveVolumeState(this.video.volume, this.video.muted);
|
||||
};
|
||||
|
||||
handleOpenVideo = () => {
|
||||
|
|
|
@ -2,6 +2,13 @@ import { Map as ImmutableMap } from 'immutable';
|
|||
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
|
||||
export let currentMedia = null;
|
||||
|
||||
export function setCurrentMedia(value) {
|
||||
currentMedia = value;
|
||||
}
|
||||
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
accept_content_types: [],
|
||||
});
|
||||
|
|
|
@ -47,3 +47,4 @@ export const pushNotificationsSetting = new Settings('mastodon_push_notification
|
|||
export const tagHistory = new Settings('mastodon_tag_history');
|
||||
export const bannerSettings = new Settings('mastodon_banner_settings');
|
||||
export const searchHistory = new Settings('mastodon_search_history');
|
||||
export const playerSettings = new Settings('mastodon_player');
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
.detailed-status__meta
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'name-tag', target: '_blank', rel: 'noopener noreferrer' do
|
||||
= image_tag(status.account.avatar.url, width: 15, height: 15, alt: display_name(status.account), class: 'avatar')
|
||||
= image_tag(status.account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar')
|
||||
.username= status.account.acct
|
||||
·
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
|
|
|
@ -86,115 +86,6 @@ module Mastodon
|
|||
# config.time_zone = "Central Time (US & Canada)"
|
||||
# config.eager_load_paths << Rails.root.join("extras")
|
||||
|
||||
# All translations from config/locales/*.rb,yml are auto loaded.
|
||||
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
||||
config.i18n.available_locales = [
|
||||
:af,
|
||||
:an,
|
||||
:ar,
|
||||
:ast,
|
||||
:be,
|
||||
:bg,
|
||||
:bn,
|
||||
:br,
|
||||
:bs,
|
||||
:ca,
|
||||
:ckb,
|
||||
:co,
|
||||
:cs,
|
||||
:cy,
|
||||
:da,
|
||||
:de,
|
||||
:el,
|
||||
:en,
|
||||
:'en-GB',
|
||||
:eo,
|
||||
:es,
|
||||
:'es-AR',
|
||||
:'es-MX',
|
||||
:et,
|
||||
:eu,
|
||||
:fa,
|
||||
:fi,
|
||||
:fo,
|
||||
:fr,
|
||||
:'fr-QC',
|
||||
:fy,
|
||||
:ga,
|
||||
:gd,
|
||||
:gl,
|
||||
:he,
|
||||
:hi,
|
||||
:hr,
|
||||
:hu,
|
||||
:hy,
|
||||
:id,
|
||||
:ig,
|
||||
:io,
|
||||
:is,
|
||||
:it,
|
||||
:ja,
|
||||
:ka,
|
||||
:kab,
|
||||
:kk,
|
||||
:kn,
|
||||
:ko,
|
||||
:ku,
|
||||
:kw,
|
||||
:la,
|
||||
:lt,
|
||||
:lv,
|
||||
:mk,
|
||||
:ml,
|
||||
:mr,
|
||||
:ms,
|
||||
:my,
|
||||
:nl,
|
||||
:nn,
|
||||
:no,
|
||||
:oc,
|
||||
:pa,
|
||||
:pl,
|
||||
:'pt-BR',
|
||||
:'pt-PT',
|
||||
:ro,
|
||||
:ru,
|
||||
:sa,
|
||||
:sc,
|
||||
:sco,
|
||||
:si,
|
||||
:sk,
|
||||
:sl,
|
||||
:sq,
|
||||
:sr,
|
||||
:'sr-Latn',
|
||||
:sv,
|
||||
:szl,
|
||||
:ta,
|
||||
:te,
|
||||
:th,
|
||||
:tr,
|
||||
:tt,
|
||||
:ug,
|
||||
:uk,
|
||||
:ur,
|
||||
:vi,
|
||||
:zgh,
|
||||
:'zh-CN',
|
||||
:'zh-HK',
|
||||
:'zh-TW',
|
||||
]
|
||||
|
||||
config.i18n.default_locale = begin
|
||||
custom_default_locale = ENV['DEFAULT_LOCALE']&.to_sym
|
||||
|
||||
if config.i18n.available_locales.include?(custom_default_locale)
|
||||
custom_default_locale
|
||||
else
|
||||
:en
|
||||
end
|
||||
end
|
||||
|
||||
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||
# config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.configure do
|
||||
config.i18n.available_locales = [
|
||||
:af,
|
||||
:an,
|
||||
:ar,
|
||||
:ast,
|
||||
:be,
|
||||
:bg,
|
||||
:bn,
|
||||
:br,
|
||||
:bs,
|
||||
:ca,
|
||||
:ckb,
|
||||
:co,
|
||||
:cs,
|
||||
:cy,
|
||||
:da,
|
||||
:de,
|
||||
:el,
|
||||
:en,
|
||||
:'en-GB',
|
||||
:eo,
|
||||
:es,
|
||||
:'es-AR',
|
||||
:'es-MX',
|
||||
:et,
|
||||
:eu,
|
||||
:fa,
|
||||
:fi,
|
||||
:fo,
|
||||
:fr,
|
||||
:'fr-QC',
|
||||
:fy,
|
||||
:ga,
|
||||
:gd,
|
||||
:gl,
|
||||
:he,
|
||||
:hi,
|
||||
:hr,
|
||||
:hu,
|
||||
:hy,
|
||||
:id,
|
||||
:ig,
|
||||
:io,
|
||||
:is,
|
||||
:it,
|
||||
:ja,
|
||||
:ka,
|
||||
:kab,
|
||||
:kk,
|
||||
:kn,
|
||||
:ko,
|
||||
:ku,
|
||||
:kw,
|
||||
:la,
|
||||
:lt,
|
||||
:lv,
|
||||
:mk,
|
||||
:ml,
|
||||
:mr,
|
||||
:ms,
|
||||
:my,
|
||||
:nl,
|
||||
:nn,
|
||||
:no,
|
||||
:oc,
|
||||
:pa,
|
||||
:pl,
|
||||
:'pt-BR',
|
||||
:'pt-PT',
|
||||
:ro,
|
||||
:ru,
|
||||
:sa,
|
||||
:sc,
|
||||
:sco,
|
||||
:si,
|
||||
:sk,
|
||||
:sl,
|
||||
:sq,
|
||||
:sr,
|
||||
:'sr-Latn',
|
||||
:sv,
|
||||
:szl,
|
||||
:ta,
|
||||
:te,
|
||||
:th,
|
||||
:tr,
|
||||
:tt,
|
||||
:ug,
|
||||
:uk,
|
||||
:ur,
|
||||
:vi,
|
||||
:zgh,
|
||||
:'zh-CN',
|
||||
:'zh-HK',
|
||||
:'zh-TW',
|
||||
]
|
||||
|
||||
config.i18n.default_locale = begin
|
||||
custom_default_locale = ENV['DEFAULT_LOCALE']&.to_sym
|
||||
|
||||
if Rails.configuration.i18n.available_locales.include?(custom_default_locale)
|
||||
custom_default_locale
|
||||
else
|
||||
:en
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,18 @@
|
|||
require 'sidekiq_unique_jobs/web'
|
||||
require 'sidekiq-scheduler/web'
|
||||
|
||||
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
|
||||
def build_response(req)
|
||||
super.tap do |response|
|
||||
response.headers['Vary'] = 'Origin, Accept'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_with_vary(path)
|
||||
RedirectWithVary.new(301, path)
|
||||
end
|
||||
|
||||
Rails.application.routes.draw do
|
||||
# Paths of routes on the web app that to not require to be indexed or
|
||||
# have alternative format representations requiring separate controllers
|
||||
|
@ -91,10 +103,13 @@ Rails.application.routes.draw do
|
|||
confirmations: 'auth/confirmations',
|
||||
}
|
||||
|
||||
get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/following', to: redirect('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/followers', to: redirect('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/statuses/:id', to: redirect('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
# rubocop:disable Style/FormatStringToken - those do not go through the usual formatting functions and are not safe to correct
|
||||
get '/users/:username', to: redirect_with_vary('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/following', to: redirect_with_vary('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/followers', to: redirect_with_vary('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
get '/users/:username/statuses/:id', to: redirect_with_vary('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
|
||||
# rubocop:enable Style/FormatStringToken
|
||||
|
||||
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
|
||||
|
||||
resources :accounts, path: 'users', only: [:show], param: :username do
|
||||
|
|
|
@ -136,24 +136,24 @@ module Mastodon::CLI
|
|||
Mastodon has to be stopped to run this task, which will take a long time and may be destructive.
|
||||
LONG_DESC
|
||||
def fix_duplicates
|
||||
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
|
||||
say 'Your version of the database schema is too old and is not supported by this script.', :red
|
||||
say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
|
||||
exit(1)
|
||||
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
|
||||
say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
|
||||
exit(1) unless yes?('Continue anyway? (Yes/No)')
|
||||
end
|
||||
verify_system_ready!
|
||||
|
||||
if Sidekiq::ProcessSet.new.any?
|
||||
say 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.', :red
|
||||
exit(1)
|
||||
end
|
||||
process_deduplications
|
||||
|
||||
say 'This task will take a long time to run and is potentially destructive.', :yellow
|
||||
say 'Please make sure to stop Mastodon and have a backup.', :yellow
|
||||
exit(1) unless yes?('Continue? (Yes/No)')
|
||||
deduplication_cleanup_tasks
|
||||
|
||||
say 'Finished!'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_system_ready!
|
||||
verify_schema_version!
|
||||
verify_sidekiq_not_active!
|
||||
verify_backup_warning!
|
||||
end
|
||||
|
||||
def process_deduplications
|
||||
deduplicate_users!
|
||||
deduplicate_account_domain_blocks!
|
||||
deduplicate_account_identity_proofs!
|
||||
|
@ -173,14 +173,44 @@ module Mastodon::CLI
|
|||
deduplicate_webauthn_credentials!
|
||||
deduplicate_webhooks!
|
||||
deduplicate_software_updates!
|
||||
|
||||
Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false) if ActiveRecord::Migrator.current_version >= 2020_12_06_004238
|
||||
Rails.cache.clear
|
||||
|
||||
say 'Finished!'
|
||||
end
|
||||
|
||||
private
|
||||
def deduplication_cleanup_tasks
|
||||
refresh_instances_view if schema_has_instances_view?
|
||||
Rails.cache.clear
|
||||
end
|
||||
|
||||
def refresh_instances_view
|
||||
Scenic.database.refresh_materialized_view('instances', concurrently: true, cascade: false)
|
||||
end
|
||||
|
||||
def schema_has_instances_view?
|
||||
ActiveRecord::Migrator.current_version >= 2020_12_06_004238
|
||||
end
|
||||
|
||||
def verify_schema_version!
|
||||
if ActiveRecord::Migrator.current_version < MIN_SUPPORTED_VERSION
|
||||
say 'Your version of the database schema is too old and is not supported by this script.', :red
|
||||
say 'Please update to at least Mastodon 3.0.0 before running this script.', :red
|
||||
exit(1)
|
||||
elsif ActiveRecord::Migrator.current_version > MAX_SUPPORTED_VERSION
|
||||
say 'Your version of the database schema is more recent than this script, this may cause unexpected errors.', :yellow
|
||||
exit(1) unless yes?('Continue anyway? (Yes/No)')
|
||||
end
|
||||
end
|
||||
|
||||
def verify_sidekiq_not_active!
|
||||
if Sidekiq::ProcessSet.new.any?
|
||||
say 'It seems Sidekiq is running. All Mastodon processes need to be stopped when using this script.', :red
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_backup_warning!
|
||||
say 'This task will take a long time to run and is potentially destructive.', :yellow
|
||||
say 'Please make sure to stop Mastodon and have a backup.', :yellow
|
||||
exit(1) unless yes?('Continue? (Yes/No)')
|
||||
end
|
||||
|
||||
def deduplicate_accounts!
|
||||
remove_index_if_exists!(:accounts, 'index_accounts_on_username_and_domain_lower')
|
||||
|
|
|
@ -19,10 +19,6 @@ RSpec.describe Admin::Settings::BrandingController do
|
|||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
before do
|
||||
allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true)
|
||||
end
|
||||
|
||||
around do |example|
|
||||
before = Setting.site_short_description
|
||||
Setting.site_short_description = nil
|
||||
|
|
|
@ -7,7 +7,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
describe 'GET #new' do
|
||||
it 'returns http success' do
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :new
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
before do
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
||||
|
@ -37,7 +37,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
before do
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
before do
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
sign_in(user, scope: :user)
|
||||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
@ -66,7 +66,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
before do
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
user.approved = false
|
||||
user.save!
|
||||
sign_in(user, scope: :user)
|
||||
|
@ -83,7 +83,7 @@ describe Auth::ConfirmationsController do
|
|||
|
||||
before do
|
||||
allow(BootstrapTimelineWorker).to receive(:perform_async)
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :show, params: { confirmation_token: 'foobar' }
|
||||
end
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ describe Auth::PasswordsController do
|
|||
|
||||
describe 'GET #new' do
|
||||
it 'returns http success' do
|
||||
@request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
request.env['devise.mapping'] = Devise.mappings[:user]
|
||||
get :new
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
|
|
@ -378,7 +378,7 @@ RSpec.describe Auth::SessionsController do
|
|||
|
||||
context 'when using a valid webauthn credential' do
|
||||
before do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { user: { credential: fake_credential } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
|
|
@ -8,13 +8,11 @@ describe EmojisController do
|
|||
let(:emoji) { Fabricate(:custom_emoji) }
|
||||
|
||||
describe 'GET #show' do
|
||||
subject(:body) { JSON.parse(response.body, symbolize_names: true) }
|
||||
|
||||
let(:response) { get :show, params: { id: emoji.id, format: :json } }
|
||||
|
||||
it 'returns the right response' do
|
||||
expect(response).to have_http_status 200
|
||||
expect(body[:name]).to eq ':coolcat:'
|
||||
expect(body_as_json[:name]).to eq ':coolcat:'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe HomeController do
|
|||
|
||||
context 'when not signed in' do
|
||||
it 'returns http success' do
|
||||
@request.path = '/'
|
||||
request.path = '/'
|
||||
expect(subject).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -130,7 +130,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(@controller.session[:webauthn_challenge]).to be_present
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'does not change webauthn_id' do
|
||||
|
@ -155,7 +155,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(@controller.session[:webauthn_challenge]).to be_present
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'sets user webauthn_id' do
|
||||
|
@ -218,7 +218,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
|
||||
context 'when creation succeeds' do
|
||||
it 'returns http success' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
|
@ -226,7 +226,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
end
|
||||
|
||||
it 'adds a new credential to user credentials' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
@ -234,7 +234,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
end
|
||||
|
||||
it 'does not change webauthn_id' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
@ -244,7 +244,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
|
||||
context 'when the nickname is already used' do
|
||||
it 'fails' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
||||
|
||||
|
@ -264,7 +264,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
end
|
||||
|
||||
it 'fails' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
|
@ -277,7 +277,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
context 'when user have not enabled webauthn' do
|
||||
context 'when creation succeeds' do
|
||||
it 'creates a webauthn credential' do
|
||||
@controller.session[:webauthn_challenge] = challenge
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountStatusesFilter do
|
||||
subject { described_class.new(account, current_account, params) }
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:current_account) { nil }
|
||||
let(:params) { {} }
|
||||
|
@ -38,6 +36,8 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
describe '#results' do
|
||||
subject { described_class.new(account, current_account, params).results }
|
||||
|
||||
let(:tag) { Fabricate(:tag) }
|
||||
|
||||
before do
|
||||
|
@ -56,7 +56,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
let(:params) { { only_media: true } }
|
||||
|
||||
it 'returns only statuses with media' do
|
||||
expect(subject.results.all?(&:with_media?)).to be true
|
||||
expect(subject.all?(&:with_media?)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -64,7 +64,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
let(:params) { { tagged: tag.name } }
|
||||
|
||||
it 'returns only statuses with tag' do
|
||||
expect(subject.results.all? { |s| s.tags.include?(tag) }).to be true
|
||||
expect(subject.all? { |s| s.tags.include?(tag) }).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,7 +72,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
let(:params) { { exclude_replies: true } }
|
||||
|
||||
it 'returns only statuses that are not replies' do
|
||||
expect(subject.results.none?(&:reply?)).to be true
|
||||
expect(subject.none?(&:reply?)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -80,7 +80,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
let(:params) { { exclude_reblogs: true } }
|
||||
|
||||
it 'returns only statuses that are not reblogs' do
|
||||
expect(subject.results.none?(&:reblog?)).to be true
|
||||
expect(subject.none?(&:reblog?)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -89,16 +89,12 @@ RSpec.describe AccountStatusesFilter do
|
|||
let(:current_account) { nil }
|
||||
let(:direct_status) { nil }
|
||||
|
||||
it 'returns only public statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
|
||||
end
|
||||
it 'returns only public statuses, public replies, and public reblogs' do
|
||||
expect(results_unique_visibilities).to match_array %w(unlisted public)
|
||||
|
||||
it 'returns public replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
expect(results_in_reply_to_ids).to_not be_empty
|
||||
|
||||
it 'returns public reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
expect(results_reblog_of_ids).to_not be_empty
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
|
@ -112,23 +108,19 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'returns nothing' do
|
||||
expect(subject.results.to_a).to be_empty
|
||||
expect(subject.to_a).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessed by self' do
|
||||
let(:current_account) { account }
|
||||
|
||||
it 'returns everything' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(direct private unlisted public)
|
||||
end
|
||||
it 'returns all statuses, replies, and reblogs' do
|
||||
expect(results_unique_visibilities).to match_array %w(direct private unlisted public)
|
||||
|
||||
it 'returns replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
expect(results_in_reply_to_ids).to_not be_empty
|
||||
|
||||
it 'returns reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
expect(results_reblog_of_ids).to_not be_empty
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
|
@ -141,23 +133,19 @@ RSpec.describe AccountStatusesFilter do
|
|||
current_account.follow!(account)
|
||||
end
|
||||
|
||||
it 'returns private statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(private unlisted public)
|
||||
end
|
||||
it 'returns private statuses, replies, and reblogs' do
|
||||
expect(results_unique_visibilities).to match_array %w(private unlisted public)
|
||||
|
||||
it 'returns replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
expect(results_in_reply_to_ids).to_not be_empty
|
||||
|
||||
it 'returns reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).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(subject.results.pluck(:id)).to include(direct_status.id)
|
||||
expect(results_ids).to include(direct_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -167,23 +155,19 @@ RSpec.describe AccountStatusesFilter do
|
|||
context 'when accessed by a non-follower' do
|
||||
let(:current_account) { Fabricate(:account) }
|
||||
|
||||
it 'returns only public statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
|
||||
end
|
||||
it 'returns only public statuses, replies, and reblogs' do
|
||||
expect(results_unique_visibilities).to match_array %w(unlisted public)
|
||||
|
||||
it 'returns public replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
expect(results_in_reply_to_ids).to_not be_empty
|
||||
|
||||
it 'returns public reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
expect(results_reblog_of_ids).to_not be_empty
|
||||
end
|
||||
|
||||
context 'when there is a private status mentioning the non-follower' do
|
||||
let!(:private_status) { status_with_mention!(:private, current_account) }
|
||||
|
||||
it 'returns the private status' do
|
||||
expect(subject.results.pluck(:id)).to include(private_status.id)
|
||||
expect(results_ids).to include(private_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -195,7 +179,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'does not return reblog of blocked account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
expect(results_ids).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -209,7 +193,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'does not return reblog of blocked domain' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
expect(results_ids).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -223,7 +207,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'returns the reblog from the non-blocked domain' do
|
||||
expect(subject.results.pluck(:id)).to include(reblog.id)
|
||||
expect(results_ids).to include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -235,7 +219,7 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'does not return reblog of muted account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
expect(results_ids).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -247,11 +231,29 @@ RSpec.describe AccountStatusesFilter do
|
|||
end
|
||||
|
||||
it 'does not return reblog of blocked-by account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
expect(results_ids).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def results_unique_visibilities
|
||||
subject.pluck(:visibility).uniq
|
||||
end
|
||||
|
||||
def results_in_reply_to_ids
|
||||
subject.pluck(:in_reply_to_id)
|
||||
end
|
||||
|
||||
def results_reblog_of_ids
|
||||
subject.pluck(:reblog_of_id)
|
||||
end
|
||||
|
||||
def results_ids
|
||||
subject.pluck(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,51 @@ require 'rails_helper'
|
|||
require 'mastodon/cli/maintenance'
|
||||
|
||||
describe Mastodon::CLI::Maintenance do
|
||||
let(:cli) { described_class.new }
|
||||
|
||||
describe '.exit_on_failure?' do
|
||||
it 'returns true' do
|
||||
expect(described_class.exit_on_failure?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fix_duplicates' do
|
||||
context 'when the database version is too old' do
|
||||
before do
|
||||
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2000_01_01_000000) # Earlier than minimum
|
||||
end
|
||||
|
||||
it 'Exits with error message' do
|
||||
expect { cli.invoke :fix_duplicates }.to output(
|
||||
a_string_including('is too old')
|
||||
).to_stdout.and raise_error(SystemExit)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the database version is too new and the user does not continue' do
|
||||
before do
|
||||
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2100_01_01_000000) # Later than maximum
|
||||
allow(cli.shell).to receive(:yes?).with('Continue anyway? (Yes/No)').and_return(false).once
|
||||
end
|
||||
|
||||
it 'Exits with error message' do
|
||||
expect { cli.invoke :fix_duplicates }.to output(
|
||||
a_string_including('more recent')
|
||||
).to_stdout.and raise_error(SystemExit)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Sidekiq is running' do
|
||||
before do
|
||||
allow(ActiveRecord::Migrator).to receive(:current_version).and_return(2022_01_01_000000) # Higher than minimum, lower than maximum
|
||||
allow(Sidekiq::ProcessSet).to receive(:new).and_return [:process]
|
||||
end
|
||||
|
||||
it 'Exits with error message' do
|
||||
expect { cli.invoke :fix_duplicates }.to output(
|
||||
a_string_including('Sidekiq is running')
|
||||
).to_stdout.and raise_error(SystemExit)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -124,7 +124,7 @@ describe 'Caching behavior' do
|
|||
expect(response.cookies).to be_empty
|
||||
end
|
||||
|
||||
it 'sets public cache control' do
|
||||
it 'sets public cache control', :aggregate_failures do
|
||||
# expect(response.cache_control[:max_age]&.to_i).to be_positive
|
||||
expect(response.cache_control[:public]).to be_truthy
|
||||
expect(response.cache_control[:private]).to be_falsy
|
||||
|
@ -141,11 +141,8 @@ describe 'Caching behavior' do
|
|||
end
|
||||
|
||||
shared_examples 'non-cacheable error' do
|
||||
it 'does not return HTTP success' do
|
||||
it 'does not return HTTP success and does not have cache headers', :aggregate_failures do
|
||||
expect(response).to_not have_http_status(200)
|
||||
end
|
||||
|
||||
it 'does not have cache headers' do
|
||||
expect(response.cache_control[:public]).to be_falsy
|
||||
end
|
||||
end
|
||||
|
@ -171,17 +168,24 @@ describe 'Caching behavior' do
|
|||
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) }
|
||||
|
||||
before do
|
||||
# rubocop:disable Style/NumericLiterals
|
||||
status = Fabricate(:status, account: alice, id: 110224538612341312)
|
||||
Fabricate(:status, account: alice, id: 110224538643211312, visibility: :private)
|
||||
status = Fabricate(:status, account: alice, id: '110224538612341312')
|
||||
Fabricate(:status, account: alice, id: '110224538643211312', visibility: :private)
|
||||
Fabricate(:invite, code: 'abcdef')
|
||||
Fabricate(:poll, status: status, account: alice, id: 12345)
|
||||
# rubocop:enable Style/NumericLiterals
|
||||
Fabricate(:poll, status: status, account: alice, id: '12345')
|
||||
|
||||
user.account.follow!(alice)
|
||||
end
|
||||
|
||||
context 'when anonymously accessed' do
|
||||
describe '/users/alice' do
|
||||
it 'redirects with proper cache header', :aggregate_failures do
|
||||
get '/users/alice'
|
||||
|
||||
expect(response).to redirect_to('/@alice')
|
||||
expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept')
|
||||
end
|
||||
end
|
||||
|
||||
TestEndpoints::ALWAYS_CACHED.each do |endpoint|
|
||||
describe endpoint do
|
||||
before { get endpoint }
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::DeviceSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Fabricate(:device) }
|
||||
|
||||
describe 'type' do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::NoteSerializer do
|
||||
subject { JSON.parse(@serialization.to_json) }
|
||||
subject { serialized_record_json(parent, described_class, adapter: ActivityPub::Adapter) }
|
||||
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:other) { Fabricate(:account) }
|
||||
|
@ -14,10 +14,6 @@ describe ActivityPub::NoteSerializer do
|
|||
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
|
||||
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
|
||||
|
||||
before do
|
||||
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
'@context' => include('https://www.w3.org/ns/activitystreams'),
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::OneTimeKeySerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Fabricate(:one_time_key) }
|
||||
|
||||
describe 'type' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::UndoLikeSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Fabricate(:favourite) }
|
||||
|
||||
describe 'type' do
|
||||
|
|
|
@ -3,16 +3,12 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::UpdatePollSerializer do
|
||||
subject { JSON.parse(@serialization.to_json) }
|
||||
subject { serialized_record_json(status, described_class, adapter: ActivityPub::Adapter) }
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:poll) { Fabricate(:poll, account: account) }
|
||||
let!(:status) { Fabricate(:status, account: account, poll: poll) }
|
||||
|
||||
before do
|
||||
@serialization = ActiveModelSerializers::SerializableResource.new(status, serializer: described_class, adapter: ActivityPub::Adapter)
|
||||
end
|
||||
|
||||
it 'has a Update type' do
|
||||
expect(subject['type']).to eql('Update')
|
||||
end
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::VoteSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Fabricate(:poll_vote) }
|
||||
|
||||
describe 'type' do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::AccountSerializer do
|
||||
subject { JSON.parse(ActiveModelSerializers::SerializableResource.new(account, serializer: described_class).to_json) }
|
||||
subject { serialized_record_json(account, described_class) }
|
||||
|
||||
let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) }
|
||||
let(:user) { Fabricate(:user, role: role) }
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::EncryptedMessageSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Fabricate(:encrypted_message) }
|
||||
|
||||
describe 'account' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::InstanceSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { InstancePresenter.new }
|
||||
|
||||
describe 'usage' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::Keys::ClaimResultSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Keys::ClaimService::Result.new(Account.new(id: 123), 456) }
|
||||
|
||||
describe 'account' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::Keys::DeviceSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Device.new(name: 'Device name') }
|
||||
|
||||
describe 'name' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::Keys::QueryResultSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) { Keys::QueryService::Result.new(Account.new(id: 123), []) }
|
||||
|
||||
describe 'account' do
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe REST::SuggestionSerializer do
|
||||
let(:serialization) do
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record, serializer: described_class
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
let(:serialization) { serialized_record_json(record, described_class) }
|
||||
let(:record) do
|
||||
AccountSuggestions::Suggestion.new(
|
||||
account: account,
|
||||
|
|
|
@ -52,6 +52,17 @@ def json_str_to_hash(str)
|
|||
JSON.parse(str, symbolize_names: true)
|
||||
end
|
||||
|
||||
def serialized_record_json(record, serializer, adapter: nil)
|
||||
options = { serializer: serializer }
|
||||
options[:adapter] = adapter if adapter.present?
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource.new(
|
||||
record,
|
||||
options
|
||||
).to_json
|
||||
)
|
||||
end
|
||||
|
||||
def expect_push_bulk_to_match(klass, matcher)
|
||||
allow(Sidekiq::Client).to receive(:push_bulk)
|
||||
yield
|
||||
|
@ -60,122 +71,3 @@ def expect_push_bulk_to_match(klass, matcher)
|
|||
'args' => matcher,
|
||||
}))
|
||||
end
|
||||
|
||||
class StreamingServerManager
|
||||
@running_thread = nil
|
||||
|
||||
def initialize
|
||||
at_exit { stop }
|
||||
end
|
||||
|
||||
def start(port: 4020)
|
||||
return if @running_thread
|
||||
|
||||
queue = Queue.new
|
||||
|
||||
@queue = queue
|
||||
|
||||
@running_thread = Thread.new do
|
||||
Open3.popen2e(
|
||||
{
|
||||
'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
|
||||
'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
|
||||
'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
|
||||
'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
|
||||
'PORT' => port.to_s,
|
||||
},
|
||||
'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
|
||||
chdir: Rails.root.join('streaming')
|
||||
) do |_stdin, stdout_err, process_thread|
|
||||
status = :starting
|
||||
|
||||
# Spawn a thread to listen on streaming server output
|
||||
output_thread = Thread.new do
|
||||
stdout_err.each_line do |line|
|
||||
Rails.logger.info "Streaming server: #{line}"
|
||||
|
||||
if status == :starting && line.match('Streaming API now listening on')
|
||||
status = :started
|
||||
@queue.enq 'started'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# And another thread to listen on commands from the main thread
|
||||
loop do
|
||||
msg = queue.pop
|
||||
|
||||
case msg
|
||||
when 'stop'
|
||||
# we need to properly stop the reading thread
|
||||
output_thread.kill
|
||||
|
||||
# Then stop the node process
|
||||
Process.kill('KILL', process_thread.pid)
|
||||
|
||||
# And we stop ourselves
|
||||
@running_thread.kill
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# wait for 10 seconds for the streaming server to start
|
||||
Timeout.timeout(10) do
|
||||
loop do
|
||||
break if @queue.pop == 'started'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
return unless @running_thread
|
||||
|
||||
@queue.enq 'stop'
|
||||
|
||||
# Wait for the thread to end
|
||||
@running_thread.join
|
||||
end
|
||||
end
|
||||
|
||||
class SearchDataManager
|
||||
def prepare_test_data
|
||||
4.times do |i|
|
||||
username = "search_test_account_#{i}"
|
||||
account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.")
|
||||
2.times do |j|
|
||||
Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private)
|
||||
end
|
||||
end
|
||||
|
||||
3.times do |i|
|
||||
Fabricate.create(:tag, name: "search_test_tag_#{i}")
|
||||
end
|
||||
end
|
||||
|
||||
def indexes
|
||||
[
|
||||
AccountsIndex,
|
||||
PublicStatusesIndex,
|
||||
StatusesIndex,
|
||||
TagsIndex,
|
||||
]
|
||||
end
|
||||
|
||||
def populate_indexes
|
||||
indexes.each do |index_class|
|
||||
index_class.purge!
|
||||
index_class.import!
|
||||
end
|
||||
end
|
||||
|
||||
def remove_indexes
|
||||
indexes.each(&:delete!)
|
||||
end
|
||||
|
||||
def cleanup_test_data
|
||||
Status.destroy_all
|
||||
Account.destroy_all
|
||||
Tag.destroy_all
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SearchDataManager
|
||||
def prepare_test_data
|
||||
4.times do |i|
|
||||
username = "search_test_account_#{i}"
|
||||
account = Fabricate.create(:account, username: username, indexable: i.even?, discoverable: i.even?, note: "Lover of #{i}.")
|
||||
2.times do |j|
|
||||
Fabricate.create(:status, account: account, text: "#{username}'s #{j} post", visibility: j.even? ? :public : :private)
|
||||
end
|
||||
end
|
||||
|
||||
3.times do |i|
|
||||
Fabricate.create(:tag, name: "search_test_tag_#{i}")
|
||||
end
|
||||
end
|
||||
|
||||
def indexes
|
||||
[
|
||||
AccountsIndex,
|
||||
PublicStatusesIndex,
|
||||
StatusesIndex,
|
||||
TagsIndex,
|
||||
]
|
||||
end
|
||||
|
||||
def populate_indexes
|
||||
indexes.each do |index_class|
|
||||
index_class.purge!
|
||||
index_class.import!
|
||||
end
|
||||
end
|
||||
|
||||
def remove_indexes
|
||||
indexes.each(&:delete!)
|
||||
end
|
||||
|
||||
def cleanup_test_data
|
||||
Status.destroy_all
|
||||
Account.destroy_all
|
||||
Tag.destroy_all
|
||||
end
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StreamingServerManager
|
||||
@running_thread = nil
|
||||
|
||||
def initialize
|
||||
at_exit { stop }
|
||||
end
|
||||
|
||||
def start(port: 4020)
|
||||
return if @running_thread
|
||||
|
||||
queue = Queue.new
|
||||
|
||||
@queue = queue
|
||||
|
||||
@running_thread = Thread.new do
|
||||
Open3.popen2e(
|
||||
{
|
||||
'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
|
||||
'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
|
||||
'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
|
||||
'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
|
||||
'PORT' => port.to_s,
|
||||
},
|
||||
'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
|
||||
chdir: Rails.root.join('streaming')
|
||||
) do |_stdin, stdout_err, process_thread|
|
||||
status = :starting
|
||||
|
||||
# Spawn a thread to listen on streaming server output
|
||||
output_thread = Thread.new do
|
||||
stdout_err.each_line do |line|
|
||||
Rails.logger.info "Streaming server: #{line}"
|
||||
|
||||
if status == :starting && line.match('Streaming API now listening on')
|
||||
status = :started
|
||||
@queue.enq 'started'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# And another thread to listen on commands from the main thread
|
||||
loop do
|
||||
msg = queue.pop
|
||||
|
||||
case msg
|
||||
when 'stop'
|
||||
# we need to properly stop the reading thread
|
||||
output_thread.kill
|
||||
|
||||
# Then stop the node process
|
||||
Process.kill('KILL', process_thread.pid)
|
||||
|
||||
# And we stop ourselves
|
||||
@running_thread.kill
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# wait for 10 seconds for the streaming server to start
|
||||
Timeout.timeout(10) do
|
||||
loop do
|
||||
break if @queue.pop == 'started'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stop
|
||||
return unless @running_thread
|
||||
|
||||
@queue.enq 'stop'
|
||||
|
||||
# Wait for the thread to end
|
||||
@running_thread.join
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue