Conflicts:
- `.github/dependabot.yml`:
Updated upstream, we deleted it to not be flooded by Depandabot.
Kept deleted.
- `Gemfile.lock`:
Puma updated on both sides, went for the most recent version.
- `app/controllers/api/v1/mutes_controller.rb`:
Upstream updated the serializer to support timed mutes, while
glitch-soc added a custom API ages ago to get information that
is already available elsewhere.
Dropped the glitch-soc-specific API, went with upstream changes.
- `app/javascript/core/admin.js`:
Conflict due to changing how assets are loaded. Went with upstream.
- `app/javascript/packs/public.js`:
Conflict due to changing how assets are loaded. Went with upstream.
- `app/models/mute.rb`:
🤷
- `app/models/user.rb`:
New user setting added upstream while we have glitch-soc-specific
user settings. Added upstream's user setting.
- `config/settings.yml`:
Upstream added a new user setting close to a user setting we had
changed the defaults for. Added the new upstream setting.
- `package.json`:
Upstream dependency updated “too close” to a glitch-soc-specific
dependency. No real conflict. Updated the dependency.
master
commit
ec49aa8175
@ -0,0 +1,36 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController |
||||
include SignatureVerification |
||||
include AccountOwnedConcern |
||||
|
||||
before_action :require_signature! |
||||
before_action :set_items |
||||
before_action :set_cache_headers |
||||
|
||||
def show |
||||
expires_in 0, public: false |
||||
render json: collection_presenter, |
||||
serializer: ActivityPub::CollectionSerializer, |
||||
adapter: ActivityPub::Adapter, |
||||
content_type: 'application/activity+json' |
||||
end |
||||
|
||||
private |
||||
|
||||
def uri_prefix |
||||
signed_request_account.uri[/http(s?):\/\/[^\/]+\//] |
||||
end |
||||
|
||||
def set_items |
||||
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri) |
||||
end |
||||
|
||||
def collection_presenter |
||||
ActivityPub::CollectionPresenter.new( |
||||
id: account_followers_synchronization_url(@account), |
||||
type: :ordered, |
||||
items: @items |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,56 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
module Admin |
||||
class IpBlocksController < BaseController |
||||
def index |
||||
authorize :ip_block, :index? |
||||
|
||||
@ip_blocks = IpBlock.page(params[:page]) |
||||
@form = Form::IpBlockBatch.new |
||||
end |
||||
|
||||
def new |
||||
authorize :ip_block, :create? |
||||
|
||||
@ip_block = IpBlock.new(ip: '', severity: :no_access, expires_in: 1.year) |
||||
end |
||||
|
||||
def create |
||||
authorize :ip_block, :create? |
||||
|
||||
@ip_block = IpBlock.new(resource_params) |
||||
|
||||
if @ip_block.save |
||||
log_action :create, @ip_block |
||||
redirect_to admin_ip_blocks_path, notice: I18n.t('admin.ip_blocks.created_msg') |
||||
else |
||||
render :new |
||||
end |
||||
end |
||||
|
||||
def batch |
||||
@form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button)) |
||||
@form.save |
||||
rescue ActionController::ParameterMissing |
||||
flash[:alert] = I18n.t('admin.ip_blocks.no_ip_block_selected') |
||||
rescue Mastodon::NotPermittedError |
||||
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') |
||||
ensure |
||||
redirect_to admin_ip_blocks_path |
||||
end |
||||
|
||||
private |
||||
|
||||
def resource_params |
||||
params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in) |
||||
end |
||||
|
||||
def action_from_button |
||||
'delete' if params[:delete] |
||||
end |
||||
|
||||
def form_ip_block_batch_params |
||||
params.require(:form_ip_block_batch).permit(ip_block_ids: []) |
||||
end |
||||
end |
||||
end |
@ -1,38 +1,7 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
# Monkey-patch on monkey-patch. |
||||
# Because it conflicts with the request.rb patch. |
||||
class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation |
||||
def connect(socket_class, host, port, nodelay = false) |
||||
::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do |
||||
@socket = socket_class.open(host, port) |
||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay |
||||
end |
||||
end |
||||
end |
||||
|
||||
module WebfingerHelper |
||||
def webfinger!(uri) |
||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) |
||||
|
||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri |
||||
|
||||
opts = { |
||||
ssl: !hidden_service_uri, |
||||
|
||||
headers: { |
||||
'User-Agent': Mastodon::Version.user_agent, |
||||
}, |
||||
|
||||
timeout_class: HTTP::Timeout::PerOperationOriginal, |
||||
|
||||
timeout_options: { |
||||
write_timeout: 10, |
||||
connect_timeout: 5, |
||||
read_timeout: 10, |
||||
}, |
||||
} |
||||
|
||||
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger |
||||
Webfinger.new(uri).perform |
||||
end |
||||
end |
||||
|
@ -1,8 +1,21 @@ |
||||
import { changeSetting, saveSettings } from './settings'; |
||||
import { requestBrowserPermission } from './notifications'; |
||||
|
||||
export const INTRODUCTION_VERSION = 20181216044202; |
||||
|
||||
export const closeOnboarding = () => dispatch => { |
||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); |
||||
dispatch(saveSettings()); |
||||
|
||||
dispatch(requestBrowserPermission((permission) => { |
||||
if (permission === 'granted') { |
||||
dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); |
||||
dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); |
||||
dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); |
||||
dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); |
||||
dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); |
||||
dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); |
||||
dispatch(saveSettings()); |
||||
} |
||||
})); |
||||
}; |
||||
|
@ -0,0 +1,30 @@ |
||||
import React from 'react'; |
||||
import Icon from 'mastodon/components/icon'; |
||||
import Button from 'mastodon/components/button'; |
||||
import { requestBrowserPermission } from 'mastodon/actions/notifications'; |
||||
import { connect } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import { FormattedMessage } from 'react-intl'; |
||||
|
||||
export default @connect(() => {}) |
||||
class NotificationsPermissionBanner extends React.PureComponent { |
||||
|
||||
static propTypes = { |
||||
dispatch: PropTypes.func.isRequired, |
||||
}; |
||||
|
||||
handleClick = () => { |
||||
this.props.dispatch(requestBrowserPermission()); |
||||
} |
||||
|
||||
render () { |
||||
return ( |
||||
<div className='notifications-permission-banner'> |
||||
<h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2> |
||||
<p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p> |
||||
<Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,10 @@ |
||||
import ready from '../ready'; |
||||
|
||||
export let assetHost = ''; |
||||
|
||||
ready(() => { |
||||
const cdnHost = document.querySelector('meta[name=cdn-host]'); |
||||
if (cdnHost) { |
||||
assetHost = cdnHost.content || ''; |
||||
} |
||||
}); |
@ -0,0 +1,29 @@ |
||||
// Handles browser quirks, based on
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
||||
|
||||
const checkNotificationPromise = () => { |
||||
try { |
||||
Notification.requestPermission().then(); |
||||
} catch(e) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
}; |
||||
|
||||
const handlePermission = (permission, callback) => { |
||||
// Whatever the user answers, we make sure Chrome stores the information
|
||||
if(!('permission' in Notification)) { |
||||
Notification.permission = permission; |
||||
} |
||||
|
||||
callback(Notification.permission); |
||||
}; |
||||
|
||||
export const requestNotificationPermission = (callback) => { |
||||
if (checkNotificationPromise()) { |
||||
Notification.requestPermission().then((permission) => handlePermission(permission, callback)); |
||||
} else { |
||||
Notification.requestPermission((permission) => handlePermission(permission, callback)); |
||||
} |
||||
}; |
@ -0,0 +1,21 @@ |
||||
// Dynamically set webpack's loading path depending on a meta header, in order
|
||||
// to share the same assets regardless of instance configuration.
|
||||
// See https://webpack.js.org/guides/public-path/#on-the-fly
|
||||
|
||||
function removeOuterSlashes(string) { |
||||
return string.replace(/^\/*/, '').replace(/\/*$/, ''); |
||||
} |
||||
|
||||
function formatPublicPath(host = '', path = '') { |
||||
let formattedHost = removeOuterSlashes(host); |
||||
if (formattedHost && !/^http/i.test(formattedHost)) { |
||||
formattedHost = `//${formattedHost}`; |
||||
} |
||||
const formattedPath = removeOuterSlashes(path); |
||||
return `${formattedHost}/${formattedPath}/`; |
||||
} |
||||
|
||||
const cdnHost = document.querySelector('meta[name=cdn-host]'); |
||||
|
||||
// eslint-disable-next-line camelcase, no-undef, no-unused-vars
|
||||
__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH); |
@ -0,0 +1,32 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class FastIpMap |
||||
MAX_IPV4_PREFIX = 32 |
||||
MAX_IPV6_PREFIX = 128 |
||||
|
||||
# @param [Enumerable<IPAddr>] addresses |
||||
def initialize(addresses) |
||||
@fast_lookup = {} |
||||
@ranges = [] |
||||
|
||||
# Hash look-up is faster but only works for exact matches, so we split |
||||
# exact addresses from non-exact ones |
||||
addresses.each do |address| |
||||
if (address.ipv4? && address.prefix == MAX_IPV4_PREFIX) || (address.ipv6? && address.prefix == MAX_IPV6_PREFIX) |
||||
@fast_lookup[address.to_s] = true |
||||
else |
||||
@ranges << address |
||||
end |
||||
end |
||||
|
||||
# We're more likely to hit wider-reaching ranges when checking for |
||||
# inclusion, so make sure they're sorted first |
||||
@ranges.sort_by!(&:prefix) |
||||
end |
||||
|
||||
# @param [IPAddr] address |
||||
# @return [Boolean] |
||||
def include?(address) |
||||
@fast_lookup[address.to_s] || @ranges.any? { |cidr| cidr.include?(address) } |
||||
end |
||||
end |
@ -0,0 +1,93 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Webfinger |
||||
class Error < StandardError; end |
||||
|
||||
class Response |
||||
def initialize(body) |
||||
@json = Oj.load(body, mode: :strict) |
||||
end |
||||
|
||||
def subject |
||||
@json['subject'] |
||||
end |
||||
|
||||
def link(rel, attribute) |
||||
links.dig(rel, attribute) |
||||
end |
||||
|
||||
private |
||||
|
||||
def links |
||||
@links ||= @json['links'].map { |link| [link['rel'], link] }.to_h |
||||
end |
||||
end |
||||
|
||||
def initialize(uri) |
||||
_, @domain = uri.split('@') |
||||
|
||||
raise ArgumentError, 'Webfinger requested for local account' if @domain.nil? |
||||
|
||||
@uri = uri |
||||
end |
||||
|
||||
def perform |
||||
Response.new(body_from_webfinger) |
||||
rescue Oj::ParseError |
||||
raise Webfinger::Error, "Invalid JSON in response for #{@uri}" |
||||
rescue Addressable::URI::InvalidURIError |
||||
raise Webfinger::Error, "Invalid URI for #{@uri}" |
||||
end |
||||
|
||||
private |
||||
|
||||
def body_from_webfinger(url = standard_url, use_fallback = true) |
||||
webfinger_request(url).perform do |res| |
||||
if res.code == 200 |
||||
res.body_with_limit |
||||
elsif res.code == 404 && use_fallback |
||||
body_from_host_meta |
||||
else |
||||
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" |
||||
end |
||||
end |
||||
end |
||||
|
||||
def body_from_host_meta |
||||
host_meta_request.perform do |res| |
||||
if res.code == 200 |
||||
body_from_webfinger(url_from_template(res.body_with_limit), false) |
||||
else |
||||
raise Webfinger::Error, "Request for #{@uri} returned HTTP #{res.code}" |
||||
end |
||||
end |
||||
end |
||||
|
||||
def url_from_template(str) |
||||
link = Nokogiri::XML(str).at_xpath('//xmlns:Link[@rel="lrdd"]') |
||||
|
||||
if link.present? |
||||
link['template'].gsub('{uri}', @uri) |
||||
else |
||||
raise Webfinger::Error, "Request for #{@uri} returned host-meta without link to Webfinger" |
||||
end |
||||
rescue Nokogiri::XML::XPath::SyntaxError |
||||
raise Webfinger::Error, "Invalid XML encountered in host-meta for #{@uri}" |
||||
end |
||||
|
||||
def host_meta_request |
||||
Request.new(:get, host_meta_url).add_headers('Accept' => 'application/xrd+xml, application/xml, text/xml') |
||||
end |
||||
|
||||
def webfinger_request(url) |
||||
Request.new(:get, url).add_headers('Accept' => 'application/jrd+json, application/json') |
||||
end |
||||
|
||||
def standard_url |
||||
"https://#{@domain}/.well-known/webfinger?resource=#{@uri}" |
||||
end |
||||
|
||||
def host_meta_url |
||||
"https://#{@domain}/.well-known/host-meta" |
||||
end |
||||
end |
@ -0,0 +1,31 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class Form::IpBlockBatch |
||||
include ActiveModel::Model |
||||
include Authorization |
||||
include AccountableConcern |
||||
|
||||
attr_accessor :ip_block_ids, :action, :current_account |
||||
|
||||
def save |
||||
case action |
||||
when 'delete' |
||||
delete! |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def ip_blocks |
||||
@ip_blocks ||= IpBlock.where(id: ip_block_ids) |
||||
end |
||||
|
||||
def delete! |
||||
ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) } |
||||
|
||||
ip_blocks.each do |ip_block| |
||||
ip_block.destroy |
||||
log_action :destroy, ip_block |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,41 @@ |
||||
# frozen_string_literal: true |
||||
# == Schema Information |
||||
# |
||||
# Table name: ip_blocks |
||||
# |
||||
# id :bigint(8) not null, primary key |
||||
# created_at :datetime not null |
||||
# updated_at :datetime not null |
||||
# expires_at :datetime |
||||
# ip :inet default(#<IPAddr: IPv4:0.0.0.0/255.255.255.255>), not null |
||||
# severity :integer default(NULL), not null |
||||
# comment :text default(""), not null |
||||
# |
||||
|
||||
class IpBlock < ApplicationRecord |
||||
CACHE_KEY = 'blocked_ips' |
||||
|
||||
include Expireable |
||||
|
||||
enum severity: { |
||||
sign_up_requires_approval: 5000, |
||||
no_access: 9999, |
||||
} |
||||
|
||||
validates :ip, :severity, presence: true |
||||
|
||||
after_commit :reset_cache |
||||
|
||||
class << self |
||||
def blocked?(remote_ip) |
||||
blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) } |
||||
blocked_ips_map.include?(remote_ip) |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def reset_cache |
||||
Rails.cache.delete(CACHE_KEY) |
||||
end |
||||
end |
@ -0,0 +1,15 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class IpBlockPolicy < ApplicationPolicy |
||||
def index? |
||||
admin? |
||||
end |
||||
|
||||
def create? |
||||
admin? |
||||
end |
||||
|
||||
def destroy? |
||||
admin? |
||||
end |
||||
end |
@ -0,0 +1,10 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class REST::MutedAccountSerializer < REST::AccountSerializer |
||||
attribute :mute_expires_at |
||||
|
||||
def mute_expires_at |
||||
mute = current_user.account.mute_relationships.find_by(target_account_id: object.id) |
||||
mute && !mute.expired? ? mute.expires_at : nil |
||||
end |
||||
end |
@ -0,0 +1,13 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::PrepareFollowersSynchronizationService < BaseService |
||||
include JsonLdHelper |
||||
|
||||
def call(account, params) |
||||
@account = account |
||||
|
||||
return if params['collectionId'] != @account.followers_url || invalid_origin?(params['url']) || @account.local_followers_hash == params['digest'] |
||||
|
||||
ActivityPub::FollowersSynchronizationWorker.perform_async(@account.id, params['url']) |
||||
end |
||||
end |
@ -0,0 +1,74 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::SynchronizeFollowersService < BaseService |
||||
include JsonLdHelper |
||||
include Payloadable |
||||
|
||||
def call(account, partial_collection_url) |
||||
@account = account |
||||
|
||||
items = collection_items(partial_collection_url) |
||||
return if items.nil? |
||||
|
||||
# There could be unresolved accounts (hence the call to .compact) but this |
||||
# should never happen in practice, since in almost all cases we keep an |
||||
# Account record, and should we not do that, we should have sent a Delete. |
||||
# In any case there is not much we can do if that occurs. |
||||
@expected_followers = items.map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }.compact |
||||
|
||||
remove_unexpected_local_followers! |
||||
handle_unexpected_outgoing_follows! |
||||
end |
||||
|
||||
private |
||||
|
||||
def remove_unexpected_local_followers! |
||||
@account.followers.local.where.not(id: @expected_followers.map(&:id)).each do |unexpected_follower| |
||||
UnfollowService.new.call(unexpected_follower, @account) |
||||
end |
||||
end |
||||
|
||||
def handle_unexpected_outgoing_follows! |
||||
@expected_followers.each do |expected_follower| |
||||
next if expected_follower.following?(@account) |
||||
|
||||
if expected_follower.requested?(@account) |
||||
# For some reason the follow request went through but we missed it |
||||
expected_follower.follow_requests.find_by(target_account: @account)&.authorize! |
||||
else |
||||
# Since we were not aware of the follow from our side, we do not have an |
||||
# ID for it that we can include in the Undo activity. For this reason, |
||||
# the Undo may not work with software that relies exclusively on |
||||
# matching activity IDs and not the actor and target |
||||
follow = Follow.new(account: expected_follower, target_account: @account) |
||||
ActivityPub::DeliveryWorker.perform_async(build_undo_follow_json(follow), follow.account_id, follow.target_account.inbox_url) |
||||
end |
||||
end |
||||
end |
||||
|
||||
def build_undo_follow_json(follow) |
||||
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)) |
||||
end |
||||
|
||||
def collection_items(collection_or_uri) |
||||
collection = fetch_collection(collection_or_uri) |
||||
return unless collection.is_a?(Hash) |
||||
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present? |
||||
return unless collection.is_a?(Hash) |
||||
|
||||
case collection['type'] |
||||
when 'Collection', 'CollectionPage' |
||||
collection['items'] |
||||
when 'OrderedCollection', 'OrderedCollectionPage' |
||||
collection['orderedItems'] |
||||
end |
||||
end |
||||
|
||||
def fetch_collection(collection_or_uri) |
||||
return collection_or_uri if collection_or_uri.is_a?(Hash) |
||||
return if invalid_origin?(collection_or_uri) |
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true) |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
.batch-table__row |
||||
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox |
||||
= f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id |
||||
.batch-table__row__content |
||||
.batch-table__row__content__text |
||||
%samp= "#{ip_block.ip}/#{ip_block.ip.prefix}" |
||||
- if ip_block.comment.present? |
||||
• |
||||
= ip_block.comment |
||||
%br/ |
||||
= t("simple_form.labels.ip_block.severities.#{ip_block.severity}") |
@ -0,0 +1,28 @@ |
||||
- content_for :page_title do |
||||
= t('admin.ip_blocks.title') |
||||
|
||||
- content_for :header_tags do |
||||
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' |
||||
|
||||
- if can?(:create, :ip_block) |
||||
- content_for :heading_actions do |
||||
= link_to t('admin.ip_blocks.add_new'), new_admin_ip_block_path, class: 'button' |
||||
|
||||
= form_for(@form, url: batch_admin_ip_blocks_path) do |f| |
||||
= hidden_field_tag :page, params[:page] || 1 |
||||
|
||||
.batch-table |
||||
.batch-table__toolbar |
||||
%label.batch-table__toolbar__select.batch-checkbox-all |
||||
= check_box_tag :batch_checkbox_all, nil, false |
||||
.batch-table__toolbar__actions |
||||
- if can?(:destroy, :ip_block) |
||||
= f.button safe_join([fa_icon('times'), t('admin.ip_blocks.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } |
||||
.batch-table__body |
||||
- if @ip_blocks.empty? |
||||
= nothing_here 'nothing-here--under-tabs' |
||||
- else |
||||
= render partial: 'ip_block', collection: @ip_blocks, locals: { f: f } |
||||
|
||||
= paginate @ip_blocks |
||||
|
@ -0,0 +1,20 @@ |
||||
- content_for :page_title do |
||||
= t('.title') |
||||
|
||||
= simple_form_for @ip_block, url: admin_ip_blocks_path do |f| |
||||
= render 'shared/error_messages', object: @ip_block |
||||
|
||||
.fields-group |
||||
= f.input :ip, as: :string, wrapper: :with_block_label, input_html: { placeholder: '192.0.2.0/24' } |
||||
|
||||
.fields-group |
||||
= f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: lambda { |i| I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') |
||||
|
||||
.fields-group |
||||
= f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: lambda { |severity| safe_join([I18n.t("simple_form.labels.ip_block.severities.#{severity}"), content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint')]) } |
||||
|
||||
.fields-group |
||||
= f.input :comment, as: :string, wrapper: :with_block_label |
||||
|
||||
.actions |
||||
= f.button :button, t('admin.ip_blocks.add_new'), type: :submit |
@ -0,0 +1,14 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class ActivityPub::FollowersSynchronizationWorker |
||||
include Sidekiq::Worker |
||||
|
||||
sidekiq_options queue: 'push', lock: :until_executed |
||||
|
||||
def perform(account_id, url) |
||||
@account = Account.find_by(id: account_id) |
||||
return true if @account.nil? |
||||
|
||||
ActivityPub::SynchronizeFollowersService.new.call(@account, url) |
||||
end |
||||
end |
@ -0,0 +1,10 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class DeleteMuteWorker |
||||
include Sidekiq::Worker |
||||
|
||||
def perform(mute_id) |
||||
mute = Mute.find_by(id: mute_id) |
||||
UnmuteService.new.call(mute.account, mute.target_account) if mute&.expired? |
||||
end |
||||
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue