]> cat aescling's git repositories - mastodon.git/commitdiff
Change IDs to strings rather than numbers in API JSON output (#5019)
authoraschmitz <andy.schmitz@gmail.com>
Wed, 20 Sep 2017 12:53:48 +0000 (07:53 -0500)
committerEugen Rochko <eugen@zeonfederated.com>
Wed, 20 Sep 2017 12:53:48 +0000 (14:53 +0200)
* Fix JavaScript interface with long IDs

Somewhat predictably, the JS interface handled IDs as numbers, which in
JS are IEEE double-precision floats. This loses some precision when
working with numbers as large as those generated by the new ID scheme,
so we instead handle them here as strings. This is relatively simple,
and doesn't appear to have caused any problems, but should definitely
be tested more thoroughly than the built-in tests. Several days of use
appear to support this working properly.

BREAKING CHANGE:

The major(!) change here is that IDs are now returned as strings by the
REST endpoints, rather than as integers. In practice, relatively few
changes were required to make the existing JS UI work with this change,
but it will likely hit API clients pretty hard: it's an entirely
different type to consume. (The one API client I tested, Tusky, handles
this with no problems, however.)

Twitter ran into this issue when introducing Snowflake IDs, and decided
to instead introduce an `id_str` field in JSON responses. I have opted
to *not* do that, and instead force all IDs to 64-bit integers
represented by strings in one go. (I believe Twitter exacerbated their
problem by rolling out the changes three times: once for statuses, once
for DMs, and once for user IDs, as well as by leaving an integer ID
value in JSON. As they said, "If you’re using the `id` field with JSON
in a Javascript-related language, there is a very high likelihood that
the integers will be silently munged by Javascript interpreters. In most
cases, this will result in behavior such as being unable to load or
delete a specific direct message, because the ID you're sending to the
API is different than the actual identifier associated with the
message." [1]) However, given that this is a significant change for API
users, alternatives or a transition time may be appropriate.

1: https://blog.twitter.com/developer/en_us/a/2011/direct-messages-going-snowflake-on-sep-30-2011.html

* Additional fixes for stringified IDs in JSON

These should be the last two. These were identified using eslint to try
to identify any plain casts to JavaScript numbers. (Some such casts are
legitimate, but these were not.)

Adding the following to .eslintrc.yml will identify casts to numbers:

~~~
  no-restricted-syntax:
  - warn
  - selector: UnaryExpression[operator='+'] > :not(Literal)
    message: Avoid the use of unary +
  - selector: CallExpression[callee.name='Number']
    message: Casting with Number() may coerce string IDs to numbers
~~~

The remaining three casts appear legitimate: two casts to array indices,
one in a server to turn an environment variable into a number.

* Back out RelationshipsController Change

This was made to make a test a bit less flakey, but has nothing to
do with this branch.

* Change internal streaming payloads to stringified IDs as well

Per
https://github.com/tootsuite/mastodon/pull/5019#issuecomment-330736452
we need these changes to send deleted status IDs as strings, not
integers.

34 files changed:
app/javascript/mastodon/actions/store.js
app/javascript/mastodon/components/account.js
app/javascript/mastodon/components/autosuggest_textarea.js
app/javascript/mastodon/components/status.js
app/javascript/mastodon/components/status_action_bar.js
app/javascript/mastodon/features/account/components/action_bar.js
app/javascript/mastodon/features/account/components/header.js
app/javascript/mastodon/features/account_gallery/index.js
app/javascript/mastodon/features/account_timeline/components/header.js
app/javascript/mastodon/features/account_timeline/containers/header_container.js
app/javascript/mastodon/features/account_timeline/index.js
app/javascript/mastodon/features/compose/components/compose_form.js
app/javascript/mastodon/features/compose/components/upload_form.js
app/javascript/mastodon/features/favourites/index.js
app/javascript/mastodon/features/followers/index.js
app/javascript/mastodon/features/following/index.js
app/javascript/mastodon/features/reblogs/index.js
app/javascript/mastodon/features/status/components/action_bar.js
app/javascript/mastodon/features/status/index.js
app/serializers/initial_state_serializer.rb
app/serializers/rest/account_serializer.rb
app/serializers/rest/application_serializer.rb
app/serializers/rest/media_attachment_serializer.rb
app/serializers/rest/notification_serializer.rb
app/serializers/rest/relationship_serializer.rb
app/serializers/rest/report_serializer.rb
app/serializers/rest/status_serializer.rb
app/services/batched_remove_status_service.rb
app/services/remove_status_service.rb
spec/controllers/api/v1/accounts/relationships_controller_spec.rb
spec/controllers/api/v1/media_controller_spec.rb
spec/controllers/api/v1/statuses/favourites_controller_spec.rb
spec/controllers/api/v1/statuses/pins_controller_spec.rb
spec/controllers/api/v1/statuses/reblogs_controller_spec.rb

index 0597d265ec40b5c7acffd0fa43453def924b4da1..a1db0fdd51c60c90fb93d32bb4f80e3e13a86e82 100644 (file)
@@ -5,8 +5,7 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
 
 const convertState = rawState =>
   fromJS(rawState, (k, v) =>
-    Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
-      Number.isNaN(x * 1) ? x : x * 1));
+    Iterable.isIndexed(v) ? v.toList() : v.toMap());
 
 export function hydrateStore(rawState) {
   const state = convertState(rawState);
index 6456c12baa04b0d7ad550a322834aef993a86b95..d614a52c9cb97c6e8ca7cfc58c7aa4d118f3715d 100644 (file)
@@ -21,7 +21,7 @@ export default class Account extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
index 35b37600fe540567c44a90428688929c8207440c..30e3049dfff2859639cea4f6d14d2c8215eb244d 100644 (file)
@@ -128,7 +128,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
   }
 
   onSuggestionClick = (e) => {
-    const suggestion = Number(e.currentTarget.getAttribute('data-index'));
+    const suggestion = e.currentTarget.getAttribute('data-index');
     e.preventDefault();
     this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
     this.textarea.focus();
index 82359156df8ed50f99fa428bc18ed5813a29c088..3716d522ea2020990174e7cdb0ea9ca9dcee7a58 100644 (file)
@@ -34,7 +34,7 @@ export default class Status extends ImmutablePureComponent {
     onBlock: PropTypes.func,
     onEmbed: PropTypes.func,
     onHeightChange: PropTypes.func,
-    me: PropTypes.number,
+    me: PropTypes.string,
     boostModal: PropTypes.bool,
     autoPlayGif: PropTypes.bool,
     muted: PropTypes.bool,
@@ -70,7 +70,7 @@ export default class Status extends ImmutablePureComponent {
 
   handleAccountClick = (e) => {
     if (this.context.router && e.button === 0) {
-      const id = Number(e.currentTarget.getAttribute('data-id'));
+      const id = e.currentTarget.getAttribute('data-id');
       e.preventDefault();
       this.context.router.history.push(`/accounts/${id}`);
     }
index 692b1ef2bc455e2d6f493756f70bd353916e91c0..803b730d91283e1166f6116797cf8ca4bbacc8b9 100644 (file)
@@ -46,7 +46,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
     onEmbed: PropTypes.func,
     onMuteConversation: PropTypes.func,
     onPin: PropTypes.func,
-    me: PropTypes.number,
+    me: PropTypes.string,
     withDismiss: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
index c12c0889e4b0c6628a035af2a8539a2de2bd7d92..9e8fea69d129837b65d16f3c7be77a7abcdab8fd 100644 (file)
@@ -26,7 +26,7 @@ export default class ActionBar extends React.PureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
index 6eb51a5c7f7766b17a884c8977be55d19cde0f43..9ee7a56d945a893c36cd975c2810bc0a80df3bb4 100644 (file)
@@ -77,7 +77,7 @@ export default class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     autoPlayGif: PropTypes.bool.isRequired,
index 0cfd98f23157aa60c81be064ce57be5363055902..2a88addc422b742b238a17ee8ffe74e7473db764 100644 (file)
@@ -16,9 +16,9 @@ import { ScrollContainer } from 'react-router-scroll';
 import LoadMore from '../../components/load_more';
 
 const mapStateToProps = (state, props) => ({
-  medias: getAccountGallery(state, Number(props.params.accountId)),
-  isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'isLoading']),
-  hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'next']),
+  medias: getAccountGallery(state, props.params.accountId),
+  isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
+  hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
   autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
 });
 
