about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJantsoP <jantso.porali@gmail.com>2017-04-05 10:07:17 +0200
committerGitHub <noreply@github.com>2017-04-05 10:07:17 +0200
commit20b53e6add351e5bb5ff1bd4d675db6252e03293 (patch)
treee252e354e63765d1f28919f2ac58de26001806cb
parent3ec221d3b7093e2a2606ec04036cca9e9f1f733d (diff)
parentccb6a658fd1a2e596c95d5b8e7a39f72a5f5b14b (diff)
Merge branch 'master' into master
-rw-r--r--.env.production.sample2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock7
-rw-r--r--ISSUE_TEMPLATE.md5
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx2
-rw-r--r--app/assets/stylesheets/about.scss10
-rw-r--r--app/assets/stylesheets/accounts.scss3
-rw-r--r--app/controllers/about_controller.rb4
-rw-r--r--app/controllers/admin/settings_controller.rb14
-rw-r--r--app/controllers/auth/registrations_controller.rb10
-rw-r--r--app/controllers/remote_follow_controller.rb3
-rw-r--r--app/lib/email_validator.rb17
-rw-r--r--app/lib/feed_manager.rb107
-rw-r--r--app/lib/inline_rabl_scope.rb17
-rw-r--r--app/models/block.rb5
-rw-r--r--app/models/follow.rb9
-rw-r--r--app/models/follow_request.rb5
-rw-r--r--app/models/mention.rb5
-rw-r--r--app/services/fan_out_on_write_service.rb13
-rw-r--r--app/services/notify_service.rb2
-rw-r--r--app/services/precompute_feed_service.rb8
-rw-r--r--app/views/about/index.html.haml37
-rw-r--r--app/views/admin/settings/index.html.haml12
-rw-r--r--app/workers/feed_insert_worker.rb15
-rw-r--r--app/workers/processing_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb1
-rw-r--r--app/workers/regeneration_worker.rb2
-rw-r--r--app/workers/salmon_worker.rb2
-rw-r--r--config/initializers/blacklists.rb1
-rw-r--r--config/locales/de.yml22
-rw-r--r--config/locales/devise.de.yml50
-rw-r--r--config/locales/doorkeeper.fr.yml10
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/locales/fr.yml1
-rw-r--r--config/locales/simple_form.en.yml2
-rw-r--r--config/settings.yml3
-rw-r--r--docs/Running-Mastodon/Administration-guide.md2
-rw-r--r--docs/Running-Mastodon/Heroku-guide.md4
-rw-r--r--docs/Running-Mastodon/Production-guide.md8
-rw-r--r--docs/Using-Mastodon/List-of-Mastodon-instances.md28
-rw-r--r--docs/Using-Mastodon/User-guide.md6
-rw-r--r--spec/fabricators/account_fabricator.rb2
-rw-r--r--spec/fabricators/block_fabricator.rb3
-rw-r--r--spec/fabricators/follow_fabricator.rb3
-rw-r--r--spec/fabricators/follow_request_fabricator.rb3
-rw-r--r--spec/fabricators/mention_fabricator.rb4
-rw-r--r--spec/fabricators/user_fabricator.rb2
-rw-r--r--spec/models/account_spec.rb69
-rw-r--r--spec/models/block_spec.rb17
-rw-r--r--spec/models/domain_block_spec.rb18
-rw-r--r--spec/models/follow_request_spec.rb19
-rw-r--r--spec/models/follow_spec.rb19
-rw-r--r--spec/models/mention_spec.rb17
-rw-r--r--spec/models/user_spec.rb83
-rw-r--r--spec/rails_helper.rb2
-rw-r--r--spec/services/fan_out_on_write_service_spec.rb1
-rw-r--r--spec/support/matchers/model/model_have_error_on_field.rb15
57 files changed, 573 insertions, 167 deletions
diff --git a/.env.production.sample b/.env.production.sample
index bd81b8fca..a7f9eb4bf 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -22,6 +22,8 @@ OTP_SECRET=
 # SINGLE_USER_MODE=true
 # Prevent registrations with following e-mail domains
 # EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
+# Only allow registrations with the following e-mail domains
+# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
 
 # E-mail configuration
 SMTP_SERVER=smtp.mailgun.org
diff --git a/Gemfile b/Gemfile
index 46baed307..4e7ff6621 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,7 +38,7 @@ gem 'rqrcode'
 gem 'twitter-text'
 gem 'oj'
 gem 'hiredis'
-gem 'redis', '~>3.2'
+gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
 gem 'fast_blank'
 gem 'htmlentities'
 gem 'simple_form'
@@ -46,6 +46,7 @@ gem 'will_paginate'
 gem 'rack-attack'
 gem 'rack-cors', require: 'rack/cors'
 gem 'sidekiq'
+gem 'sidekiq-unique-jobs'
 gem 'rails-settings-cached'
 gem 'simple-navigation'
 gem 'statsd-instrument'
@@ -66,9 +67,10 @@ group :development, :test do
 end
 
 group :test do
+  gem 'faker'
+  gem 'rspec-sidekiq'
   gem 'simplecov', require: false
   gem 'webmock'
-  gem 'rspec-sidekiq'
 end
 
 group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index 6e3115249..a774a89ba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -149,6 +149,8 @@ GEM
     erubis (2.7.0)
     execjs (2.7.0)
     fabrication (2.15.2)
+    faker (1.6.6)
+      i18n (~> 0.5)
     fast_blank (1.0.0)
     font-awesome-rails (4.6.3.1)
       railties (>= 3.2, < 5.1)
@@ -387,6 +389,9 @@ GEM
       connection_pool (~> 2.2, >= 2.2.0)
       rack-protection (>= 1.5.0)
       redis (~> 3.2, >= 3.2.1)
+    sidekiq-unique-jobs (4.0.18)
+      sidekiq (>= 2.6)
+      thor
     simple-navigation (4.0.3)
       activesupport (>= 2.3.2)
     simple_form (3.2.1)
@@ -467,6 +472,7 @@ DEPENDENCIES
   doorkeeper
   dotenv-rails
   fabrication
+  faker
   fast_blank
   font-awesome-rails
   fuubar
@@ -510,6 +516,7 @@ DEPENDENCIES
   sass-rails (~> 5.0)
   sdoc (~> 0.4.0)
   sidekiq
+  sidekiq-unique-jobs
   simple-navigation
   simple_form
   simplecov
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..8394b2424
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,5 @@
+[Issue text goes here].
+
+* * * *
+
+- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
index d75149a0e..6aa9d1efa 100644
--- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -9,7 +9,7 @@ const iconStyle = {
 };
 
 const ClearColumnButton = ({ onClick }) => (
-  <div className='column-icon' style={iconStyle} onClick={onClick}>
+  <div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
     <i className='fa fa-trash' />
   </div>
 );
diff --git a/app/assets/stylesheets/about.scss b/app/assets/stylesheets/about.scss
index 2ff1d1453..c9d9dc5d5 100644
--- a/app/assets/stylesheets/about.scss
+++ b/app/assets/stylesheets/about.scss
@@ -319,7 +319,7 @@
     }
   }
 
-  .simple_form {
+  .simple_form, .closed-registrations-message {
     width: 300px;
     flex: 0 0 auto;
     background: rgba(darken($color1, 7%), 0.5);
@@ -340,3 +340,11 @@
     }
   }
 }
+
+.closed-registrations-message {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+}
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index 25e24a95a..b3ae33500 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -34,6 +34,7 @@
     text-align: center;
     position: relative;
     z-index: 2;
