Add subscribe features

This commit is contained in:
noellabo 2019-08-30 17:02:41 +09:00
parent fd5b7c14fa
commit 92a9a23eb6
70 changed files with 1456 additions and 25 deletions

View file

@ -56,7 +56,7 @@ class StatusesIndex < Chewy::Index
field :id, type: 'long'
field :account_id, type: 'long'
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :text, type: 'text', value: ->(status) { status.index_text } do
field :stemmed, type: 'text', analyzer: 'content'
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class Api::V1::AccountSubscribesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:follows' }, except: [:index, :show]
before_action :require_user!
before_action :set_account_subscribe, except: [:index, :create]
def index
@account_subscribes = AccountSubscribe.where(account: current_account).all
render json: @account_subscribes, each_serializer: REST::AccountSubscribeSerializer
end
def show
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
end
def create
@account_subscribe = AccountSubscribe.create!(account_subscribe_params.merge(account: current_account))
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
end
def update
@account_subscribe.update!(account_subscribe_params)
render json: @account_subscribe, serializer: REST::AccountSubscribeSerializer
end
def destroy
@account_subscribe.destroy!
render_empty
end
private
def set_account_subscribe
@account_subscribe = AccountSubscribe.where(account: current_account).find(params[:id])
end
def account_subscribe_params
params.permit(:acct)
end
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
class Api::V1::DomainSubscribesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:follows' }, except: :index
before_action :require_user!
before_action :set_domain_subscribe, except: [:index, :create]
def index
@domain_subscribes = DomainSubscribe.where(account: current_account).all
render json: @domain_subscribes, each_serializer: REST::DomainSubscribeSerializer
end
def create
@domain_subscribe = DomainSubscribe.create!(domain_subscribe_params.merge(account: current_account))
render json: @domain_subscribe, serializer: REST::DomainSubscribeSerializer
end
def destroy
@domain_subscribe.destroy!
render_empty
end
private
def set_domain_subscribe
@domain_subscribe = DomainSubscribe.where(account: current_account).find(params[:id])
end
def domain_subscribe_params
params.permit(:domain, :list_id, :exclude_reblog)
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class Api::V1::FollowTagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:follows' }, except: [:index, :show]
before_action :require_user!
before_action :set_follow_tag, except: [:index, :create]
def index
@follow_tags = FollowTag.where(account: current_account).all
render json: @follow_tags, each_serializer: REST::FollowTagSerializer
end
def show
render json: @follow_tag, serializer: REST::FollowTagSerializer
end
def create
@follow_tag = FollowTag.create!(follow_tag_params.merge(account: current_account))
render json: @follow_tag, serializer: REST::FollowTagSerializer
end
def update
@follow_tag.update!(follow_tag_params)
render json: @follow_tag, serializer: REST::FollowTagSerializer
end
def destroy
@follow_tag.destroy!
render_empty
end
private
def set_follow_tag
@follow_tag = FollowTag.where(account: current_account).find(params[:id])
end
def follow_tag_params
params.permit(:name)
end
end

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
class Api::V1::KeywordSubscribesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:follows' }, except: [:index, :show]
before_action :require_user!
before_action :set_keyword_subscribes, only: :index
before_action :set_keyword_subscribe, only: [:show, :update, :destroy]
respond_to :json
def index
render json: @keyword_subscribes, each_serializer: REST::KeywordSubscribesSerializer
end
def create
@keyword_subscribe = current_account.keyword_subscribes.create!(resource_params)
render json: @keyword_subscribe, serializer: REST::KeywordSubscribesSerializer
end
def show
render json: @keyword_subscribe, serializer: REST::KeywordSubscribesSerializer
end
def update
@keyword_subscribe.update!(resource_params)
render json: @keyword_subscribe, serializer: REST::KeywordSubscribesSerializer
end
def destroy
@keyword_subscribe.destroy!
render_empty
end
private
def set_keyword_subscribes
@keyword_subscribes = current_account.keyword_subscribes
end
def set_keyword_subscribe
@keyword_subscribe = current_account.keyword_subscribes.find(params[:id])
end
def resource_params
params.permit(:name, :keyword, :exclude_keyword, :ignorecase, :regexp, :ignore_block, :disabled, :list_id)
end
end

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
class Settings::AccountSubscribesController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_account_subscribings, only: :index
class AccountInput
include ActiveModel::Model
include ActiveModel::Attributes
attribute :acct, :string
end
def index
@account_input = AccountInput.new
end
def create
acct = account_subscribe_params[:acct].strip
acct = acct[1..-1] if acct.start_with?("@")
begin
target_account = AccountSubscribeService.new.call(current_account, acct)
rescue
target_account = nil
end
if target_account
redirect_to settings_account_subscribes_path
else
set_account_subscribings
render :index
end
end
def destroy
target_account = current_account.active_subscribes.find(params[:id]).target_account
UnsubscribeAccountService.new.call(current_account, target_account)
redirect_to settings_account_subscribes_path
end
private
def set_account_subscribings
@account_subscribings = current_account.active_subscribes.order(:updated_at).reject(&:new_record?).map do |subscribing|
{id: subscribing.id, acct: subscribing.target_account.acct}
end
end
def account_subscribe_params
params.require(:account_input).permit(:acct)
end
end

View file

@ -0,0 +1,73 @@
# frozen_string_literal: true
class Settings::DomainSubscribesController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_lists, only: [:index, :new, :edit, :update]
before_action :set_domain_subscribes, only: :index
before_action :set_domain_subscribe, only: [:edit, :update, :destroy]
def index
@domain_subscribe = DomainSubscribe.new
end
def new
@domain_subscribe = current_account.domain_subscribes.build
end
def create
@domain_subscribe = current_account.domain_subscribes.new(domain_subscribe_params)
if @domain_subscribe.save
redirect_to settings_domain_subscribes_path
else
set_domain_subscribe
render :index
end
end
def edit; end
def update
if @domain_subscribe.update(domain_subscribe_params)
redirect_to settings_domain_subscribes_path
else
render action: :edit
end
end
def destroy
@domain_subscribe.destroy!
redirect_to settings_domain_subscribes_path
end
private
def set_domain_subscribe
@domain_subscribe = current_account.domain_subscribes.find(params[:id])
end
def set_domain_subscribes
@domain_subscribes = current_account.domain_subscribes.includes(:list).order('list_id NULLS FIRST', :domain).reject(&:new_record?)
end
def set_lists
@lists = List.where(account: current_account).all
end
def domain_subscribe_params
new_params = resource_params.permit!.to_h
if resource_params[:list_id] == '-1'
list = List.find_or_create_by!({ account: current_account, title: new_params[:domain] })
new_params.merge!({list_id: list.id})
end
new_params
end
def resource_params
params.require(:domain_subscribe).permit(:domain, :list_id, :exclude_reblog)
end
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Settings::FollowTagsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_follow_tags, only: :index
before_action :set_follow_tag, except: [:index, :create]
def index
@follow_tag = FollowTag.new
end
def create
@follow_tag = current_account.follow_tags.new(follow_tag_params)
if @follow_tag.save
redirect_to settings_follow_tags_path
else
set_follow_tags
render :index
end
end
def destroy
@follow_tag.destroy!
redirect_to settings_follow_tags_path
end
private
def set_follow_tag
@follow_tag = current_account.follow_tags.find(params[:id])
end
def set_follow_tags
@follow_tags = current_account.follow_tags.order(:updated_at).reject(&:new_record?)
end
def follow_tag_params
params.require(:follow_tag).permit(:name)
end
end

