Merge branch 'master' into master

This commit is contained in:
André Lewin
2017-04-05 20:28:58 +02:00
committed by GitHub
117 changed files with 1783 additions and 276 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -46,6 +46,7 @@ import fr from 'react-intl/locale-data/fr';
import pt from 'react-intl/locale-data/pt';
import hu from 'react-intl/locale-data/hu';
import uk from 'react-intl/locale-data/uk';
import fi from 'react-intl/locale-data/fi';
import eo from 'react-intl/locale-data/eo';
import getMessagesForLocale from '../locales';
import { hydrateStore } from '../actions/store';
@ -59,7 +60,7 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
basename: '/web'
});
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...eo]);
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
const Mastodon = React.createClass({

View File

@ -43,7 +43,7 @@ const GettingStarted = ({ intl, me }) => {
<div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
<div className='static-content getting-started'>
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
<p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
</div>
</div>
</Column>

View File

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

View File

@ -25,7 +25,7 @@ const en = {
"getting_started.heading": "Getting started",
"getting_started.about_addressing": "You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the search form.",
"getting_started.about_shortcuts": "If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on github at {github}. {apps}.",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.",
"column.home": "Home",
"column.community": "Local timeline",
"column.public": "Federated timeline",

View File

@ -0,0 +1,68 @@
const fi = {
"column_back_button.label": "Takaisin",
"lightbox.close": "Sulje",
"loading_indicator.label": "Ladataan...",
"status.mention": "Mainitse @{name}",
"status.delete": "Poista",
"status.reply": "Vastaa",
"status.reblog": "Buustaa",
"status.favourite": "Tykkää",
"status.reblogged_by": "{name} buustasi",
"status.sensitive_warning": "Arkaluontoista sisältöä",
"status.sensitive_toggle": "Klikkaa nähdäksesi",
"video_player.toggle_sound": "Äänet päälle/pois",
"account.mention": "Mainitse @{name}",
"account.edit_profile": "Muokkaa",
"account.unblock": "Salli @{name}",
"account.unfollow": "Lopeta seuraaminen",
"account.block": "Estä @{name}",
"account.follow": "Seuraa",
"account.posts": "Postit",
"account.follows": "Seuraa",
"account.followers": "Seuraajia",
"account.follows_you": "Seuraa sinua",
"account.requested": "Odottaa hyväksyntää",
"getting_started.heading": "Aloitus",
"getting_started.about_addressing": "Voit seurata ihmisiä jos tiedät heidän käyttäjänimensä ja domainin missä he ovat syöttämällä e-mail-esque osoitteen Etsi kenttään.",
"getting_started.about_shortcuts": "Jos etsimäsi henkilö on samassa domainissa kuin sinä, pelkkä käyttäjänimi kelpaa. Sama pätee kun mainitset ihmisiä statuksessasi",
"getting_started.open_source_notice": "Mastodon Mastodon on avoimen lähdekoodin ohjelma. Voit avustaa tai raportoida ongelmia GitHub palvelussa {github}. {apps}.",
"column.home": "Koti",
"column.community": "Paikallinen aikajana",
"column.public": "Yleinen aikajana",
"column.notifications": "Ilmoitukset",
"tabs_bar.compose": "Luo",
"tabs_bar.home": "Koti",
"tabs_bar.mentions": "Maininnat",
"tabs_bar.public": "Yleinen aikajana",
"tabs_bar.notifications": "Ilmoitukset",
"compose_form.placeholder": "Mitä sinulla on mielessä?",
"compose_form.publish": "Toot",
"compose_form.sensitive": "Merkitse media herkäksi",
"compose_form.spoiler": "Piiloita teksti varoituksen taakse",
"compose_form.private": "Merkitse yksityiseksi",
"compose_form.privacy_disclaimer": "Sinun yksityinen status toimitetaan mainitsemallesi käyttäjille domaineissa {domains}. Luotatko {domainsCount, plural, one {tähän palvelimeen} other {näihin palvelimiin}}? Postauksen yksityisyys toimii van Mastodon palvelimilla. Jos {domains} {domainsCount, plural, one {ei ole Mastodon palvelin} other {eivät ole Mastodon palvelin}}, viestiin ei tule Yksityinen-merkintää, ja sitä voidaan boostata tai muuten tehdä näkyväksi muille vastaanottajille.",
"compose_form.unlisted": "Älä näytä yleisillä aikajanoilla",
"navigation_bar.edit_profile": "Muokkaa profiilia",
"navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.community_timeline": "Paikallinen aikajana",
"navigation_bar.public_timeline": "Yleinen aikajana",
"navigation_bar.logout": "Kirjaudu ulos",
"reply_indicator.cancel": "Peruuta",
"search.placeholder": "Hae",
"search.account": "Tili",
"search.hashtag": "Hashtag",
"upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru",
"notification.follow": "{name} seurasi sinua",
"notification.favourite": "{name} tykkäsi statuksestasi",
"notification.reblog": "{name} buustasi statustasi",
"notification.mention": "{name} mainitsi sinut",
"notifications.column_settings.alert": "Työpöytä ilmoitukset",
"notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.follow": "Uusia seuraajia:",
"notifications.column_settings.favourite": "Tykkäyksiä:",
"notifications.column_settings.mention": "Mainintoja:",
"notifications.column_settings.reblog": "Buusteja:",
};
export default fi;

View File

@ -5,6 +5,7 @@ import hu from './hu';
import fr from './fr';
import pt from './pt';
import uk from './uk';
import fi from './fi';
import eo from './eo';
const locales = {
@ -15,6 +16,7 @@ const locales = {
fr,
pt,
uk,
fi,
eo
};

View File

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

View File

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

View File

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

View File

@ -9,6 +9,24 @@ class Admin::DomainBlocksController < ApplicationController
@blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
end
def new
@domain_block = DomainBlock.new
end
def create
@domain_block = DomainBlock.new(resource_params)
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
else
render action: :new
end
end
private
def resource_params
params.require(:domain_block).permit(:domain, :severity)
end
end

View File

@ -16,19 +16,19 @@ class Admin::ReportsController < ApplicationController
end
def resolve
@report.update(action_taken: true)
@report.update(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end
def suspend
Admin::SuspensionWorker.perform_async(@report.target_account.id)
@report.update(action_taken: true)
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end
def silence
@report.target_account.update(silenced: true)
@report.update(action_taken: true)
Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
redirect_to admin_report_path(@report)
end

View File

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

View File

@ -4,6 +4,12 @@ class Api::V1::AppsController < ApiController
respond_to :json
def create
@app = Doorkeeper::Application.create!(name: params[:client_name], redirect_uri: params[:redirect_uris], scopes: (params[:scopes] || Doorkeeper.configuration.default_scopes), website: params[:website])
@app = Doorkeeper::Application.create!(name: app_params[:client_name], redirect_uri: app_params[:redirect_uris], scopes: (app_params[:scopes] || Doorkeeper.configuration.default_scopes), website: app_params[:website])
end
private
def app_params
params.permit(:client_name, :redirect_uris, :scopes, :website)
end
end

View File

@ -7,7 +7,7 @@ class Api::V1::FollowsController < ApiController
respond_to :json
def create
raise ActiveRecord::RecordNotFound if params[:uri].blank?
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
render action: :show
@ -16,6 +16,10 @@ class Api::V1::FollowsController < ApiController
private
def target_uri
params[:uri].strip.gsub(/\A@/, '')
follow_params[:uri].strip.gsub(/\A@/, '')
end
def follow_params
params.permit(:uri)
end
end

View File

@ -10,10 +10,16 @@ class Api::V1::MediaController < ApiController
respond_to :json
def create
@media = MediaAttachment.create!(account: current_user.account, file: params[:file])
@media = MediaAttachment.create!(account: current_user.account, file: media_params[:file])
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: { error: 'File type of uploaded media could not be verified' }, status: 422
rescue Paperclip::Error
render json: { error: 'Error processing thumbnail for uploaded media' }, status: 500
end
private
def media_params
params.permit(:file)
end
end

View File

@ -12,13 +12,19 @@ class Api::V1::ReportsController < ApiController
end
def create
status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]]
status_ids = report_params[:status_ids].is_a?(Enumerable) ? report_params[:status_ids] : [report_params[:status_ids]]
@report = Report.create!(account: current_account,
target_account: Account.find(params[:account_id]),
target_account: Account.find(report_params[:account_id]),
status_ids: Status.find(status_ids).pluck(:id),
comment: params[:comment])
comment: report_params[:comment])
render :show
end
private
def report_params
params.permit(:account_id, :comment, status_ids: [])
end
end

View File

@ -62,11 +62,11 @@ class Api::V1::StatusesController < ApiController
end
def create
@status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids],
sensitive: params[:sensitive],
spoiler_text: params[:spoiler_text],
visibility: params[:visibility],
application: doorkeeper_token.application)
@status = PostStatusService.new.call(current_user.account, status_params[:status], status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]), media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
application: doorkeeper_token.application)
render action: :show
end
@ -111,4 +111,8 @@ class Api::V1::StatusesController < ApiController
@status = Status.find(params[:id])
raise ActiveRecord::RecordNotFound unless @status.permitted?(current_account)
end
def status_params
params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: [])
end
end

View File

@ -39,7 +39,14 @@ class ApplicationController < ActionController::Base
end
def set_user_activity
current_user.touch(:current_sign_in_at) if !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago)
return unless !current_user.nil? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < 24.hours.ago)
# Mark user as signed-in today
current_user.update_tracked_fields(request)
# If the sign in is after a two week break, we need to regenerate their feed
RegenerationWorker.perform_async(current_user.account_id) if current_user.last_sign_in_at < 14.days.ago
return
end
def check_suspension

View File

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

View File

@ -3,6 +3,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
skip_before_action :authenticate_resource_owner!
before_action :set_locale
before_action :store_current_location
before_action :authenticate_resource_owner!
@ -11,4 +12,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def store_current_location
store_location_for(:user, request.url)
end
def set_locale
I18n.locale = current_user.try(:locale) || I18n.default_locale
rescue I18n::InvalidLocale
I18n.locale = I18n.default_locale
end
end

View File

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

View File

@ -10,6 +10,7 @@ module SettingsHelper
hu: 'Magyar',
uk: 'Українська',
'zh-CN': '简体中文',
fi: 'Suomi',
eo: 'Esperanto',
}.freeze

View File

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

View File

@ -4,4 +4,5 @@ module Mastodon
class Error < StandardError; end
class NotPermittedError < Error; end
class ValidationError < Error; end
class RaceConditionError < Error; end
end