+    text-shadow: 0 0 2px $color8;
 
     small {
       display: block;
@@ -128,6 +129,7 @@
       text-transform: uppercase;
       display: block;
       margin-bottom: 5px;
+      text-shadow: 0 0 2px $color8;
     }
 
     .counter-number {
@@ -385,5 +387,6 @@
   .account__header__content {
     font-size: 14px;
     color: $color1;
+    text-shadow: 0 0 2px $color8;
   }
 }
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index abf4b7df4..7fd43489f 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -4,7 +4,9 @@ class AboutController < ApplicationController
   before_action :set_body_classes
 
   def index
-    @description = Setting.site_description
+    @description                  = Setting.site_description
+    @open_registrations           = Setting.open_registrations
+    @closed_registrations_message = Setting.closed_registrations_message
 
     @user = User.new
     @user.build_account
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index af0be8823..7615c781d 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -11,9 +11,13 @@ class Admin::SettingsController < ApplicationController
 
   def update
     @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
+    value    = settings_params[:value]
 
-    if @setting.value != params[:setting][:value]
-      @setting.value = params[:setting][:value]
+    # Special cases
+    value = value == 'true' if @setting.var == 'open_registrations'
+
+    if @setting.value != value
+      @setting.value = value
       @setting.save
     end
 
@@ -22,4 +26,10 @@ class Admin::SettingsController < ApplicationController
       format.json { respond_with_bip(@setting) }
     end
   end
+
+  private
+
+  def settings_params
+    params.require(:setting).permit(:value)
+  end
 end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 501e66807..4881c074a 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -3,7 +3,7 @@
 class Auth::RegistrationsController < Devise::RegistrationsController
   layout :determine_layout
 
-  before_action :check_single_user_mode
+  before_action :check_enabled_registrations, only: [:new, :create]
   before_action :configure_sign_up_params, only: [:create]
 
   protected
@@ -27,12 +27,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
     new_user_session_path
   end
 
-  def check_single_user_mode
-    redirect_to root_path if Rails.configuration.x.single_user_mode
+  def check_enabled_registrations
+    redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
   end
-  
+
   private
-  
+
   def determine_layout
     %w(edit update).include?(action_name) ? 'admin' : 'auth'
   end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 7d4bfe6ce..1e3f786ec 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -8,6 +8,7 @@ class RemoteFollowController < ApplicationController
 
   def new
     @remote_follow = RemoteFollow.new
+    @remote_follow.acct = session[:remote_follow] if session.key?(:remote_follow)
   end
 
   def create
@@ -22,6 +23,8 @@ class RemoteFollowController < ApplicationController
         render(:new) && return
       end
 
+      session[:remote_follow] = @remote_follow.acct
+
       redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
     else
       render :new
diff --git a/app/lib/email_validator.rb b/app/lib/email_validator.rb
index 856b8b1f7..06e9375f6 100644
--- a/app/lib/email_validator.rb
+++ b/app/lib/email_validator.rb
@@ -2,17 +2,30 @@
 
 class EmailValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
-    return if Rails.configuration.x.email_domains_blacklist.empty?
-
     record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value)
   end
 
   private
 
   def blocked_email?(value)
+    on_blacklist?(value) || not_on_whitelist?(value)
+  end
+
+  def on_blacklist?(value)
+    return false if Rails.configuration.x.email_domains_blacklist.blank?
+
     domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
     regexp  = Regexp.new("@(.+\\.)?(#{domains})", true)
 
     value =~ regexp
   end
+
+  def not_on_whitelist?(value)
+    return false if Rails.configuration.x.email_domains_whitelist.blank?
+
+    domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.')
+    regexp  = Regexp.new("@(.+\\.)?(#{domains})", true)
+
+    value !~ regexp
+  end
 end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index cd6ca1291..2cca1cefe 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -5,17 +5,17 @@ require 'singleton'
 class FeedManager
   include Singleton
 
-  MAX_ITEMS = 800
+  MAX_ITEMS = 400
 
   def key(type, id)
     "feed:#{type}:#{id}"
   end
 
-  def filter?(timeline_type, status, receiver)
+  def filter?(timeline_type, status, receiver_id)
     if timeline_type == :home
-      filter_from_home?(status, receiver)
+      filter_from_home?(status, receiver_id)
     elsif timeline_type == :mentions
-      filter_from_mentions?(status, receiver)
+      filter_from_mentions?(status, receiver_id)
     else
       false
     end
@@ -50,10 +50,18 @@ class FeedManager
 
   def merge_into_timeline(from_account, into_account)
     timeline_key = key(:home, into_account.id)
+    query        = from_account.statuses.limit(FeedManager::MAX_ITEMS / 4)
 
-    from_account.statuses.limit(MAX_ITEMS).each do |status|
-      next if status.direct_visibility? || filter?(:home, status, into_account)
-      redis.zadd(timeline_key, status.id, status.id)
+    if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
+      oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
+      query = query.where('id > ?', oldest_home_score)
+    end
+
+    redis.pipelined do
+      query.each do |status|
+        next if status.direct_visibility? || filter?(:home, status, into_account)
+        redis.zadd(timeline_key, status.id, status.id)
+      end
     end
 
     trim(:home, into_account.id)
@@ -61,31 +69,20 @@ class FeedManager
 
   def unmerge_from_timeline(from_account, into_account)
     timeline_key = key(:home, into_account.id)
-
-    from_account.statuses.select('id').find_each do |status|
-      redis.zrem(timeline_key, status.id)
-      redis.zremrangebyscore(timeline_key, status.id, status.id)
+    oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
+
+    from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
+      redis.pipelined do
+        statuses.each do |status|
+          redis.zrem(timeline_key, status.id)
+          redis.zremrangebyscore(timeline_key, status.id, status.id)
+        end
+      end
     end
   end
 
   def inline_render(target_account, template, object)
-    rabl_scope = Class.new do
-      include RoutingHelper
-
-      def initialize(account)
-        @account = account
-      end
-
-      def current_user
-        @account.try(:user)
-      end
-
-      def current_account
-        @account
-      end
-    end
-
-    Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
+    Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: InlineRablScope.new(target_account)).render
   end
 
   private
@@ -94,38 +91,40 @@ class FeedManager
     Redis.current
   end
 