@@ -35,20 +35,20 @@ export default class AccountGallery extends ImmutablePureComponent {
   };
 
   componentDidMount () {
-    this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
-    this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId)));
+    this.props.dispatch(fetchAccount(this.props.params.accountId));
+    this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
-      this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId)));
+      this.props.dispatch(fetchAccount(nextProps.params.accountId));
+      this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
     }
   }
 
   handleScrollToBottom = () => {
     if (this.props.hasMore) {
-      this.props.dispatch(expandAccountMediaTimeline(Number(this.props.params.accountId)));
+      this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
     }
   }
 
index 167a2097e5c198457104536dea1c4106e0bde6ca..edfedb86446d07716870b07a48c5da934289f093 100644 (file)
@@ -10,7 +10,7 @@ export default class Header extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMention: PropTypes.func.isRequired,
index dcee78b3e0f6a1624b44e023069d0d680f51e54b..ab75b40de171af3b37eda591f1593789ebe6f218 100644 (file)
@@ -27,7 +27,7 @@ const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
 
   const mapStateToProps = (state, { accountId }) => ({
-    account: getAccount(state, Number(accountId)),
+    account: getAccount(state, accountId),
     me: state.getIn(['meta', 'me']),
     unfollowModal: state.getIn(['meta', 'unfollow_modal']),
   });