View File

@ -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
@ -34,12 +34,7 @@ class FeedManager
trim(timeline_type, account.id)
end
broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status))
end
def broadcast(timeline_id, options = {})
options[:queued_at] = (Time.now.to_f * 1000.0).to_i
ActionCable.server.broadcast("timeline:#{timeline_id}", options)
PushUpdateWorker.perform_async(account.id, status.id)
end
def trim(type, account_id)
@ -50,10 +45,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 +64,16 @@ class FeedManager
def unmerge_from_timeline(from_account, into_account)
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|
redis.zrem(timeline_key, status.id)
redis.zremrangebyscore(timeline_key, status.id, status.id)
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
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
Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
end
private
@ -94,37 +82,39 @@ class FeedManager
Redis.current
end
def filter_from_home?(status, receiver)
return true if receiver.muting?(status.account)
def filter_from_home?(status, receiver_id)
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?
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
return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
check_for_blocks = status.mentions.map(&:account_id)
check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
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
should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked
should_filter
false
end
def filter_from_mentions?(status, receiver)
should_filter = receiver.id == status.account_id # Filter if I'm mentioning myself
should_filter ||= receiver.blocking?(status.account) # or it's from someone I blocked
should_filter ||= receiver.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
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?
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
end
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

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

@ -0,0 +1,13 @@
# frozen_string_literal: true
class InlineRenderer
def self.render(status, current_account, template)
Rabl::Renderer.new(
template,
status,
view_path: 'app/views',
format: :json,
scope: InlineRablScope.new(current_account)
).render
end
end

View File

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

View File

@ -10,17 +10,9 @@ class Feed
max_id = '+inf' if max_id.blank?
since_id = '-inf' if since_id.blank?
unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
# If we're after most recent items and none are there, we need to precompute the feed
if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'
RegenerationWorker.perform_async(@account.id, @type)
@statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil)
else
status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
@statuses = unhydrated.map { |id| status_map[id] }.compact
end
@statuses
unhydrated.map { |id| status_map[id] }.compact
end
private

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
class Report < ApplicationRecord
belongs_to :account
belongs_to :target_account, class_name: 'Account'
belongs_to :action_taken_by_account, class_name: 'Account'
scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) }

View File

@ -161,9 +161,9 @@ class Status < ApplicationRecord
return where.not(visibility: [:private, :direct]) if account.nil?
if target_account.blocking?(account) # get rid of blocked peeps
where('1 = 0')
none
elsif account.id == target_account.id # author can see own stuff
where('1 = 1')
all
elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in
joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s)
.where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct])
@ -188,7 +188,7 @@ class Status < ApplicationRecord
end
before_validation do
text.strip!
text&.strip!
spoiler_text&.strip!
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply

View File