-  def filter_from_home?(status, receiver)
-    return true if receiver.muting?(status.account)
-
-    should_filter = false
-
-    if status.reply? && status.in_reply_to_id.nil?
-      should_filter = true
-    elsif status.reply? && !status.in_reply_to_account_id.nil?                # Filter out if it's a reply
-      should_filter   = !receiver.following?(status.in_reply_to_account)      # and I'm not following the person it's a reply to
-      should_filter &&= !(receiver.id == status.in_reply_to_account_id)       # and it's not a reply to me
-      should_filter &&= !(status.account_id == status.in_reply_to_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
-      should_filter ||= receiver.muting?(status.reblog.account)               # or muting that person
-      should_filter ||= status.reblog.account.blocking?(receiver)             # or if the author of the reblogged status is blocking me
-    end
+  def filter_from_home?(status, receiver_id)
+    return true if status.reply? && status.in_reply_to_id.nil?
 
-    should_filter ||= receiver.blocking?(status.mentions.map(&:account_id))   # or if it mentions someone I blocked
+    check_for_mutes = [status.account_id]
+    check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
 
-    should_filter
-  end
+    return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
 
-  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.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
-    should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them
+    check_for_blocks = status.mentions.map(&:account_id)
+    check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
 
-    if status.reply? && !status.in_reply_to_account_id.nil?                                 # or it's a reply
-      should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to a user I blocked
+    return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
+
+    if status.reply? && !status.in_reply_to_account_id.nil?                                                              # Filter out if it's a reply
+      should_filter   = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to
+      should_filter &&= !(receiver_id == status.in_reply_to_account_id)                                                  # and it's not a reply to me
+      should_filter &&= !(status.account_id == status.in_reply_to_account_id)                                            # and it's not a self-reply
+      return should_filter
+    elsif status.reblog?                                                                                                 # Filter out a reblog
+      return Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?                   # or if the author of the reblogged status is blocking me
     end
 
+    false
+  end
+
+  def filter_from_mentions?(status, receiver_id)
+    check_for_blocks = [status.account_id]
+    check_for_blocks.concat(status.mentions.pluck(:account_id))
+    check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
+
+    should_filter   = receiver_id == status.account_id                                                                                   # Filter if I'm mentioning myself
+    should_filter ||= Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?                                     # or it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked
+    should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
+
     should_filter
   end
 end
diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb
new file mode 100644
index 000000000..26adcb03a
--- /dev/null
+++ b/app/lib/inline_rabl_scope.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class InlineRablScope
+  include RoutingHelper
+
+  def initialize(account)
+    @account = account
+  end
+
+  def current_user
+    @account.try(:user)
+  end
+
+  def current_account
+    @account
+  end
+end
diff --git a/app/models/block.rb b/app/models/block.rb
index 9c55703c9..ae456a6b6 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -3,9 +3,8 @@
 class Block < ApplicationRecord
   include Paginable
 
-  belongs_to :account
-  belongs_to :target_account, class_name: 'Account'
+  belongs_to :account, required: true
+  belongs_to :target_account, class_name: 'Account', required: true
 
-  validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
 end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 8bfe8b2f6..b6b9dca7c 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -3,11 +3,14 @@
 class Follow < ApplicationRecord
   include Paginable
 
-  belongs_to :account, counter_cache: :following_count
-  belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count
+  belongs_to :account, counter_cache: :following_count, required: true
+
+  belongs_to :target_account,
+             class_name: 'Account',
+             counter_cache: :followers_count,
+             required: true
 
   has_one :notification, as: :activity, dependent: :destroy
 
-  validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
 end
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 4224ab15d..20e1332dd 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -3,12 +3,11 @@
 class FollowRequest < ApplicationRecord
   include Paginable
 
-  belongs_to :account
-  belongs_to :target_account, class_name: 'Account'
+  belongs_to :account, required: true
+  belongs_to :target_account, class_name: 'Account', required: true
 
   has_one :notification, as: :activity, dependent: :destroy
 
-  validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
 
   def authorize!
diff --git a/app/models/mention.rb b/app/models/mention.rb
index 10a9cb1cd..03e76fcc4 100644
--- a/app/models/mention.rb
+++ b/app/models/mention.rb
@@ -1,11 +1,10 @@
 # frozen_string_literal: true
 
 class Mention < ApplicationRecord
-  belongs_to :account, inverse_of: :mentions
-  belongs_to :status
+  belongs_to :account, inverse_of: :mentions, required: true
+  belongs_to :status, required: true
 
   has_one :notification, as: :activity, dependent: :destroy
 
-  validates :account, :status, presence: true
   validates :account, uniqueness: { scope: :status }
 end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index df404cbef..42222c25b 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -33,9 +33,8 @@ class FanOutOnWriteService < BaseService
   def deliver_to_followers(status)
     Rails.logger.debug "Delivering status #{status.id} to followers"
 
-    status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).find_each do |follower|
-      next if FeedManager.instance.filter?(:home, status, follower)
-      FeedManager.instance.push(:home, follower, status)
+    status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).find_each do |follower|
+      FeedInsertWorker.perform_async(status.id, follower.id)
     end
   end
 
@@ -44,7 +43,7 @@ class FanOutOnWriteService < BaseService
 
     status.mentions.includes(:account).each do |mention|
       mentioned_account = mention.account
-      next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mentioned_account)
+      next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mention.account_id)
       FeedManager.instance.push(:home, mentioned_account, status)
     end
   end
@@ -54,9 +53,9 @@ class FanOutOnWriteService < BaseService
 
     payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status)
 
-    status.tags.find_each do |tag|
-      FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'update', payload: payload)
-      FeedManager.instance.broadcast("hashtag:#{tag.name}:local", event: 'update', payload: payload) if status.account.local?
+    status.tags.pluck(:name).each do |hashtag|
+      FeedManager.instance.broadcast("hashtag:#{hashtag}", event: 'update', payload: payload)
+      FeedManager.instance.broadcast("hashtag:#{hashtag}:local", event: 'update', payload: payload) if status.account.local?
     end
   end
 
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 942cd9d21..24486f220 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -17,7 +17,7 @@ class NotifyService < BaseService
   private
 
   def blocked_mention?
-    FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
+    FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
   end
 
   def blocked_favourite?
diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb
index e1ec56e8d..07dcb81da 100644
--- a/app/services/precompute_feed_service.rb
+++ b/app/services/precompute_feed_service.rb
@@ -5,9 +5,11 @@ class PrecomputeFeedService < BaseService
   # @param [Symbol] type :home or :mentions
   # @param [Account] account
   def call(_, account)
-    Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status|
-      next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account)
-      redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
+    redis.pipelined do
+      Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status|
+        next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account.id)
+        redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
+      end
     end
   end
 
diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml
index fdfb2b916..ebca4213a 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/index.html.haml
@@ -24,21 +24,34 @@
   .screenshot-with-signup
     .mascot= image_tag 'fluffy-elephant-friend.png'
 
-    = simple_form_for(@user, url: user_registration_path) do |f|
-      = f.simple_fields_for :account do |ff|
-        = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+    - if @open_registrations
+      = simple_form_for(@user, url: user_registration_path) do |f|
+        = f.simple_fields_for :account do |ff|
+          = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
 
-      = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-      = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
-      = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
+        = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+        = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
+        = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
 
-      .actions
-        = f.button :button, t('about.get_started'), type: :submit
+        .actions
+          = f.button :button, t('about.get_started'), type: :submit
 
-      .info
-        = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-        ·
-        = link_to t('about.about_this'), about_more_path
+        .info
+          = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+          ·
+          = link_to t('about.about_this'), about_more_path
+    - else
+      .closed-registrations-message
+        - if @closed_registrations_message.blank?
+          %p= t('about.closed_registrations')
+        - else
+          = @closed_registrations_message.html_safe
+        .info
+          = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+          ·
+          = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md'
+          ·
+          = link_to t('about.about_this'), about_more_path
 
   %h3= t('about.features_headline')
 
diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml
index 1429dbd9e..02faac8c2 100644
--- a/app/views/admin/settings/index.html.haml
+++ b/app/views/admin/settings/index.html.haml
@@ -38,3 +38,15 @@
         %br/
         You can use HTML tags
       %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description'])
