diff options
author | André Lewin <andre.charles.lewin@gmail.com> | 2017-04-05 20:28:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-05 20:28:58 +0200 |
commit | bf7cefa516075b480fd5da5ea4b1385d7d17bdd5 (patch) | |
tree | 45bc6b379f5d06553d0a3e5bf4c0e2b452b7552f /app | |
parent | 8736ef50ad13d5506bd6a673d4fcb96c33b609a3 (diff) | |
parent | d13d169922c7efeb6e6c20ef6a271eeda552275f (diff) |
Merge branch 'master' into master
Diffstat (limited to 'app')
61 files changed, 480 insertions, 183 deletions
diff --git a/app/assets/images/fluffy-elephant-friend.png b/app/assets/images/fluffy-elephant-friend.png index 11787e936..f0df29927 100644 --- a/app/assets/images/fluffy-elephant-friend.png +++ b/app/assets/images/fluffy-elephant-friend.png Binary files differdiff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 2861c64e4..00f20074d 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -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({ diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx index 8253ad017..d7a78d9cc 100644 --- a/app/assets/javascripts/components/features/getting_started/index.jsx +++ b/app/assets/javascripts/components/features/getting_started/index.jsx @@ -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> diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx index d75149a0e..6aa9d1efa 100644 --- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx +++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx @@ -9,7 +9,7 @@ const iconStyle = { }; const ClearColumnButton = ({ onClick }) => ( - <div className='column-icon' style={iconStyle} onClick={onClick}> + <div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}> <i className='fa fa-trash' /> </div> ); diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx index 2d3360b6b..53e2898eb 100644 --- a/app/assets/javascripts/components/locales/en.jsx +++ b/app/assets/javascripts/components/locales/en.jsx @@ -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", diff --git a/app/assets/javascripts/components/locales/fi.jsx b/app/assets/javascripts/components/locales/fi.jsx new file mode 100644 index 000000000..b3ae4bc56 --- /dev/null +++ b/app/assets/javascripts/components/locales/fi.jsx @@ -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; diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx index 255172f1e..1e7b8b548 100644 --- a/app/assets/javascripts/components/locales/index.jsx +++ b/app/assets/javascripts/components/locales/index.jsx @@ -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 }; diff --git a/app/assets/stylesheets/about.scss b/app/assets/stylesheets/about.scss index 2ff1d1453..c9d9dc5d5 100644 --- a/app/assets/stylesheets/about.scss +++ b/app/assets/stylesheets/about.scss @@ -319,7 +319,7 @@ } } - .simple_form { + .simple_form, .closed-registrations-message { width: 300px; flex: 0 0 auto; background: rgba(darken($color1, 7%), 0.5); @@ -340,3 +340,11 @@ } } } + +.closed-registrations-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss index 25e24a95a..b3ae33500 100644 --- a/app/assets/stylesheets/accounts.scss +++ b/app/assets/stylesheets/accounts.scss @@ -34,6 +34,7 @@ text-align: center; position: relative; z-index: 2; + text-shadow: 0 0 2px $color8; small { display: block; @@ -128,6 +129,7 @@ text-transform: uppercase; display: block; margin-bottom: 5px; + text-shadow: 0 0 2px $color8; } .counter-number { @@ -385,5 +387,6 @@ .account__header__content { font-size: 14px; color: $color1; + text-shadow: 0 0 2px $color8; } } diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index abf4b7df4..7fd43489f 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,7 +4,9 @@ class AboutController < ApplicationController before_action :set_body_classes def index - @description = Setting.site_description + @description = Setting.site_description + @open_registrations = Setting.open_registrations + @closed_registrations_message = Setting.closed_registrations_message @user = User.new @user.build_account diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index e362957e7..1f4432847 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -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 diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 0117a18ee..2b3b1809f 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -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 diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index af0be8823..7615c781d 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -11,9 +11,13 @@ class Admin::SettingsController < ApplicationController def update @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id]) + value = settings_params[:value] - if @setting.value != params[:setting][:value] - @setting.value = params[:setting][:value] + # Special cases + value = value == 'true' if @setting.var == 'open_registrations' + + if @setting.value != value + @setting.value = value @setting.save end @@ -22,4 +26,10 @@ class Admin::SettingsController < ApplicationController format.json { respond_with_bip(@setting) } end end + + private + + def settings_params + params.require(:setting).permit(:value) + end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index ca9dd0b7e..2ec7280af 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -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 diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index c22dacbaa..7c0f44f03 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -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 diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index f8139ade7..aed3578d7 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -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 diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 46bdddbc1..f83c573cb 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -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 diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 024258c0e..4ece7e702 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ef9364897..c06142fd4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 501e66807..4881c074a 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -3,7 +3,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController layout :determine_layout - before_action :check_single_user_mode + before_action :check_enabled_registrations, only: [:new, :create] before_action :configure_sign_up_params, only: [:create] protected @@ -27,12 +27,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController new_user_session_path end - def check_single_user_mode - redirect_to root_path if Rails.configuration.x.single_user_mode + def check_enabled_registrations + redirect_to root_path if Rails.configuration.x.single_user_mode || !Setting.open_registrations end - + private - + def determine_layout %w(edit update).include?(action_name) ? 'admin' : 'auth' end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index feaad04f6..7c25266d8 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -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 diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 7d4bfe6ce..1e3f786ec 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -8,6 +8,7 @@ class RemoteFollowController < ApplicationController def new @remote_follow = RemoteFollow.new + @remote_follow.acct = session[:remote_follow] if session.key?(:remote_follow) end def create @@ -22,6 +23,8 @@ class RemoteFollowController < ApplicationController render(:new) && return end + session[:remote_follow] = @remote_follow.acct + redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s else render :new diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index f92abb9bc..74dc0e11d 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -10,6 +10,7 @@ module SettingsHelper hu: 'Magyar', uk: 'Українська', 'zh-CN': '简体中文', + fi: 'Suomi', eo: 'Esperanto', }.freeze diff --git a/app/lib/email_validator.rb b/app/lib/email_validator.rb index 856b8b1f7..06e9375f6 100644 --- a/app/lib/email_validator.rb +++ b/app/lib/email_validator.rb @@ -2,17 +2,30 @@ class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - return if Rails.configuration.x.email_domains_blacklist.empty? - record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value) end private def blocked_email?(value) + on_blacklist?(value) || not_on_whitelist?(value) + end + + def on_blacklist?(value) + return false if Rails.configuration.x.email_domains_blacklist.blank? + domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.') regexp = Regexp.new("@(.+\\.)?(#{domains})", true) value =~ regexp end + + def not_on_whitelist?(value) + return false if Rails.configuration.x.email_domains_whitelist.blank? + + domains = Rails.configuration.x.email_domains_whitelist.gsub('.', '\.') + regexp = Regexp.new("@(.+\\.)?(#{domains})", true) + + value !~ regexp + end end diff --git a/app/lib/exceptions.rb b/app/lib/exceptions.rb index 200da9fe1..9bc802c12 100644 --- a/app/lib/exceptions.rb +++ b/app/lib/exceptions.rb @@ -4,4 +4,5 @@ module Mastodon class Error < StandardError; end class NotPermittedError < Error; end class ValidationError < Error; end + class RaceConditionError < Error; end end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index cd6ca1291..58d9fb1fc 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -5,17 +5,17 @@ require 'singleton' class FeedManager include Singleton - MAX_ITEMS = 800 + MAX_ITEMS = 400 def key(type, id) "feed:#{type}:#{id}" end - def filter?(timeline_type, status, receiver) + def filter?(timeline_type, status, receiver_id) if timeline_type == :home - filter_from_home?(status, receiver) + filter_from_home?(status, receiver_id) elsif timeline_type == :mentions - filter_from_mentions?(status, receiver) + filter_from_mentions?(status, receiver_id) else false end @@ -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) - - 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 + oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 + + from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses| + redis.pipelined do + statuses.each do |status| + redis.zrem(timeline_key, status.id) + redis.zremrangebyscore(timeline_key, status.id, status.id) + end end end - - Rabl::Renderer.new(template, object, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render end private @@ -94,38 +82,40 @@ class FeedManager Redis.current end - def filter_from_home?(status, receiver) - return true if receiver.muting?(status.account) - - should_filter = false - - if status.reply? && status.in_reply_to_id.nil? - should_filter = true - elsif status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !receiver.following?(status.in_reply_to_account) # and I'm not following the person it's a reply to - should_filter &&= !(receiver.id == status.in_reply_to_account_id) # and it's not a reply to me - should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply - elsif status.reblog? # Filter out a reblog - should_filter = receiver.blocking?(status.reblog.account) # if I'm blocking the reblogged person - should_filter ||= receiver.muting?(status.reblog.account) # or muting that person - should_filter ||= status.reblog.account.blocking?(receiver) # or if the author of the reblogged status is blocking me - end + def filter_from_home?(status, receiver_id) + return true if status.reply? && status.in_reply_to_id.nil? - should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked + check_for_mutes = [status.account_id] + check_for_mutes.concat([status.reblog.account_id]) if status.reblog? - should_filter - end + return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any? + + check_for_blocks = status.mentions.map(&:account_id) + check_for_blocks.concat([status.reblog.account_id]) if status.reblog? - 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 + 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? # or it's a reply - should_filter ||= receiver.blocking?(status.in_reply_to_account) # to a user I blocked + if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply + should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to + should_filter &&= !(receiver_id == status.in_reply_to_account_id) # and it's not a reply to me + should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply + return should_filter + elsif status.reblog? # Filter out a reblog + return Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me end + false + end + + def filter_from_mentions?(status, receiver_id) + check_for_blocks = [status.account_id] + check_for_blocks.concat(status.mentions.pluck(:account_id)) + check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil? + + should_filter = receiver_id == status.account_id # Filter if I'm mentioning myself + should_filter ||= Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? # or it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked + should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them + should_filter end end diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb new file mode 100644 index 000000000..26adcb03a --- /dev/null +++ b/app/lib/inline_rabl_scope.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class InlineRablScope + include RoutingHelper + + def initialize(account) + @account = account + end + + def current_user + @account.try(:user) + end + + def current_account + @account + end +end diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb new file mode 100644 index 000000000..8e04ad1d5 --- /dev/null +++ b/app/lib/inline_renderer.rb @@ -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 diff --git a/app/models/block.rb b/app/models/block.rb index 9c55703c9..ae456a6b6 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -3,9 +3,8 @@ class Block < ApplicationRecord include Paginable - belongs_to :account - belongs_to :target_account, class_name: 'Account' + belongs_to :account, required: true + belongs_to :target_account, class_name: 'Account', required: true - validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5e1905e15..3cbc160a0 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -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 diff --git a/app/models/follow.rb b/app/models/follow.rb index 8bfe8b2f6..b6b9dca7c 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -3,11 +3,14 @@ class Follow < ApplicationRecord include Paginable - belongs_to :account, counter_cache: :following_count - belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count + belongs_to :account, counter_cache: :following_count, required: true + + belongs_to :target_account, + class_name: 'Account', + counter_cache: :followers_count, + required: true has_one :notification, as: :activity, dependent: :destroy - validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 4224ab15d..20e1332dd 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -3,12 +3,11 @@ class FollowRequest < ApplicationRecord include Paginable - belongs_to :account - belongs_to :target_account, class_name: 'Account' + belongs_to :account, required: true + belongs_to :target_account, class_name: 'Account', required: true has_one :notification, as: :activity, dependent: :destroy - validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } def authorize! diff --git a/app/models/mention.rb b/app/models/mention.rb index 10a9cb1cd..03e76fcc4 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true class Mention < ApplicationRecord - belongs_to :account, inverse_of: :mentions - belongs_to :status + belongs_to :account, inverse_of: :mentions, required: true + belongs_to :status, required: true has_one :notification, as: :activity, dependent: :destroy - validates :account, :status, presence: true validates :account, uniqueness: { scope: :status } end diff --git a/app/models/report.rb b/app/models/report.rb index 05dc8cff1..fd8e46aac 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -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) } diff --git a/app/models/status.rb b/app/models/status.rb index 81b26fd14..6948ad77c 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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 diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 9518b1fcf..6c131bd34 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -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 diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 402b84b2f..c63fcc1fe 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -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 diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 942cd9d21..62508a049 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -17,7 +17,7 @@ class NotifyService < BaseService private def blocked_mention? - FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient) + FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient.id) end def blocked_favourite? @@ -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 diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index e1ec56e8d..07dcb81da 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -5,9 +5,11 @@ class PrecomputeFeedService < BaseService # @param [Symbol] type :home or :mentions # @param [Account] account def call(_, account) - Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS).each do |status| - next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account) - redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + redis.pipelined do + Status.as_home_timeline(account).limit(FeedManager::MAX_ITEMS / 4).each do |status| + next if status.direct_visibility? || FeedManager.instance.filter?(:home, status, account.id) + redis.zadd(FeedManager.instance.key(:home, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id) + end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index cf1f432e4..e19fdd030 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -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 diff --git a/app/views/about/index.html.haml b/app/views/about/index.html.haml index fdfb2b916..ebca4213a 100644 --- a/app/views/about/index.html.haml +++ b/app/views/about/index.html.haml @@ -24,21 +24,34 @@ .screenshot-with-signup .mascot= image_tag 'fluffy-elephant-friend.png' - = simple_form_for(@user, url: user_registration_path) do |f| - = f.simple_fields_for :account do |ff| - = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } + - if @open_registrations + = simple_form_for(@user, url: user_registration_path) do |f| + = f.simple_fields_for :account do |ff| + = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') } - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } - = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } - = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } + = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') } + = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') } - .actions - = f.button :button, t('about.get_started'), type: :submit + .actions + = f.button :button, t('about.get_started'), type: :submit - .info - = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' - · - = link_to t('about.about_this'), about_more_path + .info + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + · + = link_to t('about.about_this'), about_more_path + - else + .closed-registrations-message + - if @closed_registrations_message.blank? + %p= t('about.closed_registrations') + - else + = @closed_registrations_message.html_safe + .info + = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn' + · + = link_to t('about.other_instances'), 'https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/List-of-Mastodon-instances.md' + · + = link_to t('about.about_this'), about_more_path %h3= t('about.features_headline') diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml index dbaeb4716..eb7894b86 100644 --- a/app/views/admin/domain_blocks/index.html.haml +++ b/app/views/admin/domain_blocks/index.html.haml @@ -14,3 +14,4 @@ %td= block.severity = will_paginate @blocks, pagination_options += link_to 'Add new', new_admin_domain_block_path, class: 'button' diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml new file mode 100644 index 000000000..fbd39d6cf --- /dev/null +++ b/app/views/admin/domain_blocks/new.html.haml @@ -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 diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 8a5414cef..839259dc2 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -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 diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 74cac016d..caa8415df 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -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 diff --git a/app/views/admin/settings/index.html.haml b/app/views/admin/settings/index.html.haml index 1429dbd9e..02faac8c2 100644 --- a/app/views/admin/settings/index.html.haml +++ b/app/views/admin/settings/index.html.haml @@ -38,3 +38,15 @@ %br/ You can use HTML tags %td= best_in_place @settings['site_extended_description'], :value, as: :textarea, url: admin_setting_path(@settings['site_extended_description']) + %tr + %td + %strong Open registration + %td= best_in_place @settings['open_registrations'], :value, as: :checkbox, collection: { false: 'Disabled', true: 'Enabled'}, url: admin_setting_path(@settings['open_registrations']) + %tr + %td + %strong Closed registration message + %br/ + Displayed on frontpage when registrations are closed + %br/ + You can use HTML tags + %td= best_in_place @settings['closed_registrations_message'], :value, as: :textarea, url: admin_setting_path(@settings['closed_registrations_message']) diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb index f1d6869cc..928069211 100644 --- a/app/workers/after_remote_follow_request_worker.rb +++ b/app/workers/after_remote_follow_request_worker.rb @@ -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 diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb index 0d04456a9..d12fa3454 100644 --- a/app/workers/after_remote_follow_worker.rb +++ b/app/workers/after_remote_follow_worker.rb @@ -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 diff --git a/app/workers/application_worker.rb b/app/workers/application_worker.rb new file mode 100644 index 000000000..f2d7c1062 --- /dev/null +++ b/app/workers/application_worker.rb @@ -0,0 +1,5 @@ +class ApplicationWorker + def info(message) + Rails.logger.info("#{self.class.name} - #{message}") + end +end diff --git a/app/workers/distribution_worker.rb b/app/workers/distribution_worker.rb index f4e738d80..9a2867ea6 100644 --- a/app/workers/distribution_worker.rb +++ b/app/workers/distribution_worker.rb @@ -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 diff --git a/app/workers/domain_block_worker.rb b/app/workers/domain_block_worker.rb new file mode 100644 index 000000000..884477829 --- /dev/null +++ b/app/workers/domain_block_worker.rb @@ -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 diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb new file mode 100644 index 000000000..a58dfaa74 --- /dev/null +++ b/app/workers/feed_insert_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class FeedInsertWorker + include Sidekiq::Worker + + def perform(status_id, follower_id) + status = Status.find(status_id) + follower = Account.find(follower_id) + + return if FeedManager.instance.filter?(:home, status, follower.id) + FeedManager.instance.push(:home, follower, status) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index a3ae2a85a..7cf29fb53 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -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) diff --git a/app/workers/link_crawl_worker.rb b/app/workers/link_crawl_worker.rb index af3394b8b..834b0088b 100644 --- a/app/workers/link_crawl_worker.rb +++ b/app/workers/link_crawl_worker.rb @@ -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)) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 0f288f43f..d745cb99c 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -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 diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb index 1a2faefd8..da1d6ab45 100644 --- a/app/workers/notification_worker.rb +++ b/app/workers/notification_worker.rb @@ -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)) diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 15005bc80..466def3a8 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -22,6 +22,7 @@ class Pubsubhubbub::DeliveryWorker .headers(headers) .post(subscription.callback_url, body: payload) + return subscription.destroy! if response.code > 299 && response.code < 500 && response.code != 429 # HTTP 4xx means error is not temporary, except for 429 (throttling) raise "Delivery failed for #{subscription.callback_url}: HTTP #{response.code}" unless response.code > 199 && response.code < 300 subscription.touch(:last_successful_delivery_at) diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb new file mode 100644 index 000000000..fbcdcf634 --- /dev/null +++ b/app/workers/push_update_worker.rb @@ -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 diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb index 3aece0ba2..da8b845f6 100644 --- a/app/workers/regeneration_worker.rb +++ b/app/workers/regeneration_worker.rb @@ -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 diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb index 593edd032..38287e8e6 100644 --- a/app/workers/thread_resolve_worker.rb +++ b/app/workers/thread_resolve_worker.rb @@ -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) diff --git a/app/workers/unmerge_worker.rb b/app/workers/unmerge_worker.rb index dbf7243de..ea6aacebf 100644 --- a/app/workers/unmerge_worker.rb +++ b/app/workers/unmerge_worker.rb @@ -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 |