summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-01-19 22:37:27 +0100
committerGitHub <noreply@github.com>2022-01-19 22:37:27 +0100
commit1060666c583670bb3b89ed5154e61038331e30c3 (patch)
tree11713b72bc62cd395dade4cb4fe7e397bf41ffec
parent2d1f082bb6bee89242ee8042dc19016179078566 (diff)
Add support for editing for published statuses (#16697)
* Add support for editing for published statuses * Fix references to stripped-out code * Various fixes and improvements * Further fixes and improvements * Fix updates being potentially sent to unauthorized recipients * Various fixes and improvements * Fix wrong words in test * Fix notifying accounts that were tagged but were not in the audience * Fix mistake
-rw-r--r--app/controllers/api/v1/statuses/histories_controller.rb21
-rw-r--r--app/controllers/api/v1/statuses/sources_controller.rb21
-rw-r--r--app/helpers/jsonld_helper.rb8
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js7
-rw-r--r--app/javascript/mastodon/actions/statuses.js3
-rw-r--r--app/javascript/mastodon/actions/streaming.js4
-rw-r--r--app/javascript/mastodon/components/status.js3
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js14
-rw-r--r--app/javascript/styles/mastodon/components.scss11
-rw-r--r--app/lib/activitypub/activity.rb43
-rw-r--r--app/lib/activitypub/activity/announce.rb18
-rw-r--r--app/lib/activitypub/activity/create.rb249
-rw-r--r--app/lib/activitypub/activity/update.rb17
-rw-r--r--app/lib/activitypub/parser/custom_emoji_parser.rb27
-rw-r--r--app/lib/activitypub/parser/media_attachment_parser.rb58
-rw-r--r--app/lib/activitypub/parser/poll_parser.rb53
-rw-r--r--app/lib/activitypub/parser/status_parser.rb118
-rw-r--r--app/lib/feed_manager.rb20
-rw-r--r--app/lib/status_reach_finder.rb31
-rw-r--r--app/models/poll.rb1
-rw-r--r--app/models/status.rb7
-rw-r--r--app/models/status_edit.rb23
-rw-r--r--app/serializers/activitypub/note_serializer.rb7
-rw-r--r--app/serializers/rest/status_edit_serializer.rb6
-rw-r--r--app/serializers/rest/status_serializer.rb2
-rw-r--r--app/serializers/rest/status_source_serializer.rb9
-rw-r--r--app/services/activitypub/fetch_remote_poll_service.rb2
-rw-r--r--app/services/activitypub/process_poll_service.rb64
-rw-r--r--app/services/activitypub/process_status_update_service.rb275
-rw-r--r--app/services/fan_out_on_write_service.rb149
-rw-r--r--app/services/process_mentions_service.rb65
-rw-r--r--app/services/remove_status_service.rb2
-rw-r--r--app/workers/activitypub/distribution_worker.rb48
-rw-r--r--app/workers/activitypub/raw_distribution_worker.rb37
-rw-r--r--app/workers/activitypub/reply_distribution_worker.rb34
-rw-r--r--app/workers/activitypub/update_distribution_worker.rb25
-rw-r--r--app/workers/distribution_worker.rb4
-rw-r--r--app/workers/feed_insert_worker.rb34
-rw-r--r--app/workers/local_notification_worker.rb2
-rw-r--r--app/workers/poll_expiration_notify_worker.rb45
-rw-r--r--app/workers/push_update_worker.rb35
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20210904215403_add_edited_at_to_statuses.rb5
-rw-r--r--db/migrate/20210908220918_create_status_edits.rb13
-rw-r--r--db/schema.rb15
-rw-r--r--spec/controllers/api/v1/statuses/histories_controller_spec.rb29
-rw-r--r--spec/controllers/api/v1/statuses/sources_controller_spec.rb29
-rw-r--r--spec/fabricators/preview_card_fabricator.rb6
-rw-r--r--spec/fabricators/status_edit_fabricator.rb7
-rw-r--r--spec/lib/status_reach_finder_spec.rb109
-rw-r--r--spec/models/status_edit_spec.rb5
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb6
-rw-r--r--spec/services/fan_out_on_write_service_spec.rb107
-rw-r--r--spec/services/process_mentions_service_spec.rb32
-rw-r--r--spec/workers/activitypub/distribution_worker_spec.rb7
-rw-r--r--spec/workers/feed_insert_worker_spec.rb2
56 files changed, 1409 insertions, 568 deletions
diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb
new file mode 100644
index 00000000000..c2c1fac5d55
--- /dev/null
+++ b/app/controllers/api/v1/statuses/histories_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::HistoriesController < Api::BaseController
+ include Authorization
+
+ before_action -> { authorize_if_got_token! :read, :'read:statuses' }
+ before_action :set_status
+
+ def show
+ render json: @status.edits, each_serializer: REST::StatusEditSerializer
+ end
+
+ private
+
+ def set_status
+ @status = Status.find(params[:status_id])
+ authorize @status, :show?
+ rescue Mastodon::NotPermittedError
+ not_found
+ end
+end
diff --git a/app/controllers/api/v1/statuses/sources_controller.rb b/app/controllers/api/v1/statuses/sources_controller.rb
new file mode 100644
index 00000000000..43408645130
--- /dev/null
+++ b/app/controllers/api/v1/statuses/sources_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::Statuses::SourcesController < Api::BaseController
+ include Authorization
+
+ before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
+ before_action :set_status
+
+ def show
+ render json: @status, serializer: REST::StatusSourceSerializer
+ end
+
+ private
+
+ def set_status
+ @status = Status.find(params[:status_id])
+ authorize @status, :show?
+ rescue Mastodon::NotPermittedError
+ not_found
+ end
+end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index 62eb50f786c..c24d2ddf106 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -34,7 +34,13 @@ module JsonLdHelper
end
def as_array(value)
- value.is_a?(Array) ? value : [value]
+ if value.nil?
+ []
+ elsif value.is_a?(Array)
+ value
+ else
+ [value]
+ end
end
def value_or_id(value)
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index 6b79e1f16d5..ca76e3494d1 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -54,9 +54,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.poll = status.poll.id;
}
- // Only calculate these values when status first encountered
- // Otherwise keep the ones already in the reducer
- if (normalOldStatus) {
+ // Only calculate these values when status first encountered and
+ // when the underlying values change. Otherwise keep the ones
+ // already in the reducer
+ if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
normalStatus.search_index = normalOldStatus.get('search_index');
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 3fc7c07023d..20d71362e9f 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -131,6 +131,9 @@ export function deleteStatusFail(id, error) {
};
};
+export const updateStatus = status => dispatch =>
+ dispatch(importFetchedStatus(status));
+
export function fetchContext(id) {
return (dispatch, getState) => {
dispatch(fetchContextRequest(id));
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index beb5c6a4a9d..8fbb22271ab 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -10,6 +10,7 @@ import {
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
+import { updateStatus } from './statuses';
import {
fetchAnnouncements,
updateAnnouncements,
@@ -75,6 +76,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
break;
+ case 'status.update':
+ dispatch(updateStatus(JSON.parse(data.payload)));
+ break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 9955046c048..fb370ca71f4 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -57,6 +57,7 @@ const messages = defineMessages({
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
+ edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
export default @injectIntl
@@ -483,7 +484,7 @@ class Status extends ImmutablePureComponent {
<div className='status__info'>
<a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
- <RelativeTimestamp timestamp={status.get('created_at')} />
+ <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 72ddeb2b24d..ee4a6b9898b 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom';
-import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
+import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
@@ -116,6 +116,7 @@ class DetailedStatus extends ImmutablePureComponent {
let reblogLink = '';
let reblogIcon = 'retweet';
let favouriteLink = '';
+ let edited = '';
if (this.props.measureHeight) {
outerStyle.height = `${this.state.height}px`;
@@ -237,6 +238,15 @@ class DetailedStatus extends ImmutablePureComponent {
);
}
+ if (status.get('edited_at')) {
+ edited = (
+ <React.Fragment>
+ <React.Fragment> · </React.Fragment>
+ <FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
+ </React.Fragment>
+ );
+ }
+
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
@@ -252,7 +262,7 @@ class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
- </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
+ </a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div>
</div>
</div>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 0a62e6b829b..02b3473a921 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -967,6 +967,17 @@
}
}
+.status__content__edited-label {
+ display: block;
+ cursor: default;
+ font-size: 15px;
+ line-height: 20px;
+ padding: 0;
+ padding-top: 8px;
+ color: $dark-text-color;
+ font-weight: 500;
+}