+    %tr
+      %td
+        %strong Open registration
+      %td= best_in_place @settings['open_registrations'], :value, as: :checkbox, collection: { false: 'Disabled', true: 'Enabled'}, url: admin_setting_path(@settings['open_registrations'])
+    %tr
+      %td
+        %strong Closed registration message
+        %br/
+        Displayed on frontpage when registrations are closed
+        %br/
+        You can use HTML tags
+      %td= best_in_place @settings['closed_registrations_message'], :value, as: :textarea, url: admin_setting_path(@settings['closed_registrations_message'])
diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb
new file mode 100644
index 000000000..a58dfaa74
--- /dev/null
+++ b/app/workers/feed_insert_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class FeedInsertWorker
+  include Sidekiq::Worker
+
+  def perform(status_id, follower_id)
+    status   = Status.find(status_id)
+    follower = Account.find(follower_id)
+
+    return if FeedManager.instance.filter?(:home, status, follower.id)
+    FeedManager.instance.push(:home, follower, status)
+  rescue ActiveRecord::RecordNotFound
+    true
+  end
+end
diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb
index 4a467d924..5df404bcc 100644
--- a/app/workers/processing_worker.rb
+++ b/app/workers/processing_worker.rb
@@ -3,7 +3,7 @@
 class ProcessingWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', backtrace: true
+  sidekiq_options backtrace: true
 
   def perform(account_id, body)
     ProcessFeedService.new.call(body, Account.find(account_id))
diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb
index 15005bc80..466def3a8 100644
--- a/app/workers/pubsubhubbub/delivery_worker.rb
+++ b/app/workers/pubsubhubbub/delivery_worker.rb
@@ -22,6 +22,7 @@ class Pubsubhubbub::DeliveryWorker
                    .headers(headers)
                    .post(subscription.callback_url, body: payload)
 
+    return subscription.destroy! if response.code > 299 && response.code < 500 && response.code != 429 # HTTP 4xx means error is not temporary, except for 429 (throttling)
     raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300
 
     subscription.touch(:last_successful_delivery_at)
diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb
index 82665b581..da8b845f6 100644
--- a/app/workers/regeneration_worker.rb
+++ b/app/workers/regeneration_worker.rb
@@ -3,7 +3,7 @@
 class RegenerationWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', backtrace: true
+  sidekiq_options queue: 'pull', backtrace: true, unique: :until_executed
 
   def perform(account_id, _ = :home)
     PrecomputeFeedService.new.call(:home, Account.find(account_id))
diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb
index 2888b574b..fc95ce47f 100644
--- a/app/workers/salmon_worker.rb
+++ b/app/workers/salmon_worker.rb
@@ -3,7 +3,7 @@
 class SalmonWorker
   include Sidekiq::Worker
 
-  sidekiq_options queue: 'pull', backtrace: true
+  sidekiq_options backtrace: true
 
   def perform(account_id, body)
     ProcessInteractionService.new.call(body, Account.find(account_id))
diff --git a/config/initializers/blacklists.rb b/config/initializers/blacklists.rb
index 52646e64d..6db7be7dc 100644
--- a/config/initializers/blacklists.rb
+++ b/config/initializers/blacklists.rb
@@ -2,4 +2,5 @@
 
 Rails.application.configure do
   config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
+  config.x.email_domains_whitelist = ENV.fetch('EMAIL_DOMAIN_WHITELIST') { '' }  
 end
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 320bd3144..d44845c6b 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1,14 +1,14 @@
 ---
 de:
   about:
-    about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Eine <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen, verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
+    about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
     get_started: Erste Schritte
     source_code: Quellcode
     terms: AGB
   accounts:
     follow: Folgen
-    followers: Folger
-    following: Folgt
+    followers: Follower
+    following: Gefolgt
     nothing_here: Hier gibt es nichts!
     people_followed_by: Nutzer, denen %{name} folgt
     people_who_follow: Nutzer, die %{name} folgen
@@ -27,7 +27,7 @@ de:
     reset_password: Passwort zurücksetzen
     set_new_password: Neues Passwort setzen
   authorize_follow:
-    error: Das entfernte Profil konnte nicht geladen werden
+    error: Das Profil konnte nicht geladen werden
     follow: Folgen
     prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
     title: "%{acct} folgen"
@@ -55,25 +55,25 @@ de:
   notification_mailer:
     favourite:
       body: 'Dein Beitrag wurde von %{name} favorisiert:'
-      subject: "%{name} hat deinen Beitrag favorisiert"
+      subject: "%{name} hat deinen Beitrag favorisiert."
     follow:
       body: "%{name} folgt dir jetzt!"
-      subject: "%{name} folgt dir nun"
+      subject: "%{name} folgt dir jetzt."
     follow_request:
       body: "%{name} möchte dir folgen:"
-      subject: "%{name} möchte dir folgen"
+      subject: "%{name} möchte dir folgen."
     mention:
       body: "%{name} hat dich erwähnt:"
-      subject: "%{name} hat dich erwähnt"
+      subject: "%{name} hat dich erwähnt."
     reblog:
       body: 'Dein Beitrag wurde von %{name} geteilt:'
-      subject: "%{name} teilte deinen Beitrag"
+      subject: "%{name} teilte deinen Beitrag."
   pagination:
     next: Vorwärts
     prev: Zurück
   remote_follow:
-    acct: Dein Nutzername@Domain, von dem du dieser Person folgen möchtest
-    missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden
+    acct: Dein Nutzername@Domain, von dem aus du dieser Person folgen möchtest.
+    missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
     proceed: Weiter
     prompt: 'Du wirst dieser Person folgen:'
   settings:
diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml
index 181502f9c..58bfaa3d6 100644
--- a/config/locales/devise.de.yml
+++ b/config/locales/devise.de.yml
@@ -2,59 +2,59 @@
 de:
   devise:
     confirmations:
-      confirmed: "Vielen Dank für Deine Registrierung. Bitte melde dich jetzt an."
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der Du Deine Registrierung bestätigen kannst."
-      send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Du Deine Registrierung bestätigen kannst."
+      confirmed: "Vielen Dank für deine Registrierung. Bitte melde dich jetzt an."
+      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail, mit der du deine Registrierung bestätigen kannst."
+      send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert, erhältst Du in wenigen Minuten eine E-Mail mit der du deine Registrierung bestätigen kannst."
     failure:
       already_authenticated: "Du bist bereits angemeldet."
       inactive: "Dein Account ist nicht aktiv."
       invalid: "Ungültige Anmeldedaten."
-      last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird"
+      last_attempt: "Du hast noch einen Versuch bevor dein Account gesperrt wird."
       locked: "Dein Account ist gesperrt."
       not_found_in_database: "E-Mail-Adresse oder Passwort ungültig."
-      timeout: "Deine Sitzung ist abgelaufen, bitte melde Dich erneut an."
-      unauthenticated: "Du musst Dich anmelden oder registrieren, bevor Du fortfahren kannst."
-      unconfirmed: "Du musst Deinen Account bestätigen, bevor Du fortfahren kannst."
+      timeout: "Deine Sitzung ist abgelaufen, bitte melde dich erneut an."
+      unauthenticated: "Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst."
+      unconfirmed: "Du musst deinen Account bestätigen, bevor du fortfahren kannst."
     mailer:
       confirmation_instructions:
-        subject: "Mastodon: Anleitung zur Bestätigung Deines Accounts"
+        subject: "Mastodon: Anleitung zur Bestätigung deines Accounts"
       password_change:
         subject: 'Mastodon: Passwort wurde geändert'
       reset_password_instructions:
-        subject: "Mastodon: Anleitung um Dein Passwort zurückzusetzen"
+        subject: "Mastodon: Anleitung um dein Passwort zurückzusetzen"
       unlock_instructions:
-        subject: "Mastodon: Anleitung um Deinen Account freizuschalten"
+        subject: "Mastodon: Anleitung um deinen Account freizuschalten"
     omniauth_callbacks:
-      failure: "Du konntest nicht Deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
-      success: "Du hast Dich erfolgreich mit Deinem %{kind}-Account angemeldet."
+      failure: "Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
+      success: "Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet."
     passwords:
