From 669fe9ee06a82482201377abd303492eb7fa7d94 Mon Sep 17 00:00:00 2001
From: aschmitz <andy.schmitz@gmail.com>
Date: Wed, 20 Sep 2017 07:53:48 -0500
Subject: [PATCH] Change IDs to strings rather than numbers in API JSON output
 (#5019)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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.
---
 app/javascript/mastodon/actions/store.js       |  3 +--
 app/javascript/mastodon/components/account.js  |  2 +-
 .../components/autosuggest_textarea.js         |  2 +-
 app/javascript/mastodon/components/status.js   |  4 ++--
 .../mastodon/components/status_action_bar.js   |  2 +-
 .../features/account/components/action_bar.js  |  2 +-
 .../features/account/components/header.js      |  2 +-
 .../mastodon/features/account_gallery/index.js | 16 ++++++++--------
 .../account_timeline/components/header.js      |  2 +-
 .../containers/header_container.js             |  2 +-
 .../features/account_timeline/index.js         | 18 +++++++++---------
 .../compose/components/compose_form.js         |  2 +-
 .../features/compose/components/upload_form.js |  2 +-
 .../mastodon/features/favourites/index.js      |  6 +++---
 .../mastodon/features/followers/index.js       | 16 ++++++++--------
 .../mastodon/features/following/index.js       | 16 ++++++++--------
 .../mastodon/features/reblogs/index.js         |  6 +++---
 .../features/status/components/action_bar.js   |  2 +-
 .../mastodon/features/status/index.js          | 12 ++++++------
 app/serializers/initial_state_serializer.rb    | 10 +++++-----
 app/serializers/rest/account_serializer.rb     |  4 ++++
 app/serializers/rest/application_serializer.rb |  6 +++++-
 .../rest/media_attachment_serializer.rb        |  4 ++++
 .../rest/notification_serializer.rb            |  4 ++++
 .../rest/relationship_serializer.rb            |  4 ++++
 app/serializers/rest/report_serializer.rb      |  4 ++++
 app/serializers/rest/status_serializer.rb      | 14 +++++++++++++-
 app/services/batched_remove_status_service.rb  |  2 +-
 app/services/remove_status_service.rb          |  2 +-
 .../accounts/relationships_controller_spec.rb  |  4 ++--
 .../api/v1/media_controller_spec.rb            |  6 +++---
 .../v1/statuses/favourites_controller_spec.rb  |  2 +-
 .../api/v1/statuses/pins_controller_spec.rb    |  2 +-
 .../api/v1/statuses/reblogs_controller_spec.rb |  2 +-
 34 files changed, 111 insertions(+), 76 deletions(-)

diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index 0597d265ec..a1db0fdd51 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -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);
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 6456c12baa..d614a52c9c 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -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,
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 35b37600fe..30e3049dff 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -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();
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 82359156df..3716d522ea 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -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}`);
     }
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 692b1ef2bc..803b730d91 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -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,
   };
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index c12c0889e4..9e8fea69d1 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -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,
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 6eb51a5c7f..9ee7a56d94 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -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,
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index 0cfd98f231..2a88addc42 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -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));
     }
   }
 
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js
index 167a2097e5..edfedb8644 100644
--- a/app/javascript/mastodon/features/account_timeline/components/header.js
+++ b/app/javascript/mastodon/features/account_timeline/components/header.js
@@ -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,
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index dcee78b3e0..ab75b40de1 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -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']),
   });
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 3c8b63114f..fe92216d53 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -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));
     }
   }
 
diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js
index f3320a42bf..413142d814 100644
--- a/app/javascript/mastodon/features/compose/components/compose_form.js
+++ b/app/javascript/mastodon/features/compose/components/compose_form.js
@@ -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,
diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js
index 78473dab40..cf2d2658ae 100644
--- a/app/javascript/mastodon/features/compose/components/upload_form.js
+++ b/app/javascript/mastodon/features/compose/components/upload_form.js
@@ -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);
   }
 
diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js
index dc8109d16e..4dbfefd876 100644
--- a/app/javascript/mastodon/features/favourites/index.js
+++ b/app/javascript/mastodon/features/favourites/index.js
@@ -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));
     }
   }
 
diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js
index 2d85b9cc0b..89445559fe 100644
--- a/app/javascript/mastodon/features/followers/index.js
+++ b/app/javascript/mastodon/features/followers/index.js
@@ -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 () {
diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js
index e4e2a4811e..c348302768 100644
--- a/app/javascript/mastodon/features/following/index.js
+++ b/app/javascript/mastodon/features/following/index.js
@@ -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 () {
diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js
index dc940ae01a..f1904786a6 100644
--- a/app/javascript/mastodon/features/reblogs/index.js
+++ b/app/javascript/mastodon/features/reblogs/index.js
@@ -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));
     }
   }
 
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index c303caf105..034cc98544 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -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,
   };
 
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index c614f6acb7..8da6e743cb 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -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));
     }
   }
 
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 32ffcc6880..9ee9bd29c8 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -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
 
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 012a4fd18a..65fdb03081 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -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
diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb
index 868a62f1e7..5eb03a513b 100644
--- a/app/serializers/rest/application_serializer.rb
+++ b/app/serializers/rest/application_serializer.rb
@@ -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
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index 31189406a1..f6e7c79d1f 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -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)
diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb
index f95d099a31..541a6b8b5c 100644
--- a/app/serializers/rest/notification_serializer.rb
+++ b/app/serializers/rest/notification_serializer.rb
@@ -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
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 1d431aa1b6..998727e37a 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -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
diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb
index 0c6bd65567..ecb88d653f 100644
--- a/app/serializers/rest/report_serializer.rb
+++ b/app/serializers/rest/report_serializer.rb
@@ -2,4 +2,8 @@
 
 class REST::ReportSerializer < ActiveModel::Serializer
   attributes :id, :action_taken
+
+  def id
+    object.id.to_s
+  end
 end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 066d65d9ea..e0fd1c77e4 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -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
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 86eaa5735f..e1e845bc0d 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -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          = {}
 
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 83fc77043b..5f45fb3ab8 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -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
diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
index a9073b197a..431fc21941 100644
--- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index 6bad3f05d8..baa22d7e48 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
index 2a029230d7..aba7cd4588 100644
--- a/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/favourites_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
index 2e170da240..79005c9dec 100644
--- a/spec/controllers/api/v1/statuses/pins_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb
@@ -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
diff --git a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
index d6d36c1b2f..7417ff672f 100644
--- a/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses/reblogs_controller_spec.rb
@@ -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