From af20b1d2aafe2e535611a2d3155306fcd1d8a89b Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 04:27:52 +0900 Subject: [PATCH 01/11] Add searchability support --- app/chewy/statuses_index.rb | 35 ++- .../api/v1/accounts/credentials_controller.rb | 1 + app/controllers/api/v1/statuses_controller.rb | 2 + .../settings/preferences_controller.rb | 1 + .../settings/profiles_controller.rb | 2 +- app/helpers/application_helper.rb | 4 + app/helpers/context_helper.rb | 2 + app/javascript/mastodon/actions/compose.js | 9 + .../compose/components/compose_form.jsx | 5 +- .../components/expiration_dropdown.jsx | 11 +- .../components/searchability_dropdown.jsx | 286 ++++++++++++++++++ .../expiration_dropdown_container.js | 1 - .../searchability_dropdown_container.js | 23 ++ .../status/components/detailed_status.jsx | 16 +- app/javascript/mastodon/reducers/compose.js | 9 + .../styles/mastodon-light/diff.scss | 9 +- .../styles/mastodon/components.scss | 37 ++- app/lib/activitypub/activity/create.rb | 35 +++ app/lib/activitypub/tag_manager.rb | 27 ++ app/lib/status_filter.rb | 19 +- app/lib/user_settings_decorator.rb | 5 + app/models/account.rb | 2 + app/models/concerns/account_interactions.rb | 4 + app/models/public_feed.rb | 5 + app/models/status.rb | 23 ++ app/models/user.rb | 5 + app/policies/status_policy.rb | 17 +- app/presenters/initial_state_presenter.rb | 2 +- .../activitypub/actor_serializer.rb | 8 +- .../activitypub/note_serializer.rb | 8 +- app/serializers/initial_state_serializer.rb | 9 +- app/serializers/rest/account_serializer.rb | 2 +- app/serializers/rest/instance_serializer.rb | 1 + app/serializers/rest/status_serializer.rb | 6 +- .../activitypub/process_account_service.rb | 28 ++ app/services/post_status_service.rb | 18 ++ app/services/search_service.rb | 46 ++- app/services/searchability_update_service.rb | 27 ++ .../settings/preferences/other/show.html.haml | 3 + app/workers/searchability_update_worker.rb | 13 + ...405121613_add_searchability_to_statuses.rb | 5 + ...405121625_add_searchability_to_accounts.rb | 5 + db/schema.rb | 5 +- 43 files changed, 716 insertions(+), 65 deletions(-) create mode 100644 app/javascript/mastodon/features/compose/components/searchability_dropdown.jsx create mode 100644 app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js create mode 100644 app/services/searchability_update_service.rb create mode 100644 app/workers/searchability_update_worker.rb create mode 100644 db/migrate/20230405121613_add_searchability_to_statuses.rb create mode 100644 db/migrate/20230405121625_add_searchability_to_accounts.rb diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 28dace0f98..7e818ba9c2 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -3,7 +3,37 @@ class StatusesIndex < Chewy::Index include FormattingHelper - settings index: { refresh_interval: '30s' }, analysis: { + DEVELOPMENT_SETTINGS = { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { + content: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + }, + } + + PRODUCTION_SETTINGS = { filter: { english_stop: { type: 'stop', @@ -46,6 +76,8 @@ class StatusesIndex < Chewy::Index } } + settings index: { refresh_interval: '30s' }, analysis: Rails.env.development? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS + # We do not use delete_if option here because it would call a method that we # expect to be called with crutches without crutches, causing n+1 queries index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) @@ -89,5 +121,6 @@ class StatusesIndex < Chewy::Index end field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } + field :searchability, type: 'keyword', value: ->(status) { status.compute_searchability } end end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 94b707771f..ca5b9cfba3 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -29,6 +29,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :locked, :bot, :discoverable, + :searchability, :hide_collections, fields_attributes: [:name, :value] ) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index fadd1b0451..51fdd92e09 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -59,6 +59,7 @@ class Api::V1::StatusesController < Api::BaseController sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], visibility: status_params[:visibility], + searchability: status_params[:searchability], language: status_params[:language], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, @@ -133,6 +134,7 @@ class Api::V1::StatusesController < Api::BaseController :sensitive, :spoiler_text, :visibility, + :searchability, :language, :scheduled_at, allowed_mentions: [], diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f5d5c12449..a3994d8f85 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -34,6 +34,7 @@ class Settings::PreferencesController < Settings::BaseController def user_settings_params params.require(:user).permit( :setting_default_privacy, + :setting_default_searchability, :setting_default_sensitive, :setting_default_language, :setting_unfollow_modal, diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 34b7c81131..d743518fca 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :my_actor_type, :group_allow_private_message, :discoverable, :hide_collections, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :my_actor_type, :searchability, :group_allow_private_message, :discoverable, :hide_collections, fields_attributes: [:name, :value]) end def set_account diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 89bbc67849..a75d7fbd83 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -202,9 +202,13 @@ module ApplicationHelper } permit_visibilities = %w(public unlisted public_unlisted private direct) + permit_searchabilities = %w(public unlisted public_unlisted private direct) default_privacy = current_account&.user&.setting_default_privacy permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] + default_searchability = current_account&.user&.setting_default_searchability + permit_searchabilities.shift(permit_searchabilities.index(default_privacy) + 1) if default_searchability.present? + state_params[:searchability] = params[:searchability] if permit_searchabilities.include? params[:searchability] if user_signed_in? && current_user.functional? state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 08cfa9c6d5..18c6c7916a 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -21,6 +21,8 @@ module ContextHelper blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, + emoji_reactions: { 'fedibird' => 'http://fedibird.com/ns#', 'emojiReactions' => { '@id' => "fedibird:emojiReactions", '@type' => '@id' } }, + searchable_by: { 'fedibird' => 'http://fedibird.com/ns#', 'searchableBy' => { '@id' => "fedibird:searchableBy", '@type' => '@id' } }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, }.freeze diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 6cf44df086..331c634897 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -53,6 +53,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; +export const COMPOSE_SEARCHABILITY_CHANGE= 'COMPOSE_SEARCHABILITY_CHANGE'; export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; @@ -192,6 +193,7 @@ export function submitCompose(routerHistory) { sensitive: getState().getIn(['compose', 'sensitive']), spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '', visibility: getState().getIn(['compose', 'privacy']), + searchability: getState().getIn(['compose', 'searchability']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), }, @@ -734,6 +736,13 @@ export function changeComposeVisibility(value) { }; } +export function changeComposeSearchability(value) { + return { + type: COMPOSE_SEARCHABILITY_CHANGE, + value, + }; +} + export function insertEmojiCompose(position, emoji, needsSpace) { return { type: COMPOSE_EMOJI_INSERT, diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index edd0c0257b..97aa443f88 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -11,6 +11,7 @@ import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; +import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container'; import ExpirationDropdownContainer from '../containers/expiration_dropdown_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import PollFormContainer from '../containers/poll_form_container'; @@ -45,6 +46,7 @@ class ComposeForm extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list, spoiler: PropTypes.bool, privacy: PropTypes.string, + searchability: PropTypes.string, spoilerText: PropTypes.string, focusDate: PropTypes.instanceOf(Date), caretPosition: PropTypes.number, @@ -272,6 +274,7 @@ class ComposeForm extends ImmutablePureComponent { lang={this.props.lang} > +
@@ -284,9 +287,9 @@ class ComposeForm extends ImmutablePureComponent { + -
diff --git a/app/javascript/mastodon/features/compose/components/expiration_dropdown.jsx b/app/javascript/mastodon/features/compose/components/expiration_dropdown.jsx index ce45363a75..ac32bea924 100644 --- a/app/javascript/mastodon/features/compose/components/expiration_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/expiration_dropdown.jsx @@ -5,7 +5,6 @@ import IconButton from '../../../components/icon_button'; import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; -import Icon from 'mastodon/components/icon'; const messages = defineMessages({ add_expiration: { id: 'status.expiration.add', defaultMessage: 'Set status expiration' }, @@ -108,7 +107,7 @@ class ExpirationDropdownMenu extends React.PureComponent {
{items.map(item => (
-
+
{item.text}
@@ -229,10 +228,10 @@ class ExpirationDropdown extends React.PureComponent { const { open, placement } = this.state; return ( -
-
+
+
{({ props, placement }) => (
-
+
{ + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + }; + + handleKeyDown = e => { + const { items } = this.props; + const value = e.currentTarget.getAttribute('data-index'); + const index = items.findIndex(item => { + return (item.value === value); + }); + let element = null; + + switch(e.key) { + case 'Escape': + this.props.onClose(); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1] || this.node.firstChild; + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1] || this.node.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + break; + case 'Home': + element = this.node.firstChild; + break; + case 'End': + element = this.node.lastChild; + break; + } + + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }; + + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + }; + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + setFocusRef = c => { + this.focusedItem = c; + }; + + render () { + const { style, items, value } = this.props; + + return ( +
+ {items.map(item => ( +
+
+ +
+ +
+ {item.text} + {item.meta} +
+
+ ))} +
+ ); + } + +} + +export default @injectIntl +class SearchabilityDropdown extends React.PureComponent { + + static propTypes = { + isUserTouching: PropTypes.func, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + noDirect: PropTypes.bool, + container: PropTypes.func, + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + open: false, + placement: 'bottom', + }; + + handleToggle = () => { + if (this.props.isUserTouching && this.props.isUserTouching()) { + if (this.state.open) { + this.props.onModalClose(); + } else { + this.props.onModalOpen({ + actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), + onClick: this.handleModalActionClick, + }); + } + } else { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + this.setState({ open: !this.state.open }); + } + }; + + handleModalActionClick = (e) => { + e.preventDefault(); + + const { value } = this.options[e.currentTarget.getAttribute('data-index')]; + + this.props.onModalClose(); + this.props.onChange(value); + }; + + handleKeyDown = e => { + switch(e.key) { + case 'Escape': + this.handleClose(); + break; + } + }; + + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + }; + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + }; + + handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + this.setState({ open: false }); + }; + + handleChange = value => { + this.props.onChange(value); + }; + + componentWillMount () { + const { intl: { formatMessage } } = this.props; + + this.options = [ + { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, + { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, + { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, + { icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, + ]; + } + + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target; + }; + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + }; + + render () { + const { value, container, disabled, intl } = this.props; + const { open, placement } = this.state; + + const valueOption = this.options.find(item => item.value === value); + + return ( +
+
+ + +
+ + + {({ props, placement }) => ( +
+
+ +
+
+ )} +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/compose/containers/expiration_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/expiration_dropdown_container.js index 2243aa70be..5de1ccb3e6 100644 --- a/app/javascript/mastodon/features/compose/containers/expiration_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/expiration_dropdown_container.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import ExpirationDropdown from '../components/expiration_dropdown'; -import { changeComposeVisibility } from '../../../actions/compose'; import { openModal, closeModal } from '../../../actions/modal'; import { isUserTouching } from '../../../is_mobile'; diff --git a/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js new file mode 100644 index 0000000000..3b4a83a1f0 --- /dev/null +++ b/app/javascript/mastodon/features/compose/containers/searchability_dropdown_container.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import SearchabilityDropdown from '../components/searchability_dropdown'; +import { changeComposeSearchability } from '../../../actions/compose'; +import { openModal, closeModal } from '../../../actions/modal'; +import { isUserTouching } from '../../../is_mobile'; + +const mapStateToProps = state => ({ + value: state.getIn(['compose', 'searchability']), +}); + +const mapDispatchToProps = dispatch => ({ + + onChange (value) { + dispatch(changeComposeSearchability(value)); + }, + + isUserTouching, + onModalOpen: props => dispatch(openModal('ACTIONS', props)), + onModalClose: () => dispatch(closeModal()), + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SearchabilityDropdown); diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 213bdf4533..1a46c19f95 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -25,6 +25,10 @@ const messages = defineMessages({ public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' }, + searchability_unlisted_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' }, + searchability_private_short: { id: 'searchability.private.short', defaultMessage: 'Reactionners' }, + searchability_direct_short: { id: 'searchability.direct.short', defaultMessage: 'Self only' }, }); export default @injectIntl @@ -218,6 +222,16 @@ class DetailedStatus extends ImmutablePureComponent { const visibilityIcon = visibilityIconInfo[status.get('visibility')]; const visibilityLink = · ; + const searchabilityIconInfo = { + 'public': { icon: 'globe', text: intl.formatMessage(messages.searchability_public_short) }, + 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.searchability_unlisted_short) }, + 'private': { icon: 'lock', text: intl.formatMessage(messages.searchability_private_short) }, + 'direct': { icon: 'at', text: intl.formatMessage(messages.searchability_direct_short) }, + }; + + const searchabilityIcon = searchabilityIconInfo[status.get('searchability')]; + const searchabilityLink = · ; + if (['private', 'direct'].includes(status.get('visibility'))) { reblogLink = ''; } else if (this.context.router) { @@ -313,7 +327,7 @@ class DetailedStatus extends ImmutablePureComponent {
- {edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink} + {edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink}
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 1483f1c2e4..78d18d3359 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -47,6 +47,7 @@ import { COMPOSE_CHANGE_MEDIA_DESCRIPTION, COMPOSE_CHANGE_MEDIA_FOCUS, COMPOSE_SET_STATUS, + COMPOSE_SEARCHABILITY_CHANGE, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; @@ -62,6 +63,7 @@ const initialState = ImmutableMap({ spoiler: false, spoiler_text: '', privacy: null, + searchability: null, id: null, text: '', focusDate: null, @@ -81,6 +83,7 @@ const initialState = ImmutableMap({ suggestion_token: null, suggestions: ImmutableList(), default_privacy: 'public', + default_searchability: 'public', default_sensitive: false, default_language: 'en', resetFileKey: Math.floor((Math.random() * 0x10000)), @@ -121,6 +124,7 @@ function clearAll(state) { map.set('is_changing_upload', false); map.set('in_reply_to', null); map.set('privacy', state.get('default_privacy')); + map.set('searchability', state.get('default_searchability')); map.set('sensitive', state.get('default_sensitive')); map.set('language', state.get('default_language')); map.update('media_attachments', list => list.clear()); @@ -328,6 +332,10 @@ export default function compose(state = initialState, action) { return state .set('privacy', action.value) .set('idempotencyKey', uuid()); + case COMPOSE_SEARCHABILITY_CHANGE: + return state + .set('searchability', action.value) + .set('idempotencyKey', uuid()); case COMPOSE_CHANGE: return state .set('text', action.text) @@ -340,6 +348,7 @@ export default function compose(state = initialState, action) { map.set('in_reply_to', action.status.get('id')); map.set('text', statusToTextMentions(state, action.status)); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.set('searchability', privacyPreference(action.status.get('searchability'), state.get('default_searchability'))) map.set('focusDate', new Date()); map.set('caretPosition', null); map.set('preselectDate', new Date()); diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 58f161f811..b1d4a334cc 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -289,7 +289,8 @@ html { background: $ui-base-color; } -.privacy-dropdown.active .privacy-dropdown__value.active .icon-button { +.privacy-dropdown.active .privacy-dropdown__value.active .icon-button, +.expiration-dropdown.active .expiration-dropdown__value.active .icon-button { color: $white; } @@ -321,6 +322,12 @@ html { .privacy-dropdown__option.active .privacy-dropdown__option__content strong, .privacy-dropdown__option:hover .privacy-dropdown__option__content, .privacy-dropdown__option:hover .privacy-dropdown__option__content strong, +.expiration-dropdown__option.active, +.expiration-dropdown__option:hover, +.expiration-dropdown__option.active .expiration-dropdown__option__content, +.expiration-dropdown__option.active .expiration-dropdown__option__content strong, +.expiration-dropdown__option:hover .expiration-dropdown__option__content, +.expiration-dropdown__option:hover .expiration-dropdown__option__content strong, .dropdown-menu__item a:active, .dropdown-menu__item a:focus, .dropdown-menu__item a:hover, diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 659fee9a48..f9803dea75 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -468,6 +468,13 @@ body > [data-popper-placement] { right: 0; } + .expiration-dropdown { + position: absolute; + top: 32px; + right: 4px; + transform: scale(1.2); + } + .compose-form__autosuggest-wrapper { position: relative; } @@ -4642,7 +4649,8 @@ a.status-card.compact:hover { filter: none; } -.privacy-dropdown__dropdown { +.privacy-dropdown__dropdown, +.expiration-dropdown__dropdown { background: $simple-background-color; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); border-radius: 4px; @@ -4658,16 +4666,19 @@ a.status-card.compact:hover { } } -.modal-root__container .privacy-dropdown { +.modal-root__container .privacy-dropdown, +.modal-root__container .expiration-dropdown { flex-grow: 0; } -.modal-root__container .privacy-dropdown__dropdown { +.modal-root__container .privacy-dropdown__dropdown, +.modal-root__container .ezpiration-dropdown__dropdown { pointer-events: auto; z-index: 9999; } -.privacy-dropdown__option { +.privacy-dropdown__option, +.expiration-dropdown__option { color: $inverted-text-color; padding: 10px; cursor: pointer; @@ -4717,8 +4728,8 @@ a.status-card.compact:hover { } } -.privacy-dropdown.active { - .privacy-dropdown__value { +.privacy-dropdown.active, .expiration-dropdown.active { + .privacy-dropdown__value, .expiration-dropdown__value { background: $simple-background-color; border-radius: 4px 4px 0 0; box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); @@ -4736,16 +4747,26 @@ a.status-card.compact:hover { } } - &.top .privacy-dropdown__value { + &.top .privacy-dropdown__value, &.top .expiration-dropdown__value { border-radius: 0 0 4px 4px; } - .privacy-dropdown__dropdown { + .privacy-dropdown__dropdown, .expiration-dropdown__dropdown { display: block; box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1); } } +.privacy-dropdown .privacy-dropdown__value.searchability { + position: relative; + + .searchability-dropdown__value-overlay { + position: absolute; + right: 0; + bottom: 0; + } +} + .language-dropdown { &__dropdown { background: $simple-background-color; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 95f558f15e..b926b3a02b 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -70,6 +70,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity as_array(@object['cc'] || @json['cc']).map { |x| value_or_id(x) } end + def audience_searchable_by + return nil if @object['searchableBy'].nil? + + @audience_searchable_by = as_array(@object['searchableBy']).map { |x| value_or_id(x) } + end + def process_status @tags = [] @mentions = [] @@ -122,6 +128,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity reply: @status_parser.reply, sensitive: @account.sensitized? || @status_parser.sensitive || false, visibility: @status_parser.visibility, + searchability: searchability, thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), @@ -429,4 +436,32 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def join_group! GroupReblogService.new.call(@status) end + + def searchability_from_audience + if audience_searchable_by.nil? + nil + elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } + :public + elsif audience_searchable_by.include?(@account.followers_url) + :private + else + :direct + end + end + + def searchability + searchability = searchability_from_audience + + return nil if searchability.nil? + + visibility = visibility_from_audience_with_silence + + if searchability === visibility + searchability + elsif [:public, :private].include?(searchability) && [:public, :unlisted].include?(visibility) + :private + else + :direct + end + end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 4358172ae4..2d8e9b5008 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -185,4 +185,31 @@ class ActivityPub::TagManager rescue ActiveRecord::RecordNotFound nil end + + def searchable_by(status) + searchable_by = + case status.compute_searchability + when 'public' + [COLLECTIONS[:public]] + when 'unlisted', 'private' + [account_followers_url(status.account)] + when 'limited' + status.conversation_id.present? ? [uri_for(status.conversation)] : [] + else + [] + end + + searchable_by.concat(mentions_uris(status)) + end + + def account_searchable_by(account) + case account.searchability + when 'public' + [COLLECTIONS[:public]] + when 'unlisted', 'private' + [account_followers_url(account)] + else + [] + end + end end diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb index 05b818f73a..70656f9c8d 100644 --- a/app/lib/status_filter.rb +++ b/app/lib/status_filter.rb @@ -3,10 +3,11 @@ class StatusFilter attr_reader :status, :account - def initialize(status, account, preloaded_relations = {}) + def initialize(status, account, preloaded_relations = {}, preloaded_status_relations = {}) @status = status @account = account @preloaded_relations = preloaded_relations + @preloaded_status_relations = preloaded_status_relations end def filtered? @@ -15,12 +16,6 @@ class StatusFilter blocked_by_policy? || (account_present? && filtered_status?) || silenced_account? end - def search_filtered? - return false if !account.nil? && account.id == status.account_id - - blocked_by_policy_search? || (account_present? && filtered_status?) || silenced_account? - end - private def account_present? @@ -59,15 +54,7 @@ class StatusFilter !policy_allows_show? end - def blocked_by_policy_search? - !policy_allows_search? - end - def policy_allows_show? - StatusPolicy.new(account, status, @preloaded_relations).show? - end - - def policy_allows_search? - StatusPolicy.new(account, status, @preloaded_relations).search? + StatusPolicy.new(account, status, @preloaded_relations, @preloaded_status_relations).show? end end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 5fb7655a9b..335a66bcc5 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -18,6 +18,7 @@ class UserSettingsDecorator user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails') user.settings['interactions'] = merged_interactions if change?('interactions') user.settings['default_privacy'] = default_privacy_preference if change?('setting_default_privacy') + user.settings['default_searchability']=default_searchability_preference if change?('setting_default_searchability') user.settings['default_sensitive'] = default_sensitive_preference if change?('setting_default_sensitive') user.settings['default_language'] = default_language_preference if change?('setting_default_language') user.settings['unfollow_modal'] = unfollow_modal_preference if change?('setting_unfollow_modal') @@ -53,6 +54,10 @@ class UserSettingsDecorator settings['setting_default_privacy'] end + def default_searchability_preference + settings['setting_default_searchability'] + end + def default_sensitive_preference boolean_cast_setting 'setting_default_sensitive' end diff --git a/app/models/account.rb b/app/models/account.rb index ef9aa29b4a..9f5d2422d7 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -51,6 +51,7 @@ # reviewed_at :datetime # requested_review_at :datetime # group_allow_private_message :boolean +# searchability :integer default(0), not null # class Account < ApplicationRecord @@ -82,6 +83,7 @@ class Account < ApplicationRecord enum protocol: { ostatus: 0, activitypub: 1 } enum suspension_origin: { local: 0, remote: 1 }, _prefix: true + enum searchability: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :searchability validates :username, presence: true validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? } diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 1898516b00..6e739d4961 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -237,6 +237,10 @@ module AccountInteractions status.proper.favourites.where(account: self).exists? end + def emoji_reactioned?(status) + status.proper.emoji_reactions.where(account: self).exists? + end + def bookmarked?(status) status.proper.bookmarks.where(account: self).exists? end diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index 51a9058a84..6609c193e1 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -29,6 +29,7 @@ class PublicFeed scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? scope.merge!(language_scope) if account&.chosen_languages.present? + scope.merge!(public_searchable_scope) if local_only? && !account? scope.cache_ids.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) end @@ -97,6 +98,10 @@ class PublicFeed Status.where(language: account.chosen_languages) end + def public_searchable_scope + Status.where(searchability: 'public').or(Status.where(searchability: nil).merge(Account.where(searchability: 'public'))) + end + def account_filters_scope Status.not_excluded_by_account(account).tap do |scope| scope.merge!(Status.not_domain_blocked_by_account(account)) unless local_only? diff --git a/app/models/status.rb b/app/models/status.rb index 800147f26a..1baca9b737 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -27,6 +27,7 @@ # edited_at :datetime # trendable :boolean # ordered_media_attachment_ids :bigint(8) is an Array +# searchability :integer # require 'ostruct' @@ -52,6 +53,7 @@ class Status < ApplicationRecord update_index('statuses', :proper) enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :visibility + enum searchability: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :searchability belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -115,6 +117,7 @@ class Status < ApplicationRecord scope :tagged_with_none, lambda { |tag_ids| where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids) } + scope :unset_searchability, -> { where(searchability: nil, reblog_of_id: nil) } cache_associated :application, :media_attachments, @@ -255,6 +258,11 @@ class Status < ApplicationRecord ordered_media_attachments.any? end + def expired? + false + # !expired_at.nil? + end + def with_preview_card? preview_cards.any? end @@ -349,6 +357,10 @@ class Status < ApplicationRecord attributes['trendable'].nil? && account.requires_review_notification? end + def compute_searchability + searchability || Status.searchabilities.invert.fetch([Account.searchabilities[account.searchability], Status.visibilities[visibility] || 0].max, nil) || 'direct' + end + after_create_commit :increment_counter_caches after_destroy_commit :decrement_counter_caches @@ -358,6 +370,7 @@ class Status < ApplicationRecord before_validation :prepare_contents, if: :local? before_validation :set_reblog before_validation :set_visibility + before_validation :set_searchability before_validation :set_conversation before_validation :set_local @@ -370,6 +383,10 @@ class Status < ApplicationRecord visibilities.keys - %w(direct limited) end + def selectable_searchabilities + searchabilities.keys - %w(public_unlisted limited) + end + def favourites_map(status_ids, account_id) Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true } end @@ -538,6 +555,12 @@ class Status < ApplicationRecord self.sensitive = false if sensitive.nil? end + def set_searchability + return if searchability.nil? + + self.searchability = [Status.searchabilities[searchability], Status.visibilities[visibility == 'public_unlisted' ? 'public' : visibility]].max + end + def set_conversation self.thread = thread.reblog if thread&.reblog? diff --git a/app/models/user.rb b/app/models/user.rb index d56a9b9ca1..9323e0d181 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,6 +39,7 @@ # webauthn_id :string # sign_up_ip :inet # role_id :bigint(8) +# settings :text # class User < ApplicationRecord @@ -314,6 +315,10 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end + def setting_default_searchability + settings.default_searchability || 'public' + end + def allows_report_emails? settings.notification_emails['report'] end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 74709647a8..a33d1f1112 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true class StatusPolicy < ApplicationPolicy - def initialize(current_account, record, preloaded_relations = {}) + def initialize(current_account, record, preloaded_relations = {}, preloaded_status_relations = {}) super(current_account, record) @preloaded_relations = preloaded_relations + @preloaded_status_relations = preloaded_status_relations end + delegate :reply?, :expired?, to: :record + def show? return false if author.suspended? @@ -19,18 +22,6 @@ class StatusPolicy < ApplicationPolicy end end - def search? - return false if author.suspended? - - if requires_mention? - owned? || mention_exists? - elsif !public? - owned? || following_author? || mention_exists? - else - current_account.nil? || (!author_blocking? && !author_blocking_domain?) - end - end - def reblog? !requires_mention? && (!private? || owned?) && show? && !blocking_author? end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb index b87cff51e1..492f658030 100644 --- a/app/presenters/initial_state_presenter.rb +++ b/app/presenters/initial_state_presenter.rb @@ -2,7 +2,7 @@ class InitialStatePresenter < ActiveModelSerializers::Model attributes :settings, :push_subscription, :token, - :current_account, :admin, :owner, :text, :visibility, + :current_account, :admin, :owner, :text, :visibility, :searchability, :disabled_account, :moved_to_account def role diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index e6dd8040e9..0a045050af 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -7,13 +7,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer context :security context_extensions :manually_approves_followers, :featured, :also_known_as, - :moved_to, :property_value, :discoverable, :olm, :suspended + :moved_to, :property_value, :discoverable, :olm, :suspended, :searchable_by attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :featured_tags, :preferred_username, :name, :summary, :url, :manually_approves_followers, - :discoverable, :published + :discoverable, :published, :searchable_by has_one :public_key, serializer: ActivityPub::PublicKeySerializer @@ -162,6 +162,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer object.created_at.midnight.iso8601 end + def searchable_by + ActivityPub::TagManager.instance.account_searchable_by(object) + end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 27e058199d..e07eff2bf3 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,13 +3,13 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by attributes :id, :type, :summary, :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, :atom_uri, :in_reply_to_atom_uri, - :conversation + :conversation, :searchable_by attribute :content attribute :content_map, if: :language? @@ -138,6 +138,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end end + def searchable_by + ActivityPub::TagManager.instance.searchable_by(object) + end + def local? object.account.local? end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 769ba653ed..9767d3a5ac 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -69,10 +69,11 @@ class InitialStateSerializer < ActiveModel::Serializer store = {} if object.current_account - store[:me] = object.current_account.id.to_s - store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy - store[:default_sensitive] = object.current_account.user.setting_default_sensitive - store[:default_language] = object.current_account.user.preferred_posting_language + store[:me] = object.current_account.id.to_s + store[:default_privacy] = object.visibility || object.current_account.user.setting_default_privacy + store[:default_searchability] = object.searchability || object.current_account.user.setting_default_searchability + store[:default_sensitive] = object.current_account.user.setting_default_sensitive + store[:default_language] = object.current_account.user.preferred_posting_language end store[:text] = object.text if object.text diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 2845470be0..bfbb484171 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer include FormattingHelper attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at, - :note, :url, :avatar, :avatar_static, :header, :header_static, + :note, :url, :avatar, :avatar_static, :header, :header_static, :searchability, :followers_count, :following_count, :statuses_count, :last_status_at has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested? diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index eb98750605..15b4cd425b 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -100,6 +100,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer :visibility_public_unlisted, :enable_wide_emoji, :enable_wide_emoji_reaction, + :searchability, ] capabilities << :profile_search unless Chewy.enabled? diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index d3c96a77e2..d4677a2dbe 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -5,7 +5,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility, :language, :translatable, - :uri, :url, :replies_count, :reblogs_count, + :uri, :url, :replies_count, :reblogs_count, :searchability, :favourites_count, :emoji_reactions, :edited_at attribute :favourited, if: :current_user? @@ -65,6 +65,10 @@ class REST::StatusSerializer < ActiveModel::Serializer end end + def searchability + object.compute_searchability + end + def sensitive if current_user? && current_user.account_id == object.account_id object.sensitive diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 603e4cf48b..e9a4d3943e 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -29,6 +29,7 @@ class ActivityPub::ProcessAccountService < BaseService @account ||= Account.find_remote(@username, @domain) @old_public_key = @account&.public_key @old_protocol = @account&.protocol + @old_searchability = @account&.searchability @suspension_changed = false if @account.nil? @@ -112,6 +113,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.fields = property_values || {} @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } @account.discoverable = @json['discoverable'] || false + @account.searchability = searchability_from_audience end def set_fetchable_key! @@ -150,6 +152,10 @@ class ActivityPub::ProcessAccountService < BaseService end end + def after_searchability_change! + SearchabilityUpdateWorker.perform_async(@account.id) if @account.statuses.unset_searchability.exists? + end + def after_protocol_change! ActivityPub::PostUpgradeWorker.perform_async(@account.domain) end @@ -224,6 +230,24 @@ class ActivityPub::ProcessAccountService < BaseService end end + def audience_searchable_by + return nil if @json['searchableBy'].nil? + + @audience_searchable_by = as_array(@json['searchableBy']).map { |x| value_or_id(x) } + end + + def searchability_from_audience + if audience_searchable_by.nil? + :direct + elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } + :public + elsif audience_searchable_by.include?(@account.followers_url) + :private + else + :direct + end + end + def property_values return unless @json['attachment'].is_a?(Array) @@ -310,6 +334,10 @@ class ActivityPub::ProcessAccountService < BaseService !@old_protocol.nil? && @old_protocol != @account.protocol end + def searchability_changed? + !@old_searchability.nil? && @old_searchability != @account.searchability + end + def process_tags return if @json['tag'].blank? diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 73d92752b1..71f109db08 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -22,6 +22,7 @@ class PostStatusService < BaseService # @option [Status] :thread Optional status to reply to # @option [Boolean] :sensitive # @option [String] :visibility + # @option [String] :searchability # @option [String] :spoiler_text # @option [String] :language # @option [String] :scheduled_at @@ -66,12 +67,28 @@ class PostStatusService < BaseService @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted) && @account.silenced? + @searchability= searchability @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError raise ActiveRecord::RecordInvalid end + def searchability + case @options[:searchability]&.to_sym + when :public + case @visibility&.to_sym when :public, :public_unlisted then :public when :unlisted, :private then :private else :direct end + when :unlisted + case @visibility&.to_sym when :public, :public_unlisted, :unlisted then :unlisted when :private then :private else :direct end + when :private + case @visibility&.to_sym when :public, :public_unlisted, :unlisted, :private then :private else :direct end + when nil + @account.searchability + else + :direct + end + end + def process_status! @status = @account.statuses.new(status_attributes) process_mentions_service.call(@status, save_records: false) @@ -196,6 +213,7 @@ class PostStatusService < BaseService sensitive: @sensitive, spoiler_text: @options[:spoiler_text] || '', visibility: @visibility, + searchability: @searchability, language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale), application: @options[:application], rate_limit: @options[:with_rate_limit], diff --git a/app/services/search_service.rb b/app/services/search_service.rb index a7adada258..2de78d8d2f 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -8,6 +8,7 @@ class SearchService < BaseService @limit = limit.to_i @offset = options[:type].blank? ? 0 : options[:offset].to_i @resolve = options[:resolve] || false + @searchability = options[:searchability] || @account.user&.setting_default_searchability || 'private' default_results.tap do |results| next if @query.blank? || @limit.zero? @@ -35,11 +36,26 @@ class SearchService < BaseService end def perform_statuses_search! - # definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id })).order(id: :desc) - definition = parsed_query.apply(StatusesIndex).order(id: :desc) + privacy_definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id })) + # 'private' searchability posts are NOT in here because it's already added at previous line. + case @searchability + when 'public' + privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'public' })) + privacy_definition = privacy_definition.or(StatusesIndex.filter(terms: { searchability: %w(unlisted) }).filter(terms: { account_id: following_account_ids })) unless following_account_ids.empty? + privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'direct' }).filter(term: { account_id: @account.id })) + when 'unlisted', 'private' + privacy_definition = privacy_definition.or(StatusesIndex.filter(terms: { searchability: %w(public unlisted) }).filter(terms: { account_id: following_account_ids })) unless following_account_ids.empty? + privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'direct' }).filter(term: { account_id: @account.id })) + when 'direct' + privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'direct' }).filter(term: { account_id: @account.id })) + end + + definition = parsed_query.apply(StatusesIndex).order(id: :desc) definition = definition.filter(term: { account_id: @options[:account_id] }) if @options[:account_id].present? + definition = definition.and(privacy_definition) + if @options[:min_id].present? || @options[:max_id].present? range = {} range[:gt] = @options[:min_id].to_i if @options[:min_id].present? @@ -50,9 +66,10 @@ class SearchService < BaseService results = definition.limit(@limit).offset(@offset).objects.compact account_ids = results.map(&:account_id) account_domains = results.map(&:account_domain) - preloaded_relations = relations_map_for_account(@account, account_ids, account_domains) + account_relations = relations_map_for_account(@account, account_ids, account_domains) # old name: preloaded_relations + status_relations = relations_map_for_status(@account, results) - results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).search_filtered? } + results.reject { |status| StatusFilter.new(status, @account, account_relations, status_relations).filtered? } rescue Faraday::ConnectionFailed, Parslet::ParseFailed [] end @@ -122,7 +139,28 @@ class SearchService < BaseService } end + def relations_map_for_status(account, statuses) + presenter = StatusRelationshipsPresenter.new(statuses, account) + { + reblogs_map: presenter.reblogs_map, + favourites_map: presenter.favourites_map, + bookmarks_map: presenter.bookmarks_map, + emoji_reactions_map: presenter.emoji_reactions_map, + mutes_map: presenter.mutes_map, + pins_map: presenter.pins_map, + } + end + def parsed_query SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query)) end + + def following_account_ids + return @following_account_ids if defined?(@following_account_ids) + + account_exists_sql = Account.where('accounts.id = follows.target_account_id').where(searchability: %w(public unlisted private)).reorder(nil).select(1).to_sql + status_exists_sql = Status.where('statuses.account_id = follows.target_account_id').where(reblog_of_id: nil).where(searchability: %w(public unlisted private)).reorder(nil).select(1).to_sql + following_accounts = Follow.where(account_id: @account.id).merge(Account.where("EXISTS (#{account_exists_sql})").or(Account.where("EXISTS (#{status_exists_sql})"))) + @following_account_ids = following_accounts.pluck(:target_account_id) + end end diff --git a/app/services/searchability_update_service.rb b/app/services/searchability_update_service.rb new file mode 100644 index 0000000000..7ced32f22c --- /dev/null +++ b/app/services/searchability_update_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class SearchabilityUpdateService < BaseService + def call(account) + statuses = account.statuses.unset_searchability + + return unless statuses.exists? + + ids = statuses.pluck(:id) + + if account.public_searchability? + statuses.update_all('searchability = CASE visibility WHEN 0 THEN 0 WHEN 10 THEN 0 WHEN 1 THEN 2 WHEN 2 THEN 2 ELSE 3 END, updated_at = CURRENT_TIMESTAMP') + elsif account.unlisted_searchability? + statuses.update_all('searchability = CASE visibility WHEN 0 THEN 1 WHEN 10 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 2 ELSE 3 END, updated_at = CURRENT_TIMESTAMP') + elsif account.private_searchability? + statuses.update_all('searchability = CASE WHEN visibility IN (0, 1, 2, 10) THEN 2 ELSE 3 END, updated_at = CURRENT_TIMESTAMP') + else + statuses.update_all('searchability = 3, updated_at = CURRENT_TIMESTAMP') + end + + return unless Chewy.enabled? + + ids.each_slice(100) do |chunk_ids| + StatusesIndex.import chunk_ids, update_fields: [:searchability] + end + end +end diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index 44f4af2eba..310a708e6a 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -22,6 +22,9 @@ .fields-group.fields-row__column.fields-row__column-6 = f.input :setting_default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false + .fields-group + = f.input :setting_default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false + .fields-group = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label diff --git a/app/workers/searchability_update_worker.rb b/app/workers/searchability_update_worker.rb new file mode 100644 index 0000000000..ce288f813a --- /dev/null +++ b/app/workers/searchability_update_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class SearchabilityUpdateWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', lock: :until_executed + + def perform(account_id) + SearchabilityUpdateService.new.call(Account.find(account_id)) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/db/migrate/20230405121613_add_searchability_to_statuses.rb b/db/migrate/20230405121613_add_searchability_to_statuses.rb new file mode 100644 index 0000000000..313509afb8 --- /dev/null +++ b/db/migrate/20230405121613_add_searchability_to_statuses.rb @@ -0,0 +1,5 @@ +class AddSearchabilityToStatuses < ActiveRecord::Migration[6.1] + def change + add_column :statuses, :searchability, :integer + end +end diff --git a/db/migrate/20230405121625_add_searchability_to_accounts.rb b/db/migrate/20230405121625_add_searchability_to_accounts.rb new file mode 100644 index 0000000000..758d052993 --- /dev/null +++ b/db/migrate/20230405121625_add_searchability_to_accounts.rb @@ -0,0 +1,5 @@ +class AddSearchabilityToAccounts < ActiveRecord::Migration[6.1] + def change + add_column :accounts, :searchability, :integer, null: false, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index fa8a541745..ad81bd6a00 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_03_20_234918) do +ActiveRecord::Schema.define(version: 2023_04_05_121625) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -188,6 +188,7 @@ ActiveRecord::Schema.define(version: 2023_03_20_234918) do t.datetime "reviewed_at" t.datetime "requested_review_at" t.boolean "group_allow_private_message" + t.integer "searchability", default: 0, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id", where: "(moved_to_account_id IS NOT NULL)" @@ -969,6 +970,7 @@ ActiveRecord::Schema.define(version: 2023_03_20_234918) do t.datetime "edited_at" t.boolean "trendable" t.bigint "ordered_media_attachment_ids", array: true + t.integer "searchability" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)" t.index ["account_id"], name: "index_statuses_on_account_id" t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)" @@ -1089,6 +1091,7 @@ ActiveRecord::Schema.define(version: 2023_03_20_234918) do t.inet "sign_up_ip" t.boolean "skip_sign_in_token" t.bigint "role_id" + t.text "settings" t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)" From 489eeb4aa7945b1d3a58d04c938ca8dc3807f52a Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 04:42:24 +0900 Subject: [PATCH 02/11] Fix activitypub searchability error --- app/lib/activitypub/activity/create.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index b926b3a02b..97a9461f41 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -464,4 +464,26 @@ class ActivityPub::Activity::Create < ActivityPub::Activity :direct end end + + def visibility_from_audience + if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) } + :public + elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } + :unlisted + elsif audience_to.include?(@account.followers_url) + :private + else + :direct + end + end + + def visibility_from_audience_with_silence + visibility = visibility_from_audience + + if @account.silenced? && %i(public).include?(visibility) + :unlisted + else + visibility + end + end end From 5b4661c2f71f003b2301e5c29b0d4c87af987f1f Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 10:06:56 +0900 Subject: [PATCH 03/11] Fix mention_urls error --- app/lib/activitypub/tag_manager.rb | 20 +++++++++++++++++++ app/services/search_service.rb | 2 +- ...405121625_add_searchability_to_accounts.rb | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 2d8e9b5008..4ff3567c8e 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -212,4 +212,24 @@ class ActivityPub::TagManager [] end end + + def mentions_uris(status) + if status.account.silenced? + # Only notify followers if the account is locally silenced + account_ids = status.active_mentions.pluck(:account_id) + uris = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| + result << uri_for(account) + result << account_followers_url(account) if account.group? + end + uris.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| + result << uri_for(request.account) + result << account_followers_url(request.account) if request.account.group? + end) + else + status.active_mentions.each_with_object([]) do |mention, result| + result << uri_for(mention.account) + result << account_followers_url(mention.account) if mention.account.group? + end + end + end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 2de78d8d2f..94037e8566 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -42,7 +42,7 @@ class SearchService < BaseService case @searchability when 'public' privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'public' })) - privacy_definition = privacy_definition.or(StatusesIndex.filter(terms: { searchability: %w(unlisted) }).filter(terms: { account_id: following_account_ids })) unless following_account_ids.empty? + privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'unlisted' }).filter(terms: { account_id: following_account_ids })) unless following_account_ids.empty? privacy_definition = privacy_definition.or(StatusesIndex.filter(term: { searchability: 'direct' }).filter(term: { account_id: @account.id })) when 'unlisted', 'private' privacy_definition = privacy_definition.or(StatusesIndex.filter(terms: { searchability: %w(public unlisted) }).filter(terms: { account_id: following_account_ids })) unless following_account_ids.empty? diff --git a/db/migrate/20230405121625_add_searchability_to_accounts.rb b/db/migrate/20230405121625_add_searchability_to_accounts.rb index 758d052993..9af68545e3 100644 --- a/db/migrate/20230405121625_add_searchability_to_accounts.rb +++ b/db/migrate/20230405121625_add_searchability_to_accounts.rb @@ -1,5 +1,5 @@ class AddSearchabilityToAccounts < ActiveRecord::Migration[6.1] def change - add_column :accounts, :searchability, :integer, null: false, default: 0 + add_column :accounts, :searchability, :integer, null: false, default: 3 end end From 165882a985ef3531db9b78a199d3973b13625f3d Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 12:35:15 +0900 Subject: [PATCH 04/11] Wip search visibility --- app/controllers/api/v2/search_controller.rb | 2 +- app/services/search_service.rb | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index 6098fc6abd..c9a8050ead 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -39,6 +39,6 @@ class Api::V2::SearchController < Api::BaseController end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :searchability) end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 94037e8566..85710f63ba 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -8,7 +8,7 @@ class SearchService < BaseService @limit = limit.to_i @offset = options[:type].blank? ? 0 : options[:offset].to_i @resolve = options[:resolve] || false - @searchability = options[:searchability] || @account.user&.setting_default_searchability || 'private' + @searchability = options[:searchability] || 'public' default_results.tap do |results| next if @query.blank? || @limit.zero? @@ -69,7 +69,7 @@ class SearchService < BaseService account_relations = relations_map_for_account(@account, account_ids, account_domains) # old name: preloaded_relations status_relations = relations_map_for_status(@account, results) - results.reject { |status| StatusFilter.new(status, @account, account_relations, status_relations).filtered? } + results.reject { |status| StatusFilter.new(status, @account, account_relations).filtered? } rescue Faraday::ConnectionFailed, Parslet::ParseFailed [] end @@ -139,18 +139,6 @@ class SearchService < BaseService } end - def relations_map_for_status(account, statuses) - presenter = StatusRelationshipsPresenter.new(statuses, account) - { - reblogs_map: presenter.reblogs_map, - favourites_map: presenter.favourites_map, - bookmarks_map: presenter.bookmarks_map, - emoji_reactions_map: presenter.emoji_reactions_map, - mutes_map: presenter.mutes_map, - pins_map: presenter.pins_map, - } - end - def parsed_query SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query)) end From 502761c0add63d05739bf9ce2e21f33a02c0f3b6 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 13:10:29 +0900 Subject: [PATCH 05/11] Wip: Fix searchability value --- app/lib/activitypub/activity/create.rb | 4 ++-- app/lib/activitypub/tag_manager.rb | 4 +++- app/services/search_service.rb | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 97a9461f41..d60ab5b113 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -443,9 +443,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } :public elsif audience_searchable_by.include?(@account.followers_url) - :private + :unlisted # Followers only in kmyblue (generics: private) else - :direct + :private # Reaction only in kmyblue (generics: direct) end end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 4ff3567c8e..bd9706fc42 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -191,8 +191,10 @@ class ActivityPub::TagManager case status.compute_searchability when 'public' [COLLECTIONS[:public]] - when 'unlisted', 'private' + when 'unlisted' # Followers only in kmyblue (generics: private) [account_followers_url(status.account)] + when 'private' # Reaction only in kmyblue (generics: direct) + [] when 'limited' status.conversation_id.present? ? [uri_for(status.conversation)] : [] else diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 85710f63ba..b7d889904a 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -67,7 +67,6 @@ class SearchService < BaseService account_ids = results.map(&:account_id) account_domains = results.map(&:account_domain) account_relations = relations_map_for_account(@account, account_ids, account_domains) # old name: preloaded_relations - status_relations = relations_map_for_status(@account, results) results.reject { |status| StatusFilter.new(status, @account, account_relations).filtered? } rescue Faraday::ConnectionFailed, Parslet::ParseFailed From c358cb1f4e78984184030c3838f708b721fbce63 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 13:20:14 +0900 Subject: [PATCH 06/11] Wip: reset db default value --- app/models/account.rb | 2 +- db/migrate/20230405121625_add_searchability_to_accounts.rb | 2 +- .../20230406041523_change_searchability_default_value.rb | 5 +++++ db/schema.rb | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20230406041523_change_searchability_default_value.rb diff --git a/app/models/account.rb b/app/models/account.rb index 9f5d2422d7..8164df116a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -51,7 +51,7 @@ # reviewed_at :datetime # requested_review_at :datetime # group_allow_private_message :boolean -# searchability :integer default(0), not null +# searchability :integer default("private"), not null # class Account < ApplicationRecord diff --git a/db/migrate/20230405121625_add_searchability_to_accounts.rb b/db/migrate/20230405121625_add_searchability_to_accounts.rb index 9af68545e3..758d052993 100644 --- a/db/migrate/20230405121625_add_searchability_to_accounts.rb +++ b/db/migrate/20230405121625_add_searchability_to_accounts.rb @@ -1,5 +1,5 @@ class AddSearchabilityToAccounts < ActiveRecord::Migration[6.1] def change - add_column :accounts, :searchability, :integer, null: false, default: 3 + add_column :accounts, :searchability, :integer, null: false, default: 0 end end diff --git a/db/migrate/20230406041523_change_searchability_default_value.rb b/db/migrate/20230406041523_change_searchability_default_value.rb new file mode 100644 index 0000000000..b3ca38387c --- /dev/null +++ b/db/migrate/20230406041523_change_searchability_default_value.rb @@ -0,0 +1,5 @@ +class ChangeSearchabilityDefaultValue < ActiveRecord::Migration[6.1] + def change + change_column_default :accounts, :searchability, from: 0, to: 2 + end +end diff --git a/db/schema.rb b/db/schema.rb index ad81bd6a00..57541246c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_04_05_121625) do +ActiveRecord::Schema.define(version: 2023_04_06_041523) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -188,7 +188,7 @@ ActiveRecord::Schema.define(version: 2023_04_05_121625) do t.datetime "reviewed_at" t.datetime "requested_review_at" t.boolean "group_allow_private_message" - t.integer "searchability", default: 0, null: false + t.integer "searchability", default: 2, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id", where: "(moved_to_account_id IS NOT NULL)" From 2420259db1c6954a32b28f31d3ed971312665398 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 13:27:27 +0900 Subject: [PATCH 07/11] Wip: change searchability --- app/services/activitypub/process_account_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index e9a4d3943e..e812314f4e 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -233,18 +233,18 @@ class ActivityPub::ProcessAccountService < BaseService def audience_searchable_by return nil if @json['searchableBy'].nil? - @audience_searchable_by = as_array(@json['searchableBy']).map { |x| value_or_id(x) } + @audience_searchable_by_processaccountservice = as_array(@json['searchableBy']).map { |x| value_or_id(x) } end def searchability_from_audience if audience_searchable_by.nil? - :direct + nil elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } :public elsif audience_searchable_by.include?(@account.followers_url) - :private + :unlisted # Followers only in kmyblue (generics: private) else - :direct + :private # Reaction only in kmyblue (generics: direct) end end From af7c3259989ff14e17f61a271e5d32d75bc218d4 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 13:34:59 +0900 Subject: [PATCH 08/11] Wip: Fix status searchability --- app/lib/activitypub/activity/create.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index d60ab5b113..9e08064191 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -458,10 +458,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity if searchability === visibility searchability - elsif [:public, :private].include?(searchability) && [:public, :unlisted].include?(visibility) - :private + elsif [:public, :unlisted].include?(searchability) && [:public, :unlisted].include?(visibility) # unlisted is Followers only in kmyblue (generics: private) + :unlisted else - :direct + :private # Reaction only in kmyblue (generics: direct) end end From 60d54b847e13d3885051e9fed86f841740b7f616 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 13:50:08 +0900 Subject: [PATCH 09/11] Fix searchability values --- app/models/status.rb | 7 ++++++- app/services/post_status_service.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/status.rb b/app/models/status.rb index 1baca9b737..06c7cd5e46 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -358,7 +358,12 @@ class Status < ApplicationRecord end def compute_searchability - searchability || Status.searchabilities.invert.fetch([Account.searchabilities[account.searchability], Status.visibilities[visibility] || 0].max, nil) || 'direct' + # Fedibird code + #searchability || Status.searchabilities.invert.fetch([Account.searchabilities[account.searchability], Status.visibilities[visibility] || 0].max, nil) || 'direct' + # Reactions only (generic: direct) + return searchability if searchability + return account.searchability if account.local? && account.searchability + 'private' end after_create_commit :increment_counter_caches diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 71f109db08..1bf094116f 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -77,7 +77,7 @@ class PostStatusService < BaseService def searchability case @options[:searchability]&.to_sym when :public - case @visibility&.to_sym when :public, :public_unlisted then :public when :unlisted, :private then :private else :direct end + case @visibility&.to_sym when :public, :public_unlisted then :public when :unlisted then :unlisted when :private then :private else :direct end when :unlisted case @visibility&.to_sym when :public, :public_unlisted, :unlisted then :unlisted when :private then :private else :direct end when :private From 6f6508d275e7e87b77c84b91d9ad3c79dca87ef2 Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 14:07:31 +0900 Subject: [PATCH 10/11] Fix languages and styles --- app/javascript/mastodon/locales/en.json | 9 +++++++++ app/javascript/mastodon/locales/ja.json | 9 +++++++++ app/javascript/styles/mastodon/components.scss | 2 ++ config/locales/en.yml | 10 ++++++++++ config/locales/ja.yml | 10 ++++++++++ config/locales/simple_form.en.yml | 1 + config/locales/simple_form.ja.yml | 1 + 7 files changed, 42 insertions(+) diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index fdfb2108ed..c7fccb1ee2 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -527,6 +527,15 @@ "report_notification.open": "Open report", "search.placeholder": "Search", "search.search_or_paste": "Search or paste URL", + "searchability.change": "Change post searchability", + "searchability.direct.long": "Nobody can find, but you can", + "searchability.direct.short": "Self only", + "searchability.private.long": "Reacter of this post can find", + "searchability.private.short": "Reactionners", + "searchability.public.long": "Anyone can find", + "searchability.public.short": "Public", + "searchability.unlisted.long": "Your followers can find", + "searchability.unlisted.short": "Followers", "search_popout.search_format": "Advanced search format", "search_popout.tips.full_text": "Simple text returns posts you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.hashtag": "hashtag", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 7879a690a8..edea587f5e 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -525,6 +525,15 @@ "report_notification.open": "通報を開く", "search.placeholder": "検索", "search.search_or_paste": "検索またはURLを入力", + "searchability.change": "検索許可範囲を変更", + "searchability.direct.long": "この投稿はあなたしか検索できません", + "searchability.direct.short": "自分のみ", + "searchability.private.long": "この投稿にリアクションした人だけが検索できます", + "searchability.private.short": "リアクションした人のみ", + "searchability.public.long": "この投稿は誰でも検索できます", + "searchability.public.short": "全て", + "searchability.unlisted.long": "この投稿はあなたのフォロワーしか検索できません", + "searchability.unlisted.short": "フォロワーのみ", "search_popout.search_format": "高度な検索フォーマット", "search_popout.tips.full_text": "表示名やユーザー名、ハッシュタグのほか、あなたの投稿やお気に入り、ブーストした投稿、返信に一致する単純なテキスト。", "search_popout.tips.hashtag": "ハッシュタグ", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f9803dea75..7443cfdb78 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4764,6 +4764,8 @@ a.status-card.compact:hover { position: absolute; right: 0; bottom: 0; + color: purple; + pointer-events: none; } } diff --git a/config/locales/en.yml b/config/locales/en.yml index b0606b62c0..97cefc852b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -795,6 +795,7 @@ en: remove: Remove post remove_media: Remove medias remove_history: Remove edit history + searchability: Searchability status_changed: Post changed title: Account posts trending: Trending @@ -1562,6 +1563,15 @@ en: one: "%{count} vote" other: "%{count} votes" vote: Vote + searchabilities: + direct: Self only + direct_long: Nobody can find, but you can + private: Reactionners + private_long: Reacter of this post can find + public: Public + public_long: Anyone can find + unlisted: Followers only + unlisted_long: Your followers can find show_more: Show more show_newer: Show newer show_older: Show older diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 3f42663bea..561a38e5e6 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -781,6 +781,7 @@ ja: remove: 投稿を削除 remove_media: メディアを削除 remove_history: 編集履歴を削除 + searchability: 検索許可 status_changed: 投稿を変更しました title: 投稿一覧 trending: トレンド @@ -1519,6 +1520,15 @@ ja: total_votes: other: "%{count}票" vote: 投票 + searchabilities: + direct: 自分のみ + direct_long: この投稿はあなたしか検索できません + private: リアクションした人 + private_long: この投稿にリアクションした人しか検索できません + public: 公開 + public_long: この投稿は誰でも検索できます + unlisted: フォロワーのみ + unlisted_long: この投稿はフォロワーのみが検索できます show_more: もっと見る show_newer: 新しいものを表示 show_older: 古いものを表示 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 96978578bb..00aa828ba2 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -206,6 +206,7 @@ en: setting_crop_images: Crop images in non-expanded posts to 16x9 setting_default_language: Posting language setting_default_privacy: Posting privacy + setting_default_searchability: Searchability setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a post setting_disable_swiping: Disable swiping motions diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 8fc8dd265e..3ffa286218 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -206,6 +206,7 @@ ja: setting_crop_images: 投稿の詳細以外では画像を16:9に切り抜く setting_default_language: 投稿する言語 setting_default_privacy: 投稿の公開範囲 + setting_default_searchability: 投稿の検索を許可する範囲 setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_delete_modal: 投稿を削除する前に確認ダイアログを表示する setting_disable_swiping: スワイプでの切り替えを無効にする From 78b7dc258fb357c4f61e4bdb1ca2d980da65c7bd Mon Sep 17 00:00:00 2001 From: KMY Date: Thu, 6 Apr 2023 15:09:24 +0900 Subject: [PATCH 11/11] Fix account searchability nil error --- app/services/activitypub/process_account_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index e812314f4e..46813efc62 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -238,7 +238,7 @@ class ActivityPub::ProcessAccountService < BaseService def searchability_from_audience if audience_searchable_by.nil? - nil + :private elsif audience_searchable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) } :public elsif audience_searchable_by.include?(@account.followers_url)