-      no_token: "Du kannst diese Seite nur von dem Link aus einer E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast stelle bitte sicher, dass du die vollständige Adresse aufrufst."
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen kannst."
-      send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Dein Passwort zurücksetzen können."
+      no_token: "Du kannst diese Seite nur über den Link aus der E-Mail zum Passwort-Zurücksetzen aufrufen. Wenn du einen solchen Link aufgerufen hast, stelle bitte sicher, dass du die vollständige Adresse aufrufst."
+      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
+      send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du dein Passwort zurücksetzen kannst."
       updated: "Dein Passwort wurde geändert. Du bist jetzt angemeldet."
       updated_not_active: "Dein Passwort wurde geändert."
     registrations:
       destroyed: "Dein Account wurde gelöscht."
       signed_up: "Du hast dich erfolgreich registriert."
-      signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account inaktiv ist."
-      signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account gesperrt ist."
-      signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten Dich noch nicht anmelden, da Dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst."
-      update_needs_confirmation: "Deine Daten wurden aktualisiert, aber Du musst Deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der Du die Änderung Deiner E-Mail-Adresse abschließen kannst."
+      signed_up_but_inactive: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account inaktiv ist."
+      signed_up_but_locked: "Du hast dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account gesperrt ist."
+      signed_up_but_unconfirmed: "Du hast Dich erfolgreich registriert. Wir konnten dich noch nicht anmelden, da dein Account noch nicht bestätigt ist. Du erhältst in Kürze eine E-Mail mit der Anleitung, wie Du Deinen Account freischalten kannst."
+      update_needs_confirmation: "Deine Daten wurden aktualisiert, aber du musst deine neue E-Mail-Adresse bestätigen. Du erhälst in wenigen Minuten eine E-Mail, mit der du die Änderung deiner E-Mail-Adresse abschließen kannst."
       updated: "Deine Daten wurden aktualisiert."
     sessions:
       already_signed_out: "Erfolgreich abgemeldet."
       signed_in: "Erfolgreich angemeldet."
       signed_out: "Erfolgreich abgemeldet."
     unlocks:
-      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren können."
-      send_paranoid_instructions: "Falls Deine E-Mail-Adresse in unserer Datenbank existiert erhältst Du in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren kannst."
+      send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren können."
+      send_paranoid_instructions: "Falls deine E-Mail-Adresse in unserer Datenbank existiert erhältst du in wenigen Minuten eine E-Mail mit der Anleitung, wie du deinen Account entsperren kannst."
       unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet."
   errors:
     messages:
-      already_confirmed: "wurde bereits bestätigt"
-      confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an"
-      expired: "ist abgelaufen, bitte neu anfordern"
-      not_found: "nicht gefunden"
+      already_confirmed: "wurde bereits bestätigt."
+      confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an."
+      expired: "ist abgelaufen, bitte neu anfordern."
+      not_found: "wurde nicht gefunden."
       not_locked: "ist nicht gesperrt"
       not_saved:
         one: "Konnte %{resource} nicht speichern: ein Fehler."
diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml
index c94e5c095..be109df9c 100644
--- a/config/locales/doorkeeper.fr.yml
+++ b/config/locales/doorkeeper.fr.yml
@@ -62,7 +62,7 @@ fr:
       buttons:
         revoke: Annuler
       confirmations:
-        revoke: Êtes-vous certain?
+        revoke: Êtes-vous certain ?
       index:
         application: Application
         created_at: Créé le
@@ -72,19 +72,19 @@ fr:
     errors:
       messages:
         access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la demande.
-        credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué en raison de Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré.
+        credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré.
         invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse, ou d'une méthode d'authentification non prise en charge.
         invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la demande d'autorisation, ou a été émis à un autre client.
         invalid_redirect_uri: L'URL de redirection n'est pas valide.
         invalid_request: La demande manque un paramètre requis, inclut une valeur de paramètre non prise en charge, ou est autrement mal formée.
-        invalid_resource_owner: Les identifiants fournis du propriétaire de la ressource ne sont pas valides, ou le propriétaire de la ressource ne peut être trouvé
+        invalid_resource_owner: Les identifiants fournis par le propriétaire de la ressource ne sont pas valides, ou le propriétaire de la ressource ne peut être trouvé
         invalid_scope: La portée demandée n'est pas valide, est inconnue, ou est mal formée.
         invalid_token:
           expired: Le jeton d'accès a expiré
           revoked: Le jeton d'accès a été révoqué
           unknown: Le jeton d'accès n'est pas valide
-        resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué en raison de Doorkeeper.configure.resource_owner_authenticator n'est pas configuré.
-        server_error: Le serveur d'autorisation a rencontré une condition inattendue qui l'a empêché de remplir la demande.
+        resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n'est pas configuré.
+        server_error: Le serveur d'autorisation a rencontré une condition inattendue l'empêchant de remplir la demande.
         temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la demande à cause d'une surcharge ou d'un entretien temporaire du serveur.
         unauthorized_client: Le client n'est pas autorisé à effectuer cette demande à l'aide de cette méthode.
         unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation.
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 157f107a5..742219df9 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -5,6 +5,7 @@ en:
     about_this: About this instance
     apps: Apps
     business_email: 'Business e-mail:'
+    closed_registrations: Registrations are currently closed on this instance.
     contact: Contact
     description_headline: What is %{domain}?
     domain_count_after: other instances
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 758501403..e9989e383 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -5,6 +5,7 @@ fr:
     about_this: À propos de cette instance
     apps: Applications
     business_email: E-mail professionnel
+    closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. . 
     description_headline: Qu'est-ce que %{domain} ?
     domain_count_after: autres instances
     domain_count_before: Connectés à
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index df4f6ca00..dfc67fdfd 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -38,7 +38,7 @@ en:
         follow: Send e-mail when someone follows you
         follow_request: Send e-mail when someone requests to follow you
         mention: Send e-mail when someone mentions you
-        reblog: Send e-mail when someone reblogs your status
+        reblog: Send e-mail when someone boosts your status
     'no': 'No'
     required:
       mark: "*"
diff --git a/config/settings.yml b/config/settings.yml
index 6ae9217a4..ffcc1eaa7 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -5,6 +5,8 @@ defaults: &defaults
   site_extended_description: ''
   site_contact_username: ''
   site_contact_email: ''
+  open_registrations: true
+  closed_registrations_message: ''
   notification_emails:
     follow: false
     reblog: false
@@ -15,6 +17,7 @@ defaults: &defaults
   interactions:
     must_be_follower: false
     must_be_following: false
+
 development:
   <<: *defaults
 
diff --git a/docs/Running-Mastodon/Administration-guide.md b/docs/Running-Mastodon/Administration-guide.md
index af78f6235..dd69eb303 100644
--- a/docs/Running-Mastodon/Administration-guide.md
+++ b/docs/Running-Mastodon/Administration-guide.md
@@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
 
 The following rake task:
 
-    rails mastodon:make_admin USERNAME=alice
+    rake mastodon:make_admin USERNAME=alice
 
 Would turn the local user "alice" into an admin.
 