View file

@ -0,0 +1,78 @@
# frozen_string_literal: true
class Settings::KeywordSubscribesController < ApplicationController
include Authorization
layout 'admin'
before_action :set_lists, only: [:index, :new, :edit, :update]
before_action :set_keyword_subscribes, only: :index
before_action :set_keyword_subscribe, only: [:edit, :update, :destroy]
before_action :set_body_classes
def index
@keyword_subscribe = KeywordSubscribe.new
end
def new
@keyword_subscribe = current_account.keyword_subscribes.build
end
def create
@keyword_subscribe = current_account.keyword_subscribes.build(keyword_subscribe_params)
if @keyword_subscribe.save
redirect_to settings_keyword_subscribes_path
else
render action: :new
end
end
def edit; end
def update
if @keyword_subscribe.update(keyword_subscribe_params)
redirect_to settings_keyword_subscribes_path
else
render action: :edit
end
end
def destroy
@keyword_subscribe.destroy
redirect_to settings_keyword_subscribes_path
end
private
def set_keyword_subscribe
@keyword_subscribe = current_account.keyword_subscribes.find(params[:id])
end
def set_keyword_subscribes
@keyword_subscribes = current_account.keyword_subscribes.includes(:list).order('list_id NULLS FIRST', :name).reject(&:new_record?)
end
def set_lists
@lists = List.where(account: current_account).all
end
def keyword_subscribe_params
new_params = resource_params.permit!.to_h
if resource_params[:list_id] == '-1'
list = List.find_or_create_by!({ account: current_account, title: new_params[:name].presence || "keyword_#{Time.now.strftime('%Y%m%d%H%M%S')}" })
new_params.merge!({list_id: list.id})
end
new_params
end
def resource_params
params.require(:keyword_subscribe).permit(:name, :keyword, :exclude_keyword, :ignorecase, :regexp, :ignore_block, :disabled, :list_id)
end
def set_body_classes
@body_classes = 'admin'
end
end

View file

@ -0,0 +1,7 @@
module ListsHelper
def home_list_new(lists)
items = { nil => t('column.home') }
items.merge!(lists&.pluck(:id, :title).to_h)
items.merge!({ -1 => t('lists.add_new') })
end
end

View file

@ -916,3 +916,7 @@ a.name-tag,
.dashboard__counters.admin-account-counters {
margin-top: 10px;
}
.exclude-keyword {
color: $error-value-color;
}

View file

@ -49,6 +49,11 @@
}
}
th.nowrap,
td.nowrap {
white-space: nowrap;
}
&.inline-table {
& > tbody > tr:nth-child(odd) {
& > td,

View file

@ -100,10 +100,10 @@ class FeedManager
# @param [Account] from_account
# @param [Account] into_account
# @return [void]
def merge_into_home(from_account, into_account)
def merge_into_home(from_account, into_account, public_only = false)
timeline_key = key(:home, into_account.id)
aggregate = into_account.user&.aggregates_reblogs?
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
query = from_account.statuses.where(visibility: public_only ? :public : [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
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
@ -360,21 +360,32 @@ class FeedManager
return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] }
if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply
should_filter = !crutches[:following][status.in_reply_to_account_id] # 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
if status.reblog? # Filter out a reblog
should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed
should_filter ||= crutches[:domain_blocking][status.account.domain] # or the reblogger's domain is blocked
should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me
should_filter ||= crutches[:domain_blocking_r][status.reblog.account.domain] # or the author's domain is blocked
return !!should_filter
elsif status.reblog? # Filter out a reblog
should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed
should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me
should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] # or the author's domain is blocked
else
if status.reply? # Filter out a reply
should_filter = !crutches[:following][status.in_reply_to_account_id] # 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
should_filter &&= !status.tags.any? { |tag| crutches[:following_tag_by][tag.id] } # and It's not follow tag
should_filter &&= !KeywordSubscribe.match?(status.index_text, account_id: receiver_id) # and It's not subscribe keywords
should_filter &&= !crutches[:domain_subscribe][status.account.domain] # and It's not domain subscribes
return true if should_filter
end
should_filter = crutches[:domain_blocking][status.account.domain]
should_filter &&= !crutches[:following][status.account_id]
should_filter &&= !crutches[:account_subscribe][status.account_id]
should_filter &&= !KeywordSubscribe.match?(status.index_text, account_id: receiver_id, as_ignore_block: true)
return !!should_filter
end
false
end
# Check if status should not be added to the mentions feed
@ -563,13 +574,16 @@ class FeedManager
arr
end
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true)
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).index_with(true)
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).index_with(true)
crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).index_with(true)
crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).index_with(true)
crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.account&.domain }.compact).pluck(:domain).index_with(true)
crutches[:domain_blocking_r] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).index_with(true)
crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).index_with(true)
crutches[:following_tag_by] = FollowTag.where(account_id: receiver_id, tag: statuses.map { |s| s.tags }.flatten.uniq.compact).pluck(:tag_id).index_with(true)
crutches[:domain_subscribe] = DomainSubscribe.where(account_id: receiver_id, list_id: nil, domain: statuses.map { |s| s&.account&.domain }.compact).pluck(:domain).index_with(true)
crutches[:account_subscribe] = AccountSubscribe.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id).compact).pluck(:target_account_id).index_with(true)
crutches
end
end

View file

@ -0,0 +1,23 @@
# == Schema Information
#
# Table name: account_subscribes
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# target_account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
# list_id :bigint(8)
#
class AccountSubscribe < ApplicationRecord
belongs_to :account
belongs_to :target_account, class_name: 'Account'
belongs_to :list, optional: true
validates :account_id, uniqueness: { scope: [:target_account_id, :list_id] }
scope :recent, -> { reorder(id: :desc) }
scope :subscribed_lists, ->(account) { AccountSubscribe.where(target_account_id: account.id).where.not(list_id: nil).select(:list_id).uniq }
end

View file

@ -61,6 +61,13 @@ module AccountAssociations
has_and_belongs_to_many :tags
has_many :favourite_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
has_many :follow_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
# KeywordSubscribes
has_many :keyword_subscribes, inverse_of: :account, dependent: :destroy
# DomainSubscribes
has_many :domain_subscribes, inverse_of: :account, dependent: :destroy
# Account deletion requests
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy

View file

@ -3,7 +3,7 @@
module AccountCounters
extend ActiveSupport::Concern
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze
ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count subscribing_count).freeze
included do
has_one :account_stat, inverse_of: :account

View file

@ -98,6 +98,13 @@ module AccountInteractions
has_many :conversation_mutes, dependent: :destroy
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
has_many :announcement_mutes, dependent: :destroy
# Subscribers
has_many :active_subscribes, class_name: 'AccountSubscribe', foreign_key: 'account_id', dependent: :destroy
has_many :passive_subscribes, class_name: 'AccountSubscribe', foreign_key: 'target_account_id', dependent: :destroy
has_many :subscribing, through: :active_subscribes, source: :target_account
has_many :subscribers, through: :passive_subscribes, source: :account
end
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
@ -183,6 +190,10 @@ module AccountInteractions
block&.destroy
end
def subscribe!(other_account)
active_subscribes.find_or_create_by!(target_account: other_account)
end
def following?(other_account)
active_relationships.where(target_account: other_account).exists?
end
@ -243,12 +254,22 @@ module AccountInteractions
account_pins.where(target_account: account).exists?
end
def subscribing?(other_account)
active_subscribes.where(target_account: other_account).exists?
end
def followers_for_local_distribution
followers.local
.joins(:user)
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
end
def subscribers_for_local_distribution
subscribers.local
.joins(:user)
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
end
def lists_for_local_distribution
lists.joins(account: :user)
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)

View file

@ -0,0 +1,24 @@
# == Schema Information
#
# Table name: domain_subscribes
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# list_id :bigint(8)
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# exclude_reblog :boolean default(TRUE)
#
class DomainSubscribe < ApplicationRecord
belongs_to :account
belongs_to :list, optional: true
validates :domain, presence: true
validates :account_id, uniqueness: { scope: [:domain, :list_id] }
scope :domain_to_home, ->(domain) { where(domain: domain).where(list_id: nil) }
scope :domain_to_list, ->(domain) { where(domain: domain).where.not(list_id: nil) }
scope :with_reblog, ->(reblog) { where(exclude_reblog: false) if reblog }
end

View file

@ -11,6 +11,7 @@
# show_reblogs :boolean default(TRUE), not null
# uri :string
# notify :boolean default(FALSE), not null
# private :boolean default(TRUE), not null
#
class Follow < ApplicationRecord

View file

@ -30,6 +30,7 @@ class FollowRequest < ApplicationRecord
def authorize!
account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri, bypass_limit: true)
UnsubscribeAccountService.new.call(account, target_account) if account.subscribing?(target_account)
MergeWorker.perform_async(target_account.id, account.id) if account.local?
destroy!
end

27
app/models/follow_tag.rb Normal file
View file

@ -0,0 +1,27 @@
# == Schema Information
#
# Table name: follow_tags
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# tag_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
# list_id :bigint(8)
#
class FollowTag < ApplicationRecord
belongs_to :account, inverse_of: :follow_tags, required: true
belongs_to :tag, inverse_of: :follow_tags, required: true
belongs_to :list, optional: true
delegate :name, to: :tag, allow_nil: true
validates_associated :tag, on: :create
validates :name, presence: true, on: :create
validates :account_id, uniqueness: { scope: [:tag_id, :list_id] }
def name=(str)
self.tag = Tag.find_or_create_by_names(str.strip)&.first
end
end

View file

@ -0,0 +1,107 @@
# == Schema Information
#
# Table name: keyword_subscribes
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# keyword :string not null
# ignorecase :boolean default(TRUE)
# regexp :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
# name :string default(""), not null
# ignore_block :boolean default(FALSE)
# disabled :boolean default(FALSE)
# exclude_keyword :string default(""), not null
# list_id :bigint(8)
#
class KeywordSubscribe < ApplicationRecord
belongs_to :account, inverse_of: :keyword_subscribes, required: true
belongs_to :list, optional: true
validates :keyword, presence: true
validate :validate_subscribes_limit, on: :create
validate :validate_keyword_regexp_syntax
validate :validate_exclude_keyword_regexp_syntax
validate :validate_uniqueness_in_account, on: :create
scope :active, -> { where(disabled: false) }
scope :ignore_block, -> { where(ignore_block: true) }
scope :home, -> { where(list_id: nil) }
scope :list, -> { where.not(list_id: nil) }
scope :without_local_followed_home, ->(account) { home.where.not(account: account.followers.local).where.not(account: account.subscribers.local) }
scope :without_local_followed_list, ->(account) { list.where.not(list_id: ListAccount.followed_lists(account)).where.not(list_id: AccountSubscribe.subscribed_lists(account)) }
def keyword=(val)
super(regexp ? val : keyword_normalization(val))
end
def exclude_keyword=(val)
super(regexp ? val : keyword_normalization(val))
end
def match?(text)
keyword_regexp.match?(text) && (exclude_keyword.empty? || !exclude_keyword_regexp.match?(text))
end
def keyword_regexp
to_regexp keyword
end
def exclude_keyword_regexp
to_regexp exclude_keyword
end
class << self
def match?(text, account_id: account_id = nil, as_ignore_block: as_ignore_block = false)
target = KeywordSubscribe.active
target = target.where(account_id: account_id) if account_id.present?
target = target.ignore_block if as_ignore_block
!target.find{ |t| t.match?(text) }.nil?
end
end
private
def keyword_normalization(val)
val.to_s.strip.gsub(/\s{2,}/, ' ').split(/\s*,\s*/).reject(&:blank?).uniq.join(',')
end
def to_regexp(words)
Regexp.new(regexp ? words : "(?<![#])(#{words.split(',').map do |k|
sb = k =~ /\A[A-Za-z0-9]/ ? '\b' : k !~ /\A[\/\.]/ ? '(?<![\/\.])' : ''
eb = k =~ /[A-Za-z0-9]\z/ ? '\b' : k !~ /[\/\.]\z/ ? '(?![\/\.])' : ''
/(?m#{ignorecase ? 'i': ''}x:#{sb}#{Regexp.quote(k).gsub("\\ ", "[[:space:]]+")}#{eb})/
end.join('|')})", ignorecase)
end
def validate_keyword_regexp_syntax
return unless regexp
begin
Regexp.compile(keyword, ignorecase)
rescue RegexpError => exception
errors.add(:base, I18n.t('keyword_subscribes.errors.regexp', message: exception.message))
end
end
def validate_exclude_keyword_regexp_syntax
return unless regexp
begin
Regexp.compile(exclude_keyword, ignorecase)
rescue RegexpError => exception
errors.add(:base, I18n.t('keyword_subscribes.errors.regexp', message: exception.message))
end
end
def validate_subscribes_limit
errors.add(:base, I18n.t('keyword_subscribes.errors.limit')) if account.keyword_subscribes.count >= 100
end
def validate_uniqueness_in_account
errors.add(:base, I18n.t('keyword_subscribes.errors.duplicate')) if account.keyword_subscribes.find_by(keyword: keyword, exclude_keyword: exclude_keyword, list_id: list_id)
end
end

View file

@ -18,6 +18,8 @@ class ListAccount < ApplicationRecord
before_validation :set_follow
scope :followed_lists, ->(account) { ListAccount.includes(:follow).where(follows: { account_id: account.id }).pluck(:list_id).uniq }
private
def set_follow

View file

@ -244,6 +244,10 @@ class Status < ApplicationRecord
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + (quote? ? CustomEmoji.from_text([quote.spoiler_text, quote.text].join(' '), quote.account.domain) : [])
end
def index_text
@index_text ||= [spoiler_text, Formatter.instance.plaintext(self)].concat(media_attachments.map(&:description)).concat(preloadable_poll ? preloadable_poll.options : []).join("\n\n")
end
def replies_count
status_stat&.replies_count || 0
end

View file

@ -23,6 +23,7 @@ class Tag < ApplicationRecord
has_many :favourite_tags, dependent: :destroy, inverse_of: :tag
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
has_many :follow_tags, dependent: :destroy, inverse_of: :tag
HASHTAG_SEPARATORS = "_\u00B7\u200c"
HASHTAG_NAME_RE = "([[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}][[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)"

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class REST::AccountSubscribeSerializer < ActiveModel::Serializer
attributes :id, :target_account, :updated_at
def id
object.id.to_s
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class REST::DomainSubscribeSerializer < ActiveModel::Serializer
attributes :id, :list_id, :domain, :updated_at
def id
object.id.to_s
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class REST::FollowTagSerializer < ActiveModel::Serializer
attributes :id, :name, :updated_at
def id
object.id.to_s
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class REST::KeywordSubscribesSerializer < ActiveModel::Serializer
attributes :id, :name, :keyword, :exclude_keyword, :ignorecase, :regexp, :ignore_block, :disabled, :exclude_home
def id
object.id.to_s
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
class AccountSubscribeService < BaseService
# Subscribe a remote user
# @param [Account] source_account From which to subscribe
# @param [String, Account] uri User URI to subscribe in the form of username@domain (or account record)
def call(source_account, target_account)
begin
target_account = ResolveAccountService.new.call(target_account, skip_webfinger: false)
rescue
target_account = nil
end
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || (!target_account.local? && target_account.ostatus?) || source_account.domain_blocking?(target_account.domain)
if source_account.following?(target_account)
return
elsif source_account.subscribing?(target_account)
return
end
ActivityTracker.increment('activity:interactions')
subscribe = source_account.subscribe!(target_account)
MergeWorker.perform_async(target_account.id, source_account.id, true)
subscribe
end
end

View file

@ -18,11 +18,18 @@ class FanOutOnWriteService < BaseService
deliver_to_lists(status)
end
return if status.account.silenced? || !status.public_visibility? || status.reblog?
return if status.account.silenced? || !status.public_visibility?
deliver_to_domain_subscribers(status)
return if status.reblog?
render_anonymous_payload(status)
deliver_to_hashtags(status)
deliver_to_hashtag_followers(status)
deliver_to_subscribers(status)
deliver_to_keyword_subscribers(status)
return if status.reply? && status.in_reply_to_account_id != status.account_id
@ -47,6 +54,72 @@ class FanOutOnWriteService < BaseService
end
end
def deliver_to_subscribers(status)
Rails.logger.debug "Delivering status #{status.id} to subscribers"
status.account.subscribers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |subscribings|
FeedInsertWorker.push_bulk(subscribings) do |subscribing|
[status.id, subscribing.id, :home]
end
end
end
def deliver_to_domain_subscribers(status)
Rails.logger.debug "Delivering status #{status.id} to domain subscribers"
deliver_to_domain_subscribers_home(status)
deliver_to_domain_subscribers_list(status)
end
def deliver_to_domain_subscribers_home(status)
DomainSubscribe.domain_to_home(status.account.domain).with_reblog(status.reblog?).select(:id, :account_id).find_in_batches do |subscribes|
FeedInsertWorker.push_bulk(subscribes) do |subscribe|
[status.id, subscribe.account_id, :home]
end
end
end
def deliver_to_domain_subscribers_list(status)
DomainSubscribe.domain_to_list(status.account.domain).with_reblog(status.reblog?).select(:id, :list_id).find_in_batches do |subscribes|
FeedInsertWorker.push_bulk(subscribes) do |subscribe|
[status.id, subscribe.list_id, :list]
end
end
end
def deliver_to_keyword_subscribers(status)
Rails.logger.debug "Delivering status #{status.id} to keyword subscribers"
deliver_to_keyword_subscribers_home(status)
deliver_to_keyword_subscribers_list(status)
end
def deliver_to_keyword_subscribers_home(status)
match_accounts = []
KeywordSubscribe.active.without_local_followed_home(status.account).order(:account_id).each do |keyword_subscribe|
next if match_accounts[-1] == keyword_subscribe.account_id
match_accounts << keyword_subscribe.account_id if keyword_subscribe.match?(status.index_text)
end
FeedInsertWorker.push_bulk(match_accounts) do |match_account|
[status.id, match_account, :home]
end
end
def deliver_to_keyword_subscribers_list(status)
match_lists = []
KeywordSubscribe.active.without_local_followed_list(status.account).order(:list_id).each do |keyword_subscribe|
next if match_lists[-1] == keyword_subscribe.list_id
match_lists << keyword_subscribe.list_id if keyword_subscribe.match?(status.index_text)
end
FeedInsertWorker.push_bulk(match_lists) do |match_list|
[status.id, match_list, :list]
end
end
def deliver_to_lists(status)
Rails.logger.debug "Delivering status #{status.id} to lists"
@ -81,6 +154,14 @@ class FanOutOnWriteService < BaseService
end
end
def deliver_to_hashtag_followers(status)
Rails.logger.debug "Delivering status #{status.id} to hashtag followers"
FeedInsertWorker.push_bulk(FollowTag.where(tag: status.tags).pluck(:account_id).uniq) do |follower|
[status.id, follower, :home]
end
end
def deliver_to_public(status)
Rails.logger.debug "Delivering status #{status.id} to public timeline"

View file

@ -79,6 +79,7 @@ class FollowService < BaseService
def direct_follow!
follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
UnsubscribeAccountService.new.call(@source_account, @target_account) if @source_account.subscribing?(@target_account)
LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
MergeWorker.perform_async(@target_account.id, @source_account.id)

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class UnsubscribeAccountService < BaseService
# UnsubscribeAccount
# @param [Account] source_account Where to unsubscribe from
# @param [Account] target_account Which to unsubscribe
def call(source_account, target_account)
subscribe = AccountSubscribe.find_by(account: source_account, target_account: target_account)
return unless subscribe
subscribe.destroy!
UnmergeWorker.perform_async(target_account.id, source_account.id)
subscribe
end
end

View file

@ -0,0 +1,25 @@
- content_for :page_title do
= t('settings.account_subscribes')
%p= t('account_subscribes.hint_html')
%hr.spacer/
= simple_form_for :account_input, url: settings_account_subscribes_path do |f|
.fields-group
= f.input :acct, wrapper: :with_block_label, hint: false
.actions
= f.button :button, t('account_subscribes.add_new'), type: :submit
%hr.spacer/
- @account_subscribings.each do |account_subscribing|
.directory__tag
%div
%h4
= fa_icon 'user'
= account_subscribing[:acct]
%small
= table_link_to 'trash', t('filters.index.delete'), settings_account_subscribe_path(account_subscribing), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View file

@ -0,0 +1,11 @@
.fields-group
= f.input :domain, wrapper: :with_label
.fields-group
= f.input :exclude_reblog, wrapper: :with_label
.fields-group
= f.label :list_id
= f.collection_select :list_id, home_list_new(@lists), :first, :last
%hr.spacer/

View file

@ -0,0 +1,9 @@
- content_for :page_title do
= t('domain_subscribes.edit.title')
= simple_form_for @domain_subscribe, url: settings_domain_subscribe_path(@domain_subscribe), method: :put do |f|
= render 'shared/error_messages', object: @domain_subscribe
= render 'fields', f: f
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,35 @@
- content_for :page_title do
= t('settings.domain_subscribes')
%p= t('domain_subscribes.hint_html')
%hr.spacer/
.table-wrapper
%table.table
%thead
%tr
%th= t('simple_form.labels.domain_subscribe.domain')
%th.nowrap= t('simple_form.labels.domain_subscribe.reblog')
%th.nowrap= t('simple_form.labels.domain_subscribe.timeline')
%th.nowrap
%tbody
- @domain_subscribes.each do |domain_subscribe|
%tr
%td
= domain_subscribe.domain
%td.nowrap
- if domain_subscribe.exclude_reblog
= fa_icon('times')
%td.nowrap
- if domain_subscribe.list_id
= fa_icon 'list-ul'
= domain_subscribe.list&.title
- else
= fa_icon 'home'
= t 'domain_subscribes.home'
%td.nowrap
= table_link_to 'pencil', t('domain_subscribes.edit.title'), edit_settings_domain_subscribe_path(domain_subscribe)
= table_link_to 'trash', t('filters.index.delete'), settings_domain_subscribe_path(domain_subscribe), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
= link_to t('domain_subscribes.new.title'), new_settings_domain_subscribe_path, class: 'button'

View file

@ -0,0 +1,9 @@
- content_for :page_title do
= t('domain_subscribes.new.title')
= simple_form_for @domain_subscribe, url: settings_domain_subscribes_path do |f|
= render 'shared/error_messages', object: @domain_subscribe
= render 'fields', f: f
.actions
= f.button :button, t('domain_subscribes.new.title'), type: :submit

View file

@ -0,0 +1,26 @@
- content_for :page_title do
= t('settings.follow_tags')
%p= t('follow_tags.hint_html')
%hr.spacer/
= simple_form_for @follow_tag, url: settings_follow_tags_path do |f|
= render 'shared/error_messages', object: @follow_tag
.fields-group
= f.input :name, wrapper: :with_block_label, hint: false
.actions
= f.button :button, t('follow_tags.add_new'), type: :submit
%hr.spacer/
- @follow_tags.each do |follow_tag|
.directory__tag{ class: params[:tag] == follow_tag.name ? 'active' : nil }
%div
%h4
= fa_icon 'hashtag'
= follow_tag.name
%small
= table_link_to 'trash', t('filters.index.delete'), settings_follow_tag_path(follow_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View file

@ -0,0 +1,24 @@
.fields-group
= f.input :name, as: :string, wrapper: :with_label
.fields-group
= f.input :keyword, as: :string, wrapper: :with_label
.fields-group
= f.input :exclude_keyword, as: :string, wrapper: :with_label
.fields-group
= f.input :ignorecase, wrapper: :with_label
.fields-group
= f.input :regexp, wrapper: :with_label
.fields-group
= f.input :ignore_block, wrapper: :with_label
.fields-group
= f.label :list_id
= f.collection_select :list_id, home_list_new(@lists), :first, :last
.fields-group
= f.input :disabled, wrapper: :with_label

View file

@ -0,0 +1,9 @@
- content_for :page_title do
= t('keyword_subscribes.edit.title')
= simple_form_for @keyword_subscribe, url: settings_keyword_subscribe_path(@keyword_subscribe), method: :put do |f|
= render 'shared/error_messages', object: @keyword_subscribe
= render 'fields', f: f
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,66 @@
- content_for :page_title do
= t('keyword_subscribes.index.title')
%p= t('keyword_subscribes.hint_html')
%hr.spacer/
= render 'shared/error_messages', object: @keyword_subscribe
.table-wrapper
%table.table
%thead
%tr
%th.nowrap= t('simple_form.labels.keyword_subscribes.name')
%th.nowrap= t('simple_form.labels.keyword_subscribes.regexp')
%th= t('simple_form.labels.keyword_subscribes.keyword')
%th.nowrap= t('simple_form.labels.keyword_subscribes.ignorecase')
%th.nowrap= t('simple_form.labels.keyword_subscribes.ignore_block')
%th.nowrap= t('simple_form.labels.keyword_subscribes.timeline')
%th.nowrap= t('simple_form.labels.keyword_subscribes.disabled')
%th.nowrap
%tbody
- @keyword_subscribes.each do |keyword_subscribe|
%tr
%td.nowrap= keyword_subscribe.name
%td.nowrap
- if keyword_subscribe.regexp
= t 'keyword_subscribes.regexp.enabled'
- else
= t 'keyword_subscribes.regexp.disabled'
%td
.include-keyword
= keyword_subscribe.keyword
.exclude-keyword
= keyword_subscribe.exclude_keyword
%td.nowrap
- if keyword_subscribe.ignorecase
= t 'keyword_subscribes.ignorecase.enabled'
- else
= t 'keyword_subscribes.ignorecase.disabled'
%td.nowrap
- if keyword_subscribe.ignore_block
= t 'keyword_subscribes.ignore_block'
%td.nowrap
- if keyword_subscribe.list_id
= fa_icon 'list-ul'
= keyword_subscribe.list&.title
- else
= fa_icon 'home'
= t 'keyword_subscribe.home'
%td.nowrap
- if !keyword_subscribe.disabled
%span.positive-hint
= fa_icon('check')
= ' '
= t 'keyword_subscribes.enabled'
- else
%span.negative-hint
= fa_icon('times')
= ' '
= t 'keyword_subscribes.disabled'
%td.nowrap
= table_link_to 'pencil', t('keyword_subscribes.edit.title'), edit_settings_keyword_subscribe_path(keyword_subscribe)
= table_link_to 'times', t('keyword_subscribes.index.delete'), settings_keyword_subscribe_path(keyword_subscribe), method: :delete
= link_to t('keyword_subscribes.new.title'), new_settings_keyword_subscribe_path, class: 'button'

View file

@ -0,0 +1,9 @@
- content_for :page_title do
= t('keyword_subscribes.new.title')
= simple_form_for @keyword_subscribe, url: settings_keyword_subscribes_path do |f|
= render 'shared/error_messages', object: @keyword_subscribe
= render 'fields', f: f
.actions
= f.button :button, t('keyword_subscribes.new.title'), type: :submit

View file

@ -3,8 +3,8 @@
class MergeWorker
include Sidekiq::Worker
def perform(from_account_id, into_account_id)
FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id))
def perform(from_account_id, into_account_id, public_only = false)
FeedManager.instance.merge_into_home(Account.find(from_account_id), Account.find(into_account_id), public_only)
rescue ActiveRecord::RecordNotFound
true
ensure

View file

@ -87,6 +87,9 @@ en:
moderator: Mod
unavailable: Profile unavailable
unfollow: Unfollow
account_subscribes:
add_new: Add
hint_html: "<strong>What are account subscription?</strong> Insert public posts from the specified account into the home timeline. Posts received by the server (federated timeline) are targets. You cannot subscribe if you are following."
admin:
account_actions:
action: Perform action
@ -876,6 +879,33 @@ en:
directory: Profile directory
explanation: Discover users based on their interests
explore_mastodon: Explore %{title}
domain_blocks:
blocked_domains: List of limited and blocked domains
description: This is the list of servers that %{instance} limits or reject federation with.
domain: Domain
media_block: Media block
no_domain_blocks: "(No domain blocks)"
severity: Severity
severity_legend:
media_block: Media files coming from the server are neither fetched, stored, or displayed to the user.
silence: Accounts from silenced servers can be found, followed and interacted with, but their toots will not appear in the public timelines, and notifications from them will not reach local users who are not following them.
suspension: No content from suspended servers is stored or displayed, nor is any content sent to them. Interactions from suspended servers are ignored.
suspension_disclaimer: Suspended servers may occasionally retrieve public content from this server.
title: Severities
show_rationale: Show rationale
silence: Silence
suspension: Suspension
title: "%{instance} List of blocked instances"
domain_subscribes:
add_new: Add
edit:
title: Edit
exclude_reblog: Exclude
hint_html: "<strong>What are domain subscription?</strong> Insert public posts from the specified domain into the home or list timeline. Posts received by the server (federated timeline) are targets."
home: Home
include_reblog: Include
new:
title: Add new domain subscription
domain_validator:
invalid_domain: is not a valid domain name
errors:
@ -939,6 +969,9 @@ en:
title: Filters
new:
title: Add new filter
follow_tags:
add_new: Add new
hint_html: "<strong>What are follow hashtags?</strong> They are a collection of hashtags you follow. From the posts with hashtags received by the server, the one with the hashtag specified here is inserted into the home timeline."
footer:
developers: Developers
more: More…
@ -1017,7 +1050,32 @@ en:
expires_at: Expires
uses: Uses
title: Invite people
keyword_subscribes:
add_new: Add
disabled: Disabled
edit:
title: Edit
enabled: Enabled
errors:
duplicate: The same content has already been registered
limit: You have reached the maximum number of "Keyword subscribes" that can be registered
regexp: "Regular expression error: %{message}"
hint_html: "<strong>What is a keyword subscribes?</strong> Inserts a public post that matches one of the specified words or a regular expression into the home timeline. Posts received by the server (federated timeline) are targets."
home: Home
ignorecase:
enabled: Ignore
disabled: Sensitive
ignore_block: Ignore
index:
delete: Delete
title: Keyword subscribe
new:
title: Add new keyword subscribe
regexp:
enabled: Regex
disabled: Keyword
lists:
add_new: Add new list
errors:
limit: You have reached the maximum amount of lists
login_activities:
@ -1242,19 +1300,24 @@ en:
settings:
account: Account
account_settings: Account settings
account_subscribes: Account subscribes
aliases: Account aliases
appearance: Appearance
authorized_apps: Authorized apps
back: Back to Mastodon
delete: Account deletion
development: Development
domain_subscribes: Domain subscribes
edit_profile: Edit profile
export: Data export
favourite_tags: Favourite hashtags
featured_tags: Featured hashtags
follow_and_subscriptions: Follows and subscriptions
follow_tags: Following hashtags
identity_proofs: Identity proofs
import: Import
import_and_export: Import and export
keyword_subscribes: Keyword subscribes
migrate: Account migration
notifications: Notifications
preferences: Preferences

View file

@ -81,6 +81,9 @@ ja:
moderator: Mod
unavailable: プロフィールは利用できません
unfollow: フォロー解除
account_subscribes:
add_new: 追加
hint_html: "<strong>アカウントの購読とは何ですか?</strong> 指定したアカウントの公開投稿をホームタイムラインに挿入します。サーバが受け取っている投稿(連合タイムライン)が対象です。フォローしている場合は購読できません。"
admin:
account_actions:
action: アクションを実行
@ -854,6 +857,16 @@ ja:
directory: ディレクトリ
explanation: 関心を軸にユーザーを発見しよう
explore_mastodon: "%{title}を探索"
domain_subscribes:
add_new: 追加
edit:
title: 編集
exclude_reblog: 含めない
hint_html: "<strong>ドメインの購読とは何ですか?</strong> 指定したドメインの公開投稿をホームタイムラインまたはリストに挿入します。サーバが受け取っている投稿(連合タイムライン)が対象です。"
home: ホーム
include_reblog: 含める
new:
title: 新規ドメイン購読を追加
domain_validator:
invalid_domain: は無効なドメイン名です
errors:
@ -899,6 +912,9 @@ ja:
errors:
limit: 注目のハッシュタグの上限に達しました
hint_html: "<strong>注目のハッシュタグとは?</strong>プロフィールページに目立つ形で表示され、そのハッシュタグのついたあなたの公開投稿だけを抽出して閲覧できるようにします。クリエイティブな仕事や長期的なプロジェクトを追うのに優れた機能です。"
follow_tags:
add_new: 追加
hint_html: "<strong>ハッシュタグのフォローとは何ですか?</strong> それらはあなたがフォローするハッシュタグのコレクションです。サーバが受け取ったハッシュタグ付きの投稿の中から、ここで指定したハッシュタグのついた投稿をホームタイムラインに挿入します。"
filters:
contexts:
account: プロフィール
@ -993,7 +1009,32 @@ ja:
expires_at: 有効期限
uses: 使用
title: 新規ユーザーの招待
keyword_subscribes:
add_new: 追加
disabled: 無効
edit:
title: 編集
enabled: 有効
errors:
duplicate: 既に同じ内容が登録されています
limit: キーワード購読の登録可能数の上限に達しました
regexp: "正規表現に誤りがあります: %{message}"
hint_html: "<strong>キーワードの購読とは何ですか?</strong> 指定した単語のいずれか、または正規表現に一致する公開投稿をホームタイムラインに挿入します。サーバが受け取っている投稿(連合タイムライン)が対象です。"
home: ホーム
ignorecase:
enabled: 無視
disabled: 区別
ignore_block: 無視
index:
delete: 削除
title: キーワードの購読
new:
title: 新規キーワード購読を追加
regexp:
enabled: 正規表現
disabled: キーワード
lists:
add_new: 新しいリストを追加
errors:
limit: リストの上限に達しました
media_attachments:
@ -1203,19 +1244,24 @@ ja:
settings:
account: アカウント
account_settings: アカウント設定
account_subscribes: アカウントの購読
aliases: アカウントエイリアス
appearance: 外観
authorized_apps: 認証済みアプリ
back: Mastodon に戻る
delete: アカウントの削除
development: 開発
domain_subscribes: ドメインの購読
edit_profile: プロフィールを編集
export: データのエクスポート
favourite_tags: お気に入りハッシュタグ
featured_tags: 注目のハッシュタグ
follow_and_subscriptions: フォロー・購読
follow_tags: ハッシュタグのフォロー
identity_proofs: Identity proofs
import: データのインポート
import_and_export: インポート・エクスポート
keyword_subscribes: キーワードの購読
migrate: アカウントの引っ越し
notifications: 通知
preferences: ユーザー設定

View file

