forked from cybrespace/mastodon
duplicates. Web UI regenerates UUID for that header every time the compose form is changed or successfully submitted Also, fix Farsi i18n overwriting the English one
This commit is contained in:
parent
3ea5b948a4
commit
8b5179d006
|
@ -85,6 +85,10 @@ export function submitCompose() {
|
|||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||
visibility: getState().getIn(['compose', 'privacy'])
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey'])
|
||||
}
|
||||
}).then(function (response) {
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STORE_HYDRATE } from '../actions/store';
|
||||
import Immutable from 'immutable';
|
||||
import uuid from '../uuid';
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
mounted: false,
|
||||
|
@ -45,7 +46,8 @@ const initialState = Immutable.Map({
|
|||
suggestions: Immutable.List(),
|
||||
me: null,
|
||||
default_privacy: 'public',
|
||||
resetFileKey: Math.floor((Math.random() * 0x10000))
|
||||
resetFileKey: Math.floor((Math.random() * 0x10000)),
|
||||
idempotencyKey: null
|
||||
});
|
||||
|
||||
function statusToTextMentions(state, status) {
|
||||
|
@ -69,6 +71,7 @@ function clearAll(state) {
|
|||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('sensitive', false);
|
||||
map.update('media_attachments', list => list.clear());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -79,6 +82,7 @@ function appendMedia(state, media) {
|
|||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||
map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -89,6 +93,7 @@ function removeMedia(state, mediaId) {
|
|||
return state.withMutations(map => {
|
||||
map.update('media_attachments', list => list.filterNot(item => item.get('id') === mediaId));
|
||||
map.update('text', text => text.replace(media.get('text_url'), '').trim());
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (prevSize === 1) {
|
||||
map.set('sensitive', false);
|
||||
|
@ -102,6 +107,7 @@ const insertSuggestion = (state, position, token, completion) => {
|
|||
map.set('suggestion_token', null);
|
||||
map.update('suggestions', Immutable.List(), list => list.clear());
|
||||
map.set('focusDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -111,6 +117,7 @@ const insertEmoji = (state, position, emojiData) => {
|
|||
return state.withMutations(map => {
|
||||
map.update('text', oldText => `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`);
|
||||
map.set('focusDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -135,18 +142,27 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_UNMOUNT:
|
||||
return state.set('mounted', false);
|
||||
case COMPOSE_SENSITIVITY_CHANGE:
|
||||
return state.set('sensitive', !state.get('sensitive'));
|
||||
return state
|
||||
.set('sensitive', !state.get('sensitive'))
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_SPOILERNESS_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('spoiler_text', '');
|
||||
map.set('spoiler', !state.get('spoiler'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SPOILER_TEXT_CHANGE:
|
||||
return state.set('spoiler_text', action.text);
|
||||
return state
|
||||
.set('spoiler_text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_VISIBILITY_CHANGE:
|
||||
return state.set('privacy', action.value);
|
||||
return state
|
||||
.set('privacy', action.value)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_CHANGE:
|
||||
return state.set('text', action.text);
|
||||
return state
|
||||
.set('text', action.text)
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_REPLY:
|
||||
return state.withMutations(map => {
|
||||
map.set('in_reply_to', action.status.get('id'));
|
||||
|
@ -154,6 +170,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('preselectDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
@ -170,6 +187,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('privacy', state.get('default_privacy'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUBMIT_REQUEST:
|
||||
return state.set('is_submitting', true);
|
||||
|
@ -190,7 +208,10 @@ export default function compose(state = initialState, action) {
|
|||
case COMPOSE_UPLOAD_PROGRESS:
|
||||
return state.set('progress', Math.round((action.loaded / action.total) * 100));
|
||||
case COMPOSE_MENTION:
|
||||
return state.update('text', text => `${text}@${action.account.get('acct')} `).set('focusDate', new Date());
|
||||
return state
|
||||
.update('text', text => `${text}@${action.account.get('acct')} `)
|
||||
.set('focusDate', new Date())
|
||||
.set('idempotencyKey', uuid());
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default function uuid(a) {
|
||||
return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
|
||||
};
|
|
@ -57,11 +57,16 @@ class Api::V1::StatusesController < ApiController
|
|||
end
|
||||
|
||||
def create
|
||||
@status = PostStatusService.new.call(current_user.account, status_params[:status], status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
application: doorkeeper_token.application)
|
||||
@status = PostStatusService.new.call(current_user.account,
|
||||
status_params[:status],
|
||||
status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
visibility: status_params[:visibility],
|
||||
application: doorkeeper_token.application,
|
||||
idempotency: request.headers['Idempotency-Key'])
|
||||
|
||||
render :show
|
||||
end
|
||||
|
||||
|
|
|
@ -11,8 +11,14 @@ class PostStatusService < BaseService
|
|||
# @option [String] :spoiler_text
|
||||
# @option [Enumerable] :media_ids Optional array of media IDs to attach
|
||||
# @option [Doorkeeper::Application] :application
|
||||
# @option [String] :idempotency Optional idempotency key
|
||||
# @return [Status]
|
||||
def call(account, text, in_reply_to = nil, options = {})
|
||||
if options[:idempotency].present?
|
||||
existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}")
|
||||
return Status.find(existing_id) if existing_id
|
||||
end
|
||||
|
||||
media = validate_media!(options[:media_ids])
|
||||
status = account.statuses.create!(text: text,
|
||||
thread: in_reply_to,
|
||||
|
@ -30,6 +36,10 @@ class PostStatusService < BaseService
|
|||
DistributionWorker.perform_async(status.id)
|
||||
Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
|
||||
|
||||
if options[:idempotency].present?
|
||||
redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id)
|
||||
end
|
||||
|
||||
status
|
||||
end
|
||||
|
||||
|
@ -63,4 +73,8 @@ class PostStatusService < BaseService
|
|||
def process_hashtags_service
|
||||
@process_hashtags_service ||= ProcessHashtagsService.new
|
||||
end
|
||||
|
||||
def redis
|
||||
Redis.current
|
||||
end
|
||||
end
|
||||
|
|
|
@ -176,7 +176,14 @@ RSpec.describe PostStatusService do
|
|||
)
|
||||
end
|
||||
|
||||
it 'returns existing status when used twice with idempotency key' do
|
||||
account = Fabricate(:account)
|
||||
status1 = subject.call(account, 'test', nil, idempotency: 'meepmeep')
|
||||
status2 = subject.call(account, 'test', nil, idempotency: 'meepmeep')
|
||||
expect(status2.id).to eq status1.id
|
||||
end
|
||||
|
||||
def create_status_with_options(options = {})
|
||||
subject.call(Fabricate(:account), "test", nil, options)
|
||||
subject.call(Fabricate(:account), 'test', nil, options)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue