diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index f5735421c7c90bccc616cacbbd89b312eeb4629c..2e0c0ccc398abd7c2359a72d0451e82c419587a1 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -51,7 +51,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 diff --git a/app/controllers/api/v1/account_subscribes_controller.rb b/app/controllers/api/v1/account_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..5da428bbd991a300a69713f7d20fedc676e82f74 --- /dev/null +++ b/app/controllers/api/v1/account_subscribes_controller.rb @@ -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 diff --git a/app/controllers/api/v1/domain_subscribes_controller.rb b/app/controllers/api/v1/domain_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..40cf753330e0d687eb55439f72725b55fca5ef9c --- /dev/null +++ b/app/controllers/api/v1/domain_subscribes_controller.rb @@ -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 diff --git a/app/controllers/api/v1/follow_tags_controller.rb b/app/controllers/api/v1/follow_tags_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..95682ec7d2286323cb803952968cab56c9d5f90c --- /dev/null +++ b/app/controllers/api/v1/follow_tags_controller.rb @@ -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 diff --git a/app/controllers/api/v1/keyword_subscribes_controller.rb b/app/controllers/api/v1/keyword_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec80143ce55483a1e69ab7b4c4fff757cd11ab1c --- /dev/null +++ b/app/controllers/api/v1/keyword_subscribes_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class Api::V1::KeywordSubscribeController < 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::KeywordSubscribeSerializer + end + + def create + @keyword_subscribe = current_account.keyword_subscribes.create!(resource_params) + render json: @keyword_subscribe, serializer: REST::KeywordSubscribeSerializer + end + + def show + render json: @keyword_subscribe, serializer: REST::KeywordSubscribeSerializer + end + + def update + @keyword_subscribe.update!(resource_params) + render json: @keyword_subscribe, serializer: REST::KeywordSubscribeSerializer + 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 diff --git a/app/controllers/settings/account_subscribes_controller.rb b/app/controllers/settings/account_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ca838ac3cb96f40e3e35047de6396af3e16be6f --- /dev/null +++ b/app/controllers/settings/account_subscribes_controller.rb @@ -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 diff --git a/app/controllers/settings/domain_subscribes_controller.rb b/app/controllers/settings/domain_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..42fcbc6e42f3964b7dbe4a016ccb71af1ddb26a5 --- /dev/null +++ b/app/controllers/settings/domain_subscribes_controller.rb @@ -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 diff --git a/app/controllers/settings/follow_tags_controller.rb b/app/controllers/settings/follow_tags_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..776a4850426e1fd94a04397b3c15354f06dd4984 --- /dev/null +++ b/app/controllers/settings/follow_tags_controller.rb @@ -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 diff --git a/app/controllers/settings/keyword_subscribes_controller.rb b/app/controllers/settings/keyword_subscribes_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..fd38581d9e2cfd7ff4ed302af6aaf940b13594d0 --- /dev/null +++ b/app/controllers/settings/keyword_subscribes_controller.rb @@ -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 diff --git a/app/helpers/lists_helper.rb b/app/helpers/lists_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b85405be4897de516b69a6dc3f64a4c41c93f58 --- /dev/null +++ b/app/helpers/lists_helper.rb @@ -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 diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 7f22f58a1de074f88e8eeb8f196663163614600a..5468b337074937c347bd0cbef7c195ed0eaa2a63 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -883,3 +883,7 @@ a.name-tag, .center-text { text-align: center; } + +.exclude-keyword { + color: $error-value-color; +} diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 62f5554ffc0886fc357765574c75836e977c34a2..eb070e0d86937254e7a500eb43294b84e7c8b13c 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -49,6 +49,11 @@ } } + th.nowrap, + td.nowrap { + white-space: nowrap; + } + &.inline-table { & > tbody > tr:nth-child(odd) { & > td, diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e979d2866adbdf201b3bb97cb751afbc34bb5b3f..a598e7d500a136d9b2e8a0ee08ed76a950a96c0c 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -88,10 +88,10 @@ class FeedManager end end - def merge_into_timeline(from_account, into_account) + def merge_into_timeline(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 @@ -188,21 +188,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 - - return !!should_filter - elsif status.reblog? # Filter out a reblog + 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][status.reblog.account.domain] # or the author's domain is blocked + should_filter ||= crutches[:domain_blocking_r][status.reblog.account.domain] # or the author's domain is blocked return !!should_filter - end + 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 - false + 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 end def filter_from_mentions?(status, receiver_id) @@ -349,13 +360,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).each_with_object({}) { |id, mapping| mapping[id] = 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).each_with_object({}) { |id, mapping| mapping[id] = true } - crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } - crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } - crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } - crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).each_with_object({}) { |id, mapping| mapping[id] = true } - + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = 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).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } + crutches[:domain_blocking_r] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:following_tag_by] = FollowTag.where(account_id: receiver_id, tag: statuses.map { |s| s.tags }.flatten.uniq.compact).pluck(:tag_id).each_with_object({}) { |tag_id, mapping| mapping[tag_id] = true } + crutches[:domain_subscribe] = DomainSubscribe.where(account_id: receiver_id, list_id: nil, domain: statuses.map { |s| s&.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } + crutches[:account_subscribe] = AccountSubscribe.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id).compact).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } crutches end end diff --git a/app/models/account_subscribe.rb b/app/models/account_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..09775878641d1af7eab1e395daa9ceb53f4bb44b --- /dev/null +++ b/app/models/account_subscribe.rb @@ -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 diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index f901c446d1ae46cc898b5a4209a930ca9e1ecdd5..3dc2f0a18e0b77da6594a2232015705caf78562b 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -60,5 +60,12 @@ 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 end end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index f27d39483a365fc523ea5a9367eaeb44693fd9ea..e4b2bef32ba1ada1ee1545734c2f45a0f2c427ce 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -84,6 +84,13 @@ module AccountInteractions has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account has_many :conversation_mutes, dependent: :destroy has_many :domain_blocks, class_name: 'AccountDomainBlock', 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, uri: nil) @@ -150,6 +157,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 @@ -202,12 +213,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) diff --git a/app/models/domain_subscribe.rb b/app/models/domain_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..48eef4fb51ebc9e803166eebf0c776c352f5dbae --- /dev/null +++ b/app/models/domain_subscribe.rb @@ -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 diff --git a/app/models/follow.rb b/app/models/follow.rb index 87fa114253b4e6b4b454cfd53d9f65c937646e23..7f2d3e5b671929483f5036be60cdd105e506c0e8 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -10,6 +10,7 @@ # target_account_id :bigint(8) not null # show_reblogs :boolean default(TRUE), not null # uri :string +# private :boolean default(TRUE), not null # class Follow < ApplicationRecord diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 96ac7eaa593d9fac53f30d3a9444c7ba3d30ea99..23ade6215378d6217309e1f762744121eb119794 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -26,6 +26,7 @@ class FollowRequest < ApplicationRecord def authorize! account.follow!(target_account, reblogs: show_reblogs, uri: uri) + UnsubscribeAccountService.new.call(account, target_account) if account.subscribing?(target_account) MergeWorker.perform_async(target_account.id, account.id) if account.local? destroy! end diff --git a/app/models/follow_tag.rb b/app/models/follow_tag.rb new file mode 100644 index 0000000000000000000000000000000000000000..90b428b7455628dc62b7c9df8009f94b9060bdcf --- /dev/null +++ b/app/models/follow_tag.rb @@ -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 diff --git a/app/models/keyword_subscribe.rb b/app/models/keyword_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..1df016eecc19f51fa9927739158557fd53b90c6f --- /dev/null +++ b/app/models/keyword_subscribe.rb @@ -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 diff --git a/app/models/list_account.rb b/app/models/list_account.rb index 785923c4cf0260b71d54e8c5b6c446c9433bca70..0cb4339be1c91394c591570e9c020c4d9ecd1207 100644 --- a/app/models/list_account.rb +++ b/app/models/list_account.rb @@ -18,6 +18,8 @@ class ListAccount < ApplicationRecord before_validation :set_follow + scope :followed_lists, ->(account) { ListAccount.includes(:follow).where(follows: { account_id: account.id }).select(:list_id).uniq } + private def set_follow diff --git a/app/models/status.rb b/app/models/status.rb index 81f7e6576a06bb697a4ea012bb0815ab632d8674..862755c17e54bff65c9e75f8d68a18d003ea1b7d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -242,6 +242,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 mark_for_mass_destruction! @marked_for_mass_destruction = true end diff --git a/app/models/tag.rb b/app/models/tag.rb index 8e6fc404dbeb779e4fabc7c502f207ea1175b676..29891e0c53200f3398afa5cb045f3ead3fc94a60 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -24,6 +24,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 has_one :account_tag_stat, dependent: :destroy HASHTAG_SEPARATORS = "_\u00B7\u200c" diff --git a/app/serializers/rest/account_subscribe_serializer.rb b/app/serializers/rest/account_subscribe_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..50ef86ae344ccc088bbb64aa5c4126d160eedd6d --- /dev/null +++ b/app/serializers/rest/account_subscribe_serializer.rb @@ -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 diff --git a/app/serializers/rest/domain_subscribe_serializer.rb b/app/serializers/rest/domain_subscribe_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..fef4e3b83acd4fdd75047c2d4967cddba06c1269 --- /dev/null +++ b/app/serializers/rest/domain_subscribe_serializer.rb @@ -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 diff --git a/app/serializers/rest/follow_tag_serializer.rb b/app/serializers/rest/follow_tag_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea9db960135ff24adb6238389baabc30179e7d0f --- /dev/null +++ b/app/serializers/rest/follow_tag_serializer.rb @@ -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 diff --git a/app/serializers/rest/keyword_subscribes_serializer.rb b/app/serializers/rest/keyword_subscribes_serializer.rb new file mode 100644 index 0000000000000000000000000000000000000000..4253d02308da548abb036c8b96e19c65e5a6f231 --- /dev/null +++ b/app/serializers/rest/keyword_subscribes_serializer.rb @@ -0,0 +1,9 @@ + # frozen_string_literal: true + +class REST::KeywordSubscribeSerializer < ActiveModel::Serializer + attributes :id, :name, :keyword, :exclude_keyword, :ignorecase, :regexp, :ignore_block, :disabled, :exclude_home + + def id + object.id.to_s + end +end diff --git a/app/services/account_subscribe_service.rb b/app/services/account_subscribe_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..8e9b0adf3217eb44e9a139d2b00c02d00c9b977f --- /dev/null +++ b/app/services/account_subscribe_service.rb @@ -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 diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 7fd4e6903586e6e77cb0209220894886130c9f14..5e12c035f460f1acfbed335a82b72e12585e1551 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -18,9 +18,16 @@ 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? 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 @@ -45,6 +52,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" @@ -82,6 +155,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" diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index dc47804c05bc4b1e6dd385356aa8d43ad807a8ed..91c8e2e6d375fa41c5583770f0b5b3edeaddd18b 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -54,6 +54,7 @@ class FollowService < BaseService def direct_follow(source_account, target_account, reblogs: true) follow = source_account.follow!(target_account, reblogs: reblogs) + UnsubscribeAccountService.new.call(source_account, target_account) if source_account.subscribing?(target_account) LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) MergeWorker.perform_async(target_account.id, source_account.id) diff --git a/app/services/unsubscribe_account_service.rb b/app/services/unsubscribe_account_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..534f5d366802f79196289ce4044ebcd003610ccf --- /dev/null +++ b/app/services/unsubscribe_account_service.rb @@ -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 diff --git a/app/views/settings/account_subscribes/index.html.haml b/app/views/settings/account_subscribes/index.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..0cbb8aab51d439dc019b529227f9ed062c3dc01a --- /dev/null +++ b/app/views/settings/account_subscribes/index.html.haml @@ -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') } diff --git a/app/views/settings/domain_subscribes/_fields.html.haml b/app/views/settings/domain_subscribes/_fields.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..30c5b566d77c305730169d3e99940ca4912f43a2 --- /dev/null +++ b/app/views/settings/domain_subscribes/_fields.html.haml @@ -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/ diff --git a/app/views/settings/domain_subscribes/edit.html.haml b/app/views/settings/domain_subscribes/edit.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..12e3c0a1b0a391db197389bfb381c97f4e2d4db4 --- /dev/null +++ b/app/views/settings/domain_subscribes/edit.html.haml @@ -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 diff --git a/app/views/settings/domain_subscribes/index.html.haml b/app/views/settings/domain_subscribes/index.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..7602eff3f334b3b5f182c3aab966a62d2e5c498a --- /dev/null +++ b/app/views/settings/domain_subscribes/index.html.haml @@ -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' diff --git a/app/views/settings/domain_subscribes/new.html.haml b/app/views/settings/domain_subscribes/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..023a9888dd2a2846b35954f502a461a6a3db28bb --- /dev/null +++ b/app/views/settings/domain_subscribes/new.html.haml @@ -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 diff --git a/app/views/settings/follow_tags/index.html.haml b/app/views/settings/follow_tags/index.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..18fce3987213badc75c3676b55dccd61e5c2998e --- /dev/null +++ b/app/views/settings/follow_tags/index.html.haml @@ -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') } diff --git a/app/views/settings/keyword_subscribes/_fields.html.haml b/app/views/settings/keyword_subscribes/_fields.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..c40939e1bd6f5db031783db3d1e8b0d8e01f461e --- /dev/null +++ b/app/views/settings/keyword_subscribes/_fields.html.haml @@ -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 diff --git a/app/views/settings/keyword_subscribes/edit.html.haml b/app/views/settings/keyword_subscribes/edit.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..d11d94e2d3542abf5c8e0a5806ef3500bd92ef10 --- /dev/null +++ b/app/views/settings/keyword_subscribes/edit.html.haml @@ -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 diff --git a/app/views/settings/keyword_subscribes/index.html.haml b/app/views/settings/keyword_subscribes/index.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..750a816a4b7126596586e9ab0c7991b7f2cadf3f --- /dev/null +++ b/app/views/settings/keyword_subscribes/index.html.haml @@ -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' diff --git a/app/views/settings/keyword_subscribes/new.html.haml b/app/views/settings/keyword_subscribes/new.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..b6e97912760e9c3c8b7fef9a45c2af9f64da9407 --- /dev/null +++ b/app/views/settings/keyword_subscribes/new.html.haml @@ -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 diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index d745cb99c7b9aada475223d9250a0315dc0c6c5b..0de0acc9a977b180f1881c0bb6af0feec852cca4 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -5,7 +5,7 @@ class MergeWorker 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)) + def perform(from_account_id, into_account_id, public_only = false) + FeedManager.instance.merge_into_timeline(Account.find(from_account_id), Account.find(into_account_id), public_only) end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 172bc51d697e2624dddf8c06e4f477d11fa1f9c3..bf01a12f6751a0cebdc300d034912c5bac962c8c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -82,6 +82,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 @@ -693,6 +696,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: @@ -754,6 +784,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… @@ -826,7 +859,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 media_attachments: @@ -1015,19 +1073,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 diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 5993feb6490fb28987d7ed834a832e0bfff57fed..b020d5ddcf75e8f6c826eb92e186b17011e87d62 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -78,6 +78,9 @@ ja: moderator: Mod unavailable: プãƒãƒ•ィールã¯åˆ©ç”¨ã§ãã¾ã›ã‚“ unfollow: フォãƒãƒ¼è§£é™¤ + account_subscribes: + add_new: è¿½åŠ + hint_html: "<strong>アカウントã®è³¼èªã¨ã¯ä½•ã§ã™ã‹ï¼Ÿ</strong> 指定ã—ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®å…¬é–‹æŠ•ç¨¿ã‚’ãƒ›ãƒ¼ãƒ ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æŒ¿å…¥ã—ã¾ã™ã€‚サーãƒãŒå—ã‘å–ã£ã¦ã„る投稿(連åˆã‚¿ã‚¤ãƒ ライン)ãŒå¯¾è±¡ã§ã™ã€‚フォãƒãƒ¼ã—ã¦ã„ã‚‹å ´åˆã¯è³¼èªã§ãã¾ã›ã‚“。" admin: account_actions: action: アクションを実行 @@ -685,6 +688,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: @@ -729,6 +742,9 @@ ja: errors: limit: 注目ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ä¸Šé™ã«é”ã—ã¾ã—㟠hint_html: "<strong>注目ã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã¨ã¯ï¼Ÿ</strong>プãƒãƒ•ィールページã«ç›®ç«‹ã¤å½¢ã§è¡¨ç¤ºã•れã€ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ã¤ã„ãŸã‚ãªãŸã®å…¬é–‹æŠ•稿ã ã‘を抽出ã—ã¦é–²è¦§ã§ãるよã†ã«ã—ã¾ã™ã€‚クリエイティブãªä»•事や長期的ãªãƒ—ãƒã‚¸ã‚§ã‚¯ãƒˆã‚’追ã†ã®ã«å„ªã‚ŒãŸæ©Ÿèƒ½ã§ã™ã€‚" + follow_tags: + add_new: è¿½åŠ + hint_html: "<strong>ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒ•ã‚©ãƒãƒ¼ã¨ã¯ä½•ã§ã™ã‹ï¼Ÿ</strong> ãれらã¯ã‚ãªãŸãŒãƒ•ã‚©ãƒãƒ¼ã™ã‚‹ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ã‚³ãƒ¬ã‚¯ã‚·ãƒ§ãƒ³ã§ã™ã€‚サーãƒãŒå—ã‘å–ã£ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ä»˜ãã®æŠ•ç¨¿ã®ä¸ã‹ã‚‰ã€ã“ã“ã§æŒ‡å®šã—ãŸãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ã¤ã„ãŸæŠ•ç¨¿ã‚’ãƒ›ãƒ¼ãƒ ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æŒ¿å…¥ã—ã¾ã™ã€‚" filters: contexts: home: ホームタイムライン @@ -816,7 +832,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: @@ -1001,19 +1042,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: ユーザーè¨å®š diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 66f518c1b9cba8b215382903b30a2f297fe2dc5d..4d07bd6ee4e93108ecf3a3c2b0a146f8b9ea34b2 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -47,6 +47,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 featured_tag: name: 'You might want to use one of these:' form_challenge: @@ -55,6 +58,11 @@ en: data: CSV file exported from another Mastodon server invite_request: text: This will help us review your application + 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 sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' tag: @@ -68,6 +76,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: @@ -138,6 +148,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 featured_tag: name: Hashtag interactions: @@ -148,6 +164,23 @@ en: comment: Comment invite_request: text: Why do you want to join? + 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 status diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index fdccc13448b94f4603eceabfd19934b9b7c2649f..271d6496c6286304536ea6ec989ffd0dec0eb448 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -47,6 +47,9 @@ ja: whole_word: ã‚ーワードã¾ãŸã¯ãƒ•レーズãŒè‹±æ•°å—ã®ã¿ã®å ´åˆã€å˜èªžå…¨ä½“ã¨ä¸€è‡´ã™ã‚‹å ´åˆã®ã¿é©ç”¨ã•れるよã†ã«ãªã‚Šã¾ã™ domain_allow: domain: 登録ã™ã‚‹ã¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ãƒ‡ãƒ¼ã‚¿ã‚’å—ä¿¡ã—ãŸã‚Šã€ã“ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‹ã‚‰å—ä¿¡ã™ã‚‹ãƒ‡ãƒ¼ã‚¿ã‚’処ç†ã—ã¦ä¿å˜ã§ãるよã†ã«ãªã‚Šã¾ã™ + domain_subscribe: + domain: è³¼èªã—ãŸã„サーãƒã®ãƒ‰ãƒ¡ã‚¤ãƒ³åを指定ã—ã¾ã™ + exclude_reblog: ブーストã•ã‚ŒãŸæŠ•ç¨¿ã‚’è³¼èªã‹ã‚‰é™¤å¤–ã—ã¾ã™ featured_tag: name: 'ã“れらを使ã†ã¨ã„ã„ã‹ã‚‚ã—れã¾ã›ã‚“:' form_challenge: @@ -55,6 +58,11 @@ ja: data: ä»–ã® Mastodon サーãƒãƒ¼ã‹ã‚‰ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ãŸCSVãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¦ä¸‹ã•ã„ invite_request: text: ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾åœ¨æ‰¿èªåˆ¶ã§ã™ã€‚申請を承èªã™ã‚‹éš›ã«å½¹ç«‹ã¤ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’æ·»ãˆã¦ãã ã•ã„ + keyword_subscribe: + exclude_keyword: カンマã§åŒºåˆ‡ã£ã¦è¤‡æ•°ã®é™¤å¤–ã™ã‚‹ã‚ーワードを並ã¹ã¾ã™ï¼ˆã¾ãŸã¯æ£è¦è¡¨ç¾ã§æŒ‡å®šã—ã¾ã™ï¼‰ + ignore_block: ドメイン全体をéžè¡¨ç¤ºã«ã—ãŸã¾ã¾ã€ã‚ーワードã®è³¼èªã‚’優先ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ + keyword: カンマã§åŒºåˆ‡ã£ã¦è¤‡æ•°ã®ã‚ーワードを並ã¹ã¾ã™ï¼ˆã¾ãŸã¯æ£è¦è¡¨ç¾ã§æŒ‡å®šã—ã¾ã™ï¼‰ + name: オプションã§ã™ sessions: otp: 'æºå¸¯é›»è©±ã®ã‚¢ãƒ—リã§ç”Ÿæˆã•れãŸäºŒæ®µéšŽèªè¨¼ã‚³ãƒ¼ãƒ‰ã‚’入力ã™ã‚‹ã‹ã€ãƒªã‚«ãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã‚’使用ã—ã¦ãã ã•ã„:' tag: @@ -68,6 +76,8 @@ ja: value: 内容 account_alias: acct: 引ã£è¶Šã—å…ƒã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ ID + account_input: + acct: アカウント (account@domain.tld) account_migration: acct: 引ã£è¶Šã—å…ˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ ID account_warning_preset: @@ -138,10 +148,18 @@ ja: username: ユーザーå username_or_email: ユーザーåã¾ãŸã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ whole_word: å˜èªžå…¨ä½“ã«ãƒžãƒƒãƒ + domain_subscribe: + domain: ドメイン + exclude_reblog: ブースト除外 + list_id: 対象タイムライン + reblog: ブースト + timeline: タイムライン favourite_tag: name: ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° featured_tag: name: ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° + follow_tag: + name: ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° interactions: must_be_follower: フォãƒãƒ¯ãƒ¼ä»¥å¤–ã‹ã‚‰ã®é€šçŸ¥ã‚’ブãƒãƒƒã‚¯ must_be_following: フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®é€šçŸ¥ã‚’ブãƒãƒƒã‚¯ @@ -150,6 +168,23 @@ ja: comment: コメント invite_request: text: æ„æ°—è¾¼ã¿ã‚’ãŠèžã‹ã›ãã ã•ã„ + 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: ãŠæ°—ã«å…¥ã‚Šç™»éŒ²ã•ã‚ŒãŸæ™‚ diff --git a/config/navigation.rb b/config/navigation.rb index f67e6dcc3440c64974b1ad375e28a161b2efa609..2cb2cdc3f7e56c0da5c659b42c204a44c3628f4b 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -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 :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| diff --git a/config/routes.rb b/config/routes.rb index 6b245a1eb7668e09616282f27a3366d6b5b1d9ff..0e13d8cb2bf5bd2ca4dbc7698005927eb82a4ebb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -147,6 +147,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] end resources :media, only: [:show] do @@ -402,6 +406,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' diff --git a/db/migrate/20190829202944_create_follow_tags.rb b/db/migrate/20190829202944_create_follow_tags.rb new file mode 100644 index 0000000000000000000000000000000000000000..1116ad640c64a6842ae4a8103accfd6cef6041bb --- /dev/null +++ b/db/migrate/20190829202944_create_follow_tags.rb @@ -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 diff --git a/db/migrate/20190901090544_create_account_subscribes.rb b/db/migrate/20190901090544_create_account_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..1fbb20ec09f3df8bd1c5186055feb0fa1ebca7a0 --- /dev/null +++ b/db/migrate/20190901090544_create_account_subscribes.rb @@ -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 diff --git a/db/migrate/20190903113117_create_keyword_subscribes.rb b/db/migrate/20190903113117_create_keyword_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..6fd1f1f87c8c61bb7f59c3a7eddb3b2fcbaf3369 --- /dev/null +++ b/db/migrate/20190903113117_create_keyword_subscribes.rb @@ -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 diff --git a/db/migrate/20190910140929_add_name_and_flag_to_keyword_subscribe.rb b/db/migrate/20190910140929_add_name_and_flag_to_keyword_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..30e0a4bdf179a5243fa4159e5a9086ee4a9a90bf --- /dev/null +++ b/db/migrate/20190910140929_add_name_and_flag_to_keyword_subscribe.rb @@ -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 diff --git a/db/migrate/20190911093445_create_domain_subscribes.rb b/db/migrate/20190911093445_create_domain_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..0a007b0a2befb165240dc03b5de26bddf7994e8f --- /dev/null +++ b/db/migrate/20190911093445_create_domain_subscribes.rb @@ -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 diff --git a/db/migrate/20190914231645_add_exclude_keyword_to_keyword_subscribe.rb b/db/migrate/20190914231645_add_exclude_keyword_to_keyword_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c3c1df3f8071b8ce9f90be7f9c42e2bc01696e8 --- /dev/null +++ b/db/migrate/20190914231645_add_exclude_keyword_to_keyword_subscribe.rb @@ -0,0 +1,5 @@ +class AddExcludeKeywordToKeywordSubscribe < ActiveRecord::Migration[5.2] + def change + add_column :keyword_subscribes, :exclude_keyword, :string, default: '', null: false + end +end diff --git a/db/migrate/20191022105417_add_exclude_reblog_to_domain_subscribe.rb b/db/migrate/20191022105417_add_exclude_reblog_to_domain_subscribe.rb new file mode 100644 index 0000000000000000000000000000000000000000..3459b0052458c1c7f99e6188e2ae7832e5ae2003 --- /dev/null +++ b/db/migrate/20191022105417_add_exclude_reblog_to_domain_subscribe.rb @@ -0,0 +1,5 @@ +class AddExcludeReblogToDomainSubscribe < ActiveRecord::Migration[5.2] + def change + add_column :domain_subscribes, :exclude_reblog, :boolean, default: true + end +end diff --git a/db/migrate/20191025031836_remove_exclude_home_from_keyword_subscribes.rb b/db/migrate/20191025031836_remove_exclude_home_from_keyword_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..66fd3b9d47cfbf7e8ff954f01fa808787a81674a --- /dev/null +++ b/db/migrate/20191025031836_remove_exclude_home_from_keyword_subscribes.rb @@ -0,0 +1,5 @@ +class RemoveExcludeHomeFromKeywordSubscribes < ActiveRecord::Migration[5.2] + def change + safety_assured { remove_column :keyword_subscribes, :exclude_home, :boolean } + end +end diff --git a/db/migrate/20191025190919_add_list_to_keyword_subscribes.rb b/db/migrate/20191025190919_add_list_to_keyword_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..e7973dd864b9dafd8103e9cc2b2ce942df3741e6 --- /dev/null +++ b/db/migrate/20191025190919_add_list_to_keyword_subscribes.rb @@ -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 diff --git a/db/migrate/20191026110416_add_list_to_account_subscribes.rb b/db/migrate/20191026110416_add_list_to_account_subscribes.rb new file mode 100644 index 0000000000000000000000000000000000000000..7420c8bf63b1c75962a4a7a30bd4285d078e5c8c --- /dev/null +++ b/db/migrate/20191026110416_add_list_to_account_subscribes.rb @@ -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 diff --git a/db/migrate/20191026110502_add_list_to_follow_tags.rb b/db/migrate/20191026110502_add_list_to_follow_tags.rb new file mode 100644 index 0000000000000000000000000000000000000000..ecb0714bc5009168a258ecceb24af17835c88a60 --- /dev/null +++ b/db/migrate/20191026110502_add_list_to_follow_tags.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 65a200371b0aa2d7abf25ce53df8f87ca3c0572d..0ceb16e18d54268e2f1002cc769efdd783a6cfb7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -101,6 +101,17 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true 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_tag_stats", force: :cascade do |t| t.bigint "tag_id", null: false t.bigint "accounts_count", default: 0, null: false @@ -295,6 +306,17 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) 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 @@ -342,6 +364,17 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) 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 @@ -349,6 +382,7 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do t.bigint "target_account_id", null: false t.boolean "show_reblogs", default: true, null: false t.string "uri" + 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 @@ -389,6 +423,22 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do t.index ["user_id"], name: "index_invites_on_user_id" 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 @@ -823,6 +873,9 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade add_foreign_key "account_pins", "accounts", on_delete: :cascade add_foreign_key "account_stats", "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_tag_stats", "tags", 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 @@ -836,6 +889,8 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade + add_foreign_key "domain_subscribes", "accounts", on_delete: :cascade + add_foreign_key "domain_subscribes", "lists", on_delete: :cascade add_foreign_key "favourite_tags", "accounts", on_delete: :cascade add_foreign_key "favourite_tags", "tags", on_delete: :cascade add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade @@ -844,11 +899,16 @@ ActiveRecord::Schema.define(version: 2020_01_19_112504) do add_foreign_key "featured_tags", "tags", 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 diff --git a/spec/fabricators/account_subscribe_fabricator.rb b/spec/fabricators/account_subscribe_fabricator.rb new file mode 100644 index 0000000000000000000000000000000000000000..c9fbf4706a317327a3d91b5168842330e81e7536 --- /dev/null +++ b/spec/fabricators/account_subscribe_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:account_subscribe) do + account + target_account +end diff --git a/spec/fabricators/domain_subscribe_fabricator.rb b/spec/fabricators/domain_subscribe_fabricator.rb new file mode 100644 index 0000000000000000000000000000000000000000..8dd39b0d6b6be99d5603ab665508e322c30a96c2 --- /dev/null +++ b/spec/fabricators/domain_subscribe_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:domain_subscribe) do + account + list + domain +end diff --git a/spec/fabricators/follow_tag_fabricator.rb b/spec/fabricators/follow_tag_fabricator.rb new file mode 100644 index 0000000000000000000000000000000000000000..3c39f781b76382741158a0ce191869e1ab4c6f83 --- /dev/null +++ b/spec/fabricators/follow_tag_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:follow_tag) do + account + tag +end diff --git a/spec/fabricators/keyword_subscribe_fabricator.rb b/spec/fabricators/keyword_subscribe_fabricator.rb new file mode 100644 index 0000000000000000000000000000000000000000..3913be0ce6b663496c5b829c052b6338198b53b1 --- /dev/null +++ b/spec/fabricators/keyword_subscribe_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:keyword_subscribe) do + account + keyword + ignorecase + regexp +end diff --git a/spec/models/account_subscribe_spec.rb b/spec/models/account_subscribe_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..283f53b2041edeffc89f6a223d8e20ee70145f3b --- /dev/null +++ b/spec/models/account_subscribe_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe AccountSubscribe, type: :model do +end diff --git a/spec/models/domain_subscribe_spec.rb b/spec/models/domain_subscribe_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..167c574c128a8e39fdc48266dccc93f3350c59ea --- /dev/null +++ b/spec/models/domain_subscribe_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe DomainSubscribe, type: :model do +end diff --git a/spec/models/follow_tag_spec.rb b/spec/models/follow_tag_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..07c708859e07dcf44ec782cd64c44d89f1cbb204 --- /dev/null +++ b/spec/models/follow_tag_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe FollowTag, type: :model do +end diff --git a/spec/models/keyword_subscribe_spec.rb b/spec/models/keyword_subscribe_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..87fa7935ff08cce9fe331f9a35a6751a28fba7d8 --- /dev/null +++ b/spec/models/keyword_subscribe_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe KeywordSubscribe, type: :model do +end