Merge branch 'master' into master

This commit is contained in:
JantsoP 2017-04-05 10:07:17 +02:00 committed by GitHub
commit 20b53e6add
57 changed files with 570 additions and 164 deletions

View file

@ -22,6 +22,8 @@ OTP_SECRET=
# SINGLE_USER_MODE=true # SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains # Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc # 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 # E-mail configuration
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org

View file

@ -38,7 +38,7 @@ gem 'rqrcode'
gem 'twitter-text' gem 'twitter-text'
gem 'oj' gem 'oj'
gem 'hiredis' gem 'hiredis'
gem 'redis', '~>3.2' gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
gem 'fast_blank' gem 'fast_blank'
gem 'htmlentities' gem 'htmlentities'
gem 'simple_form' gem 'simple_form'
@ -46,6 +46,7 @@ gem 'will_paginate'
gem 'rack-attack' gem 'rack-attack'
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'sidekiq' gem 'sidekiq'
gem 'sidekiq-unique-jobs'
gem 'rails-settings-cached' gem 'rails-settings-cached'
gem 'simple-navigation' gem 'simple-navigation'
gem 'statsd-instrument' gem 'statsd-instrument'
@ -66,9 +67,10 @@ group :development, :test do
end end
group :test do group :test do
gem 'faker'
gem 'rspec-sidekiq'
gem 'simplecov', require: false gem 'simplecov', require: false
gem 'webmock' gem 'webmock'
gem 'rspec-sidekiq'
end end
group :development do group :development do

View file

@ -149,6 +149,8 @@ GEM
erubis (2.7.0) erubis (2.7.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.15.2) fabrication (2.15.2)
faker (1.6.6)
i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
font-awesome-rails (4.6.3.1) font-awesome-rails (4.6.3.1)
railties (>= 3.2, < 5.1) railties (>= 3.2, < 5.1)
@ -387,6 +389,9 @@ GEM
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1) redis (~> 3.2, >= 3.2.1)
sidekiq-unique-jobs (4.0.18)
sidekiq (>= 2.6)
thor
simple-navigation (4.0.3) simple-navigation (4.0.3)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (3.2.1) simple_form (3.2.1)
@ -467,6 +472,7 @@ DEPENDENCIES
doorkeeper doorkeeper
dotenv-rails dotenv-rails
fabrication fabrication
faker
fast_blank fast_blank
font-awesome-rails font-awesome-rails
fuubar fuubar
@ -510,6 +516,7 @@ DEPENDENCIES
sass-rails (~> 5.0) sass-rails (~> 5.0)
sdoc (~> 0.4.0) sdoc (~> 0.4.0)
sidekiq sidekiq
sidekiq-unique-jobs
simple-navigation simple-navigation
simple_form simple_form
simplecov simplecov

5
ISSUE_TEMPLATE.md Normal file
View file

@ -0,0 +1,5 @@
[Issue text goes here].
* * * *
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.

View file

@ -9,7 +9,7 @@ const iconStyle = {
}; };
const ClearColumnButton = ({ onClick }) => ( 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' /> <i className='fa fa-trash' />
</div> </div>
); );

View file

@ -319,7 +319,7 @@
} }
} }
.simple_form { .simple_form, .closed-registrations-message {
width: 300px; width: 300px;
flex: 0 0 auto; flex: 0 0 auto;
background: rgba(darken($color1, 7%), 0.5); 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;
}

View file

@ -34,6 +34,7 @@
text-align: center; text-align: center;
position: relative; position: relative;
z-index: 2; z-index: 2;
text-shadow: 0 0 2px $color8;
small { small {
display: block; display: block;
@ -128,6 +129,7 @@
text-transform: uppercase; text-transform: uppercase;
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
text-shadow: 0 0 2px $color8;
} }
.counter-number { .counter-number {
@ -385,5 +387,6 @@
.account__header__content { .account__header__content {
font-size: 14px; font-size: 14px;
color: $color1; color: $color1;
text-shadow: 0 0 2px $color8;
} }
} }

View file

@ -4,7 +4,9 @@ class AboutController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
def index 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 = User.new
@user.build_account @user.build_account

View file

@ -11,9 +11,13 @@ class Admin::SettingsController < ApplicationController
def update def update
@setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
value = settings_params[:value]
if @setting.value != params[:setting][:value] # Special cases
@setting.value = params[:setting][:value] value = value == 'true' if @setting.var == 'open_registrations'
if @setting.value != value
@setting.value = value
@setting.save @setting.save
end end
@ -22,4 +26,10 @@ class Admin::SettingsController < ApplicationController
format.json { respond_with_bip(@setting) } format.json { respond_with_bip(@setting) }
end end
end end
private
def settings_params
params.require(:setting).permit(:value)
end
end end

View file

@ -3,7 +3,7 @@
class Auth::RegistrationsController < Devise::RegistrationsController class Auth::RegistrationsController < Devise::RegistrationsController
layout :determine_layout 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] before_action :configure_sign_up_params, only: [:create]
protected protected
@ -27,12 +27,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
new_user_session_path new_user_session_path
end end
def check_single_user_mode def check_enabled_registrations
redirect_to root_path if Rails.configuration.x.single_user_mode redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations
end end
private private
def determine_layout def determine_layout
%w(edit update).include?(action_name) ? 'admin' : 'auth' %w(edit update).include?(action_name) ? 'admin' : 'auth'
end end

View file

@ -8,6 +8,7 @@ class RemoteFollowController < ApplicationController
def new def new
@remote_follow = RemoteFollow.new @remote_follow = RemoteFollow.new
@remote_follow.acct = session[:remote_follow] if session.key?(:remote_follow)
end end
def create def create
@ -22,6 +23,8 @@ class RemoteFollowController < ApplicationController
render(:new) && return render(:new) && return
end 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 redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
else else
render :new render :new

View file

@ -2,17 +2,30 @@
class EmailValidator < ActiveModel::EachValidator class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) 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) record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value)
end end
private private
def blocked_email?(value) 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('.', '\.') domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
regexp = Regexp.new("@(.+\\.)?(#{domains})", true) regexp = Regexp.new("@(.+\\.)?(#{domains})", true)
value =~ regexp value =~ regexp
end 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 end

View file

@ -5,17 +5,17 @@ require 'singleton'
class FeedManager class FeedManager
include Singleton include Singleton
MAX_ITEMS = 800 MAX_ITEMS = 400
def key(type, id) def key(type, id)
"feed:#{type}:#{id}" "feed:#{type}:#{id}"
end end
def filter?(timeline_type, status, receiver) def filter?(timeline_type, status, receiver_id)
if timeline_type == :home if timeline_type == :home
filter_from_home?(status, receiver) filter_from_home?(status, receiver_id)
elsif timeline_type == :mentions elsif timeline_type == :mentions
filter_from_mentions?(status, receiver) filter_from_mentions?(status, receiver_id)
else else
false false
end end
@ -50,10 +50,18 @@ class FeedManager
def merge_into_timeline(from_account, into_account) def merge_into_timeline(from_account, into_account)
timeline_key = key(:home, into_account.id) 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| if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
next if status.direct_visibility? || filter?(:home, status, into_account) oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
redis.zadd(timeline_key, status.id, status.id) 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 end
trim(:home, into_account.id) trim(:home, into_account.id)
@ -61,31 +69,20 @@ class FeedManager
def unmerge_from_timeline(from_account, into_account) def unmerge_from_timeline(from_account, into_account)
timeline_key = key(:home, into_account.id) timeline_key = key(:home, into_account.id)
oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
from_account.statuses.select('id').find_each do |status| from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
redis.zrem(timeline_key, status.id) redis.pipelined do
redis.zremrangebyscore(timeline_key, status.id, status.id) statuses.each do |status|
redis.zrem(timeline_key, status.id)
redis.zremrangebyscore(timeline_key, status.id, status.id)
end
end
end end
end end
def inline_render(target_account, template, object) def inline_render(target_account, template, object)
rabl_scope = Class.new do Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: InlineRablScope.new(target_account)).render
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
end end
private private
@ -94,37 +91,39 @@ class FeedManager
Redis.current Redis.current
end end
def filter_from_home?(status, receiver) def filter_from_home?(status, receiver_id)
return true if receiver.muting?(status.account) return true if status.reply? && status.in_reply_to_id.nil?
should_filter = false check_for_mutes = [status.account_id]
check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
if status.reply? && status.in_reply_to_id.nil? return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
should_filter = true
elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply check_for_blocks = status.mentions.map(&:account_id)
should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
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 true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
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.in_reply_to_account_id.nil? # Filter out if it's a reply
should_filter ||= receiver.muting?(status.reblog.account) # or muting that person 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 ||= status.reblog.account.blocking?(receiver) # or if the author of the reblogged status is blocking me 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 end
should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked false
should_filter
end end
def filter_from_mentions?(status, receiver) def filter_from_mentions?(status, receiver_id)
should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself check_for_blocks = [status.account_id]
should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked check_for_blocks.concat(status.mentions.pluck(:account_id))
should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
should_filter ||= (status.account.silenced? && !receiver.following?(status.account)) # of if the account is silenced and I'm not following them
if status.reply? && !status.in_reply_to_account_id.nil? # or it's a reply should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself
should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked 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
end 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 should_filter
end end

