diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index f8d91c5f7f..9a2e0d5d17 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -56,11 +56,11 @@ class Api::V1::FiltersController < Api::BaseController end def resource_params - params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :whole_word, context: []) + params.permit(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_profile, :whole_word, context: []) end def filter_params - resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :context) + resource_params.slice(:phrase, :expires_in, :irreversible, :exclude_follows, :exclude_localusers, :with_profile, :context) end def keyword_params diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 51e778e1d6..fda4c0c215 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -43,6 +43,6 @@ class Api::V2::FiltersController < Api::BaseController end def resource_params - params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + params.permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :with_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 20b8135908..dc47bd77ec 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -47,6 +47,6 @@ class FiltersController < ApplicationController end def resource_params - params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]]) + params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]]) end end diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 077c5272a5..cb8fb46466 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -29,7 +29,6 @@ module ContextHelper limited_scope: { 'kmyblue' => 'http://kmy.blue/ns#', 'limitedScope' => 'kmyblue:limitedScope' }, other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' }, references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } }, - quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, keywords: { 'schema' => 'http://schema.org#', 'keywords' => 'schema:keywords' }, license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, diff --git a/app/helpers/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb index 279505bec8..39669c0680 100644 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ b/app/helpers/kmyblue_capabilities_helper.rb @@ -15,7 +15,6 @@ module KmyblueCapabilitiesHelper kmyblue_limited_scope kmyblue_antenna kmyblue_bookmark_category - kmyblue_quote kmyblue_searchability_limited kmyblue_circle_history kmyblue_list_notification @@ -41,7 +40,6 @@ module KmyblueCapabilitiesHelper capabilities = %i( enable_wide_emoji status_reference - quote emoji_keywords circle ) diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index fc165b1a1f..a527043940 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -70,10 +70,6 @@ export function importFetchedStatuses(statuses) { processStatus(status.reblog); } - if (status.quote?.id && !getState().getIn(['statuses', status.id])) { - processStatus(status.quote); - } - if (status.poll?.id) { pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index b643cf5613..93fc3b9fef 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -66,11 +66,6 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoiler_text = normalOldStatus.get('spoiler_text'); normalStatus.hidden = normalOldStatus.get('hidden'); - // for quoted post - if (!normalStatus.filtered && normalOldStatus.get('filtered')) { - normalStatus.filtered = normalOldStatus.get('filtered'); - } - if (normalOldStatus.get('translation')) { normalStatus.translation = normalOldStatus.get('translation'); } diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 87b842e51f..cc258db307 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -33,7 +33,7 @@ const messages = defineMessages({ message_poll: { id: 'notification.poll', defaultMessage: 'A poll you voted in has ended' }, message_reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your post' }, message_status: { id: 'notification.status', defaultMessage: '{name} just posted' }, - message_status_reference: { id: 'notification.status_reference', defaultMessage: '{name} quoted your post' }, + message_status_reference: { id: 'notification.status_reference', defaultMessage: '{name} linked your post' }, message_update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, }); diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 9d7974eda0..a2a6dfdf28 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -20,7 +20,6 @@ export interface ApiAccountOtherSettingsJSON { hide_followers_count: boolean; translatable_private: boolean; link_preview: boolean; - allow_quote: boolean; emoji_reaction_policy: | 'allow' | 'outside_only' @@ -34,7 +33,6 @@ export interface ApiAccountOtherSettingsJSON { export interface ApiServerFeaturesJSON { circle: boolean; emoji_reaction: boolean; - quote: boolean; status_reference: boolean; } diff --git a/app/javascript/mastodon/components/compacted_status.jsx b/app/javascript/mastodon/components/compacted_status.jsx deleted file mode 100644 index 6986bcd34c..0000000000 --- a/app/javascript/mastodon/components/compacted_status.jsx +++ /dev/null @@ -1,580 +0,0 @@ -import PropTypes from 'prop-types'; - -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { HotKeys } from 'react-hotkeys'; - -import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react'; -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; -import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; -import AttachmentList from 'mastodon/components/attachment_list'; -import { ContentWarning } from 'mastodon/components/content_warning'; -import { Icon } from 'mastodon/components/icon'; -import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; -import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router'; - -import Card from '../features/status/components/card'; -// We use the component (and not the container) since we do not want -// to use the progress bar to show download progress -import Bundle from '../features/ui/components/bundle'; -import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; -import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context'; -import { displayMedia } from '../initial_state'; - -import { Avatar } from './avatar'; -import { DisplayName } from './display_name'; -import { getHashtagBarForStatus } from './hashtag_bar'; -import { RelativeTimestamp } from './relative_timestamp'; -import StatusContent from './status_content'; -import { VisibilityIcon } from './visibility_icon'; - -const domParser = new DOMParser(); - -export const textForScreenReader = (intl, status, rebloggedByText = false) => { - const displayName = status.getIn(['account', 'display_name']); - - const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text'); - const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); - const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent; - - const values = [ - displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName, - spoilerText && status.get('hidden') ? spoilerText : contentText, - intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), - status.getIn(['account', 'acct']), - ]; - - if (rebloggedByText) { - values.push(rebloggedByText); - } - - return values.join(', '); -}; - -export const defaultMediaVisibility = (status) => { - if (!status) { - return undefined; - } - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - status = status.get('reblog'); - } - - return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); -}; - -const messages = defineMessages({ - limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited' }, - edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, -}); - -class CompactedStatus extends ImmutablePureComponent { - - static contextType = SensitiveMediaContext; - - static propTypes = { - status: ImmutablePropTypes.map, - account: ImmutablePropTypes.record, - previousId: PropTypes.string, - nextInReplyToId: PropTypes.string, - rootId: PropTypes.string, - onClick: PropTypes.func, - onReply: PropTypes.func, - onFavourite: PropTypes.func, - onEmojiReact: PropTypes.func, - onUnEmojiReact: PropTypes.func, - onReblog: PropTypes.func, - onReblogForceModal: PropTypes.func, - onDelete: PropTypes.func, - onDirect: PropTypes.func, - onMention: PropTypes.func, - onPin: PropTypes.func, - onOpenMedia: PropTypes.func, - onOpenVideo: PropTypes.func, - onBlock: PropTypes.func, - onAddFilter: PropTypes.func, - onEmbed: PropTypes.func, - onHeightChange: PropTypes.func, - onToggleHidden: PropTypes.func, - onToggleCollapsed: PropTypes.func, - onTranslate: PropTypes.func, - onInteractionModal: PropTypes.func, - muted: PropTypes.bool, - hidden: PropTypes.bool, - unread: PropTypes.bool, - onMoveUp: PropTypes.func, - onMoveDown: PropTypes.func, - showThread: PropTypes.bool, - getScrollPosition: PropTypes.func, - updateScrollBottom: PropTypes.func, - cacheMediaWidth: PropTypes.func, - cachedMediaWidth: PropTypes.number, - scrollKey: PropTypes.string, - skipPrepend: PropTypes.bool, - deployPictureInPicture: PropTypes.func, - unfocusable: PropTypes.bool, - pictureInPicture: ImmutablePropTypes.contains({ - inUse: PropTypes.bool, - available: PropTypes.bool, - }), - withoutEmojiReactions: PropTypes.bool, - ...WithOptionalRouterPropTypes, - }; - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps = [ - 'status', - 'account', - 'muted', - 'hidden', - 'unread', - 'pictureInPicture', - ]; - - state = { - showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault), - }; - - componentDidUpdate (prevProps) { - // This will potentially cause a wasteful redraw, but in most cases `Status` components are used - // with a `key` directly depending on their `id`, preventing re-use of the component across - // different IDs. - // But just in case this does change, reset the state on status change. - - if (this.props.status?.get('id') !== prevProps.status?.get('id')) { - this.setState({ - showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault), - }); - } - } - - handleToggleMediaVisibility = () => { - this.setState({ showMedia: !this.state.showMedia }); - }; - - handleClick = e => { - if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { - return; - } - - if (e) { - e.preventDefault(); - } - - this.handleHotkeyOpen(); - }; - - handlePrependAccountClick = e => { - this.handleAccountClick(e, false); - }; - - handleAccountClick = (e, proper = true) => { - if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { - return; - } - - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - - this._openProfile(proper); - }; - - handleExpandedToggle = () => { - this.props.onToggleHidden(this._properStatus()); - }; - - handleCollapsedToggle = isCollapsed => { - this.props.onToggleCollapsed(this._properStatus(), isCollapsed); - }; - - handleTranslate = () => { - this.props.onTranslate(this._properStatus()); - }; - - getAttachmentAspectRatio () { - const attachments = this._properStatus().get('media_attachments'); - - if (attachments.getIn([0, 'type']) === 'video') { - return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`; - } else if (attachments.getIn([0, 'type']) === 'audio') { - return '16 / 9'; - } else { - return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2'; - } - } - - renderLoadingMediaGallery = () => { - return ( -
- ); - }; - - renderLoadingVideoPlayer = () => { - return ( -
- ); - }; - - renderLoadingAudioPlayer = () => { - return ( -
- ); - }; - - handleOpenVideo = (options) => { - const status = this._properStatus(); - const lang = status.getIn(['translation', 'language']) || status.get('language'); - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options); - }; - - handleOpenMedia = (media, index) => { - const status = this._properStatus(); - const lang = status.getIn(['translation', 'language']) || status.get('language'); - this.props.onOpenMedia(status.get('id'), media, index, lang); - }; - - handleHotkeyOpenMedia = e => { - const { onOpenMedia, onOpenVideo } = this.props; - const status = this._properStatus(); - - e.preventDefault(); - - if (status.get('media_attachments').size > 0) { - const lang = status.getIn(['translation', 'language']) || status.get('language'); - if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 }); - } else { - onOpenMedia(status.get('id'), status.get('media_attachments'), 0, lang); - } - } - }; - - handleDeployPictureInPicture = (type, mediaProps) => { - const { deployPictureInPicture } = this.props; - const status = this._properStatus(); - - deployPictureInPicture(status, type, mediaProps); - }; - - handleHotkeyReply = e => { - e.preventDefault(); - this.props.onReply(this._properStatus()); - }; - - handleHotkeyFavourite = () => { - this.props.onFavourite(this._properStatus()); - }; - - handleHotkeyBoost = e => { - this.props.onReblog(this._properStatus(), e); - }; - - handleHotkeyMention = e => { - e.preventDefault(); - this.props.onMention(this._properStatus().get('account')); - }; - - handleHotkeyOpen = () => { - if (this.props.onClick) { - this.props.onClick(); - return; - } - - const { history } = this.props; - const status = this._properStatus(); - - if (!history) { - return; - } - - history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); - }; - - handleHotkeyOpenProfile = () => { - this._openProfile(); - }; - - _openProfile = (proper = true) => { - const { history } = this.props; - const status = proper ? this._properStatus() : this.props.status; - - if (!history) { - return; - } - - history.push(`/@${status.getIn(['account', 'acct'])}`); - }; - - handleHotkeyMoveUp = e => { - this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured')); - }; - - handleHotkeyMoveDown = e => { - this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured')); - }; - - handleHotkeyToggleHidden = () => { - const { onToggleHidden } = this.props; - const status = this._properStatus(); - - onToggleHidden(status); - }; - - handleHotkeyToggleSensitive = () => { - this.handleToggleMediaVisibility(); - }; - - _properStatus () { - const { status } = this.props; - - if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - return status.get('reblog'); - } else { - return status; - } - } - - handleRef = c => { - this.node = c; - }; - - render () { - const { intl, hidden, featured, unfocusable, unread, showThread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend } = this.props; - - let { status } = this.props; - - if (status === null) { - return null; - } - - const handlers = this.props.muted ? {} : { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleHotkeyToggleHidden, - toggleSensitive: this.handleHotkeyToggleSensitive, - openMedia: this.handleHotkeyOpenMedia, - }; - - let media, prepend, rebloggedByText; - - if (hidden) { - return ( - -
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.get('content')} -
-
- ); - } - - const connectUp = previousId && previousId === status.get('in_reply_to_id'); - const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); - const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); - - let visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility'); - - if (featured) { - prepend = ( -
-
- -
- ); - } else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; - - prepend = ( -
-
-
- }} /> -
- ); - - rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) }); - - status = status.get('reblog'); - } else if (status.get('visibility') === 'direct') { - prepend = ( -
-
- -
- ); - } else if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) { - const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; - - prepend = ( -
-
- }} /> -
- ); - } - - if (pictureInPicture.get('inUse')) { - media = ; - } else if (status.get('media_attachments').size > 0) { - const language = status.getIn(['translation', 'language']) || status.get('language'); - - if (this.props.muted) { - media = ( - - ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { - const attachment = status.getIn(['media_attachments', 0]); - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); - - media = ( - - {Component => ( - - )} - - ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const attachment = status.getIn(['media_attachments', 0]); - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); - - media = ( - - {Component => ( - - )} - - ); - } else { - media = ( - - {Component => ( - - )} - - ); - } - } else if (status.get('card') && !this.props.muted) { - media = ( - - ); - } - - visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility'); - - const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); - const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0; - - return ( - -
- {!skipPrepend && prepend} - -
- - {(connectReply || connectUp || connectToRoot) &&
} - - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} - - - {status.get('spoiler_text').length > 0 && } - - {expanded && ( - <> - - - {media} - {hashtagBar} - - )} -
-
- - ); - } - -} - -export default withOptionalRouter(injectIntl(CompactedStatus)); diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 0efea48f87..de30325e69 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -12,7 +12,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { HotKeys } from 'react-hotkeys'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; -import QuoteIcon from '@/material-icons/400-24px/format_quote.svg?react'; import ReferenceIcon from '@/material-icons/400-24px/link.svg?react'; import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; @@ -25,7 +24,6 @@ import { Icon } from 'mastodon/components/icon'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import { withOptionalRouter, WithOptionalRouterPropTypes } from 'mastodon/utils/react_router'; -import CompactedStatusContainer from '../containers/compacted_status_container'; import Card from '../features/status/components/card'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -136,7 +134,6 @@ class Status extends ImmutablePureComponent { available: PropTypes.bool, }), withoutEmojiReactions: PropTypes.bool, - withoutQuote: PropTypes.bool, ...WithOptionalRouterPropTypes, }; @@ -382,7 +379,7 @@ class Status extends ImmutablePureComponent { }; render () { - const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, rootId, withoutQuote, skipPrepend, avatarSize = 46 } = this.props; + const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, rootId, skipPrepend, avatarSize = 46 } = this.props; let { status, account, ...other } = this.props; @@ -582,12 +579,9 @@ class Status extends ImmutablePureComponent { const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status); const withLimited = status.get('visibility_ex') === 'limited' && status.get('limited_scope') ? : null; - const withQuote = status.get('quote_id') ? : null; - const withReference = (!withQuote && status.get('status_references_count') > 0) ? : null; + const withReference = status.get('status_references_count') > 0 ? : null; const withExpiration = status.get('expires_at') ? : null; - const quote = !this.props.muted && !withoutQuote && status.get('quote_id') && (['public', 'community'].includes(contextType) ? isShowItem('quote_in_public') : isShowItem('quote_in_home')) && ; - return (
@@ -598,7 +592,6 @@ class Status extends ImmutablePureComponent { {(!matchedFilters || expanded || isShowItem('avatar_on_filter')) && (
- {withQuote} {withReference} {withExpiration} {withLimited} @@ -633,7 +626,6 @@ class Status extends ImmutablePureComponent { {media} {hashtagBar} - {quote} {emojiReactionsBar} )} diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 4721e9da93..ada72a0792 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -68,8 +68,7 @@ const messages = defineMessages({ admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, - reference: { id: 'status.reference', defaultMessage: 'Quiet quote' }, - quote: { id: 'status.quote', defaultMessage: 'Quote' }, + reference: { id: 'status.reference', defaultMessage: 'Link' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, @@ -110,7 +109,6 @@ class StatusActionBar extends ImmutablePureComponent { onFilter: PropTypes.func, onAddFilter: PropTypes.func, onReference: PropTypes.func, - onQuote: PropTypes.func, onInteractionModal: PropTypes.func, withDismiss: PropTypes.bool, withCounters: PropTypes.bool, @@ -288,10 +286,6 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onReference(this.props.status, this.props.history); }; - handleQuote = () => { - this.props.onQuote(this.props.status, this.props.history); - }; - render () { const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { signedIn, permissions } = this.props.identity; @@ -303,7 +297,6 @@ class StatusActionBar extends ImmutablePureComponent { const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); - const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']); let menu = []; @@ -335,10 +328,6 @@ class StatusActionBar extends ImmutablePureComponent { } if (!boostMenu) { - if (publicStatus && allowQuote && (account.getIn(['server_features', 'quote']) || !isHideItem('quote_unavailable_server'))) { - menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote, tag: 'reblog' }); - } - if (account.getIn(['server_features', 'status_reference']) || !isHideItem('status_reference_unavailable_server')) { menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference, tag: 'reblog' }); } @@ -423,10 +412,6 @@ class StatusActionBar extends ImmutablePureComponent { } if (publicStatus) { - if (allowQuote && (account.getIn(['server_features', 'quote']) || !isHideItem('quote_unavailable_server'))) { - reblogMenu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote }); - } - if (account.getIn(['server_features', 'status_reference']) || !isHideItem('status_reference_unavailable_server')) { reblogMenu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); } diff --git a/app/javascript/mastodon/containers/compacted_status_container.jsx b/app/javascript/mastodon/containers/compacted_status_container.jsx deleted file mode 100644 index 8d483ed36f..0000000000 --- a/app/javascript/mastodon/containers/compacted_status_container.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import { injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { openModal } from '../actions/modal'; -import { - hideStatus, - revealStatus, - toggleStatusCollapse, - translateStatus, - undoStatusTranslation, -} from '../actions/statuses'; -import CompactedStatus from '../components/compacted_status'; -import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - const getPictureInPicture = makeGetPictureInPicture(); - - const mapStateToProps = (state, props) => ({ - status: getStatus(state, props), - nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, - pictureInPicture: getPictureInPicture(state, props), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch) => ({ - - onTranslate (status) { - if (status.get('translation')) { - dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); - } else { - dispatch(translateStatus(status.get('id'))); - } - }, - - onOpenMedia (statusId, media, index, lang) { - dispatch(openModal({ - modalType: 'MEDIA', - modalProps: { statusId, media, index, lang }, - })); - }, - - onOpenVideo (statusId, media, lang, options) { - dispatch(openModal({ - modalType: 'VIDEO', - modalProps: { statusId, media, lang, options }, - })); - }, - - onToggleHidden (status) { - if (status.get('hidden')) { - dispatch(revealStatus(status.get('id'))); - } else { - dispatch(hideStatus(status.get('id'))); - } - }, - - onToggleCollapsed (status, isCollapsed) { - dispatch(toggleStatusCollapse(status.get('id'), isCollapsed)); - }, - - onInteractionModal (type, status) { - dispatch(openModal({ - modalType: 'INTERACTION', - modalProps: { - type, - accountId: status.getIn(['account', 'id']), - url: status.get('uri'), - }, - })); - }, - -}); - -export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(CompactedStatus)); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 225fb328fb..3371bd37d2 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -152,10 +152,6 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({ dispatch(insertReferenceCompose(0, status.get('url'), 'BT', router)); }, - onQuote (status, router) { - dispatch(insertReferenceCompose(0, status.get('url'), 'QT', router)); - }, - onTranslate (status) { if (status.get('translation')) { dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index e8f02f1718..ea03d5fa58 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -164,7 +164,7 @@ class ColumnSettings extends PureComponent {
-

+

diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index 90c32c5642..1de09ef405 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -42,7 +42,7 @@ const messages = defineMessages({ reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your post' }, status: { id: 'notification.status', defaultMessage: '{name} just posted' }, listStatus: { id: 'notification.list_status', defaultMessage: '{name} post is added to {listName}' }, - statusReference: { id: 'notification.status_reference', defaultMessage: '{name} quoted your post' }, + statusReference: { id: 'notification.status_reference', defaultMessage: '{name} linked your post' }, update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' }, adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, @@ -305,7 +305,7 @@ class Notification extends ImmutablePureComponent {
- +
diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_status_reference.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_status_reference.tsx index 49674f42aa..ece42ea34b 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_status_reference.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_status_reference.tsx @@ -9,7 +9,7 @@ import { NotificationWithStatus } from './notification_with_status'; const labelRenderer: LabelRenderer = (displayedName) => ( ); diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index f56a616b9d..63b91b287c 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -64,8 +64,7 @@ const messages = defineMessages({ admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, - reference: { id: 'status.reference', defaultMessage: 'Quiet quote' }, - quote: { id: 'status.quote', defaultMessage: 'Quote' }, + reference: { id: 'status.reference', defaultMessage: 'Link' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, @@ -89,7 +88,6 @@ class ActionBar extends PureComponent { onFavourite: PropTypes.func.isRequired, onEmojiReact: PropTypes.func.isRequired, onReference: PropTypes.func.isRequired, - onQuote: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onBookmarkCategoryAdder: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, @@ -225,10 +223,6 @@ class ActionBar extends PureComponent { this.props.onReference(this.props.status, this.props.history); }; - handleQuote = () => { - this.props.onQuote(this.props.status, this.props.history); - }; - handleEmojiPick = (data) => { this.props.onEmojiReact(this.props.status, data); }; @@ -244,7 +238,6 @@ class ActionBar extends PureComponent { const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); - const allowQuote = status.getIn(['account', 'other_settings', 'allow_quote']); let menu = []; @@ -269,10 +262,6 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog : messages.reblog), action: this.handleReblogForceModalClick, tag: 'reblog' }); if (publicStatus) { - if (allowQuote && (account.getIn(['server_features', 'quote']) || !isHideItem('quote_unavailable_server'))) { - menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote, tag: 'reblog' }); - } - if (account.getIn(['server_features', 'status_reference']) || !isHideItem('status_reference_unavailable_server')) { menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference, tag: 'reblog' }); } @@ -350,10 +339,6 @@ class ActionBar extends PureComponent { } if (publicStatus) { - if (allowQuote && (account.getIn(['server_features', 'quote']) || !isHideItem('quote_unavailable_server'))) { - reblogMenu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote }); - } - if (account.getIn(['server_features', 'status_reference']) || !isHideItem('status_reference_unavailable_server')) { reblogMenu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.tsx b/app/javascript/mastodon/features/status/components/detailed_status.tsx index e0a68ad263..e680a6641f 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.tsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.tsx @@ -32,7 +32,6 @@ import { DisplayName } from '../../../components/display_name'; import MediaGallery from '../../../components/media_gallery'; import StatusContent from '../../../components/status_content'; import StatusEmojiReactionsBar from '../../../components/status_emoji_reactions_bar'; -import CompactedStatusContainer from '../../../containers/compacted_status_container'; import Audio from '../../audio'; import scheduleIdleTask from '../../ui/util/schedule_idle_task'; @@ -77,7 +76,6 @@ export const DetailedStatus: React.FC<{ onToggleHidden, onEmojiReact, onUnEmojiReact, - muted, }) => { const properStatus = status?.get('reblog') ?? status; const [height, setHeight] = useState(0); @@ -330,7 +328,7 @@ export const DetailedStatus: React.FC<{ @@ -383,13 +381,6 @@ export const DetailedStatus: React.FC<{ (!matchedFilters || showDespiteFilter) && (!status.get('hidden') || status.get('spoiler_text').length === 0); - const quote = !muted && status.get('quote_id') && ( - <> - {/* @ts-expect-error: CompactedStatusContainer class is not typescript still. */} - - - ); - return (
@@ -459,7 +450,6 @@ export const DetailedStatus: React.FC<{ {media} {hashtagBar} - {quote} {emojiReactionsBar} )} diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index e9d0c9f8d6..5d42220173 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -156,7 +156,7 @@ const makeMapStateToProps = () => { if (status) { ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); descendantsIds = getDescendantsIds(state, { id: status.get('id') }); - referenceIds = getReferenceIds(state, { id: status.get('id') }).filter((id) => id !== status.get('quote_id')); + referenceIds = getReferenceIds(state, { id: status.get('id') }); } return { @@ -341,10 +341,6 @@ class Status extends ImmutablePureComponent { this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'BT', router)); }; - handleQuote = (status, router) => { - this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'QT', router)); - }; - handleBookmarkClick = (status) => { if (bookmarkCategoryNeeded) { this.handleBookmarkCategoryAdderClick(status); @@ -774,7 +770,6 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onReblogForceModal={this.handleReblogForceModalClick} onReference={this.handleReference} - onQuote={this.handleQuote} onBookmark={this.handleBookmarkClick} onBookmarkCategoryAdder={this.handleBookmarkCategoryAdderClick} onDelete={this.handleDeleteClick} diff --git a/app/javascript/mastodon/features/status_references/index.jsx b/app/javascript/mastodon/features/status_references/index.jsx index e2cc86f426..c36d6fadc4 100644 --- a/app/javascript/mastodon/features/status_references/index.jsx +++ b/app/javascript/mastodon/features/status_references/index.jsx @@ -81,7 +81,7 @@ class StatusReferences extends ImmutablePureComponent { bindToDocument={!multiColumn} > {accountIds.map(id => - , + , )} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d16d4d64ee..5e3ad55db9 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -2,14 +2,10 @@ /** - * @typedef { 'blocking_quote' - * | 'emoji_reaction_on_timeline' + * @typedef { 'emoji_reaction_on_timeline' * | 'emoji_reaction_unavailable_server' * | 'emoji_reaction_count' * | 'favourite_menu' - * | 'quote_in_home' - * | 'quote_in_public' - * | 'quote_unavailable_server' * | 'recent_emojis' * | 'relationships' * | 'status_reference_unavailable_server' diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ade34591a5..dee37a3752 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -462,7 +462,7 @@ "empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.", "empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", - "empty_column.status_references": "No one has quotes this post yet. When someone does, they will show up here.", + "empty_column.status_references": "No one has links this post yet. When someone does, they will show up here.", "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.", "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", @@ -762,7 +762,7 @@ "notification.relationships_severance_event.learn_more": "Learn more", "notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.", "notification.status": "{name} just posted", - "notification.status_reference": "{name} quoted your post", + "notification.status_reference": "{name} linked your post", "notification.update": "{name} edited a post", "notification_requests.accept": "Accept", "notification_requests.accept_multiple": "{count, plural, one {Accept # request…} other {Accept # requests…}}", @@ -804,7 +804,7 @@ "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", "notifications.column_settings.status": "New posts:", - "notifications.column_settings.status_reference": "Quotes:", + "notifications.column_settings.status_reference": "Links:", "notifications.column_settings.unread_notifications.category": "Unread notifications", "notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications", "notifications.column_settings.update": "Edits:", @@ -815,7 +815,7 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.filter.polls": "Poll results", - "notifications.filter.status_references": "Quotes", + "notifications.filter.status_references": "Links", "notifications.filter.statuses": "Updates from people you follow", "notifications.grant_permission": "Grant permission.", "notifications.group": "{count} notifications", @@ -1061,8 +1061,6 @@ "status.open": "Expand this post", "status.pin": "Pin on profile", "status.pinned": "Pinned post", - "status.quote": "Quote", - "status.quotes": "{count, plural, one {quote} other {quotes}}", "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", @@ -1071,7 +1069,8 @@ "status.reblogs": "{count, plural, one {boost} other {boosts}}", "status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.", "status.redraft": "Delete & re-draft", - "status.reference": "Quiet quote", + "status.reference": "Link", + "status.references": "{count, plural, one {link} other {links}}", "status.remove_bookmark": "Remove bookmark", "status.remove_favourite": "Remove from favorites", "status.replied_in_thread": "Replied in thread", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index a2328401f4..9f2bafc00f 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -438,7 +438,7 @@ "empty_column.notification_requests": "ここに表示するものはありません。新しい通知を受け取ったとき、フィルタリング設定で通知がブロックされたアカウントがある場合はここに表示されます。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう", - "empty_column.status_references": "まだ誰も引用していません。引用されるとここに表示されます。", + "empty_column.status_references": "まだ誰もリンクしていません。リンクされるとここに表示されます。", "error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。", "error.unexpected_crash.explanation_addons": "このページは正しく表示できませんでした。このエラーはブラウザのアドオンや自動翻訳ツールによって引き起こされることがあります。", "error.unexpected_crash.next_steps": "ページの再読み込みをお試しください。それでも解決しない場合、別のブラウザかアプリを使えば使用できることがあります。", @@ -733,7 +733,7 @@ "notification.relationships_severance_event.learn_more": "詳細を確認", "notification.relationships_severance_event.user_domain_block": "{target} のブロックにより{followersCount}フォロワーと{followingCount, plural, other {#フォロー}}が解除されました。", "notification.status": "{name}さんが投稿しました", - "notification.status_reference": "{name}さんがあなたの投稿を引用しました", + "notification.status_reference": "{name}さんがあなたの投稿をリンクしました", "notification.update": "{name}さんが投稿を編集しました", "notification_requests.accept": "受け入れる", "notification_requests.accept_multiple": "{count, plural, other {選択中の#件を受け入れる}}", @@ -774,7 +774,7 @@ "notifications.column_settings.show": "カラムに表示", "notifications.column_settings.sound": "通知音を再生", "notifications.column_settings.status": "新しい投稿:", - "notifications.column_settings.status_reference": "引用", + "notifications.column_settings.status_reference": "リンク", "notifications.column_settings.unread_notifications.category": "未読の通知:", "notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示", "notifications.column_settings.update": "編集:", @@ -786,7 +786,7 @@ "notifications.filter.mentions": "返信", "notifications.filter.polls": "アンケート結果", "notifications.filter.statuses": "フォローしている人の新着情報", - "notifications.filter.status_references": "引用", + "notifications.filter.status_references": "リンク", "notifications.grant_permission": "権限の付与", "notifications.group": "{count}件の通知", "notifications.mark_as_read": "すべて既読にする", @@ -1031,8 +1031,7 @@ "status.open": "詳細を表示", "status.pin": "プロフィールに固定表示", "status.pinned": "固定された投稿", - "status.quote": "引用", - "status.quotes": "{count, plural, one {引用} other {引用}}", + "status.quote": "リンク", "status.read_more": "もっと見る", "status.reblog": "ブースト", "status.reblog_private": "ブースト", @@ -1041,7 +1040,8 @@ "status.reblogs": "{count, plural, one {ブースト} other {ブースト}}", "status.reblogs.empty": "まだ誰もブーストしていません。ブーストされるとここに表示されます。", "status.redraft": "削除して下書きに戻す", - "status.reference": "ひかえめな引用", + "status.reference": "リンク", + "status.references": "{count, plural, one {リンク} other {リンク}}", "status.remove_bookmark": "ブックマークを削除", "status.remove_favourite": "お気に入りから削除", "status.replied_in_thread": "ほかのユーザーへ", diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 55dbbcbb34..dd9c9efa28 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -56,7 +56,6 @@ const AccountOtherSettingsFactory = ImmutableRecord({ hide_statuses_count: false, translatable_private: false, link_preview: true, - allow_quote: true, emoji_reaction_policy: 'allow', subscription_policy: 'allow', }); @@ -69,7 +68,6 @@ const AccountServerFeaturesFactory = ImmutableRecord({ circle: false, emoji_reaction: false, - quote: false, status_reference: false, }); diff --git a/app/javascript/mastodon/reducers/filters.js b/app/javascript/mastodon/reducers/filters.js index 28f0c3e6e4..566ad0c6ca 100644 --- a/app/javascript/mastodon/reducers/filters.js +++ b/app/javascript/mastodon/reducers/filters.js @@ -11,7 +11,6 @@ const normalizeFilter = (state, filter) => { filter_action: filter.filter_action, keywords: filter.keywords, expires_at: filter.expires_at ? Date.parse(filter.expires_at) : null, - with_quote: filter.with_quote, }); if (is(state.get(filter.id), normalizedFilter)) { diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index a03ee70e3f..3da829d11d 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -13,41 +13,27 @@ export const makeGetStatus = () => { [ (state, { id }) => state.getIn(['statuses', id]), (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), - (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_id'])]), - (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'quote_id'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), getFilters, (_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType), ], - (statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => { + (statusBase, statusReblog, accountBase, accountReblog, filters, warnInsteadOfHide) => { if (!statusBase || statusBase.get('isLoading')) { return null; } if (statusReblog) { statusReblog = statusReblog.set('account', accountReblog); - statusQuote = statusReblogQuote; } else { statusReblog = null; } - if (isHideItem('blocking_quote') && (statusReblog || statusBase).getIn(['quote', 'quote_muted'])) { - return null; - } - let filtered = false; let mediaFiltered = false; if ((accountReblog || accountBase).get('id') !== me && filters) { let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList(); - const quoteFilterResults = statusQuote?.get('filtered'); - if (quoteFilterResults) { - const filterWithQuote = quoteFilterResults.some((result) => filters.getIn([result.get('filter'), 'with_quote'])); - if (filterWithQuote) { - filterResults = filterResults.concat(quoteFilterResults); - } - } if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) { return null; @@ -66,7 +52,6 @@ export const makeGetStatus = () => { return statusBase.withMutations(map => { map.set('reblog', statusReblog); - map.set('quote', statusQuote); map.set('account', accountBase); map.set('matched_filters', filtered); map.set('matched_media_filters', mediaFiltered); diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 6cdc21ce75..c6748ddbae 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -555,7 +555,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def related_to_local_activity? fetch? || followed_by_local_accounts? || requested_through_relay? || - responds_to_followed_account? || addresses_local_accounts? || quote_local? || free_friend_domain? + responds_to_followed_account? || addresses_local_accounts? || free_friend_domain? end def responds_to_followed_account? @@ -621,16 +621,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity ProcessReferencesService.call_service_without_error(@status, [], reference_uris, [quote].compact) end - def quote_local? - url = quote - - if url.present? - ActivityPub::TagManager.instance.uri_to_resource(url, Status)&.local? - else - false - end - end - def free_friend_domain? FriendDomain.free_receivings.exists?(domain: @account.domain) end @@ -640,15 +630,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def quote - @quote ||= quote_from_tags || @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote'] - end - - def quote_from_tags - return @quote_from_tags if defined?(@quote_from_tags) - - hit_tag = as_array(@object['tag']).detect do |tag| - equals_or_includes?(tag['type'], 'Link') && LINK_MEDIA_TYPES.include?(tag['mediaType']) && tag['href'].present? - end - @quote_from_tags = hit_tag && hit_tag['href'] + @quote ||= nil # TODO: quote end end diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index 33e3ef6fac..05b9477907 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -5,7 +5,6 @@ module ActivityPub::CaseTransform NO_CONVERT_VALUES = %w( _misskey_content _misskey_license - _misskey_quote ).freeze def camel_lower_cache diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 04dd254458..b622e140f7 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -78,7 +78,6 @@ class StatusReachFinder reblogs_account_ids, favourites_account_ids, replies_account_ids, - quoted_account_id, ].tap do |arr| arr.flatten! arr.compact! @@ -114,10 +113,6 @@ class StatusReachFinder @status.replies.pluck(:account_id) if distributable? || unsafe? end - def quoted_account_id - @status.quote.account_id if @status.quote? - end - def followers_inboxes scope = followers_scope inboxes_without_suspended_for(scope) diff --git a/app/models/concerns/account/other_settings.rb b/app/models/concerns/account/other_settings.rb index 7968f857ff..a8311c6788 100644 --- a/app/models/concerns/account/other_settings.rb +++ b/app/models/concerns/account/other_settings.rb @@ -15,13 +15,6 @@ module Account::OtherSettings false end - def allow_quote? - return user.setting_allow_quote if local? && user.present? - return settings['allow_quote'] if settings.present? && settings.key?('allow_quote') - - true - end - def hide_statuses_count? return user&.setting_hide_statuses_count if local? && user.present? return settings['hide_statuses_count'] if settings.present? && settings.key?('hide_statuses_count') @@ -88,7 +81,6 @@ module Account::OtherSettings 'hide_following_count' => hide_following_count?, 'hide_followers_count' => hide_followers_count?, 'translatable_private' => translatable_private?, - 'allow_quote' => allow_quote?, 'emoji_reaction_policy' => Setting.enable_emoji_reaction ? emoji_reaction_policy.to_s : 'block', } end diff --git a/app/models/concerns/user/has_settings.rb b/app/models/concerns/user/has_settings.rb index 2f8b11dcec..fdd4009e84 100644 --- a/app/models/concerns/user/has_settings.rb +++ b/app/models/concerns/user/has_settings.rb @@ -107,18 +107,6 @@ module User::HasSettings settings['web.content_font_size'] end - def setting_show_quote_in_home - settings['web.show_quote_in_home'] - end - - def setting_show_quote_in_public - settings['web.show_quote_in_public'] - end - - def setting_hide_blocking_quote - settings['web.hide_blocking_quote'] - end - def setting_show_relationships settings['web.show_relationships'] end @@ -127,10 +115,6 @@ module User::HasSettings settings['web.show_avatar_on_filter'] end - def setting_allow_quote - settings['allow_quote'] - end - def setting_reject_send_limited_to_suspects settings['reject_send_limited_to_suspects'] end @@ -259,10 +243,6 @@ module User::HasSettings settings['use_public_index'] end - def setting_reverse_search_quote - settings['reverse_search_quote'] - end - def setting_disallow_unlisted_public_searchability settings['disallow_unlisted_public_searchability'] end @@ -275,10 +255,6 @@ module User::HasSettings settings['web.hide_emoji_reaction_unavailable_server'] end - def setting_hide_quote_unavailable_server - settings['web.hide_quote_unavailable_server'] - end - def setting_hide_status_reference_unavailable_server settings['web.hide_status_reference_unavailable_server'] end diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index b67105aade..ed1c782328 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -12,7 +12,6 @@ # expires_at :datetime # phrase :text default(""), not null # with_profile :boolean default(FALSE), not null -# with_quote :boolean default(TRUE), not null # created_at :datetime not null # updated_at :datetime not null # account_id :bigint(8) not null @@ -69,14 +68,6 @@ class CustomFilter < ApplicationRecord hide_action? end - def exclude_quote=(value) - self.with_quote = !ActiveModel::Type::Boolean.new.cast(value) - end - - def exclude_quote - !with_quote - end - def exclude_profile=(value) self.with_profile = !ActiveModel::Type::Boolean.new.cast(value) end @@ -111,9 +102,6 @@ class CustomFilter < ApplicationRecord end def self.apply_cached_filters(cached_filters, status, following: false) - references_text_cache = nil - references_spoiler_text_cache = nil - cached_filters.filter_map do |filter, rules| next if filter.exclude_follows && following next if filter.exclude_localusers && status.account.local? @@ -121,17 +109,10 @@ class CustomFilter < ApplicationRecord if rules[:keywords].present? match = rules[:keywords].match(status.proper.searchable_text) match = rules[:keywords].match([status.account.display_name, status.account.note].join("\n\n")) if !match && filter.with_profile - if match.nil? && filter.with_quote && status.proper.reference_objects.exists? - references_text_cache = status.proper.references.pluck(:text).join("\n\n") if references_text_cache.nil? - references_spoiler_text_cache = status.proper.references.pluck(:spoiler_text).join("\n\n") if references_spoiler_text_cache.nil? - match = rules[:keywords].match(references_text_cache) - match = rules[:keywords].match(references_spoiler_text_cache) if match.nil? - end end keyword_matches = [match.to_s] unless match.nil? - reference_ids = filter.with_quote ? status.proper.reference_objects.pluck(:target_status_id) : [] - status_matches = ([status.id, status.reblog_of_id] + reference_ids).compact & rules[:status_ids] if rules[:status_ids].present? + status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present? next if keyword_matches.blank? && status_matches.blank? diff --git a/app/models/instance_info.rb b/app/models/instance_info.rb index 7b42c2df7e..8862501ce4 100644 --- a/app/models/instance_info.rb +++ b/app/models/instance_info.rb @@ -35,8 +35,6 @@ class InstanceInfo < ApplicationRecord yojo-art ).freeze - QUOTE_AVAILABLE_SOFTWARES = EMOJI_REACTION_AVAILABLE_SOFTWARES + %w(bridgy-fed).freeze - STATUS_REFERENCE_AVAILABLE_SOFTWARES = %w(fedibird).freeze CIRCLE_AVAILABLE_SOFTWARES = %w(fedibird).freeze @@ -87,7 +85,6 @@ class InstanceInfo < ApplicationRecord { emoji_reaction: feature_available?(info, EMOJI_REACTION_AVAILABLE_SOFTWARES, 'emoji_reaction'), - quote: feature_available?(info, QUOTE_AVAILABLE_SOFTWARES, 'quote'), status_reference: feature_available?(info, STATUS_REFERENCE_AVAILABLE_SOFTWARES, 'status_reference'), circle: feature_available?(info, CIRCLE_AVAILABLE_SOFTWARES, 'circle'), } @@ -96,7 +93,6 @@ class InstanceInfo < ApplicationRecord def local_features { emoji_reaction: Setting.enable_emoji_reaction, - quote: true, status_reference: true, circle: true, } diff --git a/app/models/status.rb b/app/models/status.rb index 0325802022..a908bf2e7d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -205,19 +205,6 @@ class Status < ApplicationRecord account: [:account_stat, user: :role], active_mentions: { account: :account_stat }, ], - quote: [ - :application, - :tags, - :media_attachments, - :conversation, - :status_stat, - :preloadable_poll, - :reference_objects, - :scheduled_expiration_status, - preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, - account: [:account_stat, user: :role], - active_mentions: :account, - ], thread: :account delegate :domain, to: :account, prefix: true @@ -252,10 +239,6 @@ class Status < ApplicationRecord !reblog_of_id.nil? end - def quote? - !quote_of_id.nil? && !quote.nil? - end - def expires? scheduled_expiration_status.present? end diff --git a/app/models/status_reference.rb b/app/models/status_reference.rb index 79207291ac..b5d522baf4 100644 --- a/app/models/status_reference.rb +++ b/app/models/status_reference.rb @@ -5,12 +5,11 @@ # Table name: status_references # # id :bigint(8) not null, primary key -# status_id :bigint(8) not null -# target_status_id :bigint(8) not null +# attribute_type :string # created_at :datetime not null # updated_at :datetime not null -# attribute_type :string -# quote :boolean default(FALSE), not null +# status_id :bigint(8) not null +# target_status_id :bigint(8) not null # class StatusReference < ApplicationRecord @@ -22,8 +21,6 @@ class StatusReference < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy after_commit :reset_parent_cache - after_create_commit :set_quote - after_destroy_commit :remove_quote private @@ -31,18 +28,4 @@ class StatusReference < ApplicationRecord Rails.cache.delete("statuses/#{status_id}") Rails.cache.delete("statuses/#{target_status_id}") end - - def set_quote - return unless quote - return if status.quote_of_id.present? - - status.quote_of_id = target_status_id - end - - def remove_quote - return unless quote - return unless status.quote_of_id == target_status_id - - status.quote_of_id = nil - end end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index fb7d81a5f4..6d57e1a864 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -27,7 +27,6 @@ class UserSettings setting :default_searchability, default: :direct, in: %w(public private direct limited public_unlisted) setting :default_searchability_of_search, default: :public, in: %w(public private direct limited) setting :use_public_index, default: true - setting :reverse_search_quote, default: false setting :disallow_unlisted_public_searchability, default: false setting :public_post_to_unlisted, default: false setting :reject_public_unlisted_subscription, default: false @@ -40,7 +39,6 @@ class UserSettings setting :dtl_force_visibility, default: :unchange, in: %w(unchange public public_unlisted unlisted) setting :dtl_force_searchability, default: :unchange, in: %w(unchange public public_unlisted) setting :lock_follow_from_bot, default: false - setting :allow_quote, default: true setting :reject_send_limited_to_suspects, default: false setting_inverse_alias :indexable, :noindex @@ -75,18 +73,12 @@ class UserSettings setting :auto_play, default: true setting :simple_timeline_menu, default: false setting :boost_menu, default: false - setting :show_quote_in_home, default: true - setting :show_quote_in_public, default: false setting :show_relationships, default: true - setting :hide_blocking_quote, default: true setting :hide_emoji_reaction_unavailable_server, default: false - setting :hide_quote_unavailable_server, default: false - setting :hide_status_reference_unavailable_server, default: false setting :hide_favourite_menu, default: false setting :hide_emoji_reaction_count, default: false setting :show_avatar_on_filter, default: true - setting_inverse_alias :'web.show_blocking_quote', :'web.hide_blocking_quote' setting_inverse_alias :'web.show_emoji_reaction_count', :'web.hide_emoji_reaction_count' setting_inverse_alias :'web.show_favourite_menu', :'web.hide_favourite_menu' setting_inverse_alias :'web.show_recent_emojis', :'web.hide_recent_emojis' diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index f846a9bc75..c8f9af1ee4 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -47,10 +47,6 @@ class StatusPolicy < ApplicationPolicy show? && !blocking_author? end - def quote? - %i(public public_unlisted unlisted).include?(record.visibility.to_sym) && show? && !blocking_author? - end - def destroy? owned? end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index e42bdb560f..631d3ca9ef 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -21,7 +21,6 @@ class StatusRelationshipsPresenter @emoji_reaction_allows_map = nil else statuses = statuses.compact - statuses += statuses.filter_map(&:quote) status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact conversation_ids = statuses.filter_map(&:conversation_id).uniq pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) } diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index d778440450..49c39a2c47 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references, :limited_scope, :quote_uri + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references, :limited_scope attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -16,9 +16,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :updated, if: :edited? attribute :limited_scope, if: :limited_visibility? - attribute :quote_uri, if: :quote? - attribute :misskey_quote, key: :_misskey_quote, if: :quote? - has_many :virtual_attachments, key: :attachment has_many :virtual_tags, key: :tag @@ -158,30 +155,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def virtual_tags - object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis + virtual_tags_of_quote - end - - class NoteLink < ActiveModelSerializers::Model - attributes :href - end - - class NoteLinkSerializer < ActivityPub::Serializer - attributes :type, :href - attribute :media_type, key: :mediaType - - def type - 'Link' - end - - def media_type - 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - end - end - - def virtual_tags_of_quote - return [] unless object.quote? - - [NoteLink.new(href: quote_uri)] + object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis end def atom_uri @@ -218,20 +192,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer object.account.local? end - delegate :quote?, to: :object - - def quote_post - @quote_post ||= object.quote - end - - def quote_uri - ActivityPub::TagManager.instance.uri_for(quote_post) - end - - def misskey_quote - quote_uri - end - def poll_options object.preloadable_poll.loaded_options end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 7fbee3a626..ffac02be76 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -40,14 +40,10 @@ class InitialStateSerializer < ActiveModel::Serializer store[:hide_items] = [ object_account_user.setting_hide_favourite_menu ? 'favourite_menu' : nil, object_account_user.setting_hide_recent_emojis ? 'recent_emojis' : nil, - object_account_user.setting_hide_blocking_quote ? 'blocking_quote' : nil, object_account_user.setting_hide_emoji_reaction_unavailable_server ? 'emoji_reaction_unavailable_server' : nil, - object_account_user.setting_hide_quote_unavailable_server ? 'quote_unavailable_server' : nil, object_account_user.setting_hide_status_reference_unavailable_server ? 'status_reference_unavailable_server' : nil, object_account_user.setting_hide_emoji_reaction_count ? 'emoji_reaction_count' : nil, object_account_user.setting_show_emoji_reaction_on_timeline ? nil : 'emoji_reaction_on_timeline', - object_account_user.setting_show_quote_in_home ? nil : 'quote_in_home', - object_account_user.setting_show_quote_in_public ? nil : 'quote_in_public', object_account_user.setting_show_relationships ? nil : 'relationships', object_account_user.setting_show_avatar_on_filter ? nil : 'avatar_on_filter', ].compact diff --git a/app/serializers/rest/filter_serializer.rb b/app/serializers/rest/filter_serializer.rb index 578cf16d98..55e74de49a 100644 --- a/app/serializers/rest/filter_serializer.rb +++ b/app/serializers/rest/filter_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::FilterSerializer < ActiveModel::Serializer - attributes :id, :title, :exclude_follows, :exclude_localusers, :with_quote, :with_profile, :context, :expires_at, :filter_action + attributes :id, :title, :exclude_follows, :exclude_localusers, :with_profile, :context, :expires_at, :filter_action has_many :keywords, serializer: REST::FilterKeywordSerializer, if: :rules_requested? has_many :statuses, serializer: REST::FilterStatusSerializer, if: :rules_requested? diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 21fb3ae399..6d3c16cbea 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -18,7 +18,6 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :pinned, if: :pinnable? attribute :reactions, if: :reactions? attribute :expires_at, if: :will_expire? - attribute :quote_id, if: :quote? attribute :markdown_opt, key: :markdown has_many :filtered, serializer: REST::FilterResultSerializer, if: :current_user? @@ -37,23 +36,6 @@ class REST::StatusSerializer < ActiveModel::Serializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer - class QuotedStatusSerializer < REST::StatusSerializer - attribute :quote_muted, if: :current_user? - - def quote - nil - end - - def quote_muted - if relationships - muted || relationships.blocks_map[object.account_id] || relationships.domain_blocks_map[object.account.domain] || false - else - muted || current_user.account.blocking?(object.account_id) || current_user.account.domain_blocking?(object.account.domain) - end - end - end - belongs_to :quote, if: :quote?, serializer: QuotedStatusSerializer, relationships: -> { relationships } - def id object.id.to_s end @@ -182,12 +164,6 @@ class REST::StatusSerializer < ActiveModel::Serializer end end - def quote_id - object.quote_of_id.to_s - end - - delegate :quote?, to: :object - def reblogged if relationships relationships.reblogs_map[object.id] || false diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 10224f4d7e..9c6c3f00c5 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -351,7 +351,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def quote - @json['quote'] || @json['quoteUrl'] || @json['quoteURL'] || @json['_misskey_quote'] + # TODO: quote + nil end def local_referred_accounts diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 590c7c8e82..6e19872926 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -115,7 +115,7 @@ class FanOutOnWriteService < BaseService end def notify_about_update! - @status.reblogged_by_accounts.or(@status.quoted_by_accounts).merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts| + @status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts| LocalNotificationWorker.push_bulk(accounts) do |account| [account.id, @status.id, 'Status', 'update'] end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 61409f7e71..f093fd48ca 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -277,6 +277,7 @@ class PostStatusService < BaseService def quote_url ProcessReferencesService.extract_quote(@text) + # TODO: quote end def reference_urls diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb index bbb22a0575..0862da081d 100644 --- a/app/services/process_references_service.rb +++ b/app/services/process_references_service.rb @@ -188,7 +188,8 @@ class ProcessReferencesService < BaseService quote = quote_attribute?(attribute_type) @added_objects << @status.reference_objects.new(target_status: status, attribute_type: attribute_type, quote: quote) - @status.update!(quote_of_id: status_id) if quote + # TODO: quote + # @status.update!(quote_of_id: status_id) if quote status.increment_count!(:status_referred_by_count) @references_count += 1 @@ -214,7 +215,9 @@ class ProcessReferencesService < BaseService @removed_objects = [] @status.reference_objects.where(target_status: @removed_items.keys).destroy_all - @status.update!(quote_of_id: nil) if @status.quote_of_id.present? && @removed_items.key?(@status.quote_of_id) + + # TODO: quote + # @status.update!(quote_of_id: nil) if @status.quote_of_id.present? && @removed_items.key?(@status.quote_of_id) statuses = Status.where(id: @added_items.keys).to_a @removed_items.each_key do |status_id| @@ -240,11 +243,12 @@ class ProcessReferencesService < BaseService next unless quote_change - if quote - ref.status.update!(quote_of_id: ref.target_status.id) - else - ref.status.update!(quote_of_id: nil) - end + # TODO: quote + # if quote + # ref.status.update!(quote_of_id: ref.target_status.id) + # else + # ref.status.update!(quote_of_id: nil) + # end end end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index bc9a22d45f..0fe08ad209 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -139,6 +139,7 @@ class UpdateStatusService < BaseService def quote_url ProcessReferencesService.extract_quote(text) + # TODO: quote end def reference_urls diff --git a/app/views/filters/_filter_fields.html.haml b/app/views/filters/_filter_fields.html.haml index 18e9a6580e..f4b851c85a 100644 --- a/app/views/filters/_filter_fields.html.haml +++ b/app/views/filters/_filter_fields.html.haml @@ -38,7 +38,6 @@ = f.input :exclude_localusers, wrapper: :with_label, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_localusers') .fields-group - = f.input :exclude_quote, wrapper: :with_label, as: :boolean, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_quote') = f.input :exclude_profile, wrapper: :with_label, as: :boolean, kmyblue: true, label: t('simple_form.labels.filters.options.exclude_profile') %hr.spacer/ diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 195ad2f76e..c95d6c3c10 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -86,7 +86,6 @@ .fields-group - if Setting.enable_emoji_reaction = ff.input :'web.hide_emoji_reaction_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_emoji_reaction_unavailable_server') - = ff.input :'web.hide_quote_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_quote_unavailable_server') = ff.input :'web.hide_status_reference_unavailable_server', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_status_reference_unavailable_server') %h4= t 'appearance.saved_posts' @@ -95,13 +94,6 @@ = ff.input :'web.bookmark_category_needed', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_bookmark_category_needed'), hint: I18n.t('simple_form.hints.defaults.setting_bookmark_category_needed') = ff.input :'web.show_favourite_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_favourite_menu') - %h4= t 'appearance.quotes' - - .fields-group - = ff.input :'web.show_quote_in_home', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_quote_in_home'), hint: false - = ff.input :'web.show_quote_in_public', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_quote_in_public'), hint: false - = ff.input :'web.show_blocking_quote', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_show_blocking_quote'), hint: false - %h4= t 'appearance.status_action_bar' .fields-group diff --git a/app/views/settings/privacy_extra/show.html.haml b/app/views/settings/privacy_extra/show.html.haml index b97f949c4b..7209544ad0 100644 --- a/app/views/settings/privacy_extra/show.html.haml +++ b/app/views/settings/privacy_extra/show.html.haml @@ -18,9 +18,6 @@ .fields-group = ff.input :translatable_private, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_translatable_private') - .fields-group - = ff.input :allow_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_allow_quote'), hint: I18n.t('simple_form.hints.defaults.setting_allow_quote') - - if Setting.enable_emoji_reaction %h4= t 'preferences.emoji_reaction_permitting' diff --git a/config/locales/en.yml b/config/locales/en.yml index dca6a2d9f8..fa0a93793b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -484,7 +484,7 @@ en: reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_new_follow: Reject follows reject_new_follow_hint: Reject follows in the future - reject_reply_exclude_followers: Reject mentions/quotes exclude followers + reject_reply_exclude_followers: Reject mentions exclude followers reject_reply_exclude_followers_hint: Reject replies exclude followers in the future reject_reports: Reject reports reject_reports_hint: Ignore all reports coming from this domain. Irrelevant for suspensions @@ -629,7 +629,7 @@ en: reject_hashtag: Reject hashtags reject_media: Reject media reject_new_follow: Reject follows - reject_reply_exclude_followers: Reject reply/quote exclude followers + reject_reply_exclude_followers: Reject reply exclude followers reject_reports: Reject reports reject_send_sensitive: No Sensitive Submission Delivery reject_straight_follow: Reject straight follow @@ -781,7 +781,7 @@ en: needed: have no_needed: Should not have optional: Optional - status_allow_follower_mention: Check posts only if they contain mentions/quotes to non-followers + status_allow_follower_mention: Check posts only if they contain mentions to non-followers status_allow_follower_mention_hint: If enabled, mentions between other servers are unconditionally allowed status_cw_state: Has warning or not status_media_state: Has media or not @@ -791,8 +791,8 @@ en: status_poll_state: Has poll or not status_poll_threshold: Poll items limit status_quote_state: Has quote or not - status_reference_state: Has quiet quote or not - status_reference_threshold: Has quiet quote or not + status_reference_state: Has link or not + status_reference_threshold: Has link or not status_reply_state: Is reply or not status_searchability: Searchability status_sensitive_state: Is sensitive or not @@ -811,7 +811,7 @@ en: phrases: regexp_html: "Reg - If the Reg checkbox is checked, the comparison is performed using regular expressions." regexp_short: Reg - stranger_html: "UE - Items checked under Uem apply only to mentions, replies, quotes, etc. from accounts with which you have no follow relationship." + stranger_html: "UE - Items checked under Uem apply only to mentions, replies, links, etc. from accounts with which you have no follow relationship." stranger_short: Uem post_hash_tags_max: Hash tags limit of a post post_mentions_max: Mentions limit of a post @@ -1106,7 +1106,7 @@ en: special_domains: stop_fetch_activity_domains: preamble: On servers with different domains for Web and Activity, you may not be able to retrieve Activity even if you access Web. In citations, this setting suppresses fetching when such a URL is specified in the body of a post. In most cases, this setting is rarely used. - title: Domains do not fetch for quotes + title: Domains do not fetch for links stop_link_preview_domains: preamble: If a site temporarily restricts connections from IP addresses that have received many hits, your server may be treated unfavorably. This setting suppresses the process of accessing such sites and generating link previews. In most cases, this setting is rarely used. title: Domains do not fetch for link previews @@ -1435,7 +1435,6 @@ en: body: Mastodon is translated by volunteers. guide_link: https://crowdin.com/project/mastodon guide_link_text: Everyone can contribute. - quotes: Quotes remote_server_features: Other server features saved_posts: Saving posts sensitive_content: Sensitive content diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 73de14abeb..c728cd3f84 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1389,7 +1389,6 @@ ja: body: Mastodonは有志によって翻訳されています。 guide_link: https://ja.crowdin.com/project/mastodon guide_link_text: 誰でも参加することができます。 - quotes: 引用 remote_server_features: 他のサーバーの機能 saved_posts: 投稿の記録 sensitive_content: 閲覧注意コンテンツ diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 9264216121..43262b631a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -60,7 +60,6 @@ en: phrase: Will be matched regardless of casing in text or content warning of a post scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) - setting_allow_quote: Subdued quotes are allowed regardless of this setting; you can quote freely from any source except kmyblue! setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon setting_bookmark_category_needed: When removing from all category, unbookmarked automatically setting_custom_css_lead: 'Be sure to remember: In the unlikely event that you make a mistake in entering your custom CSS and the screen does not display properly, you can disable your custom CSS from the link at the bottom of the sign-in screen. Open the sign-in screen in private mode of your browser, for example, and disable it.' @@ -80,7 +79,6 @@ en: setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります setting_reject_send_limited_to_suspects: This applies to "Mutual Only" posts. Circle posts will be delivered without exception. Some Misskey servers have independently supported limited posting, but this is a setting for those who are concerned about it, as mutual-only posting exposes some of the users you are mutual with to Misskey users! setting_reject_unlisted_subscription: Misskey and its forks can **subscribe and search** for "non-following" posts from accounts they do not follow. This differs from kmyblue's behavior. It delivers posts in the specified public range to such servers as "followers only". Please understand, however, that due to its structure, it is difficult to handle perfectly and will occasionally be delivered as non-subscribed. - setting_reverse_search_quote: Double-quotes will result in a search with a wider range of notation, which is the opposite of Mastodon's default behavior. setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります setting_stay_privacy: If you choose to enable this setting, please consider manually setting the visibility of the boost setting_stop_emoji_reaction_streaming: Helps to save communication capacity. @@ -270,7 +268,6 @@ en: phrase: Keyword or phrase setting_advanced_layout: Enable advanced web interface setting_aggregate_reblogs: Group boosts in timelines - setting_allow_quote: Allow quote your posts setting_always_send_emails: Always send e-mail notifications setting_auto_play_gif: Auto-play animated GIFs setting_bio_markdown: Enable profile markdown @@ -315,8 +312,7 @@ en: setting_expand_spoilers: Always expand posts marked with content warnings setting_hide_emoji_reaction_unavailable_server: Hide emoji reaction button from unavailable server setting_hide_network: Hide your social graph - setting_hide_quote_unavailable_server: Hide quote menu from unavailable server - setting_hide_status_reference_unavailable_server: Hide quiet quote (reference named by Fedibird) menu from unavailable server + setting_hide_status_reference_unavailable_server: Hide link (reference named by Fedibird) menu from unavailable server setting_lock_follow_from_bot: Request approval about bot follow setting_missing_alt_text_modal: Show confirmation dialog before posting media without alt text setting_public_post_to_unlisted: Convert public post to public unlisted if not using Web app @@ -327,14 +323,11 @@ en: setting_reverse_search_quote: Perform word-by-word search when search keywords are not enclosed in double quotes setting_show_application: Disclose application used to send posts setting_show_avatar_on_filter: Show filtered posts with avatar and user profile - setting_show_blocking_quote: Show posts which have a quote written by the user you are blocking setting_show_emoji_reaction_count: Show emoji reaction number setting_show_emoji_reaction_on_timeline: Show all emoji reactions on timeline setting_show_favourite_menu: Show favourite menu setting_show_followers_count: Show followers count setting_show_following_count: Show following count - setting_show_quote_in_home: Show quotes in home, list or antenna timelines - setting_show_quote_in_public: Show quotes in public timelines setting_show_recent_emojis: Show recent emojis setting_show_relationships: Show relationships on account page setting_show_statuses_count: Show statuses count @@ -386,7 +379,6 @@ en: exclude_follows: Exclude following users exclude_localusers: Exclude local users exclude_profile: Exclude account name and bio - exclude_quote: Exclude quote or references form_admin_settings: activity_api_enabled: Publish aggregate statistics about user activity in the API app_icon: App icon diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index b8728f63a0..348e41820d 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -60,7 +60,6 @@ ja: phrase: 投稿内容の大文字小文字や閲覧注意に関係なく一致 scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。 setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響) - setting_allow_quote: ひかえめな引用はこの設定に関わらず可能です。kmyblue以外からは自由に引用できます setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 setting_bookmark_category_needed: すべてのカテゴリから削除したとき、ブックマークが自動で外れるようになります setting_custom_css_lead: '必ず覚えてください: 万が一カスタムCSSの入力を誤り、画面が正常に表示されなくなった場合は、サインイン画面の下にあるリンクよりカスタムCSSを無効化することができます。ブラウザのプライベートモードなどでサインイン画面を開き、無効化してください。' @@ -261,7 +260,6 @@ ja: phrase: キーワードまたはフレーズ setting_advanced_layout: 上級者向けUIを有効にする setting_aggregate_reblogs: ブーストをまとめる - setting_allow_quote: 引用を許可する setting_always_send_emails: 常にメール通知を送信する setting_auto_play_gif: アニメーションGIFを自動再生する setting_bio_markdown: プロフィールのMarkdownを有効にする @@ -306,7 +304,6 @@ ja: setting_expand_spoilers: 閲覧注意としてマークされた投稿を常に展開する setting_hide_emoji_reaction_unavailable_server: 絵文字リアクションに対応していないと思われるサーバーの投稿から絵文字リアクション機能を隠す setting_hide_network: 繋がりを隠す - setting_hide_quote_unavailable_server: 引用に対応していないと思われるサーバーの投稿からメニューを隠す setting_hide_status_reference_unavailable_server: ひかえめな引用(Fedibirdの参照)に対応していないと思われるサーバーの投稿からメニューを隠す setting_lock_follow_from_bot: botからのフォローを承認制にする setting_missing_alt_text_modal: 代替テキストなしでメディアを投稿する前に確認ダイアログを表示する @@ -318,14 +315,11 @@ ja: setting_reverse_search_quote: ダブルクオートで囲まず検索した時、単語単位で検索する setting_show_application: 送信したアプリを開示する setting_show_avatar_on_filter: フィルター対象投稿の投稿者名やアイコンを表示する - setting_show_blocking_quote: ブロックしたユーザーの投稿を引用した投稿を表示する setting_show_emoji_reaction_count: 投稿につけられた各絵文字の数を表示する setting_show_emoji_reaction_on_timeline: タイムライン上の投稿に他の人のつけた絵文字を表示する setting_show_favourite_menu: 右サイドメニューに「お気に入り」を表示する setting_show_followers_count: フォロワー数を公開する setting_show_following_count: フォロー数を公開する - setting_show_quote_in_home: ホーム・リスト・アンテナなどで引用された投稿を表示する - setting_show_quote_in_public: 公開タイムライン(ローカル・連合)で引用された投稿を表示する setting_show_recent_emojis: 絵文字ピッカーで絵文字デッキと一緒に、絵文字の使用履歴も表示する setting_show_relationships: アカウント詳細ベージで、相手からのフォロー状況を表示する setting_show_statuses_count: 投稿数を公開する @@ -376,7 +370,6 @@ ja: exclude_follows: フォロー中のユーザーをフィルターの対象にしない exclude_localusers: ローカルユーザーをフィルターの対象にしない exclude_profile: アカウント名および紹介文をフィルターの対象にしない - exclude_quote: 引用の内容をフィルターの対象にしない form_admin_settings: activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する app_icon: アプリアイコン diff --git a/db/migrate/20250423224935_remove_kmyblue_quote_accessories.rb b/db/migrate/20250423224935_remove_kmyblue_quote_accessories.rb new file mode 100644 index 0000000000..3a629b3295 --- /dev/null +++ b/db/migrate/20250423224935_remove_kmyblue_quote_accessories.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RemoveKmyblueQuoteAccessories < ActiveRecord::Migration[8.0] + def change + safety_assured do + remove_column :custom_filters, :with_quote, :boolean, default: true, null: false + remove_column :status_references, :quote, :boolean, default: false, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d9a082eb24..a1d77e716a 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[8.0].define(version: 2025_04_10_144908) do +ActiveRecord::Schema[8.0].define(version: 2025_04_23_224935) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -552,7 +552,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do t.integer "action", default: 0, null: false t.boolean "exclude_follows", default: false, null: false t.boolean "exclude_localusers", default: false, null: false - t.boolean "with_quote", default: true, null: false t.boolean "with_profile", default: false, null: false t.index ["account_id"], name: "index_custom_filters_on_account_id" end @@ -1374,7 +1373,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.string "attribute_type" - t.boolean "quote", default: false, null: false t.index ["status_id"], name: "index_status_references_on_status_id" t.index ["target_status_id"], name: "index_status_references_on_target_status_id" end diff --git a/lib/tasks/dangerous.rake b/lib/tasks/dangerous.rake index fbd9268271..d136474f7e 100644 --- a/lib/tasks/dangerous.rake +++ b/lib/tasks/dangerous.rake @@ -158,7 +158,7 @@ namespace :dangerous do %w(custom_filters exclude_follows), %w(custom_filters exclude_localusers), %w(custom_filters with_profile), - %w(custom_filters with_quote), + # Removed: custom_filters with_quote, %w(domain_blocks block_trends), %w(domain_blocks detect_invalid_subscription), %w(domain_blocks hidden), diff --git a/spec/fabricators/status_reference_fabricator.rb b/spec/fabricators/status_reference_fabricator.rb index 0eff89c14b..1a25d76fdd 100644 --- a/spec/fabricators/status_reference_fabricator.rb +++ b/spec/fabricators/status_reference_fabricator.rb @@ -4,5 +4,4 @@ Fabricator(:status_reference) do status { Fabricate.build(:status) } target_status { Fabricate.build(:status) } attribute_type 'BT' - quote false end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 108111c06b..1d09c9d4d3 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1711,104 +1711,10 @@ RSpec.describe ActivityPub::Activity::Create do status = sender.statuses.first expect(status).to_not be_nil - expect(status.quote).to be_nil expect(status.references.pluck(:id)).to eq [target_status.id] end end - context 'with quote' do - let(:recipient) { Fabricate(:account) } - let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) } - - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - quote: ActivityPub::TagManager.instance.uri_for(target_status), - } - end - - it 'creates status' do - expect { subject.perform }.to change(sender.statuses, :count).by(1) - - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.references.pluck(:id)).to eq [target_status.id] - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id - end - end - - context 'with quote as feb-e232 object links' do - let(:recipient) { Fabricate(:account) } - let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) } - - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - tag: [ - { - type: 'Link', - mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - href: ActivityPub::TagManager.instance.uri_for(target_status), - }, - ], - } - end - - it 'creates status' do - expect { subject.perform }.to change(sender.statuses, :count).by(1) - - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.references.pluck(:id)).to eq [target_status.id] - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id - end - end - - context 'with references and quote' do - let(:recipient) { Fabricate(:account) } - let!(:target_status) { Fabricate(:status, account: Fabricate(:account, domain: nil)) } - - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - quote: ActivityPub::TagManager.instance.uri_for(target_status), - references: { - id: 'target_status', - type: 'Collection', - first: { - type: 'CollectionPage', - next: nil, - partOf: 'target_status', - items: [ - ActivityPub::TagManager.instance.uri_for(target_status), - ], - }, - }, - } - end - - it 'creates status' do - expect { subject.perform }.to change(sender.statuses, :count).by(1) - - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.references.pluck(:id)).to eq [target_status.id] - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id - end - end - context 'with language' do let(:to) { 'https://www.w3.org/ns/activitystreams#Public' } let(:object_json) do @@ -2556,53 +2462,6 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'when sender quotes to local status' do - subject { described_class.new(json, sender, delivery: true) } - - let!(:local_status) { Fabricate(:status) } - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - quote: ActivityPub::TagManager.instance.uri_for(local_status), - } - end - - before do - subject.perform - end - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' - end - end - - context 'when sender quotes to non-local status' do - subject { described_class.new(json, sender, delivery: true) } - - let!(:remote_status) { Fabricate(:status, uri: 'https://foo.bar/among', account: Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/account')) } - let(:object_json) do - { - id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, - type: 'Note', - content: 'Lorem ipsum', - quote: ActivityPub::TagManager.instance.uri_for(remote_status), - } - end - - before do - subject.perform - end - - it 'creates status' do - expect(sender.statuses.count).to eq 0 - end - end - context 'when sender targets a local user' do subject { described_class.new(json, sender, delivery: true) } diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index c935ab7292..2b0f6e8f51 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -8,11 +8,10 @@ RSpec.describe StatusReachFinder do subject { described_class.new(status) } let(:parent_status) { nil } - let(:quoted_status) { nil } let(:visibility) { :public } let(:searchability) { :public } let(:alice) { Fabricate(:account, username: 'alice') } - let(:status) { Fabricate(:status, account: alice, thread: parent_status, quote_of_id: quoted_status&.id, visibility: visibility, searchability: searchability) } + let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility, searchability: searchability) } context 'with a simple case' do let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } @@ -353,15 +352,6 @@ RSpec.describe StatusReachFinder do end end end - - context 'when it is a quote to a remote account' do - let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } - let(:quoted_status) { Fabricate(:status, account: bob) } - - it 'includes the inbox of the quoted-to account' do - expect(subject.inboxes).to include 'https://foo.bar/inbox' - end - end end context 'with extended domain block' do diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 193ceaecde..defc17ab3c 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -397,13 +397,9 @@ RSpec.describe Account do describe '#public_settings_for_local' do subject { account.public_settings_for_local } - let(:account) { Fabricate(:user, settings: { allow_quote: true, hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account } + let(:account) { Fabricate(:user, settings: { hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account } shared_examples 'some settings' do |permitted, emoji_reaction_policy| - it 'allow_quote is allowed' do - expect(subject['allow_quote']).to be permitted.include?(:allow_quote) - end - it 'hide_statuses_count is allowed' do expect(subject['hide_statuses_count']).to be permitted.include?(:hide_statuses_count) end @@ -417,24 +413,18 @@ RSpec.describe Account do end end - it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only' + it_behaves_like 'some settings', %i(hide_statuses_count), 'followers_only' context 'when default true setting is set false' do - let(:account) { Fabricate(:user, settings: { allow_quote: false, hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account } + let(:account) { Fabricate(:user, settings: { hide_statuses_count: true, emoji_reaction_policy: :followers_only }).account } it_behaves_like 'some settings', %i(hide_statuses_count), 'followers_only' end context 'when remote user' do - let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor', settings: { 'allow_quote' => true, 'hide_statuses_count' => true, 'emoji_reaction_policy' => 'followers_only' }) } + let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor', settings: { 'hide_statuses_count' => true, 'emoji_reaction_policy' => 'followers_only' }) } - it_behaves_like 'some settings', %i(allow_quote hide_statuses_count), 'followers_only' - end - - context 'when remote user by server other_settings is not supported' do - let(:account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } - - it_behaves_like 'some settings', %i(allow_quote), 'allow' + it_behaves_like 'some settings', %i(hide_statuses_count), 'followers_only' end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 13acb7d909..dc4aaeb42c 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -86,39 +86,6 @@ RSpec.describe Status do end end - describe '#quote' do - let(:target_status) { Fabricate(:status) } - let(:quote) { true } - - before do - Fabricate(:status_reference, status: subject, target_status: target_status, quote: quote) - end - - context 'when quoting single' do - it 'get quote' do - expect(subject.quote).to_not be_nil - expect(subject.quote.id).to eq target_status.id - end - end - - context 'when multiple quotes' do - it 'get quote' do - target2 = Fabricate(:status) - Fabricate(:status_reference, status: subject, quote: quote) - expect(subject.quote).to_not be_nil - expect([target_status.id, target2.id].include?(subject.quote.id)).to be true - end - end - - context 'when no quote but reference' do - let(:quote) { false } - - it 'get quote' do - expect(subject.quote).to be_nil - end - end - end - describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index c564e82f7d..19bba46fb9 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -251,30 +251,6 @@ RSpec.describe StatusPolicy, type: :model do end end - context 'with the permission of quote?' do - permissions :quote? do - it 'grants access when viewer is not blocked' do - follow = Fabricate(:follow) - status.account = follow.target_account - - expect(subject).to permit(follow.account, status) - end - - it 'denies when viewer is blocked' do - block = Fabricate(:block) - status.account = block.target_account - - expect(subject).to_not permit(block.account, status) - end - - it 'denies when private visibility' do - status.visibility = :private - - expect(subject).to_not permit(Fabricate(:account), status) - end - end - end - context 'with the permission of update?' do permissions :update? do it 'grants access if owner' do diff --git a/spec/requests/api/v1/timelines/public_spec.rb b/spec/requests/api/v1/timelines/public_spec.rb index 29ece18ce6..cca41c4f45 100644 --- a/spec/requests/api/v1/timelines/public_spec.rb +++ b/spec/requests/api/v1/timelines/public_spec.rb @@ -170,11 +170,9 @@ RSpec.describe 'Public' do let(:exclude_follows) { false } let(:exclude_localusers) { false } - let(:include_quotes) { false } let(:account) { user.account } let(:remote_account) { remote_status.account } - let!(:filter) { Fabricate(:custom_filter, account: account, exclude_follows: exclude_follows, exclude_localusers: exclude_localusers, with_quote: include_quotes) } - let!(:quote_status) { Fabricate(:status, quote: Fabricate(:status, text: 'ohagi')) } + let!(:filter) { Fabricate(:custom_filter, account: account, exclude_follows: exclude_follows, exclude_localusers: exclude_localusers) } it 'load statuses', :aggregate_failures do ids = subject @@ -201,16 +199,6 @@ RSpec.describe 'Public' do expect(ids).to_not include(remote_status.id) end end - - context 'when include_quotes' do - let(:with_quote) { true } - - it 'load statuses', :aggregate_failures do - ids = subject - expect(ids).to_not include(local_status.id) - expect(ids).to include(quote_status.id) - end - end end end end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index c89bb47af6..17488e7bdd 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -16,14 +16,9 @@ RSpec.describe ActivityPub::NoteSerializer do let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) } let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) } let!(:referred) { nil } - let!(:quote) { nil } before do parent.references << referred if referred.present? - if quote.present? - parent.references << quote - parent.quote = quote - end end it 'has the expected shape and replies collection' do @@ -98,23 +93,7 @@ RSpec.describe ActivityPub::NoteSerializer do end it 'has as reference' do - expect(subject['quoteUri']).to be_nil expect(subject['references']['first']['items']).to include referred.uri end end - - context 'when has a quote' do - let(:quote) { Fabricate(:status) } - - it 'has a quote as transitional type' do - expect(subject['quoteUri']).to eq ActivityPub::TagManager.instance.uri_for(quote) - end - - it 'has a quote as feb-e232 object link' do - tag = subject['tag'].detect { |t| t['type'] == 'Link' } - expect(tag).to_not be_nil - expect(tag['mediaType']).to eq 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - expect(tag['href']).to eq ActivityPub::TagManager.instance.uri_for(quote) - end - end end diff --git a/spec/serializers/node_info/serializer_spec.rb b/spec/serializers/node_info/serializer_spec.rb index da7a6bcebb..55c8800878 100644 --- a/spec/serializers/node_info/serializer_spec.rb +++ b/spec/serializers/node_info/serializer_spec.rb @@ -30,7 +30,6 @@ RSpec.describe NodeInfo::Serializer do end it 'returns nodeinfo own features' do - expect(serialization['metadata']['features']).to include 'quote' expect(serialization['metadata']['features']).to_not include 'kmyblue_markdown' end end diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index 9319814ddc..d6bd3ea2ee 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -678,7 +678,7 @@ RSpec.describe FanOutOnWriteService do end end - context 'when updated status is already boosted or quoted' do + context 'when updated status is already boosted' do let(:custom_before) { true } before do @@ -695,13 +695,6 @@ RSpec.describe FanOutOnWriteService do expect(notification.activity_id).to eq status.id end - it 'notified to quoted account', :inline_jobs do - notification = Notification.find_by(account: tom, type: 'update') - - expect(notification).to_not be_nil - expect(notification.activity_id).to eq status.id - end - it 'notified not to non-boosted account', :inline_jobs do notification = Notification.find_by(account: ohagi, type: 'update') diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb index f10a181832..0304293359 100644 --- a/spec/services/process_references_service_spec.rb +++ b/spec/services/process_references_service_spec.rb @@ -11,7 +11,6 @@ RSpec.describe ProcessReferencesService, type: :service do let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) } let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) } let(:quote_urls) { nil } - let(:allow_quote) { true } def notify?(target_status_id = nil) target_status_id ||= target_status.id @@ -20,7 +19,6 @@ RSpec.describe ProcessReferencesService, type: :service do describe 'posting new status' do subject do - target_status.account.user.settings['allow_quote'] = false unless allow_quote target_status.account.user&.save described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote, quote_urls: quote_urls) @@ -42,7 +40,9 @@ RSpec.describe ProcessReferencesService, type: :service do end it 'not quote', :inline_jobs do - expect(status.quote).to be_nil + # TODO: quote + expect(status).to_not be_nil # Remove + # expect(status.quote).to be_nil end end @@ -89,8 +89,9 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'QT' - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?).to be true end end @@ -103,8 +104,9 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'QT' - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?).to be true end end @@ -117,8 +119,9 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'QT' - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?).to be true end end @@ -131,21 +134,23 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'QT' - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?).to be true end end context 'when quote is rejected' do let(:text) { "Hello QT #{target_status_uri}" } - let(:allow_quote) { false } + # let(:allow_quote) { false } it 'post status', :inline_jobs do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'BT' - expect(status.quote).to be_nil + # TODO: quote + # expect(status.quote).to be_nil expect(notify?).to be true end end @@ -159,8 +164,9 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 2 expect(subject).to include [target_status.id, 'QT'] expect(subject).to include [target_status2.id, 'BT'] - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?).to be true expect(notify?(target_status2.id)).to be true end @@ -407,7 +413,8 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status', :inline_jobs do expect(subject.size).to eq 0 - expect(status.quote).to be_nil + # TODO: quote + # expect(status.quote).to be_nil expect(notify?).to be false end end @@ -430,8 +437,9 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status', :inline_jobs do expect(subject.size).to eq 1 expect(subject).to include target_status2.id - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status2.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status2.id expect(notify?(target_status2.id)).to be true end end @@ -443,7 +451,8 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status', :inline_jobs do expect(subject.size).to eq 1 expect(subject).to include target_status.id - expect(status.quote).to be_nil + # TODO: quote + # expect(status.quote).to be_nil expect(notify?(target_status.id)).to be true end end @@ -455,8 +464,9 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status', :inline_jobs do expect(subject.size).to eq 1 expect(subject).to include target_status.id - expect(status.quote).to_not be_nil - expect(status.quote.id).to eq target_status.id + # TODO: quote + # expect(status.quote).to_not be_nil + # expect(status.quote.id).to eq target_status.id expect(notify?(target_status.id)).to be true end end diff --git a/streaming/index.js b/streaming/index.js index ff3ef1c7ba..29e00ac941 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -12,8 +12,18 @@ import { JSDOM } from 'jsdom'; import { WebSocketServer } from 'ws'; import * as Database from './database.js'; -import { AuthenticationError, RequestError, extractStatusAndMessage as extractErrorStatusAndMessage } from './errors.js'; -import { logger, httpLogger, initializeLogLevel, attachWebsocketHttpLogger, createWebsocketLogger } from './logging.js'; +import { + AuthenticationError, + RequestError, + extractStatusAndMessage as extractErrorStatusAndMessage, +} from './errors.js'; +import { + logger, + httpLogger, + initializeLogLevel, + attachWebsocketHttpLogger, + createWebsocketLogger, +} from './logging.js'; import { setupMetrics } from './metrics.js'; import * as Redis from './redis.js'; import { isTruthy, normalizeHashtag, firstParam } from './utils.js'; @@ -23,13 +33,11 @@ const environment = process.env.NODE_ENV || 'development'; // Correctly detect and load .env or .env.production file based on environment: const dotenvFile = environment === 'production' ? '.env.production' : '.env'; const dotenvFilePath = path.resolve( - url.fileURLToPath( - new URL(path.join('..', dotenvFile), import.meta.url) - ) + url.fileURLToPath(new URL(path.join('..', dotenvFile), import.meta.url)), ); dotenv.config({ - path: dotenvFilePath + path: dotenvFilePath, }); initializeLogLevel(process.env, environment); @@ -46,7 +54,6 @@ initializeLogLevel(process.env, environment); * @property {string[]} chosenLanguages */ - /** * Attempts to safely parse a string as JSON, used when both receiving a message * from redis and when receiving a message from a client over a websocket @@ -67,9 +74,15 @@ const parseJSON = (json, req) => { */ if (req) { if (req.accountId) { - req.log.error({ err }, `Error parsing message from user ${req.accountId}`); + req.log.error( + { err }, + `Error parsing message from user ${req.accountId}`, + ); } else { - req.log.error({ err }, `Error parsing message from ${req.remoteAddress}`); + req.log.error( + { err }, + `Error parsing message from ${req.remoteAddress}`, + ); } } else { logger.error({ err }, `Error parsing message from redis`); @@ -97,7 +110,7 @@ const CHANNEL_NAMES = [ 'user:notification', 'list', 'direct', - ...PUBLIC_CHANNELS + ...PUBLIC_CHANNELS, ]; const startServer = async () => { @@ -131,7 +144,7 @@ const startServer = async () => { * @returns {string} */ function redisUnnamespaced(channel) { - if (typeof redisConfig.namespace === "string") { + if (typeof redisConfig.namespace === 'string') { // Note: this removes the configured namespace and the colon that is used // to separate it: return channel.slice(redisConfig.namespace.length + 1); @@ -141,13 +154,18 @@ const startServer = async () => { } // Set the X-Request-Id header on WebSockets: - wss.on("headers", function onHeaders(headers, req) { + wss.on('headers', function onHeaders(headers, req) { headers.push(`X-Request-Id: ${req.id}`); }); const app = express(); - app.set('trust proxy', process.env.TRUSTED_PROXY_IP ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) : 'loopback,uniquelocal'); + app.set( + 'trust proxy', + process.env.TRUSTED_PROXY_IP + ? process.env.TRUSTED_PROXY_IP.split(/(?:\s*,\s*|\s+)/) + : 'loopback,uniquelocal', + ); app.use(httpLogger); app.use(cors()); @@ -161,7 +179,7 @@ const startServer = async () => { // logger. This decorates the `request` object. attachWebsocketHttpLogger(request); - request.log.info("HTTP Upgrade Requested"); + request.log.info('HTTP Upgrade Requested'); /** @param {Error} err */ const onSocketError = (err) => { @@ -179,15 +197,15 @@ const startServer = async () => { // Unfortunately for using the on('upgrade') setup, we need to manually // write a HTTP Response to the Socket to close the connection upgrade // attempt, so the following code is to handle all of that. - const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); + const { statusCode, errorMessage } = extractErrorStatusAndMessage(err); /** @type {Record} */ const headers = { - 'Connection': 'close', + Connection: 'close', 'Content-Type': 'text/plain', 'Content-Length': 0, 'X-Request-Id': request.id, - 'X-Error-Message': errorMessage + 'X-Error-Message': errorMessage, }; // Ensure the socket is closed once we've finished writing to it: @@ -196,16 +214,25 @@ const startServer = async () => { }); // Write the HTTP response manually: - socket.end(`HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode]}\r\n${Object.keys(headers).map((key) => `${key}: ${headers[key]}`).join('\r\n')}\r\n\r\n`); + socket.end( + `HTTP/1.1 ${statusCode} ${http.STATUS_CODES[statusCode]}\r\n${Object.keys( + headers, + ) + .map((key) => `${key}: ${headers[key]}`) + .join('\r\n')}\r\n\r\n`, + ); // Finally, log the error: - request.log.error({ - err, - res: { - statusCode, - headers - } - }, errorMessage); + request.log.error( + { + err, + res: { + statusCode, + headers, + }, + }, + errorMessage, + ); return; } @@ -214,7 +241,9 @@ const startServer = async () => { socket.removeListener('error', onSocketError); wss.handleUpgrade(request, socket, head, function done(ws) { - request.log.info("Authenticated request & upgraded to WebSocket connection"); + request.log.info( + 'Authenticated request & upgraded to WebSocket connection', + ); const wsLogger = createWebsocketLogger(request, resolvedAccount); @@ -236,7 +265,10 @@ const startServer = async () => { app.get('/favicon.ico', (_req, res) => res.status(404).end()); app.get('/api/v1/streaming/health', (_req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain', 'Cache-Control': 'private, no-store' }); + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'private, no-store', + }); res.end('OK'); }); @@ -246,11 +278,18 @@ const startServer = async () => { * @param {string[]} channels * @returns {function(): void} */ - const subscriptionHeartbeat = channels => { + const subscriptionHeartbeat = (channels) => { const interval = 6 * 60; const tellSubscribed = () => { - channels.forEach(channel => redisClient.set(redisNamespaced(`subscribed:${channel}`), '1', 'EX', interval * 3)); + channels.forEach((channel) => + redisClient.set( + redisNamespaced(`subscribed:${channel}`), + '1', + 'EX', + interval * 3, + ), + ); }; tellSubscribed(); @@ -279,9 +318,9 @@ const startServer = async () => { const json = parseJSON(message, null); if (!json) return; - callbacks.forEach(callback => callback(json)); + callbacks.forEach((callback) => callback(json)); }; - redisSubscribeClient.on("message", onRedisMessage); + redisSubscribeClient.on('message', onRedisMessage); /** * @callback SubscriptionListener @@ -324,19 +363,22 @@ const startServer = async () => { return; } - subs[channel] = subs[channel].filter(item => item !== callback); + subs[channel] = subs[channel].filter((item) => item !== callback); if (subs[channel].length === 0) { logger.debug(`Unsubscribe ${channel}`); // FIXME: https://github.com/redis/ioredis/issues/1910 - redisSubscribeClient.unsubscribe(redisNamespaced(channel), (err, count) => { - if (err) { - logger.error(`Error unsubscribing to ${channel}`); - } else if (typeof count === 'number') { - metrics.redisSubscriptions.set(count); - } - }); + redisSubscribeClient.unsubscribe( + redisNamespaced(channel), + (err, count) => { + if (err) { + logger.error(`Error unsubscribing to ${channel}`); + } else if (typeof count === 'number') { + metrics.redisSubscriptions.set(count); + } + }, + ); delete subs[channel]; } }; @@ -347,7 +389,7 @@ const startServer = async () => { * @returns {boolean} */ const isInScope = (req, necessaryScopes) => - req.scopes.some(scope => necessaryScopes.includes(scope)); + req.scopes.some((scope) => necessaryScopes.includes(scope)); /** * @param {string} token @@ -355,7 +397,10 @@ const startServer = async () => { * @returns {Promise} */ const accountFromToken = async (token, req) => { - const result = await pgPool.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token]); + const result = await pgPool.query( + 'SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', + [token], + ); if (result.rows.length === 0) { throw new AuthenticationError('Invalid access token'); @@ -378,52 +423,56 @@ const startServer = async () => { * @param {any} req * @returns {Promise} */ - const accountFromRequest = (req) => new Promise((resolve, reject) => { - const authorization = req.headers.authorization; - const location = url.parse(req.url, true); - const accessToken = location.query.access_token || req.headers['sec-websocket-protocol']; + const accountFromRequest = (req) => + new Promise((resolve, reject) => { + const authorization = req.headers.authorization; + const location = url.parse(req.url, true); + const accessToken = + location.query.access_token || req.headers['sec-websocket-protocol']; - if (!authorization && !accessToken) { - reject(new AuthenticationError('Missing access token')); - return; - } + if (!authorization && !accessToken) { + reject(new AuthenticationError('Missing access token')); + return; + } - const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken; + const token = authorization + ? authorization.replace(/^Bearer /, '') + : accessToken; - resolve(accountFromToken(token, req)); - }); + resolve(accountFromToken(token, req)); + }); /** * @param {any} req * @returns {string|undefined} */ - const channelNameFromPath = req => { + const channelNameFromPath = (req) => { const { path, query } = req; const onlyMedia = isTruthy(query.only_media); switch (path) { - case '/api/v1/streaming/user': - return 'user'; - case '/api/v1/streaming/user/notification': - return 'user:notification'; - case '/api/v1/streaming/public': - return onlyMedia ? 'public:media' : 'public'; - case '/api/v1/streaming/public/local': - return onlyMedia ? 'public:local:media' : 'public:local'; - case '/api/v1/streaming/public/remote': - return onlyMedia ? 'public:remote:media' : 'public:remote'; - case '/api/v1/streaming/hashtag': - return 'hashtag'; - case '/api/v1/streaming/hashtag/local': - return 'hashtag:local'; - case '/api/v1/streaming/direct': - return 'direct'; - case '/api/v1/streaming/list': - return 'list'; - case '/api/v1/streaming/antenna': - return 'antenna'; - default: - return undefined; + case '/api/v1/streaming/user': + return 'user'; + case '/api/v1/streaming/user/notification': + return 'user:notification'; + case '/api/v1/streaming/public': + return onlyMedia ? 'public:media' : 'public'; + case '/api/v1/streaming/public/local': + return onlyMedia ? 'public:local:media' : 'public:local'; + case '/api/v1/streaming/public/remote': + return onlyMedia ? 'public:remote:media' : 'public:remote'; + case '/api/v1/streaming/hashtag': + return 'hashtag'; + case '/api/v1/streaming/hashtag/local': + return 'hashtag:local'; + case '/api/v1/streaming/direct': + return 'direct'; + case '/api/v1/streaming/list': + return 'list'; + case '/api/v1/streaming/antenna': + return 'antenna'; + default: + return undefined; } }; @@ -433,38 +482,48 @@ const startServer = async () => { * @param {string|undefined} channelName * @returns {Promise.} */ - const checkScopes = (req, logger, channelName) => new Promise((resolve, reject) => { - logger.debug(`Checking OAuth scopes for ${channelName}`); + const checkScopes = (req, logger, channelName) => + new Promise((resolve, reject) => { + logger.debug(`Checking OAuth scopes for ${channelName}`); - // When accessing public channels, no scopes are needed - if (channelName && PUBLIC_CHANNELS.includes(channelName)) { - resolve(); - return; - } + // When accessing public channels, no scopes are needed + if (channelName && PUBLIC_CHANNELS.includes(channelName)) { + resolve(); + return; + } - // The `read` scope has the highest priority, if the token has it - // then it can access all streams - const requiredScopes = ['read']; + // The `read` scope has the highest priority, if the token has it + // then it can access all streams + const requiredScopes = ['read']; - // When accessing specifically the notifications stream, - // we need a read:notifications, while in all other cases, - // we can allow access with read:statuses. Mind that the - // user stream will not contain notifications unless - // the token has either read or read:notifications scope - // as well, this is handled separately. - if (channelName === 'user:notification') { - requiredScopes.push('read:notifications'); - } else { - requiredScopes.push('read:statuses'); - } + // When accessing specifically the notifications stream, + // we need a read:notifications, while in all other cases, + // we can allow access with read:statuses. Mind that the + // user stream will not contain notifications unless + // the token has either read or read:notifications scope + // as well, this is handled separately. + if (channelName === 'user:notification') { + requiredScopes.push('read:notifications'); + } else { + requiredScopes.push('read:statuses'); + } - if (req.scopes && requiredScopes.some(requiredScope => req.scopes.includes(requiredScope))) { - resolve(); - return; - } + if ( + req.scopes && + requiredScopes.some((requiredScope) => + req.scopes.includes(requiredScope), + ) + ) { + resolve(); + return; + } - reject(new AuthenticationError('Access token does not have the required scopes')); - }); + reject( + new AuthenticationError( + 'Access token does not have the required scopes', + ), + ); + }); /** * @typedef SystemMessageHandlers @@ -477,7 +536,7 @@ const startServer = async () => { * @returns {SubscriptionListener} */ const createSystemMessageListener = (req, eventHandlers) => { - return message => { + return (message) => { if (!message?.event) { return; } @@ -487,7 +546,9 @@ const startServer = async () => { req.log.debug(`System message for ${req.accountId}: ${event}`); if (event === 'kill') { - req.log.debug(`Closing connection for ${req.accountId} due to expired access token`); + req.log.debug( + `Closing connection for ${req.accountId} due to expired access token`, + ); eventHandlers.onKill(); } else if (event === 'filters_changed') { req.log.debug(`Invalidating filters cache for ${req.accountId}`); @@ -514,13 +575,17 @@ const startServer = async () => { unsubscribe(accessTokenChannelId, listener); unsubscribe(systemChannelId, listener); - metrics.connectedChannels.labels({ type: 'eventsource', channel: 'system' }).dec(2); + metrics.connectedChannels + .labels({ type: 'eventsource', channel: 'system' }) + .dec(2); }); subscribe(accessTokenChannelId, listener); subscribe(systemChannelId, listener); - metrics.connectedChannels.labels({ type: 'eventsource', channel: 'system' }).inc(2); + metrics.connectedChannels + .labels({ type: 'eventsource', channel: 'system' }) + .inc(2); }; /** @@ -543,13 +608,17 @@ const startServer = async () => { return; } - accountFromRequest(req).then(() => checkScopes(req, req.log, channelName)).then(() => { - subscribeHttpToSystemChannel(req, res); - }).then(() => { - next(); - }).catch(err => { - next(err); - }); + accountFromRequest(req) + .then(() => checkScopes(req, req.log, channelName)) + .then(() => { + subscribeHttpToSystemChannel(req, res); + }) + .then(() => { + next(); + }) + .catch((err) => { + next(err); + }); }; /** @@ -566,7 +635,7 @@ const startServer = async () => { return; } - const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); + const { statusCode, errorMessage } = extractErrorStatusAndMessage(err); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: errorMessage })); @@ -578,7 +647,8 @@ const startServer = async () => { * @returns {string} */ // @ts-ignore - const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); + const placeholders = (arr, shift = 0) => + arr.map((_, i) => `$${i + 1 + shift}`).join(', '); /** * @param {string} listId @@ -588,7 +658,10 @@ const startServer = async () => { const authorizeListAccess = async (listId, req) => { const { accountId } = req; - const result = await pgPool.query('SELECT id, account_id FROM lists WHERE id = $1 AND account_id = $2 LIMIT 1', [listId, accountId]); + const result = await pgPool.query( + 'SELECT id, account_id FROM lists WHERE id = $1 AND account_id = $2 LIMIT 1', + [listId, accountId], + ); if (result.rows.length === 0) { throw new AuthenticationError('List not found'); @@ -600,28 +673,37 @@ const startServer = async () => { * @param {any} req * @returns {Promise.} */ - const authorizeAntennaAccess = (antennaId, req) => new Promise((resolve, reject) => { - const { accountId } = req; + const authorizeAntennaAccess = (antennaId, req) => + new Promise((resolve, reject) => { + const { accountId } = req; - pgPool.connect((err, client, done) => { - if (err) { - reject(); - return; - } - - // @ts-ignore - client.query('SELECT id, account_id FROM antennas WHERE id = $1 LIMIT 1', [antennaId], (err, result) => { - done(); - - if (err || result.rows.length === 0 || result.rows[0].account_id !== accountId) { + pgPool.connect((err, client, done) => { + if (err) { reject(); return; } - resolve(); + // @ts-ignore + client.query( + 'SELECT id, account_id FROM antennas WHERE id = $1 LIMIT 1', + [antennaId], + (err, result) => { + done(); + + if ( + err || + result.rows.length === 0 || + result.rows[0].account_id !== accountId + ) { + reject(); + return; + } + + resolve(); + }, + ); }); }); - }); /** * @param {string[]} channelIds @@ -633,7 +715,15 @@ const startServer = async () => { * @param {boolean=} needsFiltering * @returns {SubscriptionListener} */ - const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, needsFiltering = false) => { + const streamFrom = ( + channelIds, + req, + log, + output, + attachCloseHandler, + destinationType, + needsFiltering = false, + ) => { log.info({ channelIds }, `Starting stream`); /** @@ -642,11 +732,15 @@ const startServer = async () => { */ const transmit = (event, payload) => { // TODO: Replace "string"-based delete payloads with object payloads: - const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; + const encodedPayload = + typeof payload === 'object' ? JSON.stringify(payload) : payload; metrics.messagesSent.labels({ type: destinationType }).inc(1); - log.debug({ event, payload }, `Transmitting ${event} to ${req.accountId}`); + log.debug( + { event, payload }, + `Transmitting ${event} to ${req.accountId}`, + ); output(event, encodedPayload); }; @@ -656,7 +750,7 @@ const startServer = async () => { // events also include a queued_at value, but this is being removed shortly. /** @type {SubscriptionListener} */ - const listener = message => { + const listener = (message) => { if (!message?.event || !message?.payload) { return; } @@ -680,7 +774,10 @@ const startServer = async () => { // // The channels that need filtering are determined in the function // `channelNameToIds` defined below: - if (!needsFiltering || (event !== 'update' && event !== 'status.update')) { + if ( + !needsFiltering || + (event !== 'update' && event !== 'status.update') + ) { transmit(event, payload); return; } @@ -689,8 +786,13 @@ const startServer = async () => { // filtering of statuses: // Filter based on language: - if (Array.isArray(req.chosenLanguages) && req.chosenLanguages.indexOf(payload.language) === -1) { - log.debug(`Message ${payload.id} filtered by language (${payload.language})`); + if ( + Array.isArray(req.chosenLanguages) && + req.chosenLanguages.indexOf(payload.language) === -1 + ) { + log.debug( + `Message ${payload.id} filtered by language (${payload.language})`, + ); return; } @@ -702,7 +804,9 @@ const startServer = async () => { // Filter based on domain blocks, blocks, mutes, or custom filters: // @ts-ignore - const targetAccountIds = [payload.account.id].concat(payload.mentions.map(item => item.id)); + const targetAccountIds = [payload.account.id].concat( + payload.mentions.map((item) => item.id), + ); const accountDomain = payload.account.acct.split('@')[1]; // TODO: Move this logic out of the message handling loop @@ -714,7 +818,8 @@ const startServer = async () => { const queries = [ // @ts-ignore - client.query(`SELECT 1 + client.query( + `SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) @@ -722,176 +827,238 @@ const startServer = async () => { SELECT 1 FROM mutes WHERE account_id = $1 - AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, payload.account.id].concat(targetAccountIds)), + AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, + [req.accountId, payload.account.id].concat(targetAccountIds), + ), ]; if (accountDomain) { // @ts-ignore - queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); + queries.push( + client.query( + 'SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', + [req.accountId, accountDomain], + ), + ); } // @ts-ignore if (!payload.filtered && !req.cachedFilters) { // @ts-ignore - queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, filter.with_quote AS with_quote, filter.with_profile AS with_profile, keyword.keyword AS keyword, keyword.whole_word AS whole_word, filter.exclude_follows AS exclude_follows, filter.exclude_localusers AS exclude_localusers FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId])); + queries.push( + client.query( + 'SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, filter.with_profile AS with_profile, keyword.keyword AS keyword, keyword.whole_word AS whole_word, filter.exclude_follows AS exclude_follows, filter.exclude_localusers AS exclude_localusers FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', + [req.accountId], + ), + ); } if (!payload.filtered) { // @ts-ignore - queries.push(client.query(`SELECT 1 + queries.push( + client.query( + `SELECT 1 FROM follows - WHERE (account_id = $1 AND target_account_id = $2)`, [req.accountId, payload.account.id])); + WHERE (account_id = $1 AND target_account_id = $2)`, + [req.accountId, payload.account.id], + ), + ); } - Promise.all(queries).then(values => { - releasePgConnection(); + Promise.all(queries) + .then((values) => { + releasePgConnection(); - // Handling blocks & mutes and domain blocks: If one of those applies, - // then we don't transmit the payload of the event to the client - if (values[0].rows.length > 0 || (accountDomain && values[1].rows.length > 0)) { - return; - } + // Handling blocks & mutes and domain blocks: If one of those applies, + // then we don't transmit the payload of the event to the client + if ( + values[0].rows.length > 0 || + (accountDomain && values[1].rows.length > 0) + ) { + return; + } - // If the payload already contains the `filtered` property, it means - // that filtering has been applied on the ruby on rails side, as - // such, we don't need to construct or apply the filters in streaming: - if (Object.hasOwn(payload, "filtered")) { - transmit(event, payload); - return; - } + // If the payload already contains the `filtered` property, it means + // that filtering has been applied on the ruby on rails side, as + // such, we don't need to construct or apply the filters in streaming: + if (Object.hasOwn(payload, 'filtered')) { + transmit(event, payload); + return; + } - const following = values[values.length - 1].rows.length > 0; - - // Handling for constructing the custom filters and caching them on the request - // TODO: Move this logic out of the message handling lifecycle - // @ts-ignore - if (!req.cachedFilters) { - const filterRows = values[accountDomain ? 2 : 1].rows; + const following = values[values.length - 1].rows.length > 0; + // Handling for constructing the custom filters and caching them on the request + // TODO: Move this logic out of the message handling lifecycle // @ts-ignore - req.cachedFilters = filterRows.reduce((cache, filter) => { - if (cache[filter.id]) { - cache[filter.id].keywords.push([filter.keyword, filter.whole_word]); - } else { - cache[filter.id] = { - keywords: [[filter.keyword, filter.whole_word]], - expires_at: filter.expires_at, - filter: { - id: filter.id, - title: filter.title, - context: filter.context, - expires_at: filter.expires_at, - // filter.filter_action is the value from the - // custom_filters.action database column, it is an integer - // representing a value in an enum defined by Ruby on Rails: - // - // enum { warn: 0, hide: 1 } - filter_action: ['warn', 'hide'][filter.filter_action], - with_quote: filter.with_quote, - withAccountName: filter.with_profile, - excludeFollows: filter.exclude_follows, - excludeLocalusers: filter.exclude_localusers, - }, - }; - } + if (!req.cachedFilters) { + const filterRows = values[accountDomain ? 2 : 1].rows; - return cache; - }, {}); - - // Construct the regular expressions for the custom filters: This - // needs to be done in a separate loop as the database returns one - // filterRow per keyword, so we need all the keywords before - // constructing the regular expression - // @ts-ignore - Object.keys(req.cachedFilters).forEach((key) => { // @ts-ignore - req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => { - let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - - if (whole_word) { - if (/^[\w]/.test(expr)) { - expr = `\\b${expr}`; - } - - if (/[\w]$/.test(expr)) { - expr = `${expr}\\b`; - } + req.cachedFilters = filterRows.reduce((cache, filter) => { + if (cache[filter.id]) { + cache[filter.id].keywords.push([ + filter.keyword, + filter.whole_word, + ]); + } else { + cache[filter.id] = { + keywords: [[filter.keyword, filter.whole_word]], + expires_at: filter.expires_at, + filter: { + id: filter.id, + title: filter.title, + context: filter.context, + expires_at: filter.expires_at, + // filter.filter_action is the value from the + // custom_filters.action database column, it is an integer + // representing a value in an enum defined by Ruby on Rails: + // + // enum { warn: 0, hide: 1 } + filter_action: ['warn', 'hide'][filter.filter_action], + withAccountName: filter.with_profile, + excludeFollows: filter.exclude_follows, + excludeLocalusers: filter.exclude_localusers, + }, + }; } - return expr; - }).join('|'), 'i'); - }); - } + return cache; + }, {}); - // Apply cachedFilters against the payload, constructing a - // `filter_results` array of FilterResult entities - // @ts-ignore - if (req.cachedFilters) { - const status = payload; - // TODO: Calculate searchableContent in Ruby on Rails: + // Construct the regular expressions for the custom filters: This + // needs to be done in a separate loop as the database returns one + // filterRow per keyword, so we need all the keywords before + // constructing the regular expression + // @ts-ignore + Object.keys(req.cachedFilters).forEach((key) => { + // @ts-ignore + req.cachedFilters[key].regexp = new RegExp( + req.cachedFilters[key].keywords + .map(([keyword, whole_word]) => { + let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + if (whole_word) { + if (/^[\w]/.test(expr)) { + expr = `\\b${expr}`; + } + + if (/[\w]$/.test(expr)) { + expr = `${expr}\\b`; + } + } + + return expr; + }) + .join('|'), + 'i', + ); + }); + } + + // Apply cachedFilters against the payload, constructing a + // `filter_results` array of FilterResult entities // @ts-ignore - const searchableContent = ([status.spoiler_text || '', status.content, ...(reference_texts || [])].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const searchableTextContent = JSDOM.fragment(searchableContent).textContent; - const searchableAccountContent = JSDOM.fragment([status.account.display_name, status.account.note].join('\n\n')).textContent; + if (req.cachedFilters) { + const status = payload; + // TODO: Calculate searchableContent in Ruby on Rails: + // @ts-ignore + const searchableContent = [ + status.spoiler_text || '', + status.content, + ...(reference_texts || []), + ] + .concat( + status.poll && status.poll.options + ? status.poll.options.map((option) => option.title) + : [], + ) + .concat(status.media_attachments.map((att) => att.description)) + .join('\n\n') + .replace(//g, '\n') + .replace(/<\/p>

/g, '\n\n'); + const searchableTextContent = + JSDOM.fragment(searchableContent).textContent; + const searchableAccountContent = JSDOM.fragment( + [status.account.display_name, status.account.note].join('\n\n'), + ).textContent; - const now = new Date(); - // @ts-ignore - const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => { - // Check the filter hasn't expired before applying: - if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) { - return results; - } + const now = new Date(); + // @ts-ignore + const filter_results = Object.values(req.cachedFilters).reduce( + (results, cachedFilter) => { + // Check the filter hasn't expired before applying: + if ( + cachedFilter.expires_at !== null && + cachedFilter.expires_at < now + ) { + return results; + } - if (cachedFilter.filter && cachedFilter.filter.excludeFollows && following) { - return results; - } + if ( + cachedFilter.filter && + cachedFilter.filter.excludeFollows && + following + ) { + return results; + } - if (cachedFilter.filter && cachedFilter.filter.excludeLocalusers && !accountDomain) { - return results; - } + if ( + cachedFilter.filter && + cachedFilter.filter.excludeLocalusers && + !accountDomain + ) { + return results; + } - // Just in-case JSDOM fails to find textContent in searchableContent - if (!searchableTextContent) { - return results; - } + // Just in-case JSDOM fails to find textContent in searchableContent + if (!searchableTextContent) { + return results; + } - const keyword_matches = searchableTextContent.match(cachedFilter.regexp) || - ((cachedFilter.withAccountName && searchableAccountContent) ? searchableAccountContent.match(cachedFilter.regexp) : null); - if (keyword_matches) { - // results is an Array of FilterResult; status_matches is always - // null as we only are only applying the keyword-based custom - // filters, not the status-based custom filters. - // https://docs.joinmastodon.org/entities/FilterResult/ - results.push({ - filter: cachedFilter.filter, - keyword_matches, - status_matches: null - }); - } + const keyword_matches = + searchableTextContent.match(cachedFilter.regexp) || + (cachedFilter.withAccountName && searchableAccountContent + ? searchableAccountContent.match(cachedFilter.regexp) + : null); + if (keyword_matches) { + // results is an Array of FilterResult; status_matches is always + // null as we only are only applying the keyword-based custom + // filters, not the status-based custom filters. + // https://docs.joinmastodon.org/entities/FilterResult/ + results.push({ + filter: cachedFilter.filter, + keyword_matches, + status_matches: null, + }); + } - return results; - }, []); + return results; + }, + [], + ); - // Send the payload + the FilterResults as the `filtered` property - // to the streaming connection. To reach this code, the `event` must - // have been either `update` or `status.update`, meaning the - // `payload` is a Status entity, which has a `filtered` property: - // - // filtered: https://docs.joinmastodon.org/entities/Status/#filtered - transmit(event, { - ...payload, - filtered: filter_results - }); - } else { - transmit(event, payload); - } - }).catch(err => { - log.error(err); - releasePgConnection(); - }); + // Send the payload + the FilterResults as the `filtered` property + // to the streaming connection. To reach this code, the `event` must + // have been either `update` or `status.update`, meaning the + // `payload` is a Status entity, which has a `filtered` property: + // + // filtered: https://docs.joinmastodon.org/entities/Status/#filtered + transmit(event, { + ...payload, + filtered: filter_results, + }); + } else { + transmit(event, payload); + } + }) + .catch((err) => { + log.error(err); + releasePgConnection(); + }); }); }; - channelIds.forEach(id => { + channelIds.forEach((id) => { subscribe(id, listener); }); @@ -914,7 +1081,9 @@ const startServer = async () => { // In theory we'll always have a channel name, but channelNameFromPath can return undefined: if (typeof channelName === 'string') { - metrics.connectedChannels.labels({ type: 'eventsource', channel: channelName }).inc(); + metrics.connectedChannels + .labels({ type: 'eventsource', channel: channelName }) + .inc(); } res.setHeader('Content-Type', 'text/event-stream'); @@ -933,7 +1102,9 @@ const startServer = async () => { metrics.connectedClients.labels({ type: 'eventsource' }).dec(); // In theory we'll always have a channel name, but channelNameFromPath can return undefined: if (typeof channelName === 'string') { - metrics.connectedChannels.labels({ type: 'eventsource', channel: channelName }).dec(); + metrics.connectedChannels + .labels({ type: 'eventsource', channel: channelName }) + .dec(); } clearInterval(heartbeat); @@ -951,17 +1122,19 @@ const startServer = async () => { * @returns {function(string[], SubscriptionListener): void} */ - const streamHttpEnd = (req, closeHandler = undefined) => (ids, listener) => { - req.on('close', () => { - ids.forEach(id => { - unsubscribe(id, listener); - }); + const streamHttpEnd = + (req, closeHandler = undefined) => + (ids, listener) => { + req.on('close', () => { + ids.forEach((id) => { + unsubscribe(id, listener); + }); - if (closeHandler) { - closeHandler(); - } - }); - }; + if (closeHandler) { + closeHandler(); + } + }); + }; /** * @param {http.IncomingMessage} req @@ -979,7 +1152,7 @@ const startServer = async () => { ws.send(message, (/** @type {Error|undefined} */ err) => { if (err) { - req.log.error({err}, `Failed to send to websocket`); + req.log.error({ err }, `Failed to send to websocket`); } }); }; @@ -987,7 +1160,7 @@ const startServer = async () => { /** * @param {http.ServerResponse} res */ - const httpNotFound = res => { + const httpNotFound = (res) => { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); }; @@ -1010,20 +1183,30 @@ const startServer = async () => { return; } - channelNameToIds(req, channelName, req.query).then(({ channelIds, options }) => { - const onSend = streamToHttp(req, res); - const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); + channelNameToIds(req, channelName, req.query) + .then(({ channelIds, options }) => { + const onSend = streamToHttp(req, res); + const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); - // @ts-ignore - streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering); - }).catch(err => { - const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); + // @ts-ignore + streamFrom( + channelIds, + req, + req.log, + onSend, + onEnd, + 'eventsource', + options.needsFiltering, + ); + }) + .catch((err) => { + const { statusCode, errorMessage } = extractErrorStatusAndMessage(err); - res.log.info({ err }, 'Eventsource subscription error'); + res.log.info({ err }, 'Eventsource subscription error'); - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: errorMessage })); - }); + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: errorMessage })); + }); }); /** @@ -1038,7 +1221,7 @@ const startServer = async () => { * @param {any} req * @returns {string[]} */ - const channelsForUserStream = req => { + const channelsForUserStream = (req) => { const arr = [`timeline:${req.accountId}`]; if (isInScope(req, ['read', 'read:notifications'])) { @@ -1054,125 +1237,134 @@ const startServer = async () => { * @param {StreamParams} params * @returns {Promise.<{ channelIds: string[], options: { needsFiltering: boolean } }>} */ - const channelNameToIds = (req, name, params) => new Promise((resolve, reject) => { - switch (name) { - case 'user': - resolve({ - channelIds: channelsForUserStream(req), - options: { needsFiltering: false }, - }); + const channelNameToIds = (req, name, params) => + new Promise((resolve, reject) => { + switch (name) { + case 'user': + resolve({ + channelIds: channelsForUserStream(req), + options: { needsFiltering: false }, + }); - break; - case 'user:notification': - resolve({ - channelIds: [`timeline:${req.accountId}:notifications`], - options: { needsFiltering: false }, - }); + break; + case 'user:notification': + resolve({ + channelIds: [`timeline:${req.accountId}:notifications`], + options: { needsFiltering: false }, + }); - break; - case 'public': - resolve({ - channelIds: ['timeline:public'], - options: { needsFiltering: true }, - }); + break; + case 'public': + resolve({ + channelIds: ['timeline:public'], + options: { needsFiltering: true }, + }); - break; - case 'public:local': - resolve({ - channelIds: ['timeline:public:local'], - options: { needsFiltering: true }, - }); + break; + case 'public:local': + resolve({ + channelIds: ['timeline:public:local'], + options: { needsFiltering: true }, + }); - break; - case 'public:remote': - resolve({ - channelIds: ['timeline:public:remote'], - options: { needsFiltering: true }, - }); + break; + case 'public:remote': + resolve({ + channelIds: ['timeline:public:remote'], + options: { needsFiltering: true }, + }); - break; - case 'public:media': - resolve({ - channelIds: ['timeline:public:media'], - options: { needsFiltering: true }, - }); + break; + case 'public:media': + resolve({ + channelIds: ['timeline:public:media'], + options: { needsFiltering: true }, + }); - break; - case 'public:local:media': - resolve({ - channelIds: ['timeline:public:local:media'], - options: { needsFiltering: true }, - }); + break; + case 'public:local:media': + resolve({ + channelIds: ['timeline:public:local:media'], + options: { needsFiltering: true }, + }); - break; - case 'public:remote:media': - resolve({ - channelIds: ['timeline:public:remote:media'], - options: { needsFiltering: true }, - }); + break; + case 'public:remote:media': + resolve({ + channelIds: ['timeline:public:remote:media'], + options: { needsFiltering: true }, + }); - break; - case 'direct': - resolve({ - channelIds: [`timeline:direct:${req.accountId}`], - options: { needsFiltering: false }, - }); + break; + case 'direct': + resolve({ + channelIds: [`timeline:direct:${req.accountId}`], + options: { needsFiltering: false }, + }); - break; - case 'hashtag': - if (!params.tag) { - reject(new RequestError('Missing tag name parameter')); - } else { - resolve({ - channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}`], - options: { needsFiltering: true }, - }); + break; + case 'hashtag': + if (!params.tag) { + reject(new RequestError('Missing tag name parameter')); + } else { + resolve({ + channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}`], + options: { needsFiltering: true }, + }); + } + + break; + case 'hashtag:local': + if (!params.tag) { + reject(new RequestError('Missing tag name parameter')); + } else { + resolve({ + channelIds: [ + `timeline:hashtag:${normalizeHashtag(params.tag)}:local`, + ], + options: { needsFiltering: true }, + }); + } + + break; + case 'list': + if (!params.list) { + reject(new RequestError('Missing list name parameter')); + return; + } + + authorizeListAccess(params.list, req) + .then(() => { + resolve({ + channelIds: [`timeline:list:${params.list}`], + options: { needsFiltering: false }, + }); + }) + .catch(() => { + reject( + new AuthenticationError('Not authorized to stream this list'), + ); + }); + + break; + case 'antenna': + // @ts-ignore + authorizeAntennaAccess(params.antenna, req) + .then(() => { + resolve({ + channelIds: [`timeline:antenna:${params.antenna}`], + options: { needsFiltering: false }, + }); + }) + .catch(() => { + reject('Not authorized to stream this antenna'); + }); + + break; + default: + reject(new RequestError('Unknown stream type')); } - - break; - case 'hashtag:local': - if (!params.tag) { - reject(new RequestError('Missing tag name parameter')); - } else { - resolve({ - channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}:local`], - options: { needsFiltering: true }, - }); - } - - break; - case 'list': - if (!params.list) { - reject(new RequestError('Missing list name parameter')); - return; - } - - authorizeListAccess(params.list, req).then(() => { - resolve({ - channelIds: [`timeline:list:${params.list}`], - options: { needsFiltering: false }, - }); - }).catch(() => { - reject(new AuthenticationError('Not authorized to stream this list')); - }); - - break; - case 'antenna': - // @ts-ignore - authorizeAntennaAccess(params.antenna, req).then(() => { - resolve({ - channelIds: [`timeline:antenna:${params.antenna}`], - options: { needsFiltering: false }, - }); - }).catch(() => { - reject('Not authorized to stream this antenna'); - }); - - break; - default: - reject(new RequestError('Unknown stream type')); - } - }); + }); /** * @param {string} channelName @@ -1184,7 +1376,10 @@ const startServer = async () => { return [channelName, params.list]; } else if (channelName === 'antenna' && params.antenna) { return [channelName, params.antenna]; - } else if (['hashtag', 'hashtag:local'].includes(channelName) && params.tag) { + } else if ( + ['hashtag', 'hashtag:local'].includes(channelName) && + params.tag + ) { return [channelName, params.tag]; } else { return [channelName]; @@ -1205,46 +1400,69 @@ const startServer = async () => { * @param {StreamParams} params * @returns {void} */ - const subscribeWebsocketToChannel = ({ websocket, request, logger, subscriptions }, channelName, params) => { - checkScopes(request, logger, channelName).then(() => channelNameToIds(request, channelName, params)).then(({ - channelIds, - options, - }) => { - if (subscriptions[channelIds.join(';')]) { - return; - } + const subscribeWebsocketToChannel = ( + { websocket, request, logger, subscriptions }, + channelName, + params, + ) => { + checkScopes(request, logger, channelName) + .then(() => channelNameToIds(request, channelName, params)) + .then(({ channelIds, options }) => { + if (subscriptions[channelIds.join(';')]) { + return; + } - const onSend = streamToWs(request, websocket, streamNameFromChannelName(channelName, params)); - const stopHeartbeat = subscriptionHeartbeat(channelIds); - const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options.needsFiltering); + const onSend = streamToWs( + request, + websocket, + streamNameFromChannelName(channelName, params), + ); + const stopHeartbeat = subscriptionHeartbeat(channelIds); + const listener = streamFrom( + channelIds, + request, + logger, + onSend, + undefined, + 'websocket', + options.needsFiltering, + ); - metrics.connectedChannels.labels({ type: 'websocket', channel: channelName }).inc(); + metrics.connectedChannels + .labels({ type: 'websocket', channel: channelName }) + .inc(); - subscriptions[channelIds.join(';')] = { - channelName, - listener, - stopHeartbeat, - }; - }).catch(err => { - const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); + subscriptions[channelIds.join(';')] = { + channelName, + listener, + stopHeartbeat, + }; + }) + .catch((err) => { + const { statusCode, errorMessage } = extractErrorStatusAndMessage(err); - logger.error({ err }, 'Websocket subscription error'); + logger.error({ err }, 'Websocket subscription error'); - // If we have a socket that is alive and open still, send the error back to the client: - if (websocket.isAlive && websocket.readyState === websocket.OPEN) { - websocket.send(JSON.stringify({ - error: errorMessage, - status: statusCode - })); - } - }); + // If we have a socket that is alive and open still, send the error back to the client: + if (websocket.isAlive && websocket.readyState === websocket.OPEN) { + websocket.send( + JSON.stringify({ + error: errorMessage, + status: statusCode, + }), + ); + } + }); }; /** * @param {WebSocketSession} session * @param {string[]} channelIds */ - const removeSubscription = ({ request, logger, subscriptions }, channelIds) => { + const removeSubscription = ( + { request, logger, subscriptions }, + channelIds, + ) => { logger.info({ channelIds, accountId: request.accountId }, `Ending stream`); const subscription = subscriptions[channelIds.join(';')]; @@ -1253,11 +1471,13 @@ const startServer = async () => { return; } - channelIds.forEach(channelId => { + channelIds.forEach((channelId) => { unsubscribe(channelId, subscription.listener); }); - metrics.connectedChannels.labels({ type: 'websocket', channel: subscription.channelName }).dec(); + metrics.connectedChannels + .labels({ type: 'websocket', channel: subscription.channelName }) + .dec(); subscription.stopHeartbeat(); delete subscriptions[channelIds.join(';')]; @@ -1272,23 +1492,31 @@ const startServer = async () => { const unsubscribeWebsocketFromChannel = (session, channelName, params) => { const { websocket, request, logger } = session; - channelNameToIds(request, channelName, params).then(({ channelIds }) => { - removeSubscription(session, channelIds); - }).catch(err => { - logger.error({err}, 'Websocket unsubscribe error'); + channelNameToIds(request, channelName, params) + .then(({ channelIds }) => { + removeSubscription(session, channelIds); + }) + .catch((err) => { + logger.error({ err }, 'Websocket unsubscribe error'); - // If we have a socket that is alive and open still, send the error back to the client: - if (websocket.isAlive && websocket.readyState === websocket.OPEN) { - // TODO: Use a better error response here - websocket.send(JSON.stringify({ error: "Error unsubscribing from channel" })); - } - }); + // If we have a socket that is alive and open still, send the error back to the client: + if (websocket.isAlive && websocket.readyState === websocket.OPEN) { + // TODO: Use a better error response here + websocket.send( + JSON.stringify({ error: 'Error unsubscribing from channel' }), + ); + } + }); }; /** * @param {WebSocketSession} session */ - const subscribeWebsocketToSystemChannel = ({ websocket, request, subscriptions }) => { + const subscribeWebsocketToSystemChannel = ({ + websocket, + request, + subscriptions, + }) => { const accessTokenChannelId = `timeline:access_token:${request.accessTokenId}`; const systemChannelId = `timeline:system:${request.accountId}`; @@ -1304,18 +1532,18 @@ const startServer = async () => { subscriptions[accessTokenChannelId] = { channelName: 'system', listener, - stopHeartbeat: () => { - }, + stopHeartbeat: () => {}, }; subscriptions[systemChannelId] = { channelName: 'system', listener, - stopHeartbeat: () => { - }, + stopHeartbeat: () => {}, }; - metrics.connectedChannels.labels({ type: 'websocket', channel: 'system' }).inc(2); + metrics.connectedChannels + .labels({ type: 'websocket', channel: 'system' }) + .inc(2); }; /** @@ -1348,7 +1576,7 @@ const startServer = async () => { ws.on('close', function onWebsocketClose() { const subscriptions = Object.keys(session.subscriptions); - subscriptions.forEach(channelIds => { + subscriptions.forEach((channelIds) => { removeSubscription(session, channelIds.split(';')); }); @@ -1375,7 +1603,10 @@ const startServer = async () => { ws.on('message', (data, isBinary) => { if (isBinary) { log.warn('Received binary data, closing connection'); - ws.close(1003, 'The mastodon streaming server does not support binary messages'); + ws.close( + 1003, + 'The mastodon streaming server does not support binary messages', + ); return; } const message = data.toString('utf8'); @@ -1401,14 +1632,18 @@ const startServer = async () => { const location = req.url && url.parse(req.url, true); if (location && location.query.stream) { - subscribeWebsocketToChannel(session, firstParam(location.query.stream), location.query); + subscribeWebsocketToChannel( + session, + firstParam(location.query.stream), + location.query, + ); } } wss.on('connection', onConnection); setInterval(() => { - wss.clients.forEach(ws => { + wss.clients.forEach((ws) => { // @ts-ignore if (ws.isAlive === false) { ws.terminate(); @@ -1421,7 +1656,7 @@ const startServer = async () => { }); }, 30000); - attachServerWithConfig(server, address => { + attachServerWithConfig(server, (address) => { logger.info(`Streaming API now listening on ${address}`); });