@ -1,13 +1,11 @@
# frozen_string_literal: true
class BlockDomainService < BaseService
def call(domain, severity)
DomainBlock.where(domain: domain).first_or_create!(domain: domain, severity: severity)
if severity == :silence
Account.where(domain: domain).update_all(silenced: true)
def call(domain_block)
if domain_block.silence?
Account.where(domain: domain_block.domain).update_all(silenced: true)
else
Account.where(domain: domain).find_each do |account|
Account.where(domain: domain_block.domain).find_each do |account|
account.subscription(api_subscription_url(account.id)).unsubscribe if account.subscribed?
SuspendAccountService.new.call(account)
end

View File

@ -4,6 +4,8 @@ class FanOutOnWriteService < BaseService
# Push a status into home and mentions feeds
# @param [Status] status
def call(status)
raise Mastodon::RaceConditionError if status.visibility.nil?
deliver_to_self(status) if status.account.local?
if status.direct_visibility?
@ -14,6 +16,7 @@ class FanOutOnWriteService < BaseService
return if status.account.silenced? || !status.public_visibility? || status.reblog?
render_anonymous_payload(status)
deliver_to_hashtags(status)
return if status.reply? && status.in_reply_to_account_id != status.account_id
@ -31,9 +34,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
@ -42,28 +44,28 @@ 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
def render_anonymous_payload(status)
@payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show')
end
def deliver_to_hashtags(status)
Rails.logger.debug "Delivering status #{status.id} to hashtags"
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|
Redis.current.publish("hashtag:#{hashtag}", Oj.dump(event: :update, payload: @payload))
Redis.current.publish("hashtag:#{hashtag}:local", Oj.dump(event: :update, payload: @payload)) if status.account.local?
end
end
def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline"
payload = FeedManager.instance.inline_render(nil, 'api/v1/statuses/show', status)
FeedManager.instance.broadcast(:public, event: 'update', payload: payload)
FeedManager.instance.broadcast('public:local', event: 'update', payload: payload) if status.account.local?
Redis.current.publish('public', Oj.dump(event: 'update', payload: @payload))
Redis.current.publish('public:local', Oj.dump(event: 'update', payload: @payload)) if status.account.local?
end
end

View File

@ -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?
@ -50,7 +50,7 @@ class NotifyService < BaseService
def create_notification
@notification.save!
return unless @notification.browserable?
FeedManager.instance.broadcast(@recipient.id, event: 'notification', payload: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification))
Redis.current.publish(@recipient.id, Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show')))
end
def send_email

View File

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

View File

@ -65,17 +65,17 @@ class RemoveStatusService < BaseService
redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
end
FeedManager.instance.broadcast(receiver.id, event: 'delete', payload: status.id)
Redis.current.publish(receiver.id, Oj.dump(event: :delete, payload: status.id))
end
def remove_from_hashtags(status)
status.tags.each do |tag|
FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'delete', payload: status.id)
Redis.current.publish("hashtag:#{tag.name}", Oj.dump(event: :delete, payload: status.id))
end
end
def remove_from_public(status)
FeedManager.instance.broadcast(:public, event: 'delete', payload: status.id)
Redis.current.publish('public', Oj.dump(event: :delete, payload: status.id))
end
def redis

View File

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

View File

@ -14,3 +14,4 @@
%td= block.severity
= will_paginate @blocks, pagination_options
= link_to 'Add new', new_admin_domain_block_path, class: 'button'

View File

@ -0,0 +1,18 @@
- content_for :page_title do
New domain block
= simple_form_for @domain_block, url: admin_domain_blocks_path do |f|
= render 'shared/error_messages', object: @domain_block
%p.hint The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
= f.input :domain, placeholder: 'Domain'
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false
%p.hint
%strong Silence
will make the account's posts invisible to anyone who isn't following them.
%strong Suspend
will remove all of the account's content, media, and profile data.
.actions
= f.button :button, 'Create block', type: :submit

View File

@ -8,20 +8,25 @@
%li= filter_link_to 'Unresolved', action_taken: nil
%li= filter_link_to 'Resolved', action_taken: '1'
%table.table
%thead
%tr
%th ID
%th Target
%th Reported by
%th Comment
%th
%tbody
- @reports.each do |report|
= form_tag do
%table.table
%thead
%tr
%td= "##{report.id}"
%td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
%td= link_to report.account.acct, admin_account_path(report.account.id)
%td= truncate(report.comment, length: 30, separator: ' ')
%td= table_link_to 'circle', 'View', admin_report_path(report)
%th
%th ID
%th Target
%th Reported by
%th Comment
%th
%tbody
- @reports.each do |report|
%tr
%td= check_box_tag 'select', report.id
%td= "##{report.id}"
%td= link_to report.target_account.acct, admin_account_path(report.target_account.id)
%td= link_to report.account.acct, admin_account_path(report.account.id)
%td= truncate(report.comment, length: 30, separator: ' ')
%td= table_link_to 'circle', 'View', admin_report_path(report)
= will_paginate @reports, pagination_options