View file

@ -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

View file

@ -3,9 +3,8 @@
class Block < ApplicationRecord class Block < ApplicationRecord
include Paginable include Paginable
belongs_to :account belongs_to :account, required: true
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account', required: true
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
end end

View file

@ -3,11 +3,14 @@
class Follow < ApplicationRecord class Follow < ApplicationRecord
include Paginable include Paginable
belongs_to :account, counter_cache: :following_count belongs_to :account, counter_cache: :following_count, required: true
belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count
belongs_to :target_account,
class_name: 'Account',
counter_cache: :followers_count,
required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
end end

View file

@ -3,12 +3,11 @@
class FollowRequest < ApplicationRecord class FollowRequest < ApplicationRecord
include Paginable include Paginable
belongs_to :account belongs_to :account, required: true
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account', required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :target_account, presence: true
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
def authorize! def authorize!

View file

@ -1,11 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Mention < ApplicationRecord class Mention < ApplicationRecord
belongs_to :account, inverse_of: :mentions belongs_to :account, inverse_of: :mentions, required: true
belongs_to :status belongs_to :status, required: true
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
validates :account, :status, presence: true
validates :account, uniqueness: { scope: :status } validates :account, uniqueness: { scope: :status }
end end

View file

@ -33,9 +33,8 @@ class FanOutOnWriteService < BaseService
def deliver_to_followers(status) def deliver_to_followers(status)
Rails.logger.debug "Delivering status #{status.id} to followers" 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| status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).find_each do |follower|
next if FeedManager.instance.filter?(:home, status, follower) FeedInsertWorker.perform_async(status.id, follower.id)
FeedManager.instance.push(:home, follower, status)
end end
end end
@ -44,7 +43,7 @@ class FanOutOnWriteService < BaseService
status.mentions.includes(:account).each do |mention| status.mentions.includes(:account).each do |mention|
mentioned_account = mention.account 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) FeedManager.instance.push(:home, mentioned_account, status)
end end
end end
@ -54,9 +53,9 @@ class FanOutOnWriteService < BaseService
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status) payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status)
status.tags.find_each do |tag| status.tags.pluck(:name).each do |hashtag|
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'update', payload: payload) FeedManager.instance.broadcast("hashtag:#{hashtag}", event: 'update', payload: payload)
FeedManager.instance.broadcast("hashtag:#{tag.name}:local", event: 'update', payload: payload) if status.account.local? FeedManager.instance.broadcast("hashtag:#{hashtag}:local", event: 'update', payload: payload) if status.account.local?
end end
end end

View file

@ -17,7 +17,7 @@ class NotifyService < BaseService
private private
def blocked_mention? def blocked_mention?
FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id)
end end
def blocked_favourite? def blocked_favourite?

View file

@ -5,9 +5,11 @@ class PrecomputeFeedService < BaseService
# @param [Symbol] type :home or :mentions # @param [Symbol] type :home or :mentions
# @param [Account] account # @param [Account] account
def call(_, account) def call(_, account)
Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| redis.pipelined do
next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account) Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status|
redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) 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
end end

View file