@ -60,6 +60,9 @@ en:
whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
domain_allow:
domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored
domain_subscribe:
domain: Specify the domain name of the server you want to subscribe to
exclude_reblog: Exclude boosted posts from subscription
email_domain_block:
domain: This can be the domain name that shows up in the e-mail address, the MX record that domain resolves to, or IP of the server that MX record resolves to. Those will be checked upon user sign-up and the sign-up will be rejected.
with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blocked
@ -79,6 +82,11 @@ en:
no_access: Block access to all resources
sign_up_requires_approval: New sign-ups will require your approval
severity: Choose what will happen with requests from this IP
keyword_subscribe:
exclude_keyword: List multiple excluded keywords separated by commas (or use regular expressions)
ignore_block: You can prioritize keyword subscriptions while keeping the entire domain block
keyword: List multiple keywords separated by commas (or use regular expressions)
name: Optional
rule:
text: Describe a rule or requirement for users on this server. Try to keep it short and simple
sessions:
@ -95,6 +103,8 @@ en:
value: Content
account_alias:
acct: Handle of the old account
account_input:
acct: Account
account_migration:
acct: Handle of the new account
account_warning_preset:
@ -176,6 +186,12 @@ en:
username: Username
username_or_email: Username or Email
whole_word: Whole word
domain_subscribe:
domain: Domain
exclude_reblog: Exclude boost
list_id: Target timeline
timeline: Timeline
reblog: Boost
email_domain_block:
with_dns_records: Include MX records and IPs of the domain
featured_tag:
@ -195,6 +211,23 @@ en:
no_access: Block access
sign_up_requires_approval: Limit sign-ups
severity: Rule
keyword_subscribe:
disabled: Temporarily disable subscription
exclude_keyword: Excluded keyword list or regular expression
ignorecase: Ignore case
ignore_block: Ignore User's domain blocking
keyword: Keyword list or regular expression
list_id: Target timeline
name: Name
regexp: Use regular expressions for keywords
keyword_subscribes:
disabled: State
ignorecase: Case
ignore_block: Block
keyword: String
name: Name
regexp: Type
timeline: Timeline
notification_emails:
digest: Send digest e-mails
favourite: Someone favourited your post

View file

@ -60,6 +60,9 @@ ja:
whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります
domain_allow:
domain: 登録するとこのサーバーからデータを受信したり、このドメインから受信するデータを処理して保存できるようになります
domain_subscribe:
domain: 購読したいサーバのドメイン名を指定します
exclude_reblog: ブーストされた投稿を購読から除外します
email_domain_block:
domain: メールアドレスのドメイン名および、名前解決したMXレコード、IPアドレスを指定できます。ユーザー登録時にこれらをチェックし、該当する場合はユーザー登録を拒否します。
with_dns_records: 指定したドメインのDNSレコードを取得し、その結果もメールドメインブロックに登録されます
@ -79,6 +82,11 @@ ja:
no_access: すべてのリソースへのアクセスをブロックします
sign_up_requires_approval: 承認するまで新規登録が完了しなくなります
severity: このIPに対する措置を選択してください
keyword_subscribe:
exclude_keyword: カンマで区切って複数の除外するキーワードを並べます(または正規表現で指定します)
ignore_block: ドメイン全体を非表示にしたまま、キーワードの購読を優先することができます
keyword: カンマで区切って複数のキーワードを並べます(または正規表現で指定します)
name: オプションです
rule:
text: ユーザーのためのルールや要件を記述してください。短くシンプルにしてください。
sessions:
@ -95,6 +103,8 @@ ja:
value: 内容
account_alias:
acct: 引っ越し元のユーザー ID
account_input:
acct: アカウント (account@domain.tld)
account_migration:
acct: 引っ越し先のユーザー ID
account_warning_preset:
@ -176,12 +186,20 @@ ja:
username: ユーザー名
username_or_email: ユーザー名またはメールアドレス
whole_word: 単語全体にマッチ
domain_subscribe:
domain: ドメイン
exclude_reblog: ブースト除外
list_id: 対象タイムライン
reblog: ブースト
timeline: タイムライン
email_domain_block:
with_dns_records: ドメインのMXレコードとIPアドレスを含む
favourite_tag:
name: ハッシュタグ
featured_tag:
name: ハッシュタグ
follow_tag:
name: ハッシュタグ
interactions:
must_be_follower: フォロワー以外からの通知をブロック
must_be_following: フォローしていないユーザーからの通知をブロック
@ -197,6 +215,23 @@ ja:
no_access: ブロック
sign_up_requires_approval: 登録を制限
severity: ルール
keyword_subscribe:
disabled: 一時的に購読を無効にする
exclude_keyword: 除外するキーワードまたは正規表現
ignorecase: 大文字と小文字を区別しない
ignore_block: ユーザーによるドメインブロックを無視する
keyword: キーワードまたは正規表現
list_id: 対象タイムライン
name: 名称
regexp: キーワードに正規表現を使う
keyword_subscribes:
disabled: 状態
ignorecase: 大小
ignore_block: ブロック
keyword: 設定値
name: 名称
regexp: 種別
timeline: タイムライン
notification_emails:
digest: タイムラインからピックアップしてメールで通知する
favourite: お気に入り登録された時

View file

@ -17,7 +17,14 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url
end
n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, if: -> { current_user.functional? }
n.item :follow_and_subscriptions, safe_join([fa_icon('users fw'), t('settings.follow_and_subscriptions')]), relationships_url, if: -> { current_user.functional? } do |s|
s.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, highlights_on: %r{/relationships}
s.item :follow_tags, safe_join([fa_icon('hashtag fw'), t('settings.follow_tags')]), settings_follow_tags_url
s.item :account_subscribes, safe_join([fa_icon('users fw'), t('settings.account_subscribes')]), settings_account_subscribes_url
s.item :domain_subscribes, safe_join([fa_icon('server fw'), t('settings.domain_subscribes')]), settings_domain_subscribes_url
s.item :keyword_subscribes, safe_join([fa_icon('search fw'), t('settings.keyword_subscribes')]), settings_keyword_subscribes_url
end
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }

View file

@ -165,6 +165,10 @@ Rails.application.routes.draw do
resources :sessions, only: [:destroy]
resources :featured_tags, only: [:index, :create, :destroy]
resources :favourite_tags, only: [:index, :create, :destroy]
resources :follow_tags, only: [:index, :create, :destroy]
resources :account_subscribes, only: [:index, :create, :destroy]
resources :domain_subscribes, except: [:show]
resources :keyword_subscribes, except: [:show]
resources :login_activities, only: [:index]
end
@ -480,6 +484,11 @@ Rails.application.routes.draw do
end
resources :featured_tags, only: [:index, :create, :destroy]
resources :favourite_tags, only: [:index, :create, :show, :update, :destroy]
resources :follow_tags, only: [:index, :create, :show, :update, :destroy]
resources :account_subscribes, only: [:index, :create, :show, :update, :destroy]
resources :domain_subscribes, only: [:index, :create, :show, :update, :destroy]
resources :keyword_subscribes, only: [:index, :create, :show, :update, :destroy]
resources :polls, only: [:create, :show] do
resources :votes, only: :create, controller: 'polls/votes'

View file

@ -0,0 +1,10 @@
class CreateFollowTags < ActiveRecord::Migration[5.2]
def change
create_table :follow_tags do |t|
t.references :account, foreign_key: { on_delete: :cascade }
t.references :tag, foreign_key: { on_delete: :cascade }
t.timestamps
end
end
end

View file

@ -0,0 +1,10 @@
class CreateAccountSubscribes < ActiveRecord::Migration[5.2]
def change
create_table :account_subscribes do |t|
t.references :account, foreign_key: { on_delete: :cascade }
t.references :target_account, foreign_key: { to_table: 'accounts', on_delete: :cascade }
t.timestamps
end
end
end

View file

@ -0,0 +1,12 @@
class CreateKeywordSubscribes < ActiveRecord::Migration[5.2]
def change
create_table :keyword_subscribes do |t|
t.references :account, foreign_key: { on_delete: :cascade }
t.string :keyword, null: false
t.boolean :ignorecase, default: true
t.boolean :regexp, default: false
t.timestamps
end
end
end

View file

@ -0,0 +1,8 @@
class AddNameAndFlagToKeywordSubscribe < ActiveRecord::Migration[5.2]
def change
add_column :keyword_subscribes, :name, :string, default: '', null: false
add_column :keyword_subscribes, :ignore_block, :boolean, default: false
add_column :keyword_subscribes, :disabled, :boolean, default: false
add_column :keyword_subscribes, :exclude_home, :boolean, default: false
end
end

View file

@ -0,0 +1,11 @@
class CreateDomainSubscribes < ActiveRecord::Migration[5.2]
def change
create_table :domain_subscribes do |t|
t.references :account, foreign_key: { on_delete: :cascade }
t.references :list, foreign_key: { on_delete: :cascade }
t.string :domain, default: '', null: false
t.timestamps
end
end
end

View file

@ -0,0 +1,5 @@
class AddExcludeKeywordToKeywordSubscribe < ActiveRecord::Migration[5.2]
def change
add_column :keyword_subscribes, :exclude_keyword, :string, default: '', null: false
end
end

View file

@ -0,0 +1,5 @@
class AddExcludeReblogToDomainSubscribe < ActiveRecord::Migration[5.2]
def change
add_column :domain_subscribes, :exclude_reblog, :boolean, default: true
end
end

View file

@ -0,0 +1,5 @@
class RemoveExcludeHomeFromKeywordSubscribes < ActiveRecord::Migration[5.2]
def change
safety_assured { remove_column :keyword_subscribes, :exclude_home, :boolean }
end
end

View file

@ -0,0 +1,8 @@
class AddListToKeywordSubscribes < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_reference :keyword_subscribes, :list, foreign_key: { on_delete: :cascade }, index: false
add_index :keyword_subscribes, :list_id, algorithm: :concurrently
end
end

View file

@ -0,0 +1,8 @@
class AddListToAccountSubscribes < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_reference :account_subscribes, :list, foreign_key: { on_delete: :cascade }, index: false
add_index :account_subscribes, :list_id, algorithm: :concurrently
end
end

View file

@ -0,0 +1,8 @@
class AddListToFollowTags < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_reference :follow_tags, :list, foreign_key: { on_delete: :cascade }, index: false
add_index :follow_tags, :list_id, algorithm: :concurrently
end
end

View file

@ -130,6 +130,17 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_account_statuses_cleanup_policies_on_account_id"
end
create_table "account_subscribes", force: :cascade do |t|
t.bigint "account_id"
t.bigint "target_account_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "list_id"
t.index ["account_id"], name: "index_account_subscribes_on_account_id"
t.index ["list_id"], name: "index_account_subscribes_on_list_id"
t.index ["target_account_id"], name: "index_account_subscribes_on_target_account_id"
end
create_table "account_warning_presets", force: :cascade do |t|
t.text "text", default: "", null: false
@ -375,6 +386,17 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
end
create_table "domain_subscribes", force: :cascade do |t|
t.bigint "account_id"
t.bigint "list_id"
t.string "domain", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "exclude_reblog", default: true
t.index ["account_id"], name: "index_domain_subscribes_on_account_id"
t.index ["list_id"], name: "index_domain_subscribes_on_list_id"
end
create_table "email_domain_blocks", force: :cascade do |t|
t.string "domain", default: "", null: false
t.datetime "created_at", null: false
@ -445,6 +467,17 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true
end
create_table "follow_tags", force: :cascade do |t|
t.bigint "account_id"
t.bigint "tag_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "list_id"
t.index ["account_id"], name: "index_follow_tags_on_account_id"
t.index ["list_id"], name: "index_follow_tags_on_list_id"
t.index ["tag_id"], name: "index_follow_tags_on_tag_id"
end
create_table "follows", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -453,6 +486,7 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
t.boolean "show_reblogs", default: true, null: false
t.string "uri"
t.boolean "notify", default: false, null: false
t.boolean "private", default: true, null: false
t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
end
@ -502,6 +536,22 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
t.text "comment", default: "", null: false
end
create_table "keyword_subscribes", force: :cascade do |t|
t.bigint "account_id"
t.string "keyword", null: false
t.boolean "ignorecase", default: true
t.boolean "regexp", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name", default: "", null: false
t.boolean "ignore_block", default: false
t.boolean "disabled", default: false
t.string "exclude_keyword", default: "", null: false
t.bigint "list_id"
t.index ["account_id"], name: "index_keyword_subscribes_on_account_id"
t.index ["list_id"], name: "index_keyword_subscribes_on_list_id"
end
create_table "list_accounts", force: :cascade do |t|
t.bigint "list_id", null: false
t.bigint "account_id", null: false
@ -1013,6 +1063,9 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
add_foreign_key "account_pins", "accounts", on_delete: :cascade
add_foreign_key "account_stats", "accounts", on_delete: :cascade
add_foreign_key "account_statuses_cleanup_policies", "accounts", on_delete: :cascade
add_foreign_key "account_subscribes", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_subscribes", "accounts", on_delete: :cascade
add_foreign_key "account_subscribes", "lists", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_warnings", "accounts", on_delete: :nullify
add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
@ -1033,6 +1086,8 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
add_foreign_key "devices", "accounts", on_delete: :cascade
add_foreign_key "devices", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
add_foreign_key "domain_subscribes", "accounts", on_delete: :cascade
add_foreign_key "domain_subscribes", "lists", on_delete: :cascade
add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade
add_foreign_key "encrypted_messages", "accounts", column: "from_account_id", on_delete: :cascade
add_foreign_key "encrypted_messages", "devices", on_delete: :cascade
@ -1045,11 +1100,16 @@ ActiveRecord::Schema.define(version: 2021_08_08_071221) do
add_foreign_key "follow_recommendation_suppressions", "accounts", on_delete: :cascade
add_foreign_key "follow_requests", "accounts", column: "target_account_id", name: "fk_9291ec025d", on_delete: :cascade
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
add_foreign_key "follow_tags", "accounts", on_delete: :cascade
add_foreign_key "follow_tags", "lists", on_delete: :cascade
add_foreign_key "follow_tags", "tags", on_delete: :cascade
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
add_foreign_key "invites", "users", on_delete: :cascade
add_foreign_key "keyword_subscribes", "accounts", on_delete: :cascade
add_foreign_key "keyword_subscribes", "lists", on_delete: :cascade
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
add_foreign_key "list_accounts", "follows", on_delete: :cascade
add_foreign_key "list_accounts", "lists", on_delete: :cascade

View file

@ -0,0 +1,4 @@
Fabricator(:account_subscribe) do
account
target_account
end

View file

@ -0,0 +1,5 @@
Fabricator(:domain_subscribe) do
account
list
domain
end

View file

@ -0,0 +1,4 @@
Fabricator(:follow_tag) do
account
tag
end

View file

@ -0,0 +1,6 @@
Fabricator(:keyword_subscribe) do
account
keyword
ignorecase
regexp
end

View file

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe AccountSubscribe, type: :model do
end

View file

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe DomainSubscribe, type: :model do
end

View file

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe FollowTag, type: :model do
end

View file

@ -0,0 +1,4 @@
require 'rails_helper'
RSpec.describe KeywordSubscribe, type: :model do
end