View File

@ -27,7 +27,7 @@
= link_to remove_admin_report_path(@report, status_id: status.id), method: :post, class: 'icon-button', style: 'font-size: 24px; width: 24px; height: 24px', title: 'Delete' do
= fa_icon 'trash'
- unless @report.action_taken?
- if !@report.action_taken?
%hr/
%div{ style: 'overflow: hidden' }
@ -36,3 +36,9 @@
= link_to 'Suspend account', suspend_admin_report_path(@report), method: :post, class: 'button'
%div{ style: 'float: left' }
= link_to 'Mark as resolved', resolve_admin_report_path(@report), method: :post, class: 'button'
- elsif !@report.action_taken_by_account.nil?
%hr/
%p
%strong Action taken by:
= @report.action_taken_by_account.acct

View File

@ -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'])

View File

@ -3,7 +3,7 @@
class AfterRemoteFollowRequestWorker
include Sidekiq::Worker
sidekiq_options retry: 5
sidekiq_options queue: 'pull', retry: 5
def perform(follow_request_id)
follow_request = FollowRequest.find(follow_request_id)
@ -13,5 +13,7 @@ class AfterRemoteFollowRequestWorker
follow_request.destroy
FollowService.new.call(follow_request.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end
end

View File

@ -3,7 +3,7 @@
class AfterRemoteFollowWorker
include Sidekiq::Worker
sidekiq_options retry: 5
sidekiq_options queue: 'pull', retry: 5
def perform(follow_id)
follow = Follow.find(follow_id)
@ -13,5 +13,7 @@ class AfterRemoteFollowWorker
follow.destroy
FollowService.new.call(follow.account, updated_account.acct)
rescue ActiveRecord::RecordNotFound
true
end
end

View File

@ -0,0 +1,5 @@
class ApplicationWorker
def info(message)
Rails.logger.info("#{self.class.name} - #{message}")
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class DistributionWorker
class DistributionWorker < ApplicationWorker
include Sidekiq::Worker
def perform(status_id)
@ -9,6 +9,6 @@ class DistributionWorker
FanOutOnWriteService.new.call(status)
WarmCacheService.new.call(status)
rescue ActiveRecord::RecordNotFound
true
info("Couldn't find the status")
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class DomainBlockWorker
include Sidekiq::Worker
def perform(domain_block_id)
BlockDomainService.new.call(DomainBlock.find(domain_block_id))
rescue ActiveRecord::RecordNotFound
true
end
end

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

@ -5,7 +5,7 @@ require 'csv'
class ImportWorker
include Sidekiq::Worker
sidekiq_options retry: false
sidekiq_options queue: 'pull', retry: false
def perform(import_id)
import = Import.find(import_id)

View File

@ -3,7 +3,7 @@
class LinkCrawlWorker
include Sidekiq::Worker
sidekiq_options retry: false
sidekiq_options queue: 'pull', retry: false
def perform(status_id)
FetchLinkCardService.new.call(Status.find(status_id))

View File

@ -3,6 +3,8 @@
class MergeWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(from_account_id, into_account_id)
FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id))
end

View File

@ -3,7 +3,7 @@
class NotificationWorker
include Sidekiq::Worker
sidekiq_options retry: 5
sidekiq_options queue: 'push', retry: 5
def perform(xml, source_account_id, target_account_id)
SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id))

View File

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

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class PushUpdateWorker
include Sidekiq::Worker
def perform(account_id, status_id)
account = Account.find(account_id)
status = Status.find(status_id)
message = InlineRenderer.render(status, account, 'api/v1/statuses/show')
Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i))
rescue ActiveRecord::RecordNotFound
true
end
end

View File

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

View File

@ -3,7 +3,7 @@
class ThreadResolveWorker
include Sidekiq::Worker
sidekiq_options retry: false
sidekiq_options queue: 'pull', retry: false
def perform(child_status_id, parent_url)
child_status = Status.find(child_status_id)

View File

@ -3,6 +3,8 @@
class UnmergeWorker
include Sidekiq::Worker
sidekiq_options queue: 'pull'
def perform(from_account_id, into_account_id)
FeedManager.instance.unmerge_from_timeline(Account.find(from_account_id), Account.find(into_account_id))
end