@ -24,21 +24,34 @@
.screenshot-with-signup .screenshot-with-signup
.mascot= image_tag 'fluffy-elephant-friend.png' .mascot= image_tag 'fluffy-elephant-friend.png'
= simple_form_for(@user, url: user_registration_path) do |f| - if @open_registrations
= f.simple_fields_for :account do |ff| = simple_form_for(@user, url: user_registration_path) do |f|
= 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.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 :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, 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 :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 .actions
= f.button :button, t('about.get_started'), type: :submit = f.button :button, t('about.get_started'), type: :submit
.info .info
= link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
· ·
= link_to t('about.about_this'), about_more_path = 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') %h3= t('about.features_headline')

View file

@ -38,3 +38,15 @@
%br/ %br/
You can use HTML tags You can use HTML tags
%td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) %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'])

View file

@ -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

View file

@ -3,7 +3,7 @@
class ProcessingWorker class ProcessingWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull', backtrace: true sidekiq_options backtrace: true
def perform(account_id, body) def perform(account_id, body)
ProcessFeedService.new.call(body, Account.find(account_id)) ProcessFeedService.new.call(body, Account.find(account_id))

View file

@ -22,6 +22,7 @@ class Pubsubhubbub::DeliveryWorker
.headers(headers) .headers(headers)
.post(subscription.callback_url, body: payload) .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 raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300
subscription.touch(:last_successful_delivery_at) subscription.touch(:last_successful_delivery_at)

View file

@ -3,7 +3,7 @@
class RegenerationWorker class RegenerationWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull', backtrace: true sidekiq_options queue: 'pull', backtrace: true, unique: :until_executed
def perform(account_id, _ = :home) def perform(account_id, _ = :home)
PrecomputeFeedService.new.call(:home, Account.find(account_id)) PrecomputeFeedService.new.call(:home, Account.find(account_id))

View file

@ -3,7 +3,7 @@
class SalmonWorker class SalmonWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull', backtrace: true sidekiq_options backtrace: true
def perform(account_id, body) def perform(account_id, body)
ProcessInteractionService.new.call(body, Account.find(account_id)) ProcessInteractionService.new.call(body, Account.find(account_id))

View file

@ -2,4 +2,5 @@
Rails.application.configure do Rails.application.configure do
config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' } config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
config.x.email_domains_whitelist = ENV.fetch('EMAIL_DOMAIN_WHITELIST') { '' }
end end

View file

@ -1,14 +1,14 @@
--- ---
de: de:
about: 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 get_started: Erste Schritte
source_code: Quellcode source_code: Quellcode
terms: AGB terms: AGB
accounts: accounts:
follow: Folgen follow: Folgen
followers: Folger followers: Follower
following: Folgt following: Gefolgt
nothing_here: Hier gibt es nichts! nothing_here: Hier gibt es nichts!
people_followed_by: Nutzer, denen %{name} folgt people_followed_by: Nutzer, denen %{name} folgt
people_who_follow: Nutzer, die %{name} folgen people_who_follow: Nutzer, die %{name} folgen
@ -27,7 +27,7 @@ de:
reset_password: Passwort zurücksetzen reset_password: Passwort zurücksetzen
set_new_password: Neues Passwort setzen set_new_password: Neues Passwort setzen
authorize_follow: authorize_follow:
error: Das entfernte Profil konnte nicht geladen werden error: Das Profil konnte nicht geladen werden
follow: Folgen follow: Folgen
prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:' prompt_html: 'Du (<strong>%{self}</strong>) möchtest dieser Person folgen:'
title: "%{acct} folgen" title: "%{acct} folgen"
@ -55,25 +55,25 @@ de:
notification_mailer: notification_mailer:
favourite: favourite:
body: 'Dein Beitrag wurde von %{name} favorisiert:' body: 'Dein Beitrag wurde von %{name} favorisiert:'
subject: "%{name} hat deinen Beitrag favorisiert" subject: "%{name} hat deinen Beitrag favorisiert."
follow: follow:
body: "%{name} folgt dir jetzt!" body: "%{name} folgt dir jetzt!"
subject: "%{name} folgt dir nun" subject: "%{name} folgt dir jetzt."
follow_request: follow_request:
body: "%{name} möchte dir folgen:" body: "%{name} möchte dir folgen:"
subject: "%{name} möchte dir folgen" subject: "%{name} möchte dir folgen."
mention: mention:
body: "%{name} hat dich erwähnt:" body: "%{name} hat dich erwähnt:"
subject: "%{name} hat dich erwähnt" subject: "%{name} hat dich erwähnt."
reblog: reblog:
body: 'Dein Beitrag wurde von %{name} geteilt:' body: 'Dein Beitrag wurde von %{name} geteilt:'
subject: "%{name} teilte deinen Beitrag" subject: "%{name} teilte deinen Beitrag."
pagination: pagination:
next: Vorwärts next: Vorwärts
prev: Zurück prev: Zurück
remote_follow: remote_follow:
acct: Dein Nutzername@Domain, von dem du dieser Person folgen möchtest 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 missing_resource: Die erforderliche Weiterleitungs-URL konnte leider in deinem Profil nicht gefunden werden.
proceed: Weiter proceed: Weiter
prompt: 'Du wirst dieser Person folgen:' prompt: 'Du wirst dieser Person folgen:'
settings: settings:

View file

@ -2,59 +2,59 @@
de: de:
devise: devise:
confirmations: confirmations:
confirmed: "Vielen Dank für Deine Registrierung. Bitte melde dich jetzt an." 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_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." 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: failure:
already_authenticated: "Du bist bereits angemeldet." already_authenticated: "Du bist bereits angemeldet."
inactive: "Dein Account ist nicht aktiv." inactive: "Dein Account ist nicht aktiv."
invalid: "Ungültige Anmeldedaten." 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." locked: "Dein Account ist gesperrt."
not_found_in_database: "E-Mail-Adresse oder Passwort ungültig." not_found_in_database: "E-Mail-Adresse oder Passwort ungültig."
timeout: "Deine Sitzung ist abgelaufen, bitte melde Dich erneut an." timeout: "Deine Sitzung ist abgelaufen, bitte melde dich erneut an."
unauthenticated: "Du musst Dich anmelden oder registrieren, bevor Du fortfahren kannst." unauthenticated: "Du musst Dich anmelden oder registrieren, bevor du fortfahren kannst."
unconfirmed: "Du musst Deinen Account bestätigen, bevor Du fortfahren kannst." unconfirmed: "Du musst deinen Account bestätigen, bevor du fortfahren kannst."
mailer: mailer:
confirmation_instructions: confirmation_instructions:
subject: "Mastodon: Anleitung zur Bestätigung Deines Accounts" subject: "Mastodon: Anleitung zur Bestätigung deines Accounts"
password_change: password_change:
subject: 'Mastodon: Passwort wurde geändert' subject: 'Mastodon: Passwort wurde geändert'
reset_password_instructions: reset_password_instructions:
subject: "Mastodon: Anleitung um Dein Passwort zurückzusetzen" subject: "Mastodon: Anleitung um dein Passwort zurückzusetzen"
unlock_instructions: unlock_instructions:
subject: "Mastodon: Anleitung um Deinen Account freizuschalten" subject: "Mastodon: Anleitung um deinen Account freizuschalten"
omniauth_callbacks: omniauth_callbacks:
failure: "Du konntest nicht Deinem %{kind}-Account angemeldet werden, weil '%{reason}'." failure: "Du konntest nicht mit deinem %{kind}-Account angemeldet werden, weil '%{reason}'."
success: "Du hast Dich erfolgreich mit Deinem %{kind}-Account angemeldet." success: "Du hast dich erfolgreich mit Deinem %{kind}-Account angemeldet."
passwords: 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." 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_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." 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: "Dein Passwort wurde geändert. Du bist jetzt angemeldet."
updated_not_active: "Dein Passwort wurde geändert." updated_not_active: "Dein Passwort wurde geändert."
registrations: registrations:
destroyed: "Dein Account wurde gelöscht." destroyed: "Dein Account wurde gelöscht."
signed_up: "Du hast dich erfolgreich registriert." 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_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_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." 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." 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." updated: "Deine Daten wurden aktualisiert."
sessions: sessions:
already_signed_out: "Erfolgreich abgemeldet." already_signed_out: "Erfolgreich abgemeldet."
signed_in: "Erfolgreich angemeldet." signed_in: "Erfolgreich angemeldet."
signed_out: "Erfolgreich abgemeldet." signed_out: "Erfolgreich abgemeldet."
unlocks: unlocks:
send_instructions: "Du erhältst in wenigen Minuten eine E-Mail mit der Anleitung, wie Du Deinen Account entsperren können." 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_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." unlocked: "Dein Account wurde entsperrt. Du bist jetzt angemeldet."
errors: errors:
messages: messages:
already_confirmed: "wurde bereits bestätigt" already_confirmed: "wurde bereits bestätigt."
confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an" confirmation_period_expired: "muss innerhalb %{period} bestätigt werden, bitte fordere einen neuen Link an."
expired: "ist abgelaufen, bitte neu anfordern" expired: "ist abgelaufen, bitte neu anfordern."
not_found: "nicht gefunden" not_found: "wurde nicht gefunden."
not_locked: "ist nicht gesperrt" not_locked: "ist nicht gesperrt"
not_saved: not_saved:
one: "Konnte %{resource} nicht speichern: ein Fehler." one: "Konnte %{resource} nicht speichern: ein Fehler."

View file

@ -62,7 +62,7 @@ fr:
buttons: buttons:
revoke: Annuler revoke: Annuler
confirmations: confirmations:
revoke: Êtes-vous certain? revoke: Êtes-vous certain ?
index: index:
application: Application application: Application
created_at: Créé le created_at: Créé le
@ -72,19 +72,19 @@ fr:
errors: errors:
messages: messages:
access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la demande. 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_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_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_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_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_scope: La portée demandée n'est pas valide, est inconnue, ou est mal formée.
invalid_token: invalid_token:
expired: Le jeton d'accès a expiré expired: Le jeton d'accès a expiré
revoked: Le jeton d'accès a été révoqué revoked: Le jeton d'accès a été révoqué
unknown: Le jeton d'accès n'est pas valide 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é. 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 qui l'a empêché de remplir la demande. 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. 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. 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. unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation.

View file

@ -5,6 +5,7 @@ en:
about_this: About this instance about_this: About this instance
apps: Apps apps: Apps
business_email: 'Business e-mail:' business_email: 'Business e-mail:'
closed_registrations: Registrations are currently closed on this instance.
contact: Contact contact: Contact
description_headline: What is %{domain}? description_headline: What is %{domain}?
domain_count_after: other instances domain_count_after: other instances

View file

@ -5,6 +5,7 @@ fr:
about_this: À propos de cette instance about_this: À propos de cette instance
apps: Applications apps: Applications
business_email: E-mail professionnel business_email: E-mail professionnel
closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. .
description_headline: Qu'est-ce que %{domain} ? description_headline: Qu'est-ce que %{domain} ?
domain_count_after: autres instances domain_count_after: autres instances
domain_count_before: Connectés à domain_count_before: Connectés à

View file

@ -38,7 +38,7 @@ en:
follow: Send e-mail when someone follows you follow: Send e-mail when someone follows you
follow_request: Send e-mail when someone requests to follow you follow_request: Send e-mail when someone requests to follow you
mention: Send e-mail when someone mentions 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' 'no': 'No'
required: required:
mark: "*" mark: "*"

View file

@ -5,6 +5,8 @@ defaults: &defaults
site_extended_description: '' site_extended_description: ''
site_contact_username: '' site_contact_username: ''
site_contact_email: '' site_contact_email: ''
open_registrations: true
closed_registrations_message: ''
notification_emails: notification_emails:
follow: false follow: false
reblog: false reblog: false
@ -15,6 +17,7 @@ defaults: &defaults
interactions: interactions:
must_be_follower: false must_be_follower: false
must_be_following: false must_be_following: false
development: development:
<<: *defaults <<: *defaults

View file

@ -7,7 +7,7 @@ So, you have a working Mastodon instance... now what?
The following rake task: 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. Would turn the local user "alice" into an admin.

View file

@ -8,6 +8,8 @@ Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.co
1. Click the above button. 1. Click the above button.
2. Fill in the options requested. 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 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. * 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. 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.

View file