diff --git a/docs/Running-Mastodon/Heroku-guide.md b/docs/Running-Mastodon/Heroku-guide.md
index 799b8a64c..0de26230c 100644
--- a/docs/Running-Mastodon/Heroku-guide.md
+++ b/docs/Running-Mastodon/Heroku-guide.md
@@ -8,6 +8,8 @@ Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.co
 1. Click the above button.
 2. Fill in the options requested.
   * You can use a .herokuapp.com domain, which will be simple to set up, or you can use a custom domain. If you want a custom domain and HTTPS, you will need to upgrade to a paid plan (to use Heroku's SSL features), or set up [CloudFlare](https://cloudflare.com) who offer free "Flexible SSL" (note: CloudFlare have some undefined limits on WebSockets. So far, no one has reported hitting concurrent connection limits).
-  * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saaved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
+  * You will want Amazon S3 for file storage. The only exception is for development purposes, where you may not care if files are not saved. Follow a guide online for creating a free Amazon S3 bucket and Access Key, then enter the details.
   * If you want your Mastodon to be able to send emails, configure SMTP settings here (or later). Consider using [Mailgun](https://mailgun.com) or similar, who offer free plans that should suit your interests.
 3. Deploy! The app should be set up, with a working web interface and database. You can change settings and manage versions from the Heroku dashboard.
+
+You may need to use the `heroku` CLI application to run `USERNAME=yourUsername rails mastodon:make_admin` to make yourself an admin.
diff --git a/docs/Running-Mastodon/Production-guide.md b/docs/Running-Mastodon/Production-guide.md
index 469fefa94..b1f7bd35b 100644
--- a/docs/Running-Mastodon/Production-guide.md
+++ b/docs/Running-Mastodon/Production-guide.md
@@ -76,7 +76,7 @@ It is recommended to create a special user for mastodon on the server (you could
 ## General dependencies
 
     curl -sL https://deb.nodesource.com/setup_4.x | sudo bash -
-    sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs
+    sudo apt-get install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev nodejs file
     sudo npm install -g yarn
 
 ## Redis
@@ -112,7 +112,7 @@ Then once `rbenv` is ready, run `rbenv install 2.3.1` to install the Ruby versio
 You need the `git-core` package installed on your system. If it is so, from the `mastodon` user:
 
     cd ~
-    git clone https://github.com/Gargron/mastodon.git live
+    git clone https://github.com/tootsuite/mastodon.git live
     cd live
 
 Then you can proceed to install project dependencies:
@@ -132,7 +132,7 @@ Fill in the important data, like host/port of the redis database, host/port/user
 
     rake secret
 
-To get a random string. If you are setting up on one single server (most likely), then REDIS_HOST is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon").
+To get a random string. If you are setting up on one single server (most likely), then `REDIS_HOST` is localhost and `DB_HOST` is `/var/run/postgresql`, `DB_USER` is `mastodon` and `DB_NAME` is `mastodon_production` while `DB_PASS` is empty because this setup will use the ident authentication method (system user "mastodon" maps to postgres user "mastodon").
 
 ## Setup
 
@@ -221,7 +221,7 @@ I recommend creating a couple cronjobs for the following tasks:
 
 You may want to run `which bundle` first and copypaste that full path instead of simply `bundle` in the above commands because cronjobs usually don't have all the paths set. The time and intervals of when to run these jobs are up to you, but once every day should be enough for all.
 
-You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e mastodon` (outside of the mastodon user).
+You can edit the cronjob file for the `mastodon` user by running `sudo crontab -e -u mastodon` (outside of the mastodon user).
 
 ## Things to look out for when upgrading Mastodon
 
diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md
index 780977bd4..0cd3f18d6 100644
--- a/docs/Using-Mastodon/List-of-Mastodon-instances.md
+++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md
@@ -7,20 +7,38 @@ There is also a list at [instances.mastodon.xyz](https://instances.mastodon.xyz)
 | -------------|-------------|---|---|
 | [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|No|
 | [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|Yes|No|
-| [social.tchncs.de](https://social.tchncs.de)|N/A|Yes|No|
 | [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No|
 | [socially.constructed.space](https://socially.constructed.space) |Single user|No|No|
 | [epiktistes.com](https://epiktistes.com) |N/A|Yes|No|
-| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|Yes|No|
+| [fern.surgeplay.com](https://fern.surgeplay.com) |Federates everywhere, Minecraft-focused|Yes|No
+| [gay.crime.team](https://gay.crime.team) |the place for doin' gay crime online (please don't actually do crime here)|No|No|
 | [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
 | [memetastic.space](https://memetastic.space) |Memes|Yes|No|
-| [social.diskseven.com](https://social.diskseven.com) |Single user|No|No (DNS entry but no response)|
+| [social.diskseven.com](https://social.diskseven.com) |Single user|No|Yes|
 | [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
 | [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
-| [social.targaryen.house](https://social.targaryen.house) |N/A|Yes|No|
-| [social.mashek.net](https://social.mashek.net) |Themed and customised for Mashekstein Labs community. Selectively federates.|Yes|No|
+| [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes|
 | [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|Yes|Yes|
 | [social.imirhil.fr](https://social.imirhil.fr) |N/A|No|Yes|
 | [social.wxcafe.net](https://social.wxcafe.net) |Open registrations, federates everywhere, no moderation yet|Yes|Yes|
+| [octodon.social](https://octodon.social) |Open registrations, federates everywhere, cutest instance yet|Yes|Yes|
+| [mastodon.club](https://mastodon.club)|Open Registration, Open Federation, Mostly Canadians|Yes|No|
+| [hostux.social](https://hostux.social) |N/A|Yes|Yes|
+| [social.alex73630.xyz](https://social.alex73630.xyz) |Francophones|Yes|Yes|
+| [oc.todon.fr](https://oc.todon.fr) |Modérée et principalement francophone, pas de tolérances pour misogynie/LGBTphobies/validisme/etc.|Yes|Yes|
+| [maly.io](https://maly.io) |N/A|Yes|No|
+| [social.lou.lt](https://social.lou.lt) |N/A|Yes|No|
+| [mastodon.ninetailed.uk](https://mastodon.ninetailed.uk) |N/A|Yes|No|
+| [soc.louiz.org](https://soc.louiz.org) |"Coucou"|Yes|No|
+| [7nw.eu](https://7nw.eu) |N/A|Yes|No|
+| [mastodon.gougere.fr](https://mastodon.gougere.fr)|N/A|Yes|No|
+| [aleph.land](https://aleph.land)|N/A|Yes|No|
+| [share.elouworld.org](https://share.elouworld.org)|N/A|No|No|
+| [social.lkw.tf](https://social.lkw.tf)|N/A|No|No|
+| [manowar.social](https://manowar.social)|N/A|No|No|
+| [social.ballpointcarrot.net](https://social.ballpointcarrot.net)|N/A|No|No|
+| [social.nasqueron.org](https://social.nasqueron.org) |Dreamers, open source developers, free culture|Yes|Yes|
+| [status.dissidence.ovh](https://status.dissidence.ovh)|N/A|Yes|Yes|
+| [mastodon.cc](https://mastodon.cc)|Art|Yes|No|
 
 Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).
diff --git a/docs/Using-Mastodon/User-guide.md b/docs/Using-Mastodon/User-guide.md
index f78921c6f..f8018909a 100644
--- a/docs/Using-Mastodon/User-guide.md
+++ b/docs/Using-Mastodon/User-guide.md
@@ -26,17 +26,17 @@ Mastodon User's Guide
 
 ## Intro
 
-Mastodon is a social network application based on the GNU Social protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "instance"), and users of any instance can interact freely with those of other instances (called "federation"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities.
+Mastodon is a social network application based on the GNU Social protocol. It behaves a lot like other social networks, especially Twitter, with one key difference - it is open-source and anyone can start their own server (also called an "*instance*"), and users of any instance can interact freely with those of other instances (called "*federation*"). Thus, it is possible for small communities to set up their own servers to use amongst themselves while also allowing interaction with other communities.
 
 #### Decentralization and Federation
 
-Mastodon is a system decentralized through a concept called "federation" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail.
+Mastodon is a system decentralized through a concept called "*federation*" - rather than depending on a single person or organization to run its infrastructure, anyone can download and run the software and run their own server. Federation means different Mastodon servers can interact with each other seamlessly, similar to e.g. e-mail.
 
 As such, anyone can download Mastodon and e.g. run it for a small community of people, but any user registered on that instance can follow and send and read posts from other Mastodon instances (as well as servers running other GNU Social-compatible services). This means that not only is users' data not inherently owned by a company with an interest in selling it to advertisers, but also that if any given server shuts down its users can set up a new one or migrate to another instance, rather than the entire service being lost.
 
 Within each Mastodon instance, usernames just appear as `@username`, similar to other services such as Twitter. Users from other instances appear, and can be searched for and followed, as `@user@servername.ext` - so e.g. `@gargron` on the `mastodon.social` instance can be followed from other instances as `@gargron@mastodon.social`).
 
-Posts from users on external instances are "federated" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section.
+Posts from users on external instances are "*federated*" into the local one, i.e. if `user1@mastodon1` follows `user2@gnusocial2`, any posts `user2@gnusocial2` makes appear in both `user1@mastodon`'s Home feed and the public timeline on the `mastodon1` server. Mastodon server administrators have some control over this and can exclude users' posts from appearing on the public timeline; post privacy settings from users on Mastodon instances also affect this, see below in the [Toot Privacy](User-guide.md#toot-privacy) section.
 
 ## Getting Started
 
diff --git a/spec/fabricators/account_fabricator.rb b/spec/fabricators/account_fabricator.rb
index 3a7c00bf5..567de05f4 100644
--- a/spec/fabricators/account_fabricator.rb
+++ b/spec/fabricators/account_fabricator.rb
@@ -1,3 +1,3 @@
 Fabricator(:account) do
-  username "alice"
+  username { Faker::Internet.user_name(nil, %w(_)) }
 end
diff --git a/spec/fabricators/block_fabricator.rb b/spec/fabricators/block_fabricator.rb
index 9a5a6808f..379931ba6 100644
--- a/spec/fabricators/block_fabricator.rb
+++ b/spec/fabricators/block_fabricator.rb
@@ -1,3 +1,4 @@
 Fabricator(:block) do
-
+  account
+  target_account { Fabricate(:account) }
 end
diff --git a/spec/fabricators/follow_fabricator.rb b/spec/fabricators/follow_fabricator.rb
index 9d9d06f12..9b25dc547 100644
--- a/spec/fabricators/follow_fabricator.rb
+++ b/spec/fabricators/follow_fabricator.rb
@@ -1,3 +1,4 @@
 Fabricator(:follow) do
-
+  account
+  target_account { Fabricate(:account) }
 end
diff --git a/spec/fabricators/follow_request_fabricator.rb b/spec/fabricators/follow_request_fabricator.rb
index 9c3733cef..78a057919 100644
--- a/spec/fabricators/follow_request_fabricator.rb
+++ b/spec/fabricators/follow_request_fabricator.rb
@@ -1,3 +1,4 @@
 Fabricator(:follow_request) do
-
+  account
+  target_account { Fabricate(:account) }
 end
diff --git a/spec/fabricators/mention_fabricator.rb b/spec/fabricators/mention_fabricator.rb
new file mode 100644
index 000000000..cb5fe4299
--- /dev/null
+++ b/spec/fabricators/mention_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:mention) do
+  account
+  status
+end
diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb
index c08559137..16b3b1f6f 100644
--- a/spec/fabricators/user_fabricator.rb
+++ b/spec/fabricators/user_fabricator.rb
@@ -1,6 +1,6 @@
 Fabricator(:user) do
   account
-  email        "alice@example.com"
+  email        { Faker::Internet.email }
   password     "123456789"
   confirmed_at { Time.now }
 end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 91c8d75cf..d7f59adb8 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -209,4 +209,73 @@ RSpec.describe Account, type: :model do
       expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
     end
   end
+
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      account = Fabricate.build(:account)
+      account.valid?
+      expect(account).to be_valid
+    end
+
+    it 'is invalid without a username' do
+      account = Fabricate.build(:account, username: nil)
+      account.valid?
+      expect(account).to model_have_error_on_field(:username)
+    end
+
+    it 'is invalid if the username already exists' do
+      account_1 = Fabricate(:account, username: 'the_doctor')
+      account_2 = Fabricate.build(:account, username: 'the_doctor')
+      account_2.valid?
+      expect(account_2).to model_have_error_on_field(:username)
+    end
+
+    context 'when is local' do
+      it 'is invalid if the username doesn\'t only contains letters, numbers and underscores' do
+        account = Fabricate.build(:account, username: 'the-doctor')
+        account.valid?
+        expect(account).to model_have_error_on_field(:username)
+      end
+
+      it 'is invalid if the username is longer then 30 characters' do
+        account = Fabricate.build(:account, username: Faker::Lorem.characters(31))
+        account.valid?
+        expect(account).to model_have_error_on_field(:username)
+      end
+    end
+  end
+
+  describe 'scopes' do
+    describe 'remote' do
+      it 'returns an array of accounts who have a domain' do
+        account_1 = Fabricate(:account, domain: nil)
+        account_2 = Fabricate(:account, domain: 'example.com')
+        expect(Account.remote).to match_array([account_2])
+      end
+    end
+
+    describe 'local' do
+      it 'returns an array of accounts who do not have a domain' do
+        account_1 = Fabricate(:account, domain: nil)
+        account_2 = Fabricate(:account, domain: 'example.com')
+        expect(Account.local).to match_array([account_1])
+      end
+    end
+
+    describe 'silenced' do
+      it 'returns an array of accounts who are silenced' do
+        account_1 = Fabricate(:account, silenced: true)
+        account_2 = Fabricate(:account, silenced: false)
+        expect(Account.silenced).to match_array([account_1])
+      end
+    end
+
+    describe 'suspended' do
+      it 'returns an array of accounts who are suspended' do
+        account_1 = Fabricate(:account, suspended: true)
+        account_2 = Fabricate(:account, suspended: false)
+        expect(Account.suspended).to match_array([account_1])
+      end
+    end
+  end
 end
diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb
index 6862de6fc..cabb41c3e 100644
--- a/spec/models/block_spec.rb
+++ b/spec/models/block_spec.rb
@@ -1,5 +1,22 @@
 require 'rails_helper'
 
 RSpec.describe Block, type: :model do
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      block = Fabricate.build(:block)
+      expect(block).to be_valid
+    end
 
+    it 'is invalid without an account' do
+      block = Fabricate.build(:block, account: nil)
+      block.valid?
+      expect(block).to model_have_error_on_field(:account)
+    end
+
+    it 'is invalid without a target_account' do
+      block = Fabricate.build(:block, target_account: nil)
+      block.valid?
+      expect(block).to model_have_error_on_field(:target_account)
+    end
+  end
 end
diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb
index ad5403110..b19c8083e 100644
--- a/spec/models/domain_block_spec.rb
+++ b/spec/models/domain_block_spec.rb
@@ -1,5 +1,23 @@
 require 'rails_helper'
 
 RSpec.describe DomainBlock, type: :model do
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      domain_block = Fabricate.build(:domain_block)
+      expect(domain_block).to be_valid
+    end
 
+    it 'is invalid without a domain' do
+      domain_block = Fabricate.build(:domain_block, domain: nil)
+      domain_block.valid?
+      expect(domain_block).to model_have_error_on_field(:domain)
+    end
+
+    it 'is invalid if the domain already exists' do
+      domain_block_1 = Fabricate(:domain_block, domain: 'dalek.com')
+      domain_block_2 = Fabricate.build(:domain_block, domain: 'dalek.com')
+      domain_block_2.valid?
+      expect(domain_block_2).to model_have_error_on_field(:domain)
+    end
+  end
 end
diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb
index f2ec642d8..cc6f8ee62 100644
--- a/spec/models/follow_request_spec.rb
+++ b/spec/models/follow_request_spec.rb
@@ -3,4 +3,23 @@ require 'rails_helper'
 RSpec.describe FollowRequest, type: :model do
   describe '#authorize!'
   describe '#reject!'
+
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      follow_request = Fabricate.build(:follow_request)
+      expect(follow_request).to be_valid
+    end
+
+    it 'is invalid without an account' do
+      follow_request = Fabricate.build(:follow_request, account: nil)
+      follow_request.valid?
+      expect(follow_request).to model_have_error_on_field(:account)
+    end
+
+    it 'is invalid without a target account' do
+      follow_request = Fabricate.build(:follow_request, target_account: nil)
+      follow_request.valid?
+      expect(follow_request).to model_have_error_on_field(:target_account)      
+    end
+  end
 end
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index eb21f3e18..0fae25352 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -5,4 +5,23 @@ RSpec.describe Follow, type: :model do
   let(:bob)   { Fabricate(:account, username: 'bob') }
 
   subject { Follow.new(account: alice, target_account: bob) }
+
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      follow = Fabricate.build(:follow)
+      expect(follow).to be_valid
+    end
+
+    it 'is invalid without an account' do
+      follow = Fabricate.build(:follow, account: nil)
+      follow.valid?
+      expect(follow).to model_have_error_on_field(:account)
+    end
+
+    it 'is invalid without a target_account' do
+      follow = Fabricate.build(:follow, target_account: nil)
+      follow.valid?
+      expect(follow).to model_have_error_on_field(:target_account)
+    end
+  end
 end
diff --git a/spec/models/mention_spec.rb b/spec/models/mention_spec.rb
index 5c91fda02..dbcf6a32c 100644
--- a/spec/models/mention_spec.rb
+++ b/spec/models/mention_spec.rb
@@ -1,5 +1,22 @@
 require 'rails_helper'
 
 RSpec.describe Mention, type: :model do
+  describe 'validations' do
+    it 'has a valid fabricator' do
+      mention = Fabricate.build(:mention)
+      expect(mention).to be_valid
+    end
 
+    it 'is invalid without an account' do
+      mention = Fabricate.build(:mention, account: nil)
+      mention.valid?
+      expect(mention).to model_have_error_on_field(:account)
+    end
+
+    it 'is invalid without a status' do
+      mention = Fabricate.build(:mention, status: nil)
+      mention.valid?
+      expect(mention).to model_have_error_on_field(:status)
+    end
+  end
 end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 64de06749..eb2a4aaea 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,5 +1,88 @@
 require 'rails_helper'
 
 RSpec.describe User, type: :model do
+  describe 'validations' do
+    it 'is invalid without an account' do
+      user = Fabricate.build(:user, account: nil)
+      user.valid?
+      expect(user).to model_have_error_on_field(:account)
+    end
 
+    it 'is invalid without a valid locale' do
+      user = Fabricate.build(:user, locale: 'toto')
+      user.valid?
+      expect(user).to model_have_error_on_field(:locale)
+    end
+
+    it 'is invalid without a valid email' do
+      user = Fabricate.build(:user, email: 'john@')
+      user.valid?
+      expect(user).to model_have_error_on_field(:email)
+    end
+  end
+
+  describe 'scopes' do
+    describe 'recent' do
+      it 'returns an array of recent users ordered by id' do
+        user_1 = Fabricate(:user)
+        user_2 = Fabricate(:user)
+        expect(User.recent).to match_array([user_2, user_1])
+      end
+    end
+
+    describe 'admins' do
+      it 'returns an array of users who are admin' do
+        user_1 = Fabricate(:user, admin: false)
+        user_2 = Fabricate(:user, admin: true)
+        expect(User.admins).to match_array([user_2])
+      end
+    end
+
+    describe 'confirmed' do
+      it 'returns an array of users who are confirmed' do
+        user_1 = Fabricate(:user, confirmed_at: nil)
+        user_2 = Fabricate(:user, confirmed_at: Time.now)
+        expect(User.confirmed).to match_array([user_2])
+      end
+    end
+  end
+
+  let(:account) { Fabricate(:account, username: 'alice') }
+  let(:password) { 'abcd1234' }
+
+  describe 'blacklist' do
+    it 'should allow a non-blacklisted user to be created' do
+      user = User.new(email: 'foo@example.com', account: account, password: password)
+
+      expect(user.valid?).to be_truthy
+    end
+
+    it 'should not allow a blacklisted user to be created' do
+      user = User.new(email: 'foo@mvrht.com', account: account, password: password)
+
+      expect(user.valid?).to be_falsey
+    end
+  end
+
+  describe 'whitelist' do
+    around(:each) do |example|
+      old_whitelist = Rails.configuration.x.email_whitelist
+
+      Rails.configuration.x.email_domains_whitelist = 'mastodon.space'
+
+      example.run
+
+      Rails.configuration.x.email_domains_whitelist = old_whitelist
+    end
+
+    it 'should not allow a user to be created unless they are whitelisted' do
+      user = User.new(email: 'foo@example.com', account: account, password: password)
+      expect(user.valid?).to be_falsey
+    end
+
+    it 'should allow a user to be created if they are whitelisted' do
+      user = User.new(email: 'foo@mastodon.space', account: account, password: password)
+      expect(user.valid?).to be_truthy
+    end
+  end
 end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 977c7bdc0..faac96982 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -8,6 +8,8 @@ require 'rspec/rails'
 require 'webmock/rspec'
 require 'paperclip/matchers'
 
+Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
+
 ActiveRecord::Migration.maintain_test_schema!
 WebMock.disable_net_connect!(allow: 'localhost:7575')
 Sidekiq::Testing.inline!
diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb
index 07f8c2dc8..6ee225c4c 100644
--- a/spec/services/fan_out_on_write_service_spec.rb
+++ b/spec/services/fan_out_on_write_service_spec.rb
@@ -23,6 +23,7 @@ RSpec.describe FanOutOnWriteService do
   end
 
   it 'delivers status to local followers' do
+    pending 'some sort of problem in test environment causes this to sometimes fail'
     expect(Feed.new(:home, follower).get(10).map(&:id)).to include status.id
   end
 
diff --git a/spec/support/matchers/model/model_have_error_on_field.rb b/spec/support/matchers/model/model_have_error_on_field.rb
new file mode 100644
index 000000000..5d5fe1c7b
--- /dev/null
+++ b/spec/support/matchers/model/model_have_error_on_field.rb
@@ -0,0 +1,15 @@
+RSpec::Matchers.define :model_have_error_on_field do |expected|
+  match do |record|
+    if record.errors.empty?
+      record.valid?
+    end
+
+    record.errors.has_key?(expected)
+  end
+
+  failure_message do |record|
+    keys = record.errors.keys
+    
+    "expect record.errors(#{keys}) to include #{expected}"
+  end
+end