index 3c8b63114f6afe6552bf814841ecdb2befe9675f..fe92216d53323c070fded0e522619983ffa0abc0 100644 (file)
@@ -13,9 +13,9 @@ import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()),
-  isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']),
-  hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']),
+  statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
+  isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
+  hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
   me: state.getIn(['meta', 'me']),
 });
 
@@ -28,24 +28,24 @@ export default class AccountTimeline extends ImmutablePureComponent {
     statusIds: ImmutablePropTypes.list,
     isLoading: PropTypes.bool,
     hasMore: PropTypes.bool,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
-    this.props.dispatch(refreshAccountTimeline(Number(this.props.params.accountId)));
+    this.props.dispatch(fetchAccount(this.props.params.accountId));
+    this.props.dispatch(refreshAccountTimeline(this.props.params.accountId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
-      this.props.dispatch(refreshAccountTimeline(Number(nextProps.params.accountId)));
+      this.props.dispatch(fetchAccount(nextProps.params.accountId));
+      this.props.dispatch(refreshAccountTimeline(nextProps.params.accountId));
     }
   }
 
   handleScrollToBottom = () => {
     if (!this.props.isLoading && this.props.hasMore) {
-      this.props.dispatch(expandAccountTimeline(Number(this.props.params.accountId)));
+      this.props.dispatch(expandAccountTimeline(this.props.params.accountId));
     }
   }
 
index f3320a42bf3c436745a65346db8ac592206c61a1..413142d814e512d81c2e7594052ed4e4f97cd214 100644 (file)
@@ -42,7 +42,7 @@ export default class ComposeForm extends ImmutablePureComponent {
     preselectDate: PropTypes.instanceOf(Date),
     is_submitting: PropTypes.bool,
     is_uploading: PropTypes.bool,
-    me: PropTypes.number,
+    me: PropTypes.string,
     onChange: PropTypes.func.isRequired,
     onSubmit: PropTypes.func.isRequired,
     onClearSuggestions: PropTypes.func.isRequired,
index 78473dab40e0616eb28fb66b9e7449c5a4756c3d..cf2d2658aeff74904c7e55876d23f42ede314587 100644 (file)
@@ -21,7 +21,7 @@ export default class UploadForm extends React.PureComponent {
   };
 
   onRemoveFile = (e) => {
-    const id = Number(e.currentTarget.parentElement.getAttribute('data-id'));
+    const id = e.currentTarget.parentElement.getAttribute('data-id');
     this.props.onRemoveFile(id);
   }
 
index dc8109d16e3a1c1af8cd2b9b0e9dcc1a498a75dc..4dbfefd87690f0edb25681327c727bf4dba4801e 100644 (file)
@@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'favourited_by', Number(props.params.statusId)]),
+  accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
 });
 
 @connect(mapStateToProps)
@@ -24,12 +24,12 @@ export default class Favourites extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchFavourites(Number(this.props.params.statusId)));
+    this.props.dispatch(fetchFavourites(this.props.params.statusId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchFavourites(Number(nextProps.params.statusId)));
+      this.props.dispatch(fetchFavourites(nextProps.params.statusId));
     }
   }
 
index 2d85b9cc0b980b06665489a7242a696026dd02a4..89445559fe392258ae148ec0f0bceaa001110ffc 100644 (file)
@@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']),
-  hasMore: !!state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'next']),
+  accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
 });
 
 @connect(mapStateToProps)
@@ -32,14 +32,14 @@ export default class Followers extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
-    this.props.dispatch(fetchFollowers(Number(this.props.params.accountId)));
+    this.props.dispatch(fetchAccount(this.props.params.accountId));
+    this.props.dispatch(fetchFollowers(this.props.params.accountId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
-      this.props.dispatch(fetchFollowers(Number(nextProps.params.accountId)));
+      this.props.dispatch(fetchAccount(nextProps.params.accountId));
+      this.props.dispatch(fetchFollowers(nextProps.params.accountId));
     }
   }
 
@@ -47,13 +47,13 @@ export default class Followers extends ImmutablePureComponent {
     const { scrollTop, scrollHeight, clientHeight } = e.target;
 
     if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
-      this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
+      this.props.dispatch(expandFollowers(this.props.params.accountId));
     }
   }
 
   handleLoadMore = (e) => {
     e.preventDefault();
-    this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
+    this.props.dispatch(expandFollowers(this.props.params.accountId));
   }
 
   render () {
index e4e2a4811eb2bd0004d674855bba2e2583e4c232..c34830276849d975a52bd72606bb7d5cd080588d 100644 (file)
@@ -17,8 +17,8 @@ import ColumnBackButton from '../../components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']),
-  hasMore: !!state.getIn(['user_lists', 'following', Number(props.params.accountId), 'next']),
+  accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
+  hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
 });
 
 @connect(mapStateToProps)
@@ -32,14 +32,14 @@ export default class Following extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchAccount(Number(this.props.params.accountId)));
-    this.props.dispatch(fetchFollowing(Number(this.props.params.accountId)));
+    this.props.dispatch(fetchAccount(this.props.params.accountId));
+    this.props.dispatch(fetchFollowing(this.props.params.accountId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
-      this.props.dispatch(fetchAccount(Number(nextProps.params.accountId)));
-      this.props.dispatch(fetchFollowing(Number(nextProps.params.accountId)));
+      this.props.dispatch(fetchAccount(nextProps.params.accountId));
+      this.props.dispatch(fetchFollowing(nextProps.params.accountId));
     }
   }
 
@@ -47,13 +47,13 @@ export default class Following extends ImmutablePureComponent {
     const { scrollTop, scrollHeight, clientHeight } = e.target;
 
     if (scrollTop === scrollHeight - clientHeight && this.props.hasMore) {
-      this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
+      this.props.dispatch(expandFollowing(this.props.params.accountId));
     }
   }
 
   handleLoadMore = (e) => {
     e.preventDefault();
-    this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
+    this.props.dispatch(expandFollowing(this.props.params.accountId));
   }
 
   render () {
index dc940ae01a620ae71d3168d61529408743232562..f1904786a611d2bf4329b4341f70b16a0bf0ca4d 100644 (file)
@@ -11,7 +11,7 @@ import ColumnBackButton from '../../components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  accountIds: state.getIn(['user_lists', 'reblogged_by', Number(props.params.statusId)]),
+  accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
 });
 
 @connect(mapStateToProps)
@@ -24,12 +24,12 @@ export default class Reblogs extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchReblogs(Number(this.props.params.statusId)));
+    this.props.dispatch(fetchReblogs(this.props.params.statusId));
   }
 
   componentWillReceiveProps(nextProps) {
     if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchReblogs(Number(nextProps.params.statusId)));
+      this.props.dispatch(fetchReblogs(nextProps.params.statusId));
     }
   }
 
index c303caf1052c5f42e519c07938a4203388a26f2b..034cc9854418e1199dfba44dab9a25e0e4cf5678 100644 (file)
@@ -36,7 +36,7 @@ export default class ActionBar extends React.PureComponent {
     onReport: PropTypes.func,
     onPin: PropTypes.func,
     onEmbed: PropTypes.func,
-    me: PropTypes.number.isRequired,
+    me: PropTypes.string.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
index c614f6acb79346cd78d7c5d1d86cc771ee6833fb..8da6e743cb3b4d2f794d022f49779bc6b8626695 100644 (file)
@@ -38,9 +38,9 @@ const makeMapStateToProps = () => {
   const getStatus = makeGetStatus();
 
   const mapStateToProps = (state, props) => ({
-    status: getStatus(state, Number(props.params.statusId)),
-    ancestorsIds: state.getIn(['contexts', 'ancestors', Number(props.params.statusId)]),
-    descendantsIds: state.getIn(['contexts', 'descendants', Number(props.params.statusId)]),
+    status: getStatus(state, props.params.statusId),
+    ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
+    descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
     me: state.getIn(['meta', 'me']),
     boostModal: state.getIn(['meta', 'boost_modal']),
     deleteModal: state.getIn(['meta', 'delete_modal']),
@@ -64,7 +64,7 @@ export default class Status extends ImmutablePureComponent {
     status: ImmutablePropTypes.map,
     ancestorsIds: ImmutablePropTypes.list,
     descendantsIds: ImmutablePropTypes.list,
-    me: PropTypes.number,
+    me: PropTypes.string,
     boostModal: PropTypes.bool,
     deleteModal: PropTypes.bool,
     autoPlayGif: PropTypes.bool,
@@ -72,12 +72,12 @@ export default class Status extends ImmutablePureComponent {
   };
 
   componentWillMount () {
-    this.props.dispatch(fetchStatus(Number(this.props.params.statusId)));
+    this.props.dispatch(fetchStatus(this.props.params.statusId));
   }
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
-      this.props.dispatch(fetchStatus(Number(nextProps.params.statusId)));
+      this.props.dispatch(fetchStatus(nextProps.params.statusId));
     }
   }
 
index 32ffcc6880feb3eb3de0824ca518eacc83a68190..9ee9bd29c828978cb77261b539e195a3de9466c1 100644 (file)
@@ -10,11 +10,11 @@ class InitialStateSerializer < ActiveModel::Serializer
       access_token: object.token,
       locale: I18n.locale,
       domain: Rails.configuration.x.local_domain,
-      admin: object.admin&.id,
+      admin: object.admin&.id&.to_s,
     }
 
     if object.current_account
-      store[:me]             = object.current_account.id
+      store[:me]             = object.current_account.id.to_s
       store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal
       store[:boost_modal]    = object.current_account.user.setting_boost_modal
       store[:delete_modal]   = object.current_account.user.setting_delete_modal
@@ -28,7 +28,7 @@ class InitialStateSerializer < ActiveModel::Serializer
     store = {}
 
     if object.current_account
-      store[:me]                = object.current_account.id
+      store[:me]                = object.current_account.id.to_s
       store[:default_privacy]   = object.current_account.user.setting_default_privacy
       store[:default_sensitive] = object.current_account.user.setting_default_sensitive
     end
@@ -40,8 +40,8 @@ class InitialStateSerializer < ActiveModel::Serializer
 
   def accounts
     store = {}
-    store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account
-    store[object.admin.id]           = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin
+    store[object.current_account.id.to_s] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) if object.current_account
+    store[object.admin.id.to_s]           = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) if object.admin
     store
   end
 
index 012a4fd18aa2df2f15ec43ba366e8670f7c38375..65fdb0308176ccd86582386d1f8ac24d9383275c 100644 (file)
@@ -7,6 +7,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
              :note, :url, :avatar, :avatar_static, :header, :header_static,
              :followers_count, :following_count, :statuses_count
 
+  def id
+    object.id.to_s
+  end
+
   def note
     Formatter.instance.simplified_format(object)
   end
index 868a62f1e73ab830c872568d643f7bd4c6e4fbe1..5eb03a513b9734af7ac2367385e5ffbd55e09f74 100644 (file)
@@ -4,8 +4,12 @@ class REST::ApplicationSerializer < ActiveModel::Serializer
   attributes :id, :name, :website, :redirect_uri,
              :client_id, :client_secret
 
+  def id
+    object.id.to_s
+  end
+
   def client_id
-    object.uid
+    object.uid.to_s
   end
 
   def client_secret
index 31189406a1ad4a8b2130bb0d202c7d99de4f22e1..f6e7c79d1fb844e2350429a41366646ec13232aa 100644 (file)
@@ -6,6 +6,10 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
   attributes :id, :type, :url, :preview_url,
              :remote_url, :text_url, :meta
 
+  def id
+    object.id.to_s
+  end
+
   def url
     if object.needs_redownload?
       media_proxy_url(object.id, :original)
index f95d099a319ce2e456573bc8d21a8dd7b897df30..541a6b8b5c4c2a83a3e421cbf27b4802568955e2 100644 (file)
@@ -6,6 +6,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer
   belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
   belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
 
+  def id
+    object.id.to_s
+  end
+
   def status_type?
     [:favourite, :reblog, :mention].include?(object.type)
   end
index 1d431aa1b660f3ea99a0a2b6b1412e7b1df5fd37..998727e37a28f296e30643bc0fb19ab285f13e2b 100644 (file)
@@ -4,6 +4,10 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
   attributes :id, :following, :followed_by, :blocking,
              :muting, :requested, :domain_blocking
 
+  def id
+    object.id.to_s
+  end
+
   def following
     instance_options[:relationships].following[object.id] || false
   end
index 0c6bd65567dc79671213c3e8483f4839064dc029..ecb88d653facd04ad4205edb50fd4e9d51a516e5 100644 (file)
@@ -2,4 +2,8 @@
 
 class REST::ReportSerializer < ActiveModel::Serializer
   attributes :id, :action_taken
+
+  def id
+    object.id.to_s
+  end
 end
index 066d65d9ea10869d1dc5aa3e5403ce189f0eff3b..e0fd1c77e40fb0a3dd814ea0e4e2c7690e59405f 100644 (file)
@@ -19,6 +19,18 @@ class REST::StatusSerializer < ActiveModel::Serializer
   has_many :tags
   has_many :emojis
 
+  def id
+    object.id.to_s
+  end
+
+  def in_reply_to_id
+    object.in_reply_to_id.to_s
+  end
+
+  def in_reply_to_account_id
+    object.in_reply_to_account_id.to_s
+  end
+
   def current_user?
     !current_user.nil?
   end
@@ -82,7 +94,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
     attributes :id, :username, :url, :acct
 
     def id
-      object.account_id
+      object.account_id.to_s
     end
 
     def username
index 86eaa5735fe8d4014e421e296c160b25c31ab6c5..e1e845bc0dd4b37cf674095a3591fd983db87e9d 100644 (file)
@@ -18,7 +18,7 @@ class BatchedRemoveStatusService < BaseService
     @stream_entry_batches  = []
     @salmon_batches        = []
     @activity_json_batches = []
-    @json_payloads         = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h
+    @json_payloads         = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
     @activity_json         = {}
     @activity_xml          = {}
 
index 83fc77043b75d279ed4439437f96b0f3f5fdc3f0..5f45fb3ab8851c5da1965365e15412c4cfd30466 100644 (file)
@@ -4,7 +4,7 @@ class RemoveStatusService < BaseService
   include StreamEntryRenderer
 
   def call(status)
-    @payload      = Oj.dump(event: :delete, payload: status.id)
+    @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
     @status       = status
     @account      = status.account
     @tags         = status.tags.pluck(:name).to_a
index a9073b197ab040c34b3c92c8e4e4124795162f3d..431fc21941479ddfdba280373fb4384db18864e0 100644 (file)
@@ -50,14 +50,14 @@ describe Api::V1::Accounts::RelationshipsController do
         json = body_as_json
 
         expect(json).to be_a Enumerable
-        expect(json.first[:id]).to eq simon.id
+        expect(json.first[:id]).to eq simon.id.to_s
         expect(json.first[:following]).to be true
         expect(json.first[:followed_by]).to be false
         expect(json.first[:muting]).to be false
         expect(json.first[:requested]).to be false
         expect(json.first[:domain_blocking]).to be false
 
-        expect(json.second[:id]).to eq lewis.id
+        expect(json.second[:id]).to eq lewis.id.to_s
         expect(json.second[:following]).to be false
         expect(json.second[:followed_by]).to be true
         expect(json.second[:muting]).to be false
index 6bad3f05d8e9ca70a86db98130bf25b37678b14d..baa22d7e4861773ecd3a5e07e5ecf7388bef8e05 100644 (file)
@@ -53,7 +53,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
 
@@ -75,7 +75,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       it 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
 
@@ -97,7 +97,7 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
 
       xit 'returns media ID in JSON' do
-        expect(body_as_json[:id]).to eq MediaAttachment.first.id
+        expect(body_as_json[:id]).to eq MediaAttachment.first.id.to_s
       end
     end
   end
index 2a029230d7f1b3e87ffd176497e6632fc1b5fbe3..aba7cd4588d58e7fe5bb5be4c34589f99d37244b 100644 (file)
@@ -36,7 +36,7 @@ describe Api::V1::Statuses::FavouritesController do
       it 'return json with updated attributes' do
         hash_body = body_as_json
 
-        expect(hash_body[:id]).to eq status.id
+        expect(hash_body[:id]).to eq status.id.to_s
         expect(hash_body[:favourites_count]).to eq 1
         expect(hash_body[:favourited]).to be true
       end
index 2e170da240938dc3bebbeb95a148fbcd24a16a88..79005c9decadf1ce354e49edfabbf5af6794ab92 100644 (file)
@@ -32,7 +32,7 @@ describe Api::V1::Statuses::PinsController do
       it 'return json with updated attributes' do
         hash_body = body_as_json
 
-        expect(hash_body[:id]).to eq status.id
+        expect(hash_body[:id]).to eq status.id.to_s
         expect(hash_body[:pinned]).to be true
       end
     end
index d6d36c1b2f618531d10f5a5784d632dbc20d70b3..7417ff672fbec0b51b35a6d11a3424ab26c2e89e 100644 (file)
@@ -36,7 +36,7 @@ describe Api::V1::Statuses::ReblogsController do
       it 'return json with updated attributes' do
         hash_body = body_as_json
 
-        expect(hash_body[:reblog][:id]).to eq status.id
+        expect(hash_body[:reblog][:id]).to eq status.id.to_s
         expect(hash_body[:reblog][:reblogs_count]).to eq 1
         expect(hash_body[:reblog][:reblogged]).to be true
       end