@ -76,7 +76,7 @@ It is recommended to create a special user for mastodon on the server (you could
## General dependencies ## General dependencies
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - 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 sudo npm install -g yarn
## Redis ## 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: You need the `git-core` package installed on your system. If it is so, from the `mastodon` user:
cd ~ cd ~
git clone https://github.com/Gargron/mastodon.git live git clone https://github.com/tootsuite/mastodon.git live
cd live cd live
Then you can proceed to install project dependencies: 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 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 ## 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 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 ## Things to look out for when upgrading Mastodon

View file

@ -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| | [mastodon.social](https://mastodon.social) |Flagship, quick updates|Yes|No|
| [awoo.space](https://awoo.space) |Intentionally moderated, only federates with mastodon.social|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| | [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|No|
| [socially.constructed.space](https://socially.constructed.space) |Single user|No|No| | [socially.constructed.space](https://socially.constructed.space) |Single user|No|No|
| [epiktistes.com](https://epiktistes.com) |N/A|Yes|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| | [icosahedron.website](https://icosahedron.website/) |Icosahedron-themed (well, visually), open registration.|Yes|No|
| [memetastic.space](https://memetastic.space) |Memes|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| | [social.gestaltzerfall.net](https://social.gestaltzerfall.net) |Single user|No|No|
| [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes| | [mastodon.xyz](https://mastodon.xyz) |N/A|Yes|Yes|
| [social.targaryen.house](https://social.targaryen.house) |N/A|Yes|No| | [social.targaryen.house](https://social.targaryen.house) |Federates everywhere, quick updates.|Yes|Yes|
| [social.mashek.net](https://social.mashek.net) |Themed and customised for Mashekstein Labs community. Selectively federates.|Yes|No|
| [masto.themimitoof.fr](https://masto.themimitoof.fr) |N/A|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.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| | [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). Let me know if you start running one so I can add it to the list! (Alternatively, add it yourself as a pull request).

View file

@ -26,17 +26,17 @@ Mastodon User's Guide
## Intro ## 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 #### 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. 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`). 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 ## Getting Started

View file

@ -1,3 +1,3 @@
Fabricator(:account) do Fabricator(:account) do
username "alice" username { Faker::Internet.user_name(nil, %w(_)) }
end end

View file

@ -1,3 +1,4 @@
Fabricator(:block) do Fabricator(:block) do
account
target_account { Fabricate(:account) }
end end

View file

@ -1,3 +1,4 @@
Fabricator(:follow) do Fabricator(:follow) do
account
target_account { Fabricate(:account) }
end end

View file

@ -1,3 +1,4 @@
Fabricator(:follow_request) do Fabricator(:follow_request) do
account
target_account { Fabricate(:account) }
end end

View file

@ -0,0 +1,4 @@
Fabricator(:mention) do
account
status
end

View file

@ -1,6 +1,6 @@
Fabricator(:user) do Fabricator(:user) do
account account
email "alice@example.com" email { Faker::Internet.email }
password "123456789" password "123456789"
confirmed_at { Time.now } confirmed_at { Time.now }
end end

View file

@ -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 expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
end end
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 end

View file

@ -1,5 +1,22 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Block, type: :model do 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 end

View file

@ -1,5 +1,23 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe DomainBlock, type: :model do 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 end

View file

@ -3,4 +3,23 @@ require 'rails_helper'
RSpec.describe FollowRequest, type: :model do RSpec.describe FollowRequest, type: :model do
describe '#authorize!' describe '#authorize!'
describe '#reject!' 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 end

View file

@ -5,4 +5,23 @@ RSpec.describe Follow, type: :model do
let(:bob) { Fabricate(:account, username: 'bob') } let(:bob) { Fabricate(:account, username: 'bob') }
subject { Follow.new(account: alice, target_account: 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 end

View file

@ -1,5 +1,22 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Mention, type: :model do 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 end

View file

@ -1,5 +1,88 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe User, type: :model do 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 end

View file

@ -8,6 +8,8 @@ require 'rspec/rails'
require 'webmock/rspec' require 'webmock/rspec'
require 'paperclip/matchers' require 'paperclip/matchers'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema! ActiveRecord::Migration.maintain_test_schema!
WebMock.disable_net_connect!(allow: 'localhost:7575') WebMock.disable_net_connect!(allow: 'localhost:7575')
Sidekiq::Testing.inline! Sidekiq::Testing.inline!

View file

@ -23,6 +23,7 @@ RSpec.describe FanOutOnWriteService do
end end
it 'delivers status to local followers' do 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 expect(Feed.new(:home, follower).get(10).map(&:id)).to include status.id
end end

View file

@ -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