...
 
Commits (11)
......@@ -37,6 +37,7 @@ class Conversation extends ImmutablePureComponent {
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
markRead: PropTypes.func.isRequired,
delete: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
......
......@@ -5,17 +5,23 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
});
export default @connect(mapStateToProps)
@injectIntl
class Favourites extends ImmutablePureComponent {
static propTypes = {
......@@ -24,6 +30,7 @@ class Favourites extends ImmutablePureComponent {
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
componentWillMount () {
......@@ -38,8 +45,12 @@ class Favourites extends ImmutablePureComponent {
}
}
handleRefresh = () => {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
}
render () {
const { shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
......@@ -52,8 +63,14 @@ class Favourites extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this toot yet. When someone does, they will show up here.' />;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn}>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
)}
/>
<ScrollableList
scrollKey='favourites'
......
......@@ -5,17 +5,23 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
});
export default @connect(mapStateToProps)
@injectIntl
class Reblogs extends ImmutablePureComponent {
static propTypes = {
......@@ -24,6 +30,7 @@ class Reblogs extends ImmutablePureComponent {
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
componentWillMount () {
......@@ -38,8 +45,12 @@ class Reblogs extends ImmutablePureComponent {
}
}
handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
render () {
const { shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
......@@ -52,8 +63,14 @@ class Reblogs extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has boosted this toot yet. When someone does, they will show up here.' />;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn}>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
)}
/>
<ScrollableList
scrollKey='reblogs'
......
......@@ -238,6 +238,14 @@
"description": "Tooltip of the \"voted\" checkmark in polls",
"id": "poll.voted"
},
{
"defaultMessage": "{count, plural, one {# person} other {# people}}",
"id": "poll.total_people"
},
{
"defaultMessage": "{count, plural, one {# vote} other {# votes}}",
"id": "poll.total_votes"
},
{
"defaultMessage": "Vote",
"id": "poll.vote"
......@@ -245,10 +253,6 @@
{
"defaultMessage": "Refresh",
"id": "poll.refresh"
},
{
"defaultMessage": "{count, plural, one {# vote} other {# votes}}",
"id": "poll.total_votes"
}
],
"path": "app/javascript/mastodon/components/poll.json"
......@@ -498,10 +502,6 @@
"defaultMessage": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"id": "confirmations.redraft.message"
},
{
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
{
"defaultMessage": "Reply",
"id": "confirmations.reply.confirm"
......@@ -509,14 +509,6 @@
{
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message"
},
{
"defaultMessage": "Block & Report",
"id": "confirmations.block.block_and_report"
},
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
}
],
"path": "app/javascript/mastodon/containers/status_container.json"
......@@ -553,26 +545,14 @@
"defaultMessage": "Unfollow",
"id": "confirmations.unfollow.confirm"
},
{
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
{
"defaultMessage": "Hide entire domain",
"id": "confirmations.domain_block.confirm"
},
{
"defaultMessage": "Block & Report",
"id": "confirmations.block.block_and_report"
},
{
"defaultMessage": "Are you sure you want to unfollow {name}?",
"id": "confirmations.unfollow.message"
},
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
},
{
"defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"id": "confirmations.domain_block.message"
......@@ -1134,15 +1114,6 @@
],
"path": "app/javascript/mastodon/features/compose/components/upload_form.json"
},
{
"descriptors": [
{
"defaultMessage": "Uploading...",
"id": "upload_progress.label"
}
],
"path": "app/javascript/mastodon/features/compose/components/upload_progress.json"
},
{
"descriptors": [
{
......@@ -1635,6 +1606,10 @@
},
{
"descriptors": [
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{
"defaultMessage": "Show boosts",
"id": "home.column_settings.show_reblogs"
......@@ -2016,14 +1991,6 @@
"defaultMessage": "Push notifications",
"id": "notifications.column_settings.push"
},
{
"defaultMessage": "Basic",
"id": "home.column_settings.basic"
},
{
"defaultMessage": "Update in real-time",
"id": "home.column_settings.update_live"
},
{
"defaultMessage": "Quick filter bar",
"id": "notifications.column_settings.filter_bar.category"
......@@ -2082,10 +2049,6 @@
},
{
"descriptors": [
{
"defaultMessage": "and {count, plural, one {# other} other {# others}}",
"id": "notification.and_n_others"
},
{
"defaultMessage": "{name} followed you",
"id": "notification.follow"
......@@ -2273,10 +2236,6 @@
"defaultMessage": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"id": "confirmations.redraft.message"
},
{
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
{
"defaultMessage": "Reply",
"id": "confirmations.reply.confirm"
......@@ -2284,14 +2243,6 @@
{
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message"
},
{
"defaultMessage": "Block & Report",
"id": "confirmations.block.block_and_report"
},
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
}
],
"path": "app/javascript/mastodon/features/status/containers/detailed_status_container.json"
......@@ -2314,10 +2265,6 @@
"defaultMessage": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"id": "confirmations.redraft.message"
},
{
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
},
{
"defaultMessage": "Show more for all",
"id": "status.show_more_all"
......@@ -2337,21 +2284,30 @@
{
"defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"id": "confirmations.reply.message"
}
],
"path": "app/javascript/mastodon/features/status/index.json"
},
{
"descriptors": [
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
},
{
"defaultMessage": "Block & Report",
"id": "confirmations.block.block_and_report"
"defaultMessage": "Cancel",
"id": "confirmation_modal.cancel"
},
{
"defaultMessage": "Toot",
"id": "column.status"
"defaultMessage": "Block & Report",
"id": "confirmations.block.block_and_report"
},
{
"defaultMessage": "Are you sure you want to block {name}?",
"id": "confirmations.block.message"
"defaultMessage": "Block",
"id": "confirmations.block.confirm"
}
],
"path": "app/javascript/mastodon/features/status/index.json"
"path": "app/javascript/mastodon/features/ui/components/block_modal.json"
},
{
"descriptors": [
......@@ -2569,6 +2525,10 @@
"defaultMessage": "Are you sure you want to mute {name}?",
"id": "confirmations.mute.message"
},
{
"defaultMessage": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts follow you.",
"id": "confirmations.mute.explanation"
},
{
"defaultMessage": "Hide notifications from this user?",
"id": "mute_modal.hide_notifications"
......
......@@ -63,7 +63,6 @@
"column.notifications": "Notifications",
"column.pins": "Pinned toots",
"column.public": "Federated timeline",
"column.status": "Toot",
"column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
......@@ -104,6 +103,7 @@
"confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
......@@ -174,7 +174,6 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
......@@ -268,7 +267,6 @@
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
......@@ -297,6 +295,7 @@
"notifications.group": "{count} notifications",
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
"poll.voted": "You voted for this answer",
......
......@@ -7,6 +7,7 @@ import {
CONVERSATIONS_FETCH_FAIL,
CONVERSATIONS_UPDATE,
CONVERSATIONS_READ,
CONVERSATIONS_DELETE_SUCCESS,
} from '../actions/conversations';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
......@@ -107,6 +108,8 @@ export default function conversations(state = initialState, action) {
return filterConversations(state, [action.relationship.id]);
case DOMAIN_BLOCK_SUCCESS:
return filterConversations(state, action.accounts);
case CONVERSATIONS_DELETE_SUCCESS:
return state.update('items', list => list.filterNot(item => item.get('id') === action.id));
default:
return state;
}
......
......@@ -145,8 +145,6 @@ $small-breakpoint: 960px;
thead tr,
tbody tr {
break-after: auto;
break-inside: avoid;
border-bottom: 1px solid lighten($ui-base-color, 4%);
font-size: 1em;
line-height: 1.625;
......@@ -167,12 +165,25 @@ $small-breakpoint: 960px;
padding: 8px;
align-self: start;
align-items: start;
word-break: break-all;
&.nowrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 25%;
position: relative;
&::before {
content: '&nbsp;';
visibility: hidden;
}
span {
position: absolute;
left: 8px;
right: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
......
......@@ -129,7 +129,7 @@ class Account < ApplicationRecord
delegate :chosen_languages, to: :user, prefix: false, allow_nil: true
update_index('accounts#account', :self) if Chewy.enabled?
update_index('accounts#account', :self)
def local?
domain.nil?
......
......@@ -16,7 +16,7 @@
class AccountStat < ApplicationRecord
belongs_to :account, inverse_of: :account_stat
update_index('accounts#account', :account) if Chewy.enabled?
update_index('accounts#account', :account)
def increment_count!(key)
update(attributes_for_increment(key))
......
......@@ -5,6 +5,12 @@ class ApplicationRecord < ActiveRecord::Base
include Remotable
class << self
def update_index(_type_name, *_args, &_block)
super if Chewy.enabled?
end
end
def boolean_with_default(key, default_value)
value = attributes[key]
......
......@@ -13,7 +13,7 @@
class Favourite < ApplicationRecord
include Paginable
update_index('statuses#status', :status) if Chewy.enabled?
update_index('statuses#status', :status)
belongs_to :account, inverse_of: :favourites
belongs_to :status, inverse_of: :favourites
......
......@@ -39,7 +39,7 @@ class Status < ApplicationRecord
# will be based on current time instead of `created_at`
attr_accessor :override_timestamps
update_index('statuses#status', :proper) if Chewy.enabled?
update_index('statuses#status', :proper)
enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility
......
......@@ -49,7 +49,7 @@ class Tag < ApplicationRecord
after_save :save_account_tag_stat
update_index('tags#tag', :self) if Chewy.enabled?
update_index('tags#tag', :self)
def account_tag_stat
super || build_account_tag_stat
......
......@@ -21,7 +21,7 @@ class InstancePresenter
end
def active_user_count(weeks = 4)
Rails.cache.fetch('active_user_count') { Redis.current.pfcount(*(0...weeks).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
Rails.cache.fetch("active_user_count/#{weeks}") { Redis.current.pfcount(*(0...weeks).map { |i| "activity:logins:#{i.weeks.ago.utc.to_date.cweek}" }) }
end
def status_count
......
......@@ -17,7 +17,11 @@ class BootstrapTimelineService < BaseService
def autofollow_bootstrap_timeline_accounts!
bootstrap_timeline_accounts.each do |target_account|
FollowService.new.call(@source_account, target_account)
begin
FollowService.new.call(@source_account, target_account)
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
nil
end
end
end
......@@ -40,7 +44,9 @@ class BootstrapTimelineService < BaseService
def local_unlocked_accounts(usernames)
Account.local
.without_suspended
.where(username: usernames)
.where(locked: false)
.where(moved_to_account_id: nil)
end
end
......@@ -39,12 +39,6 @@ class FetchLinkCardService < BaseService
def process_url
@card ||= PreviewCard.new(url: @url)
failed = Request.new(:head, @url).perform do |res|
res.code != 405 && res.code != 501 && (res.code != 200 || res.mime_type != 'text/html')
end
return if failed
Request.new(:get, @url).perform do |res|
if res.code == 200 && res.mime_type == 'text/html'
@html = res.body_with_limit
......
......@@ -6,5 +6,7 @@
%tbody
- domain_blocks.each do |domain_block|
%tr
%td.nowrap= domain_block.domain
%td= domain_block.public_comment if display_blocks_rationale?
%td.nowrap
%span{ title: domain_block.domain }= domain_block.domain
%td
= domain_block.public_comment if display_blocks_rationale?
......@@ -11,9 +11,14 @@
.dashboard__counters__num= number_with_delimiter @accounts_week
.dashboard__counters__label= t 'admin.tags.accounts_week'
%div
= link_to explore_hashtag_path(@tag) do
.dashboard__counters__num= number_with_delimiter @tag.accounts_count
.dashboard__counters__label= t 'admin.tags.directory'
- if @tag.accounts_count > 0
= link_to explore_hashtag_path(@tag) do
.dashboard__counters__num= number_with_delimiter @tag.accounts_count
.dashboard__counters__label= t 'admin.tags.directory'
- else
%div
.dashboard__counters__num= number_with_delimiter @tag.accounts_count
.dashboard__counters__label= t 'admin.tags.directory'
%hr.spacer/
......
......@@ -15,6 +15,7 @@ require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version'
require_relative '../lib/devise/two_factor_ldap_authenticatable'
require_relative '../lib/devise/two_factor_pam_authenticatable'
require_relative '../lib/chewy/strategy/custom_sidekiq'
Dotenv::Railtie.load
......
......@@ -12,8 +12,9 @@ Chewy.settings = {
sidekiq: { queue: 'pull' },
}
Chewy.root_strategy = enabled ? :sidekiq : :bypass
Chewy.request_strategy = enabled ? :sidekiq : :bypass
Chewy.root_strategy = :custom_sidekiq
Chewy.request_strategy = :custom_sidekiq
Chewy.use_after_commit_callbacks = false
module Chewy
class << self
......
......@@ -240,6 +240,7 @@ en:
delete: Delete
destroyed_msg: Emojo successfully destroyed!
disable: Disable
disabled: Disabled
disabled_msg: Successfully disabled that emoji
emoji: Emoji
enable: Enable
......
# frozen_string_literal: true
module Chewy
class Strategy
class CustomSidekiq < Base
class Worker
include ::Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(type, ids, options = {})
options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async
type.constantize.import!(ids, options)
end
end
def update(type, objects, _options = {})
return unless Chewy.enabled?
ids = type.root.id ? Array.wrap(objects) : type.adapter.identify(objects)
return if ids.empty?
Worker.perform_async(type.name, ids)
end
def leave; end
end
end
end
......@@ -12,7 +12,7 @@ require 'capybara/rspec'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema!
WebMock.disable_net_connect!
WebMock.disable_net_connect!(allow: Chewy.settings[:host])
Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)
Sidekiq::Testing.inline!
Sidekiq::Logging.logger = nil
......
......@@ -22,9 +22,10 @@ RSpec.describe BootstrapTimelineService, type: :service do
context 'when setting is set' do
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') }
let!(:eve) { Fabricate(:account, username: 'eve', suspended: true) }
before do
Setting.bootstrap_timeline_accounts = 'alice, bob'
Setting.bootstrap_timeline_accounts = 'alice, @bob, eve, unknown'
subject.call(source_account)
end
......@@ -32,6 +33,10 @@ RSpec.describe BootstrapTimelineService, type: :service do
expect(source_account.following?(alice)).to be true
expect(source_account.following?(bob)).to be true
end
it 'does not follow suspended account' do
expect(source_account.following?(eve)).to be false
end
end
end
end
......@@ -4,20 +4,13 @@ RSpec.describe FetchLinkCardService, type: :service do
subject { FetchLinkCardService.new }
before do
stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt'))
stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt'))
stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404)
stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'https://github.com/qbi/WannaCry').to_return(status: 404)
stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
stub_request(:head, 'http://example.com/windows-1251').to_return(status: 200, headers: { 'Content-Type' => 'text/html' })
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
subject.call(status)
......@@ -90,11 +83,11 @@ RSpec.describe FetchLinkCardService, type: :service do
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
it 'parses out URLs' do
expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
end
it 'ignores URLs to hashtags' do
expect(a_request(:head, 'https://quitter.se/tag/wannacry')).to_not have_been_made
expect(a_request(:get, 'https://quitter.se/tag/wannacry')).to_not have_been_made
end
end
end
This diff is collapsed.