about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample15
-rw-r--r--.travis.yml2
-rw-r--r--Dockerfile (renamed from Dockerfile.app)0
-rw-r--r--Dockerfile.neo4j17
-rw-r--r--Gemfile11
-rw-r--r--Gemfile.lock52
-rw-r--r--README.md8
-rw-r--r--app/assets/javascripts/components/actions/suggestions.jsx37
-rw-r--r--app/assets/javascripts/components/components/status.jsx2
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx4
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx3
-rw-r--r--app/assets/javascripts/components/features/compose/components/suggestions_box.jsx86
-rw-r--r--app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx8
-rw-r--r--app/assets/javascripts/components/features/compose/index.jsx15
-rw-r--r--app/assets/javascripts/components/locales/de.jsx2
-rw-r--r--app/assets/javascripts/components/locales/en.jsx8
-rw-r--r--app/assets/javascripts/components/locales/es.jsx2
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx29
-rw-r--r--app/assets/javascripts/components/locales/hu.jsx55
-rw-r--r--app/assets/javascripts/components/locales/index.jsx8
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx2
-rw-r--r--app/assets/javascripts/components/reducers/accounts.jsx2
-rw-r--r--app/assets/javascripts/components/reducers/user_lists.jsx4
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/forms.scss18
-rw-r--r--app/assets/stylesheets/tables.scss25
-rw-r--r--app/controllers/admin/pubsubhubbub_controller.rb11
-rw-r--r--app/controllers/api/push_controller.rb37
-rw-r--r--app/controllers/api/v1/accounts_controller.rb31
-rw-r--r--app/controllers/api/v1/notifications_controller.rb23
-rw-r--r--app/controllers/api/v1/statuses_controller.rb10
-rw-r--r--app/controllers/api/v1/timelines_controller.rb25
-rw-r--r--app/controllers/api_controller.rb2
-rw-r--r--app/controllers/application_controller.rb24
-rw-r--r--app/controllers/settings/preferences_controller.rb7
-rw-r--r--app/helpers/admin/pubsubhubbub_helper.rb2
-rw-r--r--app/helpers/atom_builder_helper.rb6
-rw-r--r--app/helpers/settings_helper.rb2
-rw-r--r--app/lib/feed_manager.rb26
-rw-r--r--app/models/account.rb27
-rw-r--r--app/models/feed.rb27
-rw-r--r--app/models/follow.rb28
-rw-r--r--app/models/follow_suggestion.rb50
-rw-r--r--app/models/media_attachment.rb2
-rw-r--r--app/models/status.rb15
-rw-r--r--app/models/subscription.rb29
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/fan_out_on_write_service.rb5
-rw-r--r--app/services/favourite_service.rb2
-rw-r--r--app/services/follow_remote_account_service.rb3
-rw-r--r--app/services/follow_service.rb3
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/post_status_service.rb3
-rw-r--r--app/services/process_feed_service.rb2
-rw-r--r--app/services/process_hashtags_service.rb2
-rw-r--r--app/services/process_interaction_service.rb4
-rw-r--r--app/services/pubsubhubbub/subscribe_service.rb13
-rw-r--r--app/services/pubsubhubbub/unsubscribe_service.rb15
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/services/remove_status_service.rb5
-rw-r--r--app/services/search_service.rb4
-rw-r--r--app/services/update_remote_profile_service.rb26
-rw-r--r--app/views/accounts/show.atom.ruby3
-rw-r--r--app/views/admin/pubsubhubbub/index.html.haml20
-rw-r--r--app/views/settings/preferences/show.html.haml4
-rw-r--r--app/workers/processing_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/confirmation_worker.rb36
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb30
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb18
-rw-r--r--app/workers/removal_worker.rb9
-rw-r--r--app/workers/salmon_worker.rb2
-rw-r--r--app/workers/thread_resolve_worker.rb8
-rw-r--r--config/application.rb2
-rw-r--r--config/environments/production.rb4
-rw-r--r--config/i18n-tasks.yml3
-rw-r--r--config/initializers/mini_profiler.rb17
-rw-r--r--config/initializers/neography.rb5
-rw-r--r--config/initializers/ostatus.rb16
-rw-r--r--config/initializers/paperclip.rb4
-rw-r--r--config/initializers/rack-attack.rb4
-rw-r--r--config/initializers/timeout.rb1
-rw-r--r--config/locales/devise.hu.yml61
-rw-r--r--config/locales/doorkeeper.hu.yml112
-rw-r--r--config/locales/fr.yml32
-rw-r--r--config/locales/hu.yml59
-rw-r--r--config/locales/simple_form.de.yml3
-rw-r--r--config/locales/simple_form.en.yml3
-rw-r--r--config/locales/simple_form.fr.yml11
-rw-r--r--config/locales/simple_form.hu.yml28
-rw-r--r--config/routes.rb17
-rw-r--r--db/migrate/20161128103007_create_subscriptions.rb15
-rw-r--r--db/schema.rb26
-rw-r--r--docker-compose.yml17
-rw-r--r--lib/tasks/mastodon.rake7
-rw-r--r--spec/controllers/admin/pubsubhubbub_controller_spec.rb15
-rw-r--r--spec/controllers/api/push_controller_spec.rb13
-rw-r--r--spec/controllers/api/salmon_controller_spec.rb1
-rw-r--r--spec/controllers/api/subscriptions_controller_spec.rb3
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb14
-rw-r--r--spec/fabricators/subscription_fabricator.rb6
-rw-r--r--spec/helpers/admin/pubsubhubbub_helper_spec.rb15
-rw-r--r--spec/models/subscription_spec.rb5
-rw-r--r--spec/services/process_feed_service_spec.rb1
-rw-r--r--spec/services/update_remote_profile_service_spec.rb8
105 files changed, 953 insertions, 601 deletions
diff --git a/.env.production.sample b/.env.production.sample
index a3da10b97..52d519570 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -6,16 +6,13 @@ DB_USER=postgres
 DB_NAME=postgres
 DB_PASS=
 DB_PORT=5432
-NEO4J_HOST=neo4j
-NEO4J_PORT=7474
 
 # Federation
 LOCAL_DOMAIN=example.com
 LOCAL_HTTPS=true
 
 # Application secrets
-# These are arbitrary strings. They should be long and cryptographically secure.
-# For Docker, `docker-compose run --rm web rake secret` will generate them.
+# Generate each with the `rake secret` task
 PAPERCLIP_SECRET=
 SECRET_KEY_BASE=
 
@@ -25,3 +22,13 @@ SMTP_PORT=587
 SMTP_LOGIN=
 SMTP_PASSWORD=
 SMTP_FROM_ADDRESS=notifications@example.com
+
+# Optional asset host for multi-server setups
+# CDN_HOST=assets.example.com
+
+# S3 (optional)
+S3_ENABLED=false
+S3_BUCKET=
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+S3_REGION=
diff --git a/.travis.yml b/.travis.yml
index f6841779d..fe4549edd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,8 +11,6 @@ env:
     - LOCAL_DOMAIN=cb6e6126.ngrok.io
     - LOCAL_HTTPS=true
     - RAILS_ENV=test
-    - NEO4J_HOST=localhost
-    - NEO4J_PORT=7575
 
 addons:
   postgresql: 9.4
diff --git a/Dockerfile.app b/Dockerfile
index dfc0ad5b7..dfc0ad5b7 100644
--- a/Dockerfile.app
+++ b/Dockerfile
diff --git a/Dockerfile.neo4j b/Dockerfile.neo4j
deleted file mode 100644
index 373c2abb0..000000000
--- a/Dockerfile.neo4j
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM neo4j:latest
-
-ENV NEO4J_AUTH=none
-
-RUN cd /var/lib/neo4j/plugins \
-  && wget http://products.graphaware.com/download/framework-server-community/graphaware-server-community-all-3.0.6.43.jar \
-  && wget http://products.graphaware.com/download/noderank/graphaware-noderank-3.0.6.43.3.jar
-RUN echo "dbms.unmanaged_extension_classes=com.graphaware.server=/graphaware" >> /var/lib/neo4j/conf/neo4j.conf
-RUN echo 'com.graphaware.runtime.enabled=true\n\
-com.graphaware.module.NR.1=com.graphaware.module.noderank.NodeRankModuleBootstrapper\n\
-com.graphaware.module.NR.maxTopRankNodes=10\n\
-com.graphaware.module.NR.dampingFactor=0.85\n\
-com.graphaware.module.NR.propertyKey=nodeRank\n'\
-  >> /var/lib/neo4j/conf/neo4j.conf
-RUN echo 'com.graphaware.runtime.stats.disabled=true\n\
-com.graphaware.server.stats.disabled=true\n'\
-  >> /var/lib/neo4j/conf/neo4j.conf
diff --git a/Gemfile b/Gemfile
index 327a17ee9..95fd04629 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,9 +17,9 @@ gem 'pghero'
 gem 'dotenv-rails'
 gem 'font-awesome-rails'
 
-gem 'paperclip', '~> 4.3'
+gem 'paperclip', '~> 5.0'
 gem 'paperclip-av-transcoder'
-gem 'aws-sdk', '< 2.0'
+gem 'aws-sdk', '>= 2.0'
 
 gem 'http'
 gem 'httplog'
@@ -41,20 +41,15 @@ gem 'simple_form'
 gem 'will_paginate'
 gem 'rack-attack'
 gem 'rack-cors', require: 'rack/cors'
+gem 'rack-timeout-puma'
 gem 'sidekiq'
 gem 'ledermann-rails-settings'
-gem 'neography'
 gem 'pg_search'
 
 gem 'react-rails'
 gem 'browserify-rails'
 gem 'autoprefixer-rails'
 
-gem 'rack-mini-profiler', require: false
-gem 'flamegraph'
-gem 'stackprof'
-gem 'memory_profiler'
-
 group :development, :test do
   gem 'rspec-rails'
   gem 'pry-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 28ad1abb6..aa9f59da8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -70,11 +70,14 @@ GEM
       execjs
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-sdk (1.66.0)
-      aws-sdk-v1 (= 1.66.0)
-    aws-sdk-v1 (1.66.0)
-      json (~> 1.4)
-      nokogiri (>= 1.4.4)
+    aws-sdk (2.6.28)
+      aws-sdk-resources (= 2.6.28)
+    aws-sdk-core (2.6.28)
+      aws-sigv4 (~> 1.0)
+      jmespath (~> 1.0)
+    aws-sdk-resources (2.6.28)
+      aws-sdk-core (= 2.6.28)
+    aws-sigv4 (1.0.0)
     babel-source (5.8.35)
     babel-transpiler (0.7.0)
       babel-source (>= 4.0, < 6)
@@ -132,11 +135,9 @@ GEM
       thread
       thread_safe
     erubis (2.7.0)
-    excon (0.53.0)
     execjs (2.7.0)
     fabrication (2.15.2)
     fast_blank (1.0.0)
-    flamegraph (0.9.5)
     font-awesome-rails (4.6.3.1)
       railties (>= 3.2, < 5.1)
     fuubar (2.1.1)
@@ -186,6 +187,7 @@ GEM
     jbuilder (2.6.0)
       activesupport (>= 3.0.0, < 5.1)
       multi_json (~> 1.2)
+    jmespath (1.3.1)
     jquery-rails (4.1.1)
       rails-dom-testing (>= 1, < 3)
       railties (>= 4.2.0)
@@ -206,38 +208,29 @@ GEM
       nokogiri (>= 1.5.9)
     mail (2.6.4)
       mime-types (>= 1.16, < 4)
-    memory_profiler (0.9.7)
     method_source (0.8.2)
     mime-types (3.1)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2016.0521)
-    mimemagic (0.3.0)
+    mimemagic (0.3.2)
     mini_portile2 (2.1.0)
     minitest (5.9.1)
     multi_json (1.12.1)
-    neography (1.8.0)
-      excon (>= 0.33.0)
-      json (>= 1.7.7)
-      multi_json (>= 1.3.2)
-      os (>= 0.9.6)
-      rake (>= 0.8.7)
-      rubyzip (>= 1.0.0)
     nio4r (1.2.1)
     nokogiri (1.6.8.1)
       mini_portile2 (~> 2.1.0)
     oj (2.17.3)
     orm_adapter (0.5.0)
-    os (0.9.6)
     ostatus2 (1.0.2)
       addressable (~> 2.4)
       http (~> 2.0)
       nokogiri (~> 1.6)
-    paperclip (4.3.7)
-      activemodel (>= 3.2.0)
-      activesupport (>= 3.2.0)
+    paperclip (5.1.0)
+      activemodel (>= 4.2.0)
+      activesupport (>= 4.2.0)
       cocaine (~> 0.5.5)
       mime-types
-      mimemagic (= 0.3.0)
+      mimemagic (~> 0.3.0)
     paperclip-av-transcoder (0.6.4)
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
@@ -264,12 +257,13 @@ GEM
     rack-attack (5.0.1)
       rack
     rack-cors (0.4.0)
-    rack-mini-profiler (0.10.1)
-      rack (>= 1.2.0)
     rack-protection (1.5.3)
       rack
     rack-test (0.6.3)
       rack (>= 1.0)
+    rack-timeout (0.4.2)
+    rack-timeout-puma (0.0.1)
+      rack-timeout (~> 0.2, >= 0.2.0)
     rails-dom-testing (2.0.1)
       activesupport (>= 4.2.0, < 6.0)
       nokogiri (~> 1.6.0)
@@ -343,7 +337,6 @@ GEM
       ruby-progressbar (~> 1.7)
       unicode-display_width (~> 1.0, >= 1.0.1)
     ruby-progressbar (1.8.1)
-    rubyzip (1.2.0)
     safe_yaml (1.0.4)
     sass (3.4.22)
     sass-rails (5.0.6)
@@ -376,7 +369,6 @@ GEM
       actionpack (>= 4.0)
       activesupport (>= 4.0)
       sprockets (>= 3.0.0)
-    stackprof (0.2.10)
     temple (0.7.7)
     term-ansicolor (1.4.0)
       tins (~> 1.0)
@@ -414,7 +406,7 @@ DEPENDENCIES
   active_record_query_trace
   addressable
   autoprefixer-rails
-  aws-sdk (< 2.0)
+  aws-sdk (>= 2.0)
   better_errors
   binding_of_caller
   browserify-rails
@@ -425,7 +417,6 @@ DEPENDENCIES
   dotenv-rails
   fabrication
   fast_blank
-  flamegraph
   font-awesome-rails
   fuubar
   goldfinger
@@ -441,12 +432,10 @@ DEPENDENCIES
   letter_opener
   link_header
   lograge
-  memory_profiler
-  neography
   nokogiri
   oj
   ostatus2
-  paperclip (~> 4.3)
+  paperclip (~> 5.0)
   paperclip-av-transcoder
   pg
   pg_search
@@ -456,7 +445,7 @@ DEPENDENCIES
   rabl
   rack-attack
   rack-cors
-  rack-mini-profiler
+  rack-timeout-puma
   rails!
   rails_12factor
   rails_autolink
@@ -471,7 +460,6 @@ DEPENDENCIES
   sidekiq
   simple_form
   simplecov
-  stackprof
   uglifier (>= 1.3.0)
   webmock
   will_paginate
diff --git a/README.md b/README.md
index 25d179d86..00472a616 100644
--- a/README.md
+++ b/README.md
@@ -60,8 +60,6 @@ Consult the example configuration file, `.env.production.sample` for the full li
 
 - PostgreSQL
 - Redis
-- Neo4J (optional)
-  - GraphAware NodeRank
 
 ## Running with Docker and Docker-Compose
 
@@ -90,8 +88,8 @@ The container has two volumes, for the assets and for user uploads. The default
 - `rake mastodon:media:clear` removes uploads that have not been attached to any status after a while, you would want to run this from a periodic cronjob
 - `rake mastodon:push:clear` unsubscribes from PuSH notifications for remote users that have no local followers. You may not want to actually do that, to keep a fuller footprint of the fediverse or in case your users will soon re-follow
 - `rake mastodon:push:refresh` re-subscribes PuSH for expiring remote users, this should be run periodically from a cronjob and quite often as the expiration time depends on the particular hub of the remote user
-- `rake mastodon:feeds:clear` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting
-- `rake mastodon:graphs:sync` re-imports all follow relationships into Neo4J. Only for troubleshooting
+- `rake mastodon:feeds:clear_all` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting
+- `rake mastodon:feeds:clear` removes timelines of users who haven't signed in lately, which allows to save RAM and improve message distribution. This is required to be run periodically so that when they login again the regeneration process will trigger
 
 Running any of these tasks via docker-compose would look like this:
 
@@ -117,6 +115,8 @@ Which will re-create the updated containers, leaving databases and data as is. D
 
 You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. This section may be updated with more details in the future.
 
+**IRC channel**: #mastodon on irc.freenode.net
+
 ### Extra credits
 
 - The [Emoji One](https://github.com/Ranks/emojione) pack has been used for the emojis
diff --git a/app/assets/javascripts/components/actions/suggestions.jsx b/app/assets/javascripts/components/actions/suggestions.jsx
deleted file mode 100644
index 6b3aa69dd..000000000
--- a/app/assets/javascripts/components/actions/suggestions.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import api from '../api';
-
-export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
-export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
-export const SUGGESTIONS_FETCH_FAIL    = 'SUGGESTIONS_FETCH_FAIL';
-
-export function fetchSuggestions() {
-  return (dispatch, getState) => {
-    dispatch(fetchSuggestionsRequest());
-
-    api(getState).get('/api/v1/accounts/suggestions').then(response => {
-      dispatch(fetchSuggestionsSuccess(response.data));
-    }).catch(error => {
-      dispatch(fetchSuggestionsFail(error));
-    });
-  };
-};
-
-export function fetchSuggestionsRequest() {
-  return {
-    type: SUGGESTIONS_FETCH_REQUEST
-  };
-};
-
-export function fetchSuggestionsSuccess(accounts) {
-  return {
-    type: SUGGESTIONS_FETCH_SUCCESS,
-    accounts: accounts
-  };
-};
-
-export function fetchSuggestionsFail(error) {
-  return {
-    type: SUGGESTIONS_FETCH_FAIL,
-    error: error
-  };
-};
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index 603561ab3..df5f0f2c2 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -82,7 +82,7 @@ const Status = React.createClass({
       );
     }
 
-    if (status.get('media_attachments').size > 0) {
+    if (status.get('media_attachments').size > 0 && !this.props.muted) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
       } else {
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index c9f037ec2..c42582bfd 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -39,6 +39,8 @@ import en from 'react-intl/locale-data/en';
 import de from 'react-intl/locale-data/de';
 import es from 'react-intl/locale-data/es';
 import fr from 'react-intl/locale-data/fr';
+import pt from 'react-intl/locale-data/pt';
+import hu from 'react-intl/locale-data/hu';
 import getMessagesForLocale from '../locales';
 
 const store = configureStore();
@@ -47,7 +49,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
   basename: '/web'
 });
 
-addLocaleData([...en, ...de, ...es, ...fr]);
+addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu]);
 
 const Mastodon = React.createClass({
 
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index d39a06062..b890e15c1 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -47,7 +47,7 @@ const Header = React.createClass({
     const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
 
     return (
-      <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', position: 'relative' }}>
+      <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', backgroundPosition: 'center', position: 'relative' }}>
         <div style={{ background: 'rgba(47, 52, 65, 0.9)', padding: '20px 10px' }}>
           <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
             <div style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index 5ad1ca172..b16731c05 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -55,7 +55,8 @@ const textareaStyle = {
   padding: '10px',
   fontFamily: 'Roboto',
   fontSize: '14px',
-  margin: '0'
+  margin: '0',
+  resize: 'vertical'
 };
 
 const renderInputComponent = inputProps => (
diff --git a/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx b/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx
deleted file mode 100644
index 6850629ba..000000000
--- a/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import PureRenderMixin from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import AccountContainer from '../../../containers/account_container';
-import { FormattedMessage } from 'react-intl';
-
-const outerStyle = {
-  position: 'relative'
-};
-
-const headerStyle = {
-  fontSize: '14px',
-  fontWeight: '500',
-  display: 'block',
-  padding: '10px',
-  color: '#9baec8',
-  background: '#454b5e',
-  overflow: 'hidden'
-};
-
-const nextStyle = {
-  display: 'inline-block',
-  float: 'right',
-  fontWeight: '400',
-  color: '#2b90d9'
-};
-
-const SuggestionsBox = React.createClass({
-
-  propTypes: {
-    accountIds: ImmutablePropTypes.list,
-    perWindow: React.PropTypes.number
-  },
-
-  getInitialState () {
-    return {
-      index: 0
-    };
-  },
-
-  getDefaultProps () {
-    return {
-      perWindow: 2
-    };
-  },
-
-  mixins: [PureRenderMixin],
-
-  handleNextClick (e) {
-    e.preventDefault();
-
-    let newIndex = this.state.index + 1;
-
-    if (this.props.accountIds.skip(this.props.perWindow * newIndex).size === 0) {
-      newIndex = 0;
-    }
-
-    this.setState({ index: newIndex });
-  },
-
-  render () {
-    const { accountIds, perWindow } = this.props;
-
-    if (!accountIds || accountIds.size === 0) {
-      return <div />;
-    }
-
-    let nextLink = '';
-
-    if (accountIds.size > perWindow) {
-      nextLink = <a href='#' style={nextStyle} onClick={this.handleNextClick}><FormattedMessage id='suggestions_box.refresh' defaultMessage='Refresh' /></a>;
-    }
-
-    return (
-      <div style={outerStyle}>
-        <strong style={headerStyle}>
-          <FormattedMessage id='suggestions_box.who_to_follow' defaultMessage='Who to follow' /> {nextLink}
-        </strong>
-
-        {accountIds.skip(perWindow * this.state.index).take(perWindow).map(accountId => <AccountContainer key={accountId} id={accountId} withNote={false} />)}
-      </div>
-    );
-  }
-
-});
-
-export default SuggestionsBox;
diff --git a/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx b/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx
deleted file mode 100644
index 944ceed85..000000000
--- a/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { connect }           from 'react-redux';
-import SuggestionsBox        from '../components/suggestions_box';
-
-const mapStateToProps = (state) => ({
-  accountIds: state.getIn(['user_lists', 'suggestions'])
-});
-
-export default connect(mapStateToProps)(SuggestionsBox);
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index 5c1b22e00..4017c8949 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -3,9 +3,7 @@ import ComposeFormContainer from './containers/compose_form_container';
 import UploadFormContainer from './containers/upload_form_container';
 import NavigationContainer from './containers/navigation_container';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
-import SuggestionsContainer from './containers/suggestions_container';
 import SearchContainer from './containers/search_container';
-import { fetchSuggestions } from '../../actions/suggestions';
 import { connect } from 'react-redux';
 import { mountCompose, unmountCompose } from '../../actions/compose';
 
@@ -19,7 +17,6 @@ const Compose = React.createClass({
 
   componentDidMount () {
     this.props.dispatch(mountCompose());
-    this.props.dispatch(fetchSuggestions());
   },
 
   componentWillUnmount () {
@@ -29,14 +26,10 @@ const Compose = React.createClass({
   render () {
     return (
       <Drawer>
-        <div style={{ flex: '1 1 auto' }}>
-          <SearchContainer />
-          <NavigationContainer />
-          <ComposeFormContainer />
-          <UploadFormContainer />
-        </div>
-
-        <SuggestionsContainer />
+        <SearchContainer />
+        <NavigationContainer />
+        <ComposeFormContainer />
+        <UploadFormContainer />
       </Drawer>
     );
   }
diff --git a/app/assets/javascripts/components/locales/de.jsx b/app/assets/javascripts/components/locales/de.jsx
index 85412635e..4e2a70edb 100644
--- a/app/assets/javascripts/components/locales/de.jsx
+++ b/app/assets/javascripts/components/locales/de.jsx
@@ -41,8 +41,6 @@ const en = {
   "search.placeholder": "Suche",
   "search.account": "Konto",
   "search.hashtag": "Hashtag",
-  "suggestions_box.who_to_follow": "Wem folgen",
-  "suggestions_box.refresh": "Aktualisieren",
   "upload_button.label": "Media-Datei anfügen",
   "upload_form.undo": "Entfernen",
   "notification.follow": "{name} folgt dir",
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index 0ea324f66..41a44e3dc 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -5,9 +5,9 @@ const en = {
   "status.mention": "Mention",
   "status.delete": "Delete",
   "status.reply": "Reply",
-  "status.reblog": "Reblog",
+  "status.reblog": "Boost",
   "status.favourite": "Favourite",
-  "status.reblogged_by": "{name} reblogged",
+  "status.reblogged_by": "{name} boosted",
   "status.sensitive_warning": "Sensitive content",
   "status.sensitive_toggle": "Click to view",
   "video_player.toggle_sound": "Toggle sound",
@@ -45,13 +45,11 @@ const en = {
   "search.placeholder": "Search",
   "search.account": "Account",
   "search.hashtag": "Hashtag",
-  "suggestions_box.who_to_follow": "Who to follow",
-  "suggestions_box.refresh": "Refresh",
   "upload_button.label": "Add media",
   "upload_form.undo": "Undo",
   "notification.follow": "{name} followed you",
   "notification.favourite": "{name} favourited your status",
-  "notification.reblog": "{name} reblogged your status",
+  "notification.reblog": "{name} boosted your status",
   "notification.mention": "{name} mentioned you"
 };
 
diff --git a/app/assets/javascripts/components/locales/es.jsx b/app/assets/javascripts/components/locales/es.jsx
index 47377e5ae..d4434bba7 100644
--- a/app/assets/javascripts/components/locales/es.jsx
+++ b/app/assets/javascripts/components/locales/es.jsx
@@ -42,8 +42,6 @@ const es = {
   "search.placeholder": "Buscar",
   "search.account": "Cuenta",
   "search.hashtag": "Etiqueta",
-  "suggestions_box.who_to_follow": "A quién seguir",
-  "suggestions_box.refresh": "Refrescar",
   "upload_button.label": "Añadir medio",
   "upload_form.undo": "Deshacer",
   "notification.follow": "{name} le esta ahora siguiendo",
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index 0cf4c5d52..c4458a145 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -7,22 +7,24 @@ const fr = {
   "status.reply": "Répondre",
   "status.reblog": "Partager",
   "status.favourite": "Ajouter aux favoris",
-  "status.reblogged_by": "{name} a partagé",
+  "status.reblogged_by": "{name} a partagé :",
+  "status.sensitive_warning": "Contenu délicat",
+  "status.sensitive_toggle": "Cliquer pour dévoiler",
   "video_player.toggle_sound": "Mettre/Couper le son",
   "account.mention": "Mentionner",
   "account.edit_profile": "Modifier le profil",
   "account.unblock": "Débloquer",
-  "account.unfollow": "Se désabonner",
+  "account.unfollow": "Ne plus suivre",
   "account.block": "Bloquer",
-  "account.follow": "S’abonner",
+  "account.follow": "Suivre",
   "account.posts": "Statuts",
   "account.follows": "Abonnements",
   "account.followers": "Abonnés",
   "account.follows_you": "Vous suit",
   "getting_started.heading": "Pour commencer",
-  "getting_started.about_addressing": "Vous pouvez vous abonner aux statuts de quelqu’un en entrant dans le champs de recherche leur nom d’utilisateur et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
-  "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, le nom d’utilisateur suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.",
-  "getting_started.about_developer": "Pour s’abonner au développeur de ce projet, c’est Gargron@mastodon.social",
+  "getting_started.about_addressing": "Vous pouvez vous suivre les statuts de quelqu’un en entrant dans le champs de recherche leur identifiant et le domaine de leur instance, séparés par un @ à la manière d’une adresse courriel.",
+  "getting_started.about_shortcuts": "Si cette personne utilise la même instance que vous, l’identifiant suffit. C’est le même principe pour mentionner quelqu’un dans vos statuts.",
+  "getting_started.about_developer": "Pour suivre le développeur de ce projet, c’est Gargron@mastodon.social",
   "column.home": "Accueil",
   "column.mentions": "Mentions",
   "column.public": "Fil public",
@@ -32,23 +34,22 @@ const fr = {
   "tabs_bar.mentions": "Mentions",
   "tabs_bar.public": "Public",
   "tabs_bar.notifications": "Notifications",
-  "compose_form.placeholder": "Qu’avez vous en tête&nbsp;?",
+  "compose_form.placeholder": "Qu’avez-vous en tête ?",
   "compose_form.publish": "Pouet",
+  "compose_form.sensitive": "Marquer le contenu comme délicat", 
   "navigation_bar.settings": "Paramètres",
   "navigation_bar.public_timeline": "Public",
-  "navigation_bar.logout": "Se déconnecter",
+  "navigation_bar.logout": "Déconnexion",
   "reply_indicator.cancel": "Annuler",
   "search.placeholder": "Chercher",
   "search.account": "Compte",
   "search.hashtag": "Mot-clé",
-  "suggestions_box.who_to_follow": "Suggestions",
-  "suggestions_box.refresh": "Rafraîchir",
   "upload_button.label": "Joindre un média",
   "upload_form.undo": "Annuler",
-  "notification.follow": "{name} s’est abonné⋅e à vos statuts",
-  "notification.favourite": "{name} a ajouté votre statut à ses favoris",
-  "notification.reblog": "{name} a partagé votre statut",
-  "notification.mention": "{name} vous a mentionné⋅e"
+  "notification.follow": "{name} vous suit.",
+  "notification.favourite": "{name} a ajouté à ses favoris :",
+  "notification.reblog": "{name} a partagé votre statut :",
+  "notification.mention": "{name} vous a mentionné⋅e :"
 };
 
 export default fr;
diff --git a/app/assets/javascripts/components/locales/hu.jsx b/app/assets/javascripts/components/locales/hu.jsx
new file mode 100644
index 000000000..4a446965c
--- /dev/null
+++ b/app/assets/javascripts/components/locales/hu.jsx
@@ -0,0 +1,55 @@
+const hu = {
+  "column_back_button.label": "Vissza",
+  "lightbox.close": "Bezárás",
+  "loading_indicator.label": "Betöltés...",
+  "status.mention": "Említés",
+  "status.delete": "Törlés",
+  "status.reply": "Válasz",
+  "status.reblog": "Reblog",
+  "status.favourite": "Kedvenc",
+  "status.reblogged_by": "{name} reblogolta",
+  "status.sensitive_warning": "Érzékeny tartalom",
+  "status.sensitive_toggle": "Katt a megtekintéshez",
+  "video_player.toggle_sound": "Hang kapcsolása",
+  "account.mention": "Említés",
+  "account.edit_profile": "Profil szerkesztése",
+  "account.unblock": "Blokkolás levétele",
+  "account.unfollow": "Követés abbahagyása",
+  "account.block": "Blokkolás",
+  "account.follow": "Követés",
+  "account.posts": "Posts",
+  "account.follows": "Követők",
+  "account.followers": "Követők",
+  "account.follows_you": "Követnek téged",
+  "getting_started.heading": "Első lépések",
+  "getting_started.about_addressing": "Követhetsz embereket felhasználónevük és a doménjük ismeretében, amennyiben megadod ezt az e-mail-szerű címet az oldalsáv tetején lévő rubrikában.",
+  "getting_started.about_shortcuts": "Ha a célzott személy azonos doménen tartózkodik, a felhasználónév elegendő. Ugyanez érvényes mikor személyeket említesz az állapotokban.",
+  "getting_started.about_developer": "A projekt fejlesztője követhető, mint Gargron@mastodon.social",
+  "column.home": "Kezdőlap",
+  "column.mentions": "Említések",
+  "column.public": "Nyilvános",
+  "column.notifications": "Értesítések",
+  "tabs_bar.compose": "Összeállítás",
+  "tabs_bar.home": "Kezdőlap",
+  "tabs_bar.mentions": "Említések",
+  "tabs_bar.public": "Nyilvános",
+  "tabs_bar.notifications": "Notifications",
+  "compose_form.placeholder": "Mire gondolsz?",
+  "compose_form.publish": "Tülk!",
+  "compose_form.sensitive": "Tartalom érzékenynek jelölése",
+  "navigation_bar.settings": "Beállítások",
+  "navigation_bar.public_timeline": "Nyilvános időfolyam",
+  "navigation_bar.logout": "Kijelentkezés",
+  "reply_indicator.cancel": "Mégsem",
+  "search.placeholder": "Keresés",
+  "search.account": "Fiók",
+  "search.hashtag": "Hashtag",
+  "upload_button.label": "Média hozzáadása",
+  "upload_form.undo": "Mégsem",
+  "notification.follow": "{name} követ téged",
+  "notification.favourite": "{name} kedvencnek jelölte az állapotod",
+  "notification.reblog": "{name} reblogolta az állapotod",
+  "notification.mention": "{name} megemlített"
+};
+
+export default hu;
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index 7fb43dd33..f172b1c51 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -1,11 +1,17 @@
 import en from './en';
 import de from './de';
 import es from './es';
+import hu from './hu';
+import fr from './fr';
+import pt from './pt';
 
 const locales = {
   en,
   de,
-  es
+  es,
+  hu,
+  fr,
+  pt
 };
 
 export default function getMessagesForLocale (locale) {
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index 02b21f3cb..e67bd80ac 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -40,8 +40,6 @@ const pt = {
   "search.placeholder": "Busca",
   "search.account": "Conta",
   "search.hashtag": "Hashtag",
-  "suggestions_box.who_to_follow": "Quem seguir",
-  "suggestions_box.refresh": "Recarregar",
   "upload_button.label": "Adicionar media",
   "upload_form.undo": "Desfazer"
 };
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 68247a98c..52be648b3 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -8,7 +8,6 @@ import {
   ACCOUNT_TIMELINE_FETCH_SUCCESS,
   ACCOUNT_TIMELINE_EXPAND_SUCCESS
 } from '../actions/accounts';
-import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
 import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
 import {
   REBLOG_SUCCESS,
@@ -71,7 +70,6 @@ export default function accounts(state = initialState, action) {
     case ACCOUNT_FETCH_SUCCESS:
     case NOTIFICATIONS_UPDATE:
       return normalizeAccount(state, action.account);
-    case SUGGESTIONS_FETCH_SUCCESS:
     case FOLLOWERS_FETCH_SUCCESS:
     case FOLLOWERS_EXPAND_SUCCESS:
     case FOLLOWING_FETCH_SUCCESS:
diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx
index 65598f8a0..3608e4209 100644
--- a/app/assets/javascripts/components/reducers/user_lists.jsx
+++ b/app/assets/javascripts/components/reducers/user_lists.jsx
@@ -4,7 +4,6 @@ import {
   FOLLOWING_FETCH_SUCCESS,
   FOLLOWING_EXPAND_SUCCESS
 } from '../actions/accounts';
-import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions';
 import {
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS
@@ -14,7 +13,6 @@ import Immutable from 'immutable';
 const initialState = Immutable.Map({
   followers: Immutable.Map(),
   following: Immutable.Map(),
-  suggestions: Immutable.List(),
   reblogged_by: Immutable.Map(),
   favourited_by: Immutable.Map()
 });
@@ -42,8 +40,6 @@ export default function userLists(state = initialState, action) {
       return normalizeList(state, 'following', action.id, action.accounts, action.next);
     case FOLLOWING_EXPAND_SUCCESS:
       return appendToList(state, 'following', action.id, action.accounts, action.next);
-    case SUGGESTIONS_FETCH_SUCCESS:
-      return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id)));
     case REBLOGS_FETCH_SUCCESS:
       return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
     case FAVOURITES_FETCH_SUCCESS:
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 05a309365..bbbeafefe 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -234,3 +234,4 @@ body {
 @import 'stream_entries';
 @import 'components';
 @import 'about';
+@import 'tables';
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index 306f474d6..81270edf6 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -44,15 +44,20 @@ code {
     label {
       font-family: 'Roboto';
       font-size: 14px;
-      color: #9baec8;
+      color: white;
       display: block;
     }
 
-    input[type=checkbox] {
-      display: inline-block;
+    label.checkbox {
       position: relative;
-      top: 3px;
-      margin-right: 8px;
+	    padding-left: 25px;
+    }
+
+    input[type=checkbox] {
+	    position: absolute;
+	    left: 0;
+      top: 1px;
+      margin: 0;
     }
   }
 
@@ -161,11 +166,10 @@ code {
   text-align: center;
 
   a {
-    color: #9baec8;
+    color: white;
     text-decoration: none;
 
     &:hover {
-      color: #d9e1e8;
       text-decoration: underline;
     }
   }
diff --git a/app/assets/stylesheets/tables.scss b/app/assets/stylesheets/tables.scss
new file mode 100644
index 000000000..89b35891d
--- /dev/null
+++ b/app/assets/stylesheets/tables.scss
@@ -0,0 +1,25 @@
+.table {
+  width: 100%;
+  max-width: 100%;
+  border-spacing: 0;
+  border-collapse: collapse;
+
+  th, td {
+    padding: 8px;
+    line-height: 1.42857143;
+    vertical-align: top;
+    border-top: 1px solid #ddd;
+    text-align: left;
+  }
+
+  & > thead > tr > th {
+    vertical-align: bottom;
+    border-bottom: 2px solid #ddd;
+    border-top: 0;
+    font-weight: 500;
+  }
+}
+
+samp {
+  font-family: 'Roboto Mono', monospace;
+}
diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb
new file mode 100644
index 000000000..7e6bc75ea
--- /dev/null
+++ b/app/controllers/admin/pubsubhubbub_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Admin::PubsubhubbubController < ApplicationController
+  before_action :require_admin!
+
+  layout 'public'
+
+  def index
+    @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40)
+  end
+end
diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb
new file mode 100644
index 000000000..78d4e36e6
--- /dev/null
+++ b/app/controllers/api/push_controller.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class Api::PushController < ApiController
+  def update
+    mode          = params['hub.mode']
+    topic         = params['hub.topic']
+    callback      = params['hub.callback']
+    lease_seconds = params['hub.lease_seconds']
+    secret        = params['hub.secret']
+
+    case mode
+    when 'subscribe'
+      response, status = Pubsubhubbub::SubscribeService.new.call(topic_to_account(topic), callback, secret, lease_seconds)
+    when 'unsubscribe'
+      response, status = Pubsubhubbub::UnsubscribeService.new.call(topic_to_account(topic), callback)
+    else
+      response = "Unknown mode: #{mode}"
+      status   = 422
+    end
+
+    render plain: response, status: status
+  end
+
+  private
+
+  def topic_to_account(topic_url)
+    return if topic_url.blank?
+
+    uri    = Addressable::URI.parse(topic_url)
+    params = Rails.application.routes.recognize_path(uri.path)
+    domain = uri.host + (uri.port ? ":#{uri.port}" : '')
+
+    return unless TagManager.instance.local_domain?(domain) && params[:controller] == 'accounts' && params[:action] == 'show' && params[:format] == 'atom'
+
+    Account.find_local(params[:username])
+  end
+end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 4ae900583..9a356196c 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -46,19 +46,9 @@ class Api::V1::AccountsController < ApiController
     render action: :index
   end
 
-  def common_followers
-    @accounts = @account.common_followers_with(current_user.account)
-    render action: :index
-  end
-
-  def suggestions
-    @accounts = FollowSuggestion.get(current_user.account_id)
-    render action: :index
-  end
-
   def statuses
     @statuses = @account.statuses.paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a
-    @statuses = cache(@statuses)
+    @statuses = cache_collection(@statuses, Status)
 
     set_maps(@statuses)
     set_counters_maps(@statuses)
@@ -121,23 +111,4 @@ class Api::V1::AccountsController < ApiController
     @followed_by = Account.followed_by_map([@account.id], current_user.account_id)
     @blocking    = Account.blocking_map([@account.id], current_user.account_id)
   end
-
-  def cache(raw)
-    uncached_ids           = []
-    cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
-
-    raw.each do |status|
-      uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key)
-    end
-
-    unless uncached_ids.empty?
-      uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h
-
-      uncached.values.each do |status|
-        Rails.cache.write(status.cache_key, status)
-      end
-    end
-
-    raw.map { |status| cached_keys_with_value[status.cache_key] || uncached[status.id] }
-  end
 end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index d74b99a86..a24e0beb7 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -8,7 +8,7 @@ class Api::V1::NotificationsController < ApiController
 
   def index
     @notifications = Notification.where(account: current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
-    @notifications = cache(@notifications)
+    @notifications = cache_collection(@notifications, Notification)
     statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 
     set_maps(statuses)
@@ -20,25 +20,4 @@ class Api::V1::NotificationsController < ApiController
 
     set_pagination_headers(next_path, prev_path)
   end
-
-  private
-
-  def cache(raw)
-    uncached_ids           = []
-    cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
-
-    raw.each do |notification|
-      uncached_ids << notification.id unless cached_keys_with_value.key?(notification.cache_key)
-    end
-
-    unless uncached_ids.empty?
-      uncached = Notification.where(id: uncached_ids).with_includes.map { |n| [n.id, n] }.to_h
-
-      uncached.values.each do |notification|
-        Rails.cache.write(notification.cache_key, notification)
-      end
-    end
-
-    raw.map { |notification| cached_keys_with_value[notification.cache_key] || uncached[notification.id] }
-  end
 end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index a693ce00d..a0b15cfbc 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -58,7 +58,7 @@ class Api::V1::StatusesController < ApiController
 
   def destroy
     @status = Status.where(account_id: current_user.account).find(params[:id])
-    RemoveStatusService.new.call(@status)
+    RemovalWorker.perform_async(@status.id)
     render_empty
   end
 
@@ -68,8 +68,12 @@ class Api::V1::StatusesController < ApiController
   end
 
   def unreblog
-    RemoveStatusService.new.call(Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!)
-    @status = Status.find(params[:id])
+    reblog         = Status.where(account_id: current_user.account, reblog_of_id: params[:id]).first!
+    @status        = reblog.reblog
+    @reblogged_map = { @status.id => false }
+
+    RemovalWorker.perform_async(reblog.id)
+    
     render action: :show
   end
 
diff --git a/app/controllers/api/v1/timelines_controller.rb b/app/controllers/api/v1/timelines_controller.rb
index b1d7c3052..89e54e2cf 100644
--- a/app/controllers/api/v1/timelines_controller.rb
+++ b/app/controllers/api/v1/timelines_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::TimelinesController < ApiController
 
   def home
     @statuses = Feed.new(:home, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a
+    @statuses = cache_collection(@statuses)
 
     set_maps(@statuses)
     set_counters_maps(@statuses)
@@ -23,6 +24,7 @@ class Api::V1::TimelinesController < ApiController
 
   def mentions
     @statuses = Feed.new(:mentions, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a
+    @statuses = cache_collection(@statuses)
 
     set_maps(@statuses)
     set_counters_maps(@statuses)
@@ -38,7 +40,7 @@ class Api::V1::TimelinesController < ApiController
 
   def public
     @statuses = Status.as_public_timeline(current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a
-    @statuses = cache(@statuses)
+    @statuses = cache_collection(@statuses)
 
     set_maps(@statuses)
     set_counters_maps(@statuses)
@@ -55,7 +57,7 @@ class Api::V1::TimelinesController < ApiController
   def tag
     @tag      = Tag.find_by(name: params[:id].downcase)
     @statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a
-    @statuses = cache(@statuses)
+    @statuses = cache_collection(@statuses)
 
     set_maps(@statuses)
     set_counters_maps(@statuses)
@@ -71,22 +73,7 @@ class Api::V1::TimelinesController < ApiController
 
   private
 
-  def cache(raw)
-    uncached_ids           = []
-    cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
-
-    raw.each do |status|
-      uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key)
-    end
-
-    unless uncached_ids.empty?
-      uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h
-
-      uncached.values.each do |status|
-        Rails.cache.write(status.cache_key, status)
-      end
-    end
-
-    raw.map { |status| cached_keys_with_value[status.cache_key] || uncached[status.id] }
+  def cache_collection(raw)
+    super(raw, Status)
   end
 end
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index a3a2a3275..d2d3bc4a4 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -48,7 +48,7 @@ class ApiController < ApplicationController
 
     response.headers['X-RateLimit-Limit']     = match_data[:limit].to_s
     response.headers['X-RateLimit-Remaining'] = (match_data[:limit] - match_data[:count]).to_s
-    response.headers['X-RateLimit-Reset']     = (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
+    response.headers['X-RateLimit-Reset']     = (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6)
   end
 
   def set_pagination_headers(next_path = nil, prev_path = nil)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index effb4ed78..ba0098c71 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -14,7 +14,6 @@ class ApplicationController < ActionController::Base
 
   before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
   before_action :set_locale
-  before_action :check_rack_mini_profiler
 
   def raise_not_found
     raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}"
@@ -32,8 +31,8 @@ class ApplicationController < ActionController::Base
     I18n.locale = I18n.default_locale
   end
 
-  def check_rack_mini_profiler
-    Rack::MiniProfiler.authorize_request if current_user && current_user.admin?
+  def require_admin!
+    redirect_to root_path unless current_user&.admin?
   end
 
   protected
@@ -53,4 +52,23 @@ class ApplicationController < ActionController::Base
   def current_account
     @current_account ||= current_user.try(:account)
   end
+
+  def cache_collection(raw, klass)
+    uncached_ids           = []
+    cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
+
+    raw.each do |item|
+      uncached_ids << item.id unless cached_keys_with_value.key?(item.cache_key)
+    end
+
+    unless uncached_ids.empty?
+      uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
+
+      uncached.values.each do |item|
+        Rails.cache.write(item.cache_key, item)
+      end
+    end
+
+    raw.map { |item| cached_keys_with_value[item.cache_key] || uncached[item.id] }.compact
+  end
 end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 5be8719ae..cacc03b65 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -14,7 +14,10 @@ class Settings::PreferencesController < ApplicationController
     current_user.settings(:notification_emails).favourite = user_params[:notification_emails][:favourite] == '1'
     current_user.settings(:notification_emails).mention   = user_params[:notification_emails][:mention]   == '1'
 
-    if current_user.update(user_params.except(:notification_emails))
+    current_user.settings(:interactions).must_be_follower  = user_params[:interactions][:must_be_follower]  == '1'
+    current_user.settings(:interactions).must_be_following = user_params[:interactions][:must_be_following] == '1'
+
+    if current_user.update(user_params.except(:notification_emails, :interactions))
       redirect_to settings_preferences_path, notice: I18n.t('generic.changes_saved_msg')
     else
       render action: :show
@@ -24,6 +27,6 @@ class Settings::PreferencesController < ApplicationController
   private
 
   def user_params
-    params.require(:user).permit(:locale, notification_emails: [:follow, :reblog, :favourite, :mention])
+    params.require(:user).permit(:locale, notification_emails: [:follow, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following])
   end
 end
diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb
new file mode 100644
index 000000000..41c874a62
--- /dev/null
+++ b/app/helpers/admin/pubsubhubbub_helper.rb
@@ -0,0 +1,2 @@
+module Admin::PubsubhubbubHelper
+end
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index 52190adae..13faaa261 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -116,9 +116,9 @@ module AtomBuilderHelper
   end
 
   def link_avatar(xml, account)
-    single_link_avatar(xml, account, :large,  300)
-    single_link_avatar(xml, account, :medium, 96)
-    single_link_avatar(xml, account, :small,  48)
+    single_link_avatar(xml, account, :large, 300)
+    # single_link_avatar(xml, account, :medium, 96)
+    # single_link_avatar(xml, account, :small,  48)
   end
 
   def logo(xml, url)
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 75ee2f8d9..26c4cd58f 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -5,7 +5,9 @@ module SettingsHelper
     en: 'English',
     de: 'Deutsch',
     es: 'Español',
+    pt: 'Português',
     fr: 'Français',
+    hu: 'Magyar',
   }.freeze
 
   def human_locale(locale)
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index c8512476d..b812ad1f4 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -68,30 +68,34 @@ class FeedManager
   def filter_from_home?(status, receiver)
     should_filter = false
 
-    if status.reply? && !status.thread.account.nil?                      # Filter out if it's a reply
-      should_filter   = !receiver.following?(status.thread.account)      # and I'm not following the person it's a reply to
-      should_filter &&= !(receiver.id == status.thread.account_id)       # and it's not a reply to me
-      should_filter &&= !(status.account_id == status.thread.account_id) # and it's not a self-reply
-    elsif status.reblog?                                                 # Filter out a reblog
-      should_filter = receiver.blocking?(status.reblog.account)          # if I'm blocking the reblogged person
+    if status.reply? && !status.thread.account.nil?                         # Filter out if it's a reply
+      should_filter   = !receiver.following?(status.thread.account)         # and I'm not following the person it's a reply to
+      should_filter &&= !(receiver.id == status.thread.account_id)          # and it's not a reply to me
+      should_filter &&= !(status.account_id == status.thread.account_id)    # and it's not a self-reply
+    elsif status.reblog?                                                    # Filter out a reblog
+      should_filter = receiver.blocking?(status.reblog.account)             # if I'm blocking the reblogged person
     end
 
+    should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked
+
     should_filter
   end
 
   def filter_from_mentions?(status, receiver)
-    should_filter   = receiver.id == status.account_id            # Filter if I'm mentioning myself
-    should_filter ||= receiver.blocking?(status.account)          # or it's from someone I blocked
+    should_filter   = receiver.id == status.account_id                      # Filter if I'm mentioning myself
+    should_filter ||= receiver.blocking?(status.account)                    # or it's from someone I blocked
+    should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
 
-    if status.reply? && !status.thread.account.nil?               # or it's a reply
-      should_filter ||= receiver.blocking?(status.thread.account) # to a user I blocked
+    if status.reply? && !status.thread.account.nil?                         # or it's a reply
+      should_filter ||= receiver.blocking?(status.thread.account)           # to a user I blocked
     end
 
     should_filter
   end
 
   def filter_from_public?(status, receiver)
-    should_filter = receiver.blocking?(status.account)
+    should_filter   = receiver.blocking?(status.account)
+    should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account))
 
     if status.reply? && !status.thread.account.nil?
       should_filter ||= receiver.blocking?(status.thread.account)
diff --git a/app/models/account.rb b/app/models/account.rb
index 16d654195..0f3d0dda2 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -13,12 +13,12 @@ class Account < ApplicationRecord
   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
 
   # Avatar upload
-  has_attached_file :avatar, styles: { large: '300x300#', medium: '96x96#', small: '48x48#' }
+  has_attached_file :avatar, styles: { large: '300x300#' }, convert_options: { all: '-strip' }
   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :avatar, less_than: 2.megabytes
 
   # Header upload
-  has_attached_file :header, styles: { medium: '700x335#' }
+  has_attached_file :header, styles: { medium: '700x335#' }, convert_options: { all: '-strip' }
   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :header, less_than: 2.megabytes
 
@@ -44,8 +44,12 @@ class Account < ApplicationRecord
   has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy
   has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
 
+  # Media
   has_many :media_attachments, dependent: :destroy
 
+  # PuSH subscriptions
+  has_many :subscriptions, dependent: :destroy
+
   pg_search_scope :search_for, against: { username: 'A', domain: 'B' }, using: { tsearch: { prefix: true } }
 
   scope :remote, -> { where.not(domain: nil) }
@@ -66,12 +70,12 @@ class Account < ApplicationRecord
 
   def unfollow!(other_account)
     follow = active_relationships.find_by(target_account: other_account)
-    follow.destroy unless follow.nil?
+    follow&.destroy
   end
 
   def unblock!(other_account)
     block = block_relationships.find_by(target_account: other_account)
-    block.destroy unless block.nil?
+    block&.destroy
   end
 
   def following?(other_account)
@@ -116,7 +120,11 @@ class Account < ApplicationRecord
   end
 
   def avatar_remote_url=(url)
-    self.avatar = URI.parse(url) unless self[:avatar_remote_url] == url
+    parsed_url = URI.parse(url)
+
+    return if !%w(http https).include?(parsed_url.scheme) || self[:avatar_remote_url] == url
+
+    self.avatar              = parsed_url
     self[:avatar_remote_url] = url
   rescue OpenURI::HTTPError => e
     Rails.logger.debug "Error fetching remote avatar: #{e}"
@@ -130,15 +138,6 @@ class Account < ApplicationRecord
     username
   end
 
-  def common_followers_with(other_account)
-    results  = Neography::Rest.new.execute_query('MATCH (a {account_id: {a_id}})-[:follows]->(b)-[:follows]->(c {account_id: {c_id}}) RETURN b.account_id', a_id: id, c_id: other_account.id)
-    ids      = results['data'].map(&:first)
-    accounts = Account.where(id: ids).with_counters.limit(20).map { |a| [a.id, a] }.to_h
-    ids.map { |id| accounts[id] }.compact
-  rescue Neography::NeographyError, Excon::Error::Socket
-    []
-  end
-
   class << self
     def find_local!(username)
       find_remote!(username, nil)
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 45cb923d1..7b181d529 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -16,8 +16,8 @@ class Feed
       RegenerationWorker.perform_async(@account.id, @type)
       @statuses = Status.send("as_#{@type}_timeline", @account).paginate_by_max_id(limit, nil, nil)
     else
-      status_map = cache(unhydrated)
-      @statuses = unhydrated.map { |id| status_map[id] }.compact
+      status_map = Status.where(id: unhydrated).map { |s| [s.id, s] }.to_h
+      @statuses  = unhydrated.map { |id| status_map[id] }.compact
     end
 
     @statuses
@@ -25,29 +25,6 @@ class Feed
 
   private
 
-  def cache(ids)
-    raw                    = Status.where(id: ids).to_a
-    uncached_ids           = []
-    cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
-
-    raw.each do |status|
-      uncached_ids << status.id unless cached_keys_with_value.key?(status.cache_key)
-    end
-
-    unless uncached_ids.empty?
-      uncached = Status.where(id: uncached_ids).with_includes.map { |s| [s.id, s] }.to_h
-
-      uncached.values.each do |status|
-        Rails.cache.write(status.cache_key, status)
-      end
-    end
-
-    cached = cached_keys_with_value.values.map { |s| [s.id, s] }.to_h
-    cached.merge!(uncached) unless uncached_ids.empty?
-
-    cached
-  end
-
   def key
     FeedManager.instance.key(@type, @account.id)
   end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index cc5bceb75..f83490caa 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -27,32 +27,4 @@ class Follow < ApplicationRecord
   def title
     destroyed? ? "#{account.acct} is no longer following #{target_account.acct}" : "#{account.acct} started following #{target_account.acct}"
   end
-
-  after_create  :add_to_graph
-  after_destroy :remove_from_graph
-
-  def sync!
-    add_to_graph
-  end
-
-  private
-
-  def add_to_graph
-    neo = Neography::Rest.new
-
-    a = neo.create_unique_node('account_index', 'Account', account_id.to_s, account_id: account_id)
-    b = neo.create_unique_node('account_index', 'Account', target_account_id.to_s, account_id: target_account_id)
-
-    neo.create_unique_relationship('follow_index', 'Follow', id.to_s, 'follows', a, b)
-  rescue Neography::NeographyError, Excon::Error::Socket => e
-    Rails.logger.error e
-  end
-
-  def remove_from_graph
-    neo = Neography::Rest.new
-    rel = neo.get_relationship_index('follow_index', 'Follow', id.to_s)
-    neo.delete_relationship(rel)
-  rescue Neography::NeographyError, Excon::Error::Socket => e
-    Rails.logger.error e
-  end
 end
diff --git a/app/models/follow_suggestion.rb b/app/models/follow_suggestion.rb
deleted file mode 100644
index 2daa40dcb..000000000
--- a/app/models/follow_suggestion.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-class FollowSuggestion
-  class << self
-    def get(for_account_id, limit = 10)
-      neo = Neography::Rest.new
-
-      query = <<END
-MATCH (a {account_id: {id}})-[:follows]->(b)-[:follows]->(c)
-WHERE a <> c
-AND NOT (a)-[:follows]->(c)
-RETURN DISTINCT c.account_id, count(b), c.nodeRank
-ORDER BY count(b) DESC, c.nodeRank DESC
-LIMIT {limit}
-END
-
-      results = neo.execute_query(query, id: for_account_id, limit: limit)
-
-      if results.empty? || results['data'].empty?
-        results = fallback(for_account_id, limit)
-      elsif results['data'].size < limit
-        results['data'] = (results['data'] + fallback(for_account_id, limit - results['data'].size)['data']).uniq
-      end
-
-      account_ids  = results['data'].map(&:first)
-      blocked_ids  = Block.where(account_id: for_account_id).pluck(:target_account_id)
-      accounts_map = Account.where(id: account_ids - blocked_ids).with_counters.map { |a| [a.id, a] }.to_h
-
-      account_ids.map { |id| accounts_map[id] }.compact
-    rescue Neography::NeographyError, Excon::Error::Socket => e
-      Rails.logger.error e
-      return []
-    end
-
-    private
-
-    def fallback(for_account_id, limit)
-      neo = Neography::Rest.new
-
-      query = <<END
-MATCH (b)
-RETURN b.account_id
-ORDER BY b.nodeRank DESC
-LIMIT {limit}
-END
-
-      neo.execute_query(query, id: for_account_id, limit: limit)
-    end
-  end
-end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index bfbf00d76..f1b9b8112 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -16,6 +16,8 @@ class MediaAttachment < ApplicationRecord
 
   validates :account, presence: true
 
+  default_scope { order('id asc') }
+
   def local?
     remote_url.blank?
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 3402929bf..f9dcd97e4 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -97,7 +97,10 @@ class Status < ApplicationRecord
     end
 
     def as_public_timeline(account = nil)
-      query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id').where('accounts.silenced = FALSE')
+      query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
+              .where('accounts.silenced = FALSE')
+              .where('statuses.in_reply_to_id IS NULL')
+              .where('statuses.reblog_of_id IS NULL')
       query = filter_timeline(query, account) unless account.nil?
       query
     end
@@ -106,6 +109,8 @@ class Status < ApplicationRecord
       query = tag.statuses
                  .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
                  .where('accounts.silenced = FALSE')
+                 .where('statuses.in_reply_to_id IS NULL')
+                 .where('statuses.reblog_of_id IS NULL')
       query = filter_timeline(query, account) unless account.nil?
       query
     end
@@ -123,13 +128,7 @@ class Status < ApplicationRecord
     def filter_timeline(query, account)
       blocked = Block.where(account: account).pluck(:target_account_id)
       return query if blocked.empty?
-
-      query
-        .joins('LEFT OUTER JOIN statuses AS parents ON statuses.in_reply_to_id = parents.id')
-        .joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id')
-        .where('statuses.account_id NOT IN (?)', blocked)
-        .where('(parents.id IS NULL OR parents.account_id NOT IN (?))', blocked)
-        .where('(reblogs.id IS NULL OR reblogs.account_id NOT IN (?))', blocked)
+      query.where('statuses.account_id NOT IN (?)', blocked)
     end
   end
 
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
new file mode 100644
index 000000000..497cabb09
--- /dev/null
+++ b/app/models/subscription.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Subscription < ApplicationRecord
+  MIN_EXPIRATION = 3600 * 24 * 7
+  MAX_EXPIRATION = 3600 * 24 * 30
+
+  belongs_to :account
+
+  validates :callback_url, presence: true
+  validates :callback_url, uniqueness: { scope: :account_id }
+
+  scope :active, -> { where(confirmed: true).where('expires_at > ?', Time.now.utc) }
+
+  def lease_seconds=(str)
+    self.expires_at = Time.now.utc + [[MIN_EXPIRATION, str.to_i].max, MAX_EXPIRATION].min.seconds
+  end
+
+  def lease_seconds
+    (expires_at - Time.now.utc).to_i
+  end
+
+  before_validation :set_min_expiration
+
+  private
+
+  def set_min_expiration
+    self.lease_seconds = 0 unless expires_at
+  end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 366172e9a..423833d47 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,6 +15,7 @@ class User < ApplicationRecord
 
   has_settings do |s|
     s.key :notification_emails, defaults: { follow: false, reblog: false, favourite: false, mention: false }
+    s.key :interactions, defaults: { must_be_follower: false, must_be_following: false }
   end
 
   def send_devise_notification(notification, *args)
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 78301c6ca..40d8a0fee 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -41,14 +41,17 @@ class FanOutOnWriteService < BaseService
   end
 
   def deliver_to_hashtags(status)
-    Rails.logger.debug "Delivering status #{status.id} to hashtags"
+    return if status.reblog? || status.reply?
 
+    Rails.logger.debug "Delivering status #{status.id} to hashtags"
     status.tags.find_each do |tag|
       FeedManager.instance.broadcast("hashtag:#{tag.name}", type: 'update', id: status.id)
     end
   end
 
   def deliver_to_public(status)
+    return if status.reblog? || status.reply?
+
     Rails.logger.debug "Delivering status #{status.id} to public timeline"
     FeedManager.instance.broadcast(:public, type: 'update', id: status.id)
   end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 781b03b40..2f280e03f 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -7,7 +7,9 @@ class FavouriteService < BaseService
   # @return [Favourite]
   def call(account, status)
     favourite = Favourite.create!(account: account, status: status)
+
     HubPingWorker.perform_async(account.id)
+    Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id)
 
     if status.local?
       NotifyService.new.call(status.account, favourite)
diff --git a/app/services/follow_remote_account_service.rb b/app/services/follow_remote_account_service.rb
index 37339d8ed..f640222b0 100644
--- a/app/services/follow_remote_account_service.rb
+++ b/app/services/follow_remote_account_service.rb
@@ -80,8 +80,7 @@ class FollowRemoteAccountService < BaseService
   end
 
   def get_profile(xml, account)
-    author = xml.at_xpath('/xmlns:feed/xmlns:author') || xml.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS)
-    update_remote_profile_service.call(author, account)
+    update_remote_profile_service.call(xml.at_xpath('/xmlns:feed'), account)
   end
 
   def update_remote_profile_service
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index a57e1b28a..09fa295e3 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -19,7 +19,10 @@ class FollowService < BaseService
     end
 
     merge_into_timeline(target_account, source_account)
+
     HubPingWorker.perform_async(source_account.id)
+    Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id)
+
     follow
   end
 
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 772adfb90..1efd326b0 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -36,6 +36,8 @@ class NotifyService < BaseService
     blocked   = false
     blocked ||= @recipient.id == @notification.from_account.id
     blocked ||= @recipient.blocking?(@notification.from_account)
+    blocked ||= (@recipient.user.settings(:interactions).must_be_follower  && !@notification.from_account.following?(@recipient))
+    blocked ||= (@recipient.user.settings(:interactions).must_be_following && !@recipient.following?(@notification.from_account))
     blocked ||= send("blocked_#{@notification.type}?")
     blocked
   end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 76366e984..979a157e9 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -14,8 +14,11 @@ class PostStatusService < BaseService
     attach_media(status, options[:media_ids])
     process_mentions_service.call(status)
     process_hashtags_service.call(status)
+
     DistributionWorker.perform_async(status.id)
     HubPingWorker.perform_async(account.id)
+    Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
+
     status
   end
 
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index 1cd801b80..a7a4cb2b0 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -16,7 +16,7 @@ class ProcessFeedService < BaseService
 
   def update_author(xml, account)
     return if xml.at_xpath('/xmlns:feed').nil?
-    UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed/xmlns:author'), account)
+    UpdateRemoteProfileService.new.call(xml.at_xpath('/xmlns:feed'), account, true)
   end
 
   def process_entries(xml, account)
diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb
index 3bf3471ec..fa14c44da 100644
--- a/app/services/process_hashtags_service.rb
+++ b/app/services/process_hashtags_service.rb
@@ -4,7 +4,7 @@ class ProcessHashtagsService < BaseService
   def call(status, tags = [])
     tags = status.text.scan(Tag::HASHTAG_RE).map(&:first) if status.local?
 
-    tags.map(&:downcase).uniq.each do |tag|
+    tags.map { |str| str.mb_chars.downcase }.uniq.each do |tag|
       status.tags << Tag.where(name: tag).first_or_initialize(name: tag)
     end
   end
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index e7bb3c73b..6b2f6e2d2 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -26,7 +26,7 @@ class ProcessInteractionService < BaseService
     end
 
     if salmon.verify(envelope, account.keypair)
-      update_remote_profile_service.call(xml.at_xpath('/xmlns:entry/xmlns:author'), account)
+      update_remote_profile_service.call(xml.at_xpath('/xmlns:entry'), account, true)
 
       case verb(xml)
       when :follow
@@ -74,7 +74,7 @@ class ProcessInteractionService < BaseService
   end
 
   def delete_post!(xml, account)
-    status = Status.find(activity_id(xml))
+    status = Status.find(xml.at_xpath('//xmlns:id').content)
 
     return if status.nil?
 
diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb
new file mode 100644
index 000000000..343376d77
--- /dev/null
+++ b/app/services/pubsubhubbub/subscribe_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Pubsubhubbub::SubscribeService < BaseService
+  def call(account, callback, secret, lease_seconds)
+    return ['Invalid topic URL', 422] if account.nil?
+    return ['Invalid callback URL', 422] unless !callback.blank? && callback =~ /\A#{URI.regexp(%w(http https))}\z/
+
+    subscription = Subscription.where(account: account, callback_url: callback).first_or_create!(account: account, callback_url: callback)
+    Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds)
+
+    ['', 202]
+  end
+end
diff --git a/app/services/pubsubhubbub/unsubscribe_service.rb b/app/services/pubsubhubbub/unsubscribe_service.rb
new file mode 100644
index 000000000..62459a0aa
--- /dev/null
+++ b/app/services/pubsubhubbub/unsubscribe_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class Pubsubhubbub::UnsubscribeService < BaseService
+  def call(account, callback)
+    return ['Invalid topic URL', 422] if account.nil?
+
+    subscription = Subscription.where(account: account, callback_url: callback)
+
+    unless subscription.nil?
+      Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe')
+    end
+
+    ['', 202]
+  end
+end
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 6543d4ae7..39fdb4ea7 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -7,8 +7,10 @@ class ReblogService < BaseService
   # @return [Status]
   def call(account, reblogged_status)
     reblog = account.statuses.create!(reblog: reblogged_status, text: '')
+
     DistributionWorker.perform_async(reblog.id)
     HubPingWorker.perform_async(account.id)
+    Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
 
     if reblogged_status.local?
       NotifyService.new.call(reblogged_status.account, reblog)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 689abc97b..4e03661da 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -10,6 +10,11 @@ class RemoveStatusService < BaseService
     remove_from_public(status)
 
     status.destroy!
+
+    if status.account.local?
+      HubPingWorker.perform_async(status.account.id)
+      Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
+    end
   end
 
   private
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 598c7d02c..1ae1d5a80 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -2,9 +2,9 @@
 
 class SearchService < BaseService
   def call(query, limit, resolve = false)
-    return if query.blank?
+    return if query.blank? || query.start_with?('#')
 
-    username, domain = query.split('@')
+    username, domain = query.gsub(/\A@/, '').split('@')
 
     results = if domain.nil?
                 Account.search_for(username)
diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb
index 2909ae12a..56b25816f 100644
--- a/app/services/update_remote_profile_service.rb
+++ b/app/services/update_remote_profile_service.rb
@@ -2,24 +2,24 @@
 
 class UpdateRemoteProfileService < BaseService
   POCO_NS = 'http://portablecontacts.net/spec/1.0'
+  DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0'
 
-  def call(author_xml, account)
-    return if author_xml.nil?
+  def call(xml, account, resubscribe = false)
+    return if xml.nil?
 
-    account.display_name = if author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil?
-                             account.username
-                           else
-                             author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content
-                           end
+    author_xml = xml.at_xpath('./xmlns:author') || xml.at_xpath('./dfrn:owner', dfrn: DFRN_NS)
+    hub_link   = xml.at_xpath('./xmlns:link[@rel="hub"]')
 
-    unless author_xml.at_xpath('./poco:note').nil?
-      account.note = author_xml.at_xpath('./poco:note', poco: POCO_NS).content
-    end
-
-    unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]').nil?
-      account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]').attribute('href').value
+    unless author_xml.nil?
+      account.display_name      = author_xml.at_xpath('./poco:displayName', poco: POCO_NS).content unless author_xml.at_xpath('./poco:displayName', poco: POCO_NS).nil?
+      account.note              = author_xml.at_xpath('./poco:note', poco: POCO_NS).content unless author_xml.at_xpath('./poco:note').nil?
+      account.avatar_remote_url = author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'] unless author_xml.at_xpath('./xmlns:link[@rel="avatar"]').nil? || author_xml.at_xpath('./xmlns:link[@rel="avatar"]')['href'].blank?
     end
 
+    old_hub_url     = account.hub_url
+    account.hub_url = hub_link['href'] if !hub_link.nil? && !hub_link['href'].blank? && (hub_link['href'] != old_hub_url)
     account.save!
+
+    SubscribeService.new.call(account) if resubscribe && (account.hub_url != old_hub_url)
   end
 end
diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby
index d7b2201d4..558c777f0 100644
--- a/app/views/accounts/show.atom.ruby
+++ b/app/views/accounts/show.atom.ruby
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 Nokogiri::XML::Builder.new do |xml|
   feed(xml) do
     simple_id  xml, account_url(@account, format: 'atom')
@@ -12,6 +14,7 @@ Nokogiri::XML::Builder.new do |xml|
 
     link_alternate xml, TagManager.instance.url_for(@account)
     link_self      xml, account_url(@account, format: 'atom')
+    link_hub       xml, api_push_url
     link_hub       xml, Rails.configuration.x.hub_url
     link_salmon    xml, api_salmon_url(@account.id)
 
diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml
new file mode 100644
index 000000000..bb897eb89
--- /dev/null
+++ b/app/views/admin/pubsubhubbub/index.html.haml
@@ -0,0 +1,20 @@
+%table.table
+  %thead
+    %tr
+      %th Topic
+      %th Callback URL
+      %th Confirmed
+      %th Expires in
+  %tbody
+    - @subscriptions.each do |subscription|
+      %tr
+        %td
+          %samp= subscription.account.acct
+        %td
+          %samp= subscription.callback_url
+        %td
+          - if subscription.confirmed?
+            %i.fa.fa-check
+        %td= distance_of_time_in_words(Time.now, subscription.expires_at)
+
+= will_paginate @subscriptions, pagination_options
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 693702ff7..db5b9fb48 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -12,6 +12,10 @@
     = ff.input :favourite, as: :boolean, wrapper: :with_label
     = ff.input :mention, as: :boolean, wrapper: :with_label
 
+  = f.simple_fields_for :interactions, current_user.settings(:interactions) do |ff|
+    = ff.input :must_be_follower, as: :boolean, wrapper: :with_label
+    = ff.input :must_be_following, as: :boolean, wrapper: :with_label
+
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit
 
diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb
index 3b11a4c5e..b31cd0aaf 100644
--- a/app/workers/processing_worker.rb
+++ b/app/workers/processing_worker.rb
@@ -2,6 +2,8 @@
 
 class ProcessingWorker
   include Sidekiq::Worker
+  
+  sidekiq_options backtrace: true
 
   def perform(account_id, body)
     ProcessFeedService.new.call(body, Account.find(account_id))
diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb
new file mode 100644
index 000000000..489bd8359
--- /dev/null
+++ b/app/workers/pubsubhubbub/confirmation_worker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class Pubsubhubbub::ConfirmationWorker
+  include Sidekiq::Worker
+  include RoutingHelper
+
+  sidekiq_options queue: 'push'
+
+  def perform(subscription_id, mode, secret = nil, lease_seconds = nil)
+    subscription = Subscription.find(subscription_id)
+    challenge    = SecureRandom.hex
+
+    subscription.secret        = secret
+    subscription.lease_seconds = lease_seconds
+    subscription.confirmed     = true
+
+    response = HTTP.headers(user_agent: 'Mastodon/PubSubHubbub')
+                   .timeout(:per_operation, write: 20, connect: 20, read: 50)
+                   .get(subscription.callback_url, params: {
+                          'hub.topic' => account_url(subscription.account, format: :atom),
+                          'hub.mode'          => mode,
+                          'hub.challenge'     => challenge,
+                          'hub.lease_seconds' => subscription.lease_seconds,
+                        })
+
+    body = response.body.to_s
+
+    Rails.logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{body}"
+
+    if mode == 'subscribe' && body == challenge
+      subscription.save!
+    elsif (mode == 'unsubscribe' && body == challenge) || !subscription.confirmed?
+      subscription.destroy!
+    end
+  end
+end
diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb
new file mode 100644
index 000000000..6d526c2b1
--- /dev/null
+++ b/app/workers/pubsubhubbub/delivery_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class Pubsubhubbub::DeliveryWorker
+  include Sidekiq::Worker
+  include RoutingHelper
+
+  sidekiq_options queue: 'push'
+
+  def perform(subscription_id, payload)
+    subscription = Subscription.find(subscription_id)
+    headers      = {}
+
+    headers['User-Agent']      = 'Mastodon/PubSubHubbub'
+    headers['Link']            = LinkHeader.new([[api_push_url, [%w(rel hub)]], [account_url(subscription.account, format: :atom), [%w(rel self)]]]).to_s
+    headers['X-Hub-Signature'] = signature(subscription.secret, payload) unless subscription.secret.blank?
+
+    response = HTTP.timeout(:per_operation, write: 50, connect: 20, read: 50)
+                   .headers(headers)
+                   .post(subscription.callback_url, body: payload)
+
+    raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300
+  end
+
+  private
+
+  def signature(secret, payload)
+    hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, payload)
+    "sha1=#{hmac}"
+  end
+end
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
new file mode 100644
index 000000000..b0ddc71c1
--- /dev/null
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Pubsubhubbub::DistributionWorker
+  include Sidekiq::Worker
+
+  sidekiq_options queue: 'push'
+
+  def perform(stream_entry_id)
+    stream_entry = StreamEntry.find(stream_entry_id)
+    account      = stream_entry.account
+    renderer     = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
+    payload      = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
+
+    Subscription.where(account: account).active.select('id').find_each do |subscription|
+      Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
+    end
+  end
+end
diff --git a/app/workers/removal_worker.rb b/app/workers/removal_worker.rb
new file mode 100644
index 000000000..7470c54f5
--- /dev/null
+++ b/app/workers/removal_worker.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class RemovalWorker
+  include Sidekiq::Worker
+
+  def perform(status_id)
+    RemoveStatusService.new.call(Status.find(status_id))
+  end
+end
\ No newline at end of file
diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb
index 24fb94012..0903ca487 100644
--- a/app/workers/salmon_worker.rb
+++ b/app/workers/salmon_worker.rb
@@ -2,6 +2,8 @@
 
 class SalmonWorker
   include Sidekiq::Worker
+  
+  sidekiq_options backtrace: true
 
   def perform(account_id, body)
     ProcessInteractionService.new.call(body, Account.find(account_id))
diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb
index 700161989..84eae73be 100644
--- a/app/workers/thread_resolve_worker.rb
+++ b/app/workers/thread_resolve_worker.rb
@@ -7,9 +7,9 @@ class ThreadResolveWorker
     child_status  = Status.find(child_status_id)
     parent_status = FetchRemoteStatusService.new.call(parent_url)
 
-    unless parent_status.nil?
-      child_status.thread = parent_status
-      child_status.save!
-    end
+    return if parent_status.nil?
+
+    child_status.thread = parent_status
+    child_status.save!
   end
 end
diff --git a/config/application.rb b/config/application.rb
index 7ba13bfbe..1e5fd9c7c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -20,7 +20,7 @@ module Mastodon
 
     # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
     # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
-    config.i18n.available_locales = [:en, :de, :es, :fr]
+    config.i18n.available_locales = [:en, :de, :es, :pt, :fr, :hu]
     config.i18n.default_locale    = :en
 
     # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
diff --git a/config/environments/production.rb b/config/environments/production.rb
index dcb659d6c..9254d494c 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -13,7 +13,7 @@ Rails.application.configure do
   # Full error reports are disabled and caching is turned on.
   config.consider_all_requests_local       = false
   config.action_controller.perform_caching = true
-  config.action_controller.asset_host      = ENV['CDN_HOST']
+  config.action_controller.asset_host      = ENV['CDN_HOST'] if ENV.key?('CDN_HOST')
 
   # Disable serving static files from the `/public` folder by default since
   # Apache or NGINX already handles this.
@@ -30,7 +30,7 @@ Rails.application.configure do
 
   # Specifies the header that your server uses for sending files.
   # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
-  # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+  config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
 
   # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
   config.force_ssl = false
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index d345ce6c0..4dc6985b7 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -30,9 +30,6 @@ search:
     - app/assets/fonts
     - app/assets/videos
 
-ignore_missing:
-  - '{devise,simple_form}.*'
-
 ignore_unused:
   - 'activerecord.attributes.*'
   - '{devise,will_paginate,doorkeeper}.*'
diff --git a/config/initializers/mini_profiler.rb b/config/initializers/mini_profiler.rb
deleted file mode 100644
index 265783618..000000000
--- a/config/initializers/mini_profiler.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'rack-mini-profiler'
-
-Rack::MiniProfilerRails.initialize!(Rails.application)
-
-Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler)
-Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater)
-
-Rack::MiniProfiler.config.storage = Rack::MiniProfiler::MemoryStore
-
-if Rails.env.production?
-  Rack::MiniProfiler.config.storage_options = {
-    host: ENV.fetch('REDIS_HOST') { 'localhost' },
-    port: ENV.fetch('REDIS_PORT') { 6379 },
-  }
-
-  Rack::MiniProfiler.config.storage = Rack::MiniProfiler::RedisStore
-end
diff --git a/config/initializers/neography.rb b/config/initializers/neography.rb
deleted file mode 100644
index bd6ead3b0..000000000
--- a/config/initializers/neography.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-Neography.configure do |config|
-  config.protocol             = "http"
-  config.server               = ENV.fetch('NEO4J_HOST') { 'localhost' }
-  config.port                 = ENV.fetch('NEO4J_PORT') { 7474 }
-end
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index 3dd501b65..4ba432b6a 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -1,14 +1,16 @@
-port = ENV.fetch('PORT') { 3000 }
-
+port  = ENV.fetch('PORT') { 3000 }
+host  = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
+https = ENV['LOCAL_HTTPS'] == 'true'
+  
 Rails.application.configure do
-  config.x.local_domain = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
-  config.x.hub_url      = ENV.fetch('HUB_URL')      { 'https://pubsubhubbub.superfeedr.com' }
-  config.x.use_https    = ENV['LOCAL_HTTPS'] == 'true'
+  config.x.local_domain = host
+  config.x.hub_url      = ENV.fetch('HUB_URL') { 'https://pubsubhubbub.superfeedr.com' }
+  config.x.use_https    = https
   config.x.use_s3       = ENV['S3_ENABLED'] == 'true'
 
-  config.action_mailer.default_url_options = { host: config.x.local_domain, protocol: config.x.use_https ? 'https://' : 'http://', trailing_slash: false }
+  config.action_mailer.default_url_options = { host: host, protocol: https ? 'https://' : 'http://', trailing_slash: false }
 
   if Rails.env.production?
-    config.action_cable.allowed_request_origins = ["http#{config.x.use_https ? 's' : ''}://#{config.x.local_domain}"]
+    config.action_cable.allowed_request_origins = ["http#{https ? 's' : ''}://#{host}"]
   end
 end
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 643c5d384..80effc05e 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -1,4 +1,6 @@
 if ENV['S3_ENABLED'] == 'true'
+  Aws.eager_autoload!(services: %w(S3))
+
   Paperclip::Attachment.default_options[:storage]      = :s3
   Paperclip::Attachment.default_options[:s3_protocol]  = 'https'
   Paperclip::Attachment.default_options[:url]          = ':s3_domain_url'
@@ -9,6 +11,6 @@ if ENV['S3_ENABLED'] == 'true'
     bucket: ENV.fetch('S3_BUCKET'),
     access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'),
     secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'),
-    s3_region: ENV.fetch('S3_REGION')
+    s3_region: ENV.fetch('S3_REGION'),
   }
 end
diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb
index 6d9286e66..3f0ee1d7a 100644
--- a/config/initializers/rack-attack.rb
+++ b/config/initializers/rack-attack.rb
@@ -1,7 +1,7 @@
 class Rack::Attack
   # Rate limits for the API
   throttle('api', limit: 150, period: 5.minutes) do |req|
-    req.ip if req.path.match(/\A\/api\//)
+    req.ip if req.path.match(/\A\/api\/v/)
   end
 
   self.throttled_response = lambda do |env|
@@ -11,7 +11,7 @@ class Rack::Attack
     headers = {
       'X-RateLimit-Limit'     => match_data[:limit].to_s,
       'X-RateLimit-Remaining' => '0',
-      'X-RateLimit-Reset'     => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
+      'X-RateLimit-Reset'     => (now + (match_data[:period] - now.to_i % match_data[:period])).iso8601(6)
     }
 
     [429, headers, [{ error: 'Throttled' }.to_json]]
diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb
new file mode 100644
index 000000000..8b7311e39
--- /dev/null
+++ b/config/initializers/timeout.rb
@@ -0,0 +1 @@
+Rack::Timeout.timeout = 30
\ No newline at end of file
diff --git a/config/locales/devise.hu.yml b/config/locales/devise.hu.yml
new file mode 100644
index 000000000..2eb7da45c
--- /dev/null
+++ b/config/locales/devise.hu.yml
@@ -0,0 +1,61 @@
+---
+hu:
+  devise:
+    confirmations:
+      confirmed: Az e-mail címed sikeresen meg lett erősítve.
+      send_instructions: Pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről.
+      send_paranoid_instructions: Ha az e-mail címed létezik az adatbázisunkban, pár percen belül kapni fogsz egy e-mailt az e-mail címed megerősítéséhez szükséges lépésekről.
+    failure:
+      already_authenticated: Már bejelentkeztél
+      inactive: Fiókod még nem lett aktiválva.
+      invalid: Helytelen %{authentication_keys} vagy jelszó.
+      last_attempt: Már csak egy próbálkozásod maradt mielőtt a fiókod lezárásra kerül.
+      locked: Fiókod le van zárva.
+      not_found_in_database: Helytelen %{authentication_keys} vagy jelszó.
+      timeout: A munkamenet lejárt. Jelentkezz be újra a folytatáshoz.
+      unauthenticated: A folytatás előtt be kell jelentkezned.
+      unconfirmed: A folytatás előtt meg kell erősítened az e-mail címed.
+    mailer:
+      confirmation_instructions:
+        subject: 'Mastodon: Megerősítési lépések'
+      password_change:
+        subject: 'Mastodon: Jelszó megváltoztatva'
+      reset_password_instructions:
+        subject: 'Mastodon: Jelszó visszaállítási lépések'
+      unlock_instructions:
+        subject: 'Mastodon: Feloldási lépések'
+    omniauth_callbacks:
+      failure: "%{kind} nem hitelesíthető, mert %{reason}."
+      success: Sikeres hitelesítés %{kind} fiókról.
+    passwords:
+      no_token: Nem férhetsz hozzá az oldalhoz jelszó visszaállító e-mail nélkül. Ha egy jelszó visszaállító e-mail hozott ide, ellenőrizd, hogy a megadott teljes URL-t használd. 
+      send_instructions: Pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat.
+      send_paranoid_instructions: Ha létezik az e-mail cím, pár percen belül kapni fogsz egy e-mailt arról, hogy hogyan tudod visszaállítani a jelszavadat.
+      updated: Jelszavad sikeresen frissült. Bejelentkeztél.
+      updated_not_active: Jelszavad sikeresen meg lett változtatva.
+    registrations:
+      destroyed: Viszlát! A fiókod sikeresen törölve. Reméljük hamarosan viszontláthatunk.
+      signed_up: Üdvözlünk! Sikeresen regisztráltál.
+      signed_up_but_inactive: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod még nem lett aktiválva. 
+      signed_up_but_locked: Sikeresen regisztráltál. Ennek ellenére nem tudunk beléptetni, ugyanis a fiókod le lett zárva.
+      signed_up_but_unconfirmed: Egy üzenet a megerősítési linkkel kiküldésre került az e-mail címedre. Kérjük használd a linket a fiókod aktiválásához.
+      update_needs_confirmation: Sikeresen frissítetted a fiókodat, de szükségünk van az e-mail címed megerősítésére. Kérlek ellenőrizd az e-mailedet és kövesd a levélben szereplő megerősítési linket az e-mail címed megerősítéséhez.
+      updated: Fiókod frissítése sikeres.
+    sessions:
+      already_signed_out: Sikeres kijelenkezés.
+      signed_in: Sikeres bejelentkezés.
+      signed_out: Sikeres kijelentkezés.
+    unlocks:
+      send_instructions: Pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel.
+      send_paranoid_instructions: Ha a fiókod létezik, pár percen belül egy e-mailt fogsz kapni a feloldáshoz szükséges lépésekkel.
+      unlocked: A fiókod sikeresen fel lett oldva. Jelentkezz be a folytatáshoz.
+  errors:
+    messages:
+      already_confirmed: már meg lett erősítve, kérjük jelentkezz be
+      confirmation_period_expired: "%{period} belül kellett megerősíteni, kérjük igényelj újat"
+      expired: lejárt, kérjük igényelj újat
+      not_found: nem található
+      not_locked: nincs lezárva
+      not_saved:
+        one: '1 hiba megakadályozta %{resource} mentését:'
+        other: "%{count} számú hiba megakadályozta %{resource} mentését:"
diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml
new file mode 100644
index 000000000..b1c6dd6c9
--- /dev/null
+++ b/config/locales/doorkeeper.hu.yml
@@ -0,0 +1,112 @@
+---
+hu:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Név
+        redirect_uri: Visszairányító URI
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: nem tartalmazhat töredéket.
+              invalid_uri: érvényes URI-nak kell lennie.
+              relative_uri: abszolút URI-nak kell lennie.
+              secured_uri: HTTPS/SSL URI-nak kell lennie.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Engedélyezés
+        cancel: Mégsem
+        destroy: Törlés
+        edit: Szerkesztés
+        submit: Elküldés
+      confirmations:
+        destroy: Biztos vagy benne?
+      edit:
+        title: Alkalmazás szerkesztése
+      form:
+        error: Hoppá! Ellenőrizd az űrlapot az esetleges hibák miatt
+      help:
+        native_redirect_uri: Használj %{native_redirect_uri} a helyi tesztekhez
+        redirect_uri: Egy sor URI-nként
+        scopes: A nézeteket szóközzel válaszd el. Hagyd üresen az alapértelmezett nézetekhez.
+      index:
+        callback_url: Callback URL
+        name: Név
+        new: Új alkalmazás
+        title: Alkalmazásod
+      new:
+        title: Új alkalmazás
+      show:
+        actions: Műveletek
+        application_id: Alkalmazás azonosító
+        callback_urls: Callback urlek
+        scopes: Nézetek
+        secret: Titok
+        title: 'Alkalmazás: %{name}'
+    authorizations:
+      buttons:
+        authorize: Engedélyezés
+        deny: Tiltás
+      error:
+        title: Hiba történt
+      new:
+        able_to: Képes lesz
+        prompt: "%{client_name} nevű alkalmazás engedélyt kér a fiókodhoz való hozzáféréshez."
+        title: Engedély szükséges
+      show:
+        title: Engedély kódja
+    authorized_applications:
+      buttons:
+        revoke: Visszavonás
+      confirmations:
+        revoke: Biztos vagy benne?
+      index:
+        application: Alkalmazás
+        created_at: Készítve
+        date_format: "%Y-%m-%d %H:%M:%S"
+        title: Engedélyezett alkalmazásaid
+    errors:
+      messages:
+        access_denied: Az erőforrás tulajdonosa vagy hitelesítő kiszolgálója megtakadta a kérést.
+        credential_flow_not_configured: Az erőforrás tulajdonos jelszóadatainak átadása megszakadt, mert a Doorkeeper.configure.resource_owner_from_credentials beállítatlan.
+        invalid_client: A kliens hitelesítése megszakadt, mert a ismeretlen a kliens, kliens nem küldött hitelesítést, vagy ismeretlen a kliens
+        invalid_grant: A biztosított hitelesítés érvénytelen, lejárt, visszavont, vagy nem egyezik a hitelesítéi kérésben használt URIval, vagy más kliensnek lett címezve.
+        invalid_redirect_uri: A redirect uri nem valós.
+        invalid_request: A kérésből hiányzik egy szükséges paraméter, nem támogatott paramétert tartalmaz, vagy egyéb módon hibás.
+        invalid_resource_owner: A biztosított erőforrás tulajdonosának hitelesítő adatai nem valósak, vagy az erőforrás tulajdonosa nem található.
+        invalid_scope: A kért nézet érvénytelen, ismeretlen, vagy hibás.
+        invalid_token:
+          expired: Hozzáférési kulcs lejárt
+          revoked: Hozzáférési kulcs vissza lett vonva
+          unknown: Hozzáférési kulcs érvénytelen
+        resource_owner_authenticator_not_configured: Erőforrás tulajdonos keresés megszakadt, ugyanis a Doorkeeper.configure.resource_owner_authenticator beállítatlan.
+        server_error: Hitelesítő szervert váratlan esemény érte, mely meggátolta a kérés teljesítését.
+        temporarily_unavailable: A hitelesítő szerver jelenleg nem tudja teljesíteni a kérést egy átmeneti túlterheltség vagy a kiszolgáló karbantartása miatt. 
+        unauthorized_client: A kliens nincs feljogosítva a kérés teljesítésére.
+        unsupported_grant_type: A hitelesítés módja nem támogatott a hitelesítő kiszolgálón.
+        unsupported_response_type: A hitelesítő kiszolgáló nem támogatja ezt a választ.
+    flash:
+      applications:
+        create:
+          notice: Alkalmazás létrehozva.
+        destroy:
+          notice: Alkalmazás törölve.
+        update:
+          notice: Alkalmazás frissítve.
+      authorized_applications:
+        destroy:
+          notice: Alkalmazás visszavonva.
+    layouts:
+      admin:
+        nav:
+          applications: Alkalmazások
+          oauth2_provider: OAuth2 szolgáltató
+      application:
+        title: OAuth engedély szükséges
+    scopes:
+      follow: fiókok követése, blokkoláse, blokkolás feloldása és követés abbahagyása
+      read: fiókod adatainak olvasása
+      write: bejegyzés írása a nevedben
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index c9258381c..f78cd0de5 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -7,44 +7,44 @@ fr:
     source_code: "Code source"
     terms: "Conditions d’utilisation"
   accounts:
-    follow: "S’abonner"
+    follow: "Suivre"
     followers: "Abonnés"
     following: "Abonnements"
-    nothing_here: "Rien à voir ici&nbsp;!"
-    people_followed_by: "Personnes auxquelles %{name} est abonné⋅e"
-    people_who_follow: "Personnes abonnées à %{name}"
+    nothing_here: "Rien à voir ici !"
+    people_followed_by: "Personnes suivies par %{name}"
+    people_who_follow: "Personnes qui suivent %{name}"
     posts: "Statuts"
-    unfollow: "Se désabonner"
+    unfollow: "Ne plus suivre"
   application_mailer:
     signature: "Notifications de Mastodon depuis %{instance}"
   auth:
     change_password: "Changer de mot de passe"
-    didnt_get_confirmation: "Vous n’avez pas reçu les consignes de confirmation&nbsp;?"
-    forgot_password: "Mode passe oublié&nbsp;?"
+    didnt_get_confirmation: "Vous n’avez pas reçu les consignes de confirmation ?"
+    forgot_password: "Mode passe oublié ?"
     login: "Se connecter"
     register: "S’inscrire"
     resend_confirmation: "Envoyer à nouveau les consignes de confirmation"
     reset_password: "Réinitialiser le mot de passe"
     set_new_password: "Établir le nouveau mot de passe"
   generic:
-    changes_saved_msg: "Les modifications ont été enregistrées avec succès&nbsp;!"
+    changes_saved_msg: "Les modifications ont été enregistrées avec succès !"
     powered_by: "propulsé par %{link}"
     save_changes: "Enregistrer les modifications"
     validation_errors:
-      one: "Quelque chose ne va pas&nbsp;! Vérifiez l’erreur ci-dessous."
-      other: "Quelques choses ne vont pas&nbsp;! Vérifiez les erreurs ci-dessous."
+      one: "Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous."
+      other: "Quelques choses ne vont pas ! Vérifiez les erreurs ci-dessous."
   notification_mailer:
     favourite:
-      body: "%{name} a ajouté votre statut à ses favoris&nbsp;:"
+      body: "%{name} a ajouté votre statut à ses favoris :"
       subject: "%{name} a ajouté votre statut à ses favoris"
     follow:
-      body: "%{name} s’est abonné⋅e à vos statuts&nbsp;!"
-      subject: "%{name} s’est abonné⋅e à vos statuts"
+      body: "%{name} vous suit !"
+      subject: "%{name} vous suit"
     mention:
-      body: "%{name} vous a mentionné⋅e dans&nbsp;:"
+      body: "%{name} vous a mentionné⋅e dans :"
       subject: "%{name} vous a mentionné⋅e"
     reblog:
-      body: "%{name} a partagé votre statut&nbsp;:"
+      body: "%{name} a partagé votre statut :"
       subject: "%{name} a partagé votre statut"
   pagination:
     next: "Suivant"
@@ -54,6 +54,6 @@ fr:
     preferences: "Préférences"
   stream_entries:
     favourited: "a ajouté à ses favoris un statut de"
-    is_now_following: "s’est abonné⋅e à"
+    is_now_following: "suit désormais"
   will_paginate:
     page_gap: "&hellip;"
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
new file mode 100644
index 000000000..d891b2b28
--- /dev/null
+++ b/config/locales/hu.yml
@@ -0,0 +1,59 @@
+---
+hu:
+  about:
+    about_instance: "<em>%{instance}</em> egy Mastodon másolat."
+    about_mastodon: Mastodon egy <em>szabad, nyílt forráskódú</em> szociális hálózati kiszolgálo. Egy <em>központosítatlan</em> alternatíva a kereskedelmi platformokra, elkerüli a kommunikációd monopolizációját veszélyét. Bárki futtathatja a Mastodon-t és részt vehet a <em>szociális hálózatban</em>.
+    get_started: Első lépések
+    source_code: Forráskód
+    terms: Feltételek
+  accounts:
+    follow: Követés
+    followers: Követők
+    following: Követed őket
+    nothing_here: Nincs itt semmi!
+    people_followed_by: "%{name} követett személyei"
+    people_who_follow: "%{name} követői"
+    posts: Bejegyzések
+    unfollow: Követés abbahagyása
+  application_mailer:
+    signature: "%{instance} Mastodon értesítései"
+  auth:
+    change_password: Jelszó változtatása
+    didnt_get_confirmation: Nem kaptad meg a megerősítési lépéseket?
+    forgot_password: Elfelejtetted a jelszavad?
+    login: Belépés
+    register: Regisztráció
+    resend_confirmation: Megerősítési lépések újraküldése
+    reset_password: Jelszó visszaállítása
+    set_new_password: Új jelszó beállítása
+  generic:
+    changes_saved_msg: Változások sikeresen elmentve!
+    powered_by: powered by %{link}
+    save_changes: Változások mentése
+    validation_errors:
+      one: Valami nincs rendjén! Kérlek tekintsd meg a hibát alant
+      other: Valami nincs rendjén! Kérlek tekintsd meg a %{count} darab hibát alant.
+  notification_mailer:
+    favourite:
+      body: 'Az állapotodat kedvencnek jelölte %{name}:'
+      subject: "%{name} kedvencnek jelölte az állapotod"
+    follow:
+      body: "%{name} mostantól követ téged!"
+      subject: "%{name} mostantól követ téged"
+    mention:
+      body: '%{name} megemlített téged:'
+      subject: "%{name} megemlített téged"
+    reblog:
+      body: 'Az állapotod reblogolta %{name}:'
+      subject: "%{name} reblogolta az állapotod"
+  pagination:
+    next: Következő
+    prev: Előző
+  settings:
+    edit_profile: Profil szerkesztése
+    preferences: Beállítások
+  stream_entries:
+    favourited: kedvencnek jelölték a bejegyzésedet 
+    is_now_following: mostantól követ
+  will_paginate:
+    page_gap: "&hellip;"
diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml
index 36c5141a2..47e30ccb4 100644
--- a/config/locales/simple_form.de.yml
+++ b/config/locales/simple_form.de.yml
@@ -16,6 +16,9 @@ de:
         password: Passwort
         silenced: Öffentliche Beiträge nicht auflisten
         username: Nutzername
+      interactions:
+        must_be_follower: Benachrichtigungen von nicht-Folgern blockieren
+        must_be_following: Benachrichtigungen von Nutzern blockieren, denen ich nicht folge
       notification_emails:
         favourite: E-mail senden, wenn jemand meinen Beitrag favorisiert
         follow: E-mail senden, wenn mir jemand folgt
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index a7d958c06..1e975af14 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -16,6 +16,9 @@ en:
         password: Password
         silenced: Unlisted mode
         username: Username
+      interactions:
+        must_be_follower: Block notifications from non-followers
+        must_be_following: Block notifications from people you don't follow
       notification_emails:
         favourite: Send e-mail when someone favourites your status
         follow: Send e-mail when someone follows you
diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml
index 7333c9e11..73905a7b3 100644
--- a/config/locales/simple_form.fr.yml
+++ b/config/locales/simple_form.fr.yml
@@ -15,12 +15,15 @@ fr:
         note: Présentation
         password: Mot de passe
         silenced: Ne pas apparaître dans le fil public
-        username: Nom d’utilisateur
+        username: Identifiant
       notification_emails:
-        favourite: Envoyer un courriel lorsque quelqu’un ajoute un de mes statuts à ses favoris
-        follow: Envoyer un courriel lorsque quelqu’un s’abonne à mes statuts
+        favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statut à ses favoris
+        follow: Envoyer un courriel lorsque quelqu’un me suit
         mention: Envoyer un courriel lorsque quelqu’un me mentionne
-        reblog: Envoyer un courriel lorsque quelqu’un partage un de mes statuts
+        reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts
+      interactions:
+        must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas
+        must_be_following: Masquer les notifications des personnes que vous ne suivez pas
     'no': Non
     required:
       mark: "*"
diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml
new file mode 100644
index 000000000..39c450087
--- /dev/null
+++ b/config/locales/simple_form.hu.yml
@@ -0,0 +1,28 @@
+---
+hu:
+  simple_form:
+    labels:
+      defaults:
+        avatar: Profilkép
+        confirm_new_password: Új jelszó megerősítése
+        confirm_password: Jelszó megerősítése
+        current_password: Jelenlegi jelszó
+        display_name: Megjelenített név
+        email: E-mail cím
+        header: Fejléc
+        locale: Nyelv
+        new_password: Új jelszó
+        note: Önéletrajz
+        password: Jelszó
+        silenced: Listázatlan mód
+        username: Felhasználónév
+      notification_emails:
+        favourite: E-mail küldése amikor valaki kedvencnek jelöli az állapotod
+        follow: E-mail küldése amikor valaki követni kezd téged
+        mention: E-mail küldése amikor valaki megemlít téged
+        reblog: E-mail küldése amikor valaki reblogolja az állapotod
+    'no': 'Nem'
+    required:
+      mark: "*"
+      text: kötelező
+    'yes': 'Igen'
diff --git a/config/routes.rb b/config/routes.rb
index 00185c5e8..cd544a62b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
 require 'sidekiq/web'
 
 Rails.application.routes.draw do
-  mount ActionCable.server => '/cable'
+  mount ActionCable.server, at: 'cable'
 
   authenticate :user, lambda { |u| u.admin? } do
     mount Sidekiq::Web, at: 'sidekiq'
@@ -19,7 +21,7 @@ Rails.application.routes.draw do
     sessions:           'auth/sessions',
     registrations:      'auth/registrations',
     passwords:          'auth/passwords',
-    confirmations:      'auth/confirmations'
+    confirmations:      'auth/confirmations',
   }
 
   resources :accounts, path: 'users', only: [:show], param: :username do
@@ -42,11 +44,18 @@ Rails.application.routes.draw do
   resources :media, only: [:show]
   resources :tags,  only: [:show]
 
+  namespace :admin do
+    resources :pubsubhubbub, only: [:index]
+  end
+
   namespace :api do
-    # PubSubHubbub
+    # PubSubHubbub outgoing subscriptions
     resources :subscriptions, only: [:show]
     post '/subscriptions/:id', to: 'subscriptions#update'
 
+    # PubSubHubbub incoming subscriptions
+    post '/push', to: 'push#update', as: :push
+
     # Salmon
     post '/salmon/:id', to: 'salmon#update', as: :salmon
 
@@ -80,7 +89,6 @@ Rails.application.routes.draw do
         collection do
           get :relationships
           get :verify_credentials
-          get :suggestions
           get :search
         end
 
@@ -88,7 +96,6 @@ Rails.application.routes.draw do
           get :statuses
           get :followers
           get :following
-          get :common_followers
 
           post :follow
           post :unfollow
diff --git a/db/migrate/20161128103007_create_subscriptions.rb b/db/migrate/20161128103007_create_subscriptions.rb
new file mode 100644
index 000000000..46443680a
--- /dev/null
+++ b/db/migrate/20161128103007_create_subscriptions.rb
@@ -0,0 +1,15 @@
+class CreateSubscriptions < ActiveRecord::Migration[5.0]
+  def change
+    create_table :subscriptions do |t|
+      t.string :callback_url, null: false, default: ''
+      t.string :secret
+      t.datetime :expires_at, null: true, default: nil
+      t.boolean :confirmed, null: false, default: false
+      t.integer :account_id, null: false
+
+      t.timestamps
+    end
+
+    add_index :subscriptions, [:callback_url, :account_id], unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 356badf8e..2c0e6de5b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20161123093447) do
+ActiveRecord::Schema.define(version: 20161128103007) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -143,6 +143,19 @@ ActiveRecord::Schema.define(version: 20161123093447) do
     t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
   end
 
+  create_table "pubsubhubbub_subscriptions", force: :cascade do |t|
+    t.string   "topic",      default: "",    null: false
+    t.string   "callback",   default: "",    null: false
+    t.string   "mode",       default: "",    null: false
+    t.string   "challenge",  default: "",    null: false
+    t.string   "secret"
+    t.boolean  "confirmed",  default: false, null: false
+    t.datetime "expires_at",                 null: false
+    t.datetime "created_at",                 null: false
+    t.datetime "updated_at",                 null: false
+    t.index ["topic", "callback"], name: "index_pubsubhubbub_subscriptions_on_topic_and_callback", unique: true, using: :btree
+  end
+
   create_table "settings", force: :cascade do |t|
     t.string   "var",         null: false
     t.text     "value"
@@ -185,6 +198,17 @@ ActiveRecord::Schema.define(version: 20161123093447) do
     t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type", using: :btree
   end
 
+  create_table "subscriptions", force: :cascade do |t|
+    t.string   "callback_url", default: "",    null: false
+    t.string   "secret"
+    t.datetime "expires_at"
+    t.boolean  "confirmed",    default: false, null: false
+    t.integer  "account_id",                   null: false
+    t.datetime "created_at",                   null: false
+    t.datetime "updated_at",                   null: false
+    t.index ["callback_url", "account_id"], name: "index_subscriptions_on_callback_url_and_account_id", unique: true, using: :btree
+  end
+
   create_table "tags", force: :cascade do |t|
     t.string   "name",       default: "", null: false
     t.datetime "created_at",              null: false
diff --git a/docker-compose.yml b/docker-compose.yml
index 9f9126805..e1f1f1c4c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,16 +6,9 @@ services:
   redis:
     restart: always
     image: redis
-  neo4j:
-    restart: always
-    build:
-      context: .
-      dockerfile: Dockerfile.neo4j
   web:
     restart: always
-    build:
-      context: .
-      dockerfile: Dockerfile.app
+    build: .
     env_file: .env.production
     command: bundle exec rails s -p 3000 -b '0.0.0.0'
     ports:
@@ -23,20 +16,16 @@ services:
     depends_on:
       - db
       - redis
-      - neo4j
     volumes:
       - ./public/assets:/mastodon/public/assets
       - ./public/system:/mastodon/public/system
   sidekiq:
     restart: always
-    build:
-      context: .
-      dockerfile: Dockerfile.app
+    build: .
     env_file: .env.production
-    command: bundle exec sidekiq -q default -q mailers
+    command: bundle exec sidekiq -q default -q mailers -q push
     depends_on:
       - db
       - redis
-      - neo4j
     volumes:
       - ./public/system:/mastodon/public/system
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index 93461bd0a..8730a64cd 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -49,11 +49,4 @@ namespace :mastodon do
       Redis.current.keys('feed:*').each { |key| Redis.current.del(key) }
     end
   end
-
-  namespace :graphs do
-    desc 'Syncs all follow relationships to Neo4J'
-    task sync: :environment do
-      Follow.find_each(&:sync!)
-    end
-  end
 end
diff --git a/spec/controllers/admin/pubsubhubbub_controller_spec.rb b/spec/controllers/admin/pubsubhubbub_controller_spec.rb
new file mode 100644
index 000000000..068bd09a6
--- /dev/null
+++ b/spec/controllers/admin/pubsubhubbub_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+require 'rails_helper'
+
+RSpec.describe Admin::PubsubhubbubController, type: :controller do
+  describe 'GET #index' do
+    before do
+      sign_in Fabricate(:user, admin: true), scope: :user
+    end
+
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb
new file mode 100644
index 000000000..e699006f7
--- /dev/null
+++ b/spec/controllers/api/push_controller_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+RSpec.describe Api::PushController, type: :controller do
+  describe 'POST #update' do
+    context 'with hub.mode=subscribe' do
+      pending
+    end
+
+    context 'with hub.mode=unsubscribe' do
+      pending
+    end
+  end
+end
diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb
index 6897caeeb..3d3a973d2 100644
--- a/spec/controllers/api/salmon_controller_spec.rb
+++ b/spec/controllers/api/salmon_controller_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Api::SalmonController, type: :controller do
   let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account }
 
   before do
+    stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
     stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt'))
     stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt'))
     stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt'))
diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb
index 2af6cb725..44841176a 100644
--- a/spec/controllers/api/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/subscriptions_controller_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
     let(:feed) { File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom')) }
 
     before do
+      stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {})
       stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt'))
       stub_request(:head, "https://quitter.no/notice/1269244").to_return(status: 404)
       stub_request(:head, "https://quitter.no/notice/1265331").to_return(status: 404)
@@ -37,7 +38,7 @@ RSpec.describe Api::SubscriptionsController, type: :controller do
       stub_request(:head, "https://social.umeahackerspace.se/user/2").to_return(status: 404)
       stub_request(:head, "https://gs.kawa-kun.com/user/2").to_return(status: 404)
       stub_request(:head, "https://mastodon.social/users/Gargron").to_return(status: 404)
-      
+
       request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}"
       request.env['RAW_POST_DATA'] = feed
 
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 98eea28ce..e4532305b 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -46,20 +46,6 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
     end
   end
 
-  describe 'GET #suggestions' do
-    it 'returns http success' do
-      get :suggestions
-      expect(response).to have_http_status(:success)
-    end
-  end
-
-  describe 'GET #common_followers' do
-    it 'returns http success' do
-      get :common_followers, params: { id: user.account.id }
-      expect(response).to have_http_status(:success)
-    end
-  end
-
   describe 'POST #follow' do
     let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
 
diff --git a/spec/fabricators/subscription_fabricator.rb b/spec/fabricators/subscription_fabricator.rb
new file mode 100644
index 000000000..0c8290494
--- /dev/null
+++ b/spec/fabricators/subscription_fabricator.rb
@@ -0,0 +1,6 @@
+Fabricator(:subscription) do
+  callback_url "http://example.com/callback"
+  secret       "foobar"
+  expires_at   "2016-11-28 11:30:07"
+  confirmed    false
+end
diff --git a/spec/helpers/admin/pubsubhubbub_helper_spec.rb b/spec/helpers/admin/pubsubhubbub_helper_spec.rb
new file mode 100644
index 000000000..6603e6dc0
--- /dev/null
+++ b/spec/helpers/admin/pubsubhubbub_helper_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the Admin::PubsubhubbubHelper. For example:
+#
+# describe Admin::PubsubhubbubHelper do
+#   describe "string concat" do
+#     it "concats two strings with spaces" do
+#       expect(helper.concat_strings("this","that")).to eq("this that")
+#     end
+#   end
+# end
+RSpec.describe Admin::PubsubhubbubHelper, type: :helper do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb
new file mode 100644
index 000000000..d40bf0b44
--- /dev/null
+++ b/spec/models/subscription_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Subscription, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb
index e4e5858ea..5e57d823b 100644
--- a/spec/services/process_feed_service_spec.rb
+++ b/spec/services/process_feed_service_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe ProcessFeedService do
   subject { ProcessFeedService.new }
 
   before do
+    stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
     stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt'))
     stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt'))
     stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt'))
diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb
index 1ffcfbfac..c3d76c653 100644
--- a/spec/services/update_remote_profile_service_spec.rb
+++ b/spec/services/update_remote_profile_service_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 RSpec.describe UpdateRemoteProfileService do
-  let(:xml) { Nokogiri::XML(File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom'))).at_xpath('//xmlns:author') }
+  let(:xml) { Nokogiri::XML(File.read(File.join(Rails.root, 'spec', 'fixtures', 'push', 'feed.atom'))).at_xpath('//xmlns:feed') }
 
   subject { UpdateRemoteProfileService.new }
 
@@ -13,7 +13,7 @@ RSpec.describe UpdateRemoteProfileService do
     let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') }
 
     before do
-      subject.(xml, remote_account)
+      subject.call(xml, remote_account)
     end
 
     it 'downloads new avatar' do
@@ -34,10 +34,10 @@ RSpec.describe UpdateRemoteProfileService do
   end
 
   context 'with unchanged details' do
-    let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com',display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') }
+    let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') }
 
     before do
-      subject.(xml, remote_account)
+      subject.call(xml, remote_account)
     end
 
     it 'does not re-download avatar' do