From 3de2c37953621e3ea201f8f2b6bd2dbf67982528 Mon Sep 17 00:00:00 2001 From: noellabo <noel.yoshiba@gmail.com> Date: Fri, 30 Aug 2019 17:02:41 +0900 Subject: [PATCH] Add subscribe features --- app/chewy/statuses_index.rb | 2 +- .../api/v1/account_subscribes_controller.rb | 43 +++++++ .../api/v1/domain_subscribes_controller.rb | 34 ++++++ .../api/v1/follow_tags_controller.rb | 43 +++++++ .../api/v1/keyword_subscribes_controller.rb | 48 ++++++++ .../settings/account_subscribes_controller.rb | 55 +++++++++ .../settings/domain_subscribes_controller.rb | 73 ++++++++++++ .../settings/follow_tags_controller.rb | 44 +++++++ .../settings/keyword_subscribes_controller.rb | 78 +++++++++++++ app/helpers/lists_helper.rb | 7 ++ app/javascript/styles/mastodon/admin.scss | 4 + app/javascript/styles/mastodon/tables.scss | 5 + app/lib/feed_manager.rb | 52 +++++---- app/models/account_subscribe.rb | 23 ++++ app/models/concerns/account_associations.rb | 7 ++ app/models/concerns/account_interactions.rb | 21 ++++ app/models/domain_subscribe.rb | 24 ++++ app/models/follow.rb | 1 + app/models/follow_request.rb | 1 + app/models/follow_tag.rb | 27 +++++ app/models/keyword_subscribe.rb | 107 ++++++++++++++++++ app/models/list_account.rb | 2 + app/models/status.rb | 4 + app/models/tag.rb | 1 + .../rest/account_subscribe_serializer.rb | 9 ++ .../rest/domain_subscribe_serializer.rb | 9 ++ app/serializers/rest/follow_tag_serializer.rb | 9 ++ .../rest/keyword_subscribes_serializer.rb | 9 ++ app/services/account_subscribe_service.rb | 29 +++++ app/services/fan_out_on_write_service.rb | 83 +++++++++++++- app/services/follow_service.rb | 1 + app/services/unsubscribe_account_service.rb | 16 +++ .../account_subscribes/index.html.haml | 25 ++++ .../domain_subscribes/_fields.html.haml | 11 ++ .../settings/domain_subscribes/edit.html.haml | 9 ++ .../domain_subscribes/index.html.haml | 35 ++++++ .../settings/domain_subscribes/new.html.haml | 9 ++ .../settings/follow_tags/index.html.haml | 26 +++++ .../keyword_subscribes/_fields.html.haml | 24 ++++ .../keyword_subscribes/edit.html.haml | 9 ++ .../keyword_subscribes/index.html.haml | 66 +++++++++++ .../settings/keyword_subscribes/new.html.haml | 9 ++ app/workers/merge_worker.rb | 4 +- config/locales/en.yml | 63 +++++++++++ config/locales/ja.yml | 46 ++++++++ config/locales/simple_form.en.yml | 33 ++++++ config/locales/simple_form.ja.yml | 35 ++++++ config/navigation.rb | 9 +- config/routes.rb | 9 ++ .../20190829202944_create_follow_tags.rb | 10 ++ ...0190901090544_create_account_subscribes.rb | 10 ++ ...0190903113117_create_keyword_subscribes.rb | 12 ++ ..._add_name_and_flag_to_keyword_subscribe.rb | 8 ++ ...20190911093445_create_domain_subscribes.rb | 11 ++ ...dd_exclude_keyword_to_keyword_subscribe.rb | 5 + ..._add_exclude_reblog_to_domain_subscribe.rb | 5 + ...ve_exclude_home_from_keyword_subscribes.rb | 5 + ...25190919_add_list_to_keyword_subscribes.rb | 8 ++ ...26110416_add_list_to_account_subscribes.rb | 8 ++ .../20191026110502_add_list_to_follow_tags.rb | 8 ++ db/schema.rb | 60 ++++++++++ .../account_subscribe_fabricator.rb | 4 + .../domain_subscribe_fabricator.rb | 5 + spec/fabricators/follow_tag_fabricator.rb | 4 + .../keyword_subscribe_fabricator.rb | 6 + spec/models/account_subscribe_spec.rb | 4 + spec/models/domain_subscribe_spec.rb | 4 + spec/models/follow_tag_spec.rb | 4 + spec/models/keyword_subscribe_spec.rb | 4 + 69 files changed, 1454 insertions(+), 24 deletions(-) create mode 100644 app/controllers/api/v1/account_subscribes_controller.rb create mode 100644 app/controllers/api/v1/domain_subscribes_controller.rb create mode 100644 app/controllers/api/v1/follow_tags_controller.rb create mode 100644 app/controllers/api/v1/keyword_subscribes_controller.rb create mode 100644 app/controllers/settings/account_subscribes_controller.rb create mode 100644 app/controllers/settings/domain_subscribes_controller.rb create mode 100644 app/controllers/settings/follow_tags_controller.rb create mode 100644 app/controllers/settings/keyword_subscribes_controller.rb create mode 100644 app/helpers/lists_helper.rb create mode 100644 app/models/account_subscribe.rb create mode 100644 app/models/domain_subscribe.rb create mode 100644 app/models/follow_tag.rb create mode 100644 app/models/keyword_subscribe.rb create mode 100644 app/serializers/rest/account_subscribe_serializer.rb create mode 100644 app/serializers/rest/domain_subscribe_serializer.rb create mode 100644 app/serializers/rest/follow_tag_serializer.rb create mode 100644 app/serializers/rest/keyword_subscribes_serializer.rb create mode 100644 app/services/account_subscribe_service.rb create mode 100644 app/services/unsubscribe_account_service.rb create mode 100644 app/views/settings/account_subscribes/index.html.haml create mode 100644 app/views/settings/domain_subscribes/_fields.html.haml create mode 100644 app/views/settings/domain_subscribes/edit.html.haml create mode 100644 app/views/settings/domain_subscribes/index.html.haml create mode 100644 app/views/settings/domain_subscribes/new.html.haml create mode 100644 app/views/settings/follow_tags/index.html.haml create mode 100644 app/views/settings/keyword_subscribes/_fields.html.haml create mode 100644 app/views/settings/keyword_subscribes/edit.html.haml create mode 100644 app/views/settings/keyword_subscribes/index.html.haml create mode 100644 app/views/settings/keyword_subscribes/new.html.haml create mode 100644 db/migrate/20190829202944_create_follow_tags.rb create mode 100644 db/migrate/20190901090544_create_account_subscribes.rb create mode 100644 db/migrate/20190903113117_create_keyword_subscribes.rb create mode 100644 db/migrate/20190910140929_add_name_and_flag_to_keyword_subscribe.rb create mode 100644 db/migrate/20190911093445_create_domain_subscribes.rb create mode 100644 db/migrate/20190914231645_add_exclude_keyword_to_keyword_subscribe.rb create mode 100644 db/migrate/20191022105417_add_exclude_reblog_to_domain_subscribe.rb create mode 100644 db/migrate/20191025031836_remove_exclude_home_from_keyword_subscribes.rb create mode 100644 db/migrate/20191025190919_add_list_to_keyword_subscribes.rb create mode 100644 db/migrate/20191026110416_add_list_to_account_subscribes.rb create mode 100644 db/migrate/20191026110502_add_list_to_follow_tags.rb create mode 100644 spec/fabricators/account_subscribe_fabricator.rb create mode 100644 spec/fabricators/domain_subscribe_fabricator.rb create mode 100644 spec/fabricators/follow_tag_fabricator.rb create mode 100644 spec/fabricators/keyword_subscribe_fabricator.rb create mode 100644 spec/models/account_subscribe_spec.rb create mode 100644 spec/models/domain_subscribe_spec.rb create mode 100644 spec/models/follow_tag_spec.rb create mode 100644 spec/models/keyword_subscribe_spec.rb diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index f5735421c7..2e0c0ccc39 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 0000000000..5da428bbd9 --- /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 0000000000..40cf753330 --- /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 0000000000..95682ec7d2 --- /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 0000000000..ec80143ce5 --- /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 0000000000..4ca838ac3c --- /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 0000000000..42fcbc6e42 --- /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 0000000000..776a485042 --- /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 0000000000..fd38581d9e --- /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 0000000000..1b85405be4 --- /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 7f22f58a1d..5468b33707 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 62f5554ffc..eb070e0d86 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 e979d2866a..a598e7d500 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 0000000000..0977587864 --- /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 f901c446d1..3dc2f0a18e 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 f27d39483a..e4b2bef32b 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 0000000000..48eef4fb51 --- /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 87fa114253..7f2d3e5b67 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 96ac7eaa59..23ade62153 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 0000000000..90b428b745 --- /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 0000000000..1df016eecc --- /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 785923c4cf..0cb4339be1 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 81f7e6576a..862755c17e 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 8e6fc404db..29891e0c53 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 0000000000..50ef86ae34 --- /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 0000000000..fef4e3b83a --- /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 0000000000..ea9db96013 --- /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 0000000000..4253d02308 --- /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 0000000000..8e9b0adf32 --- /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 7fd4e69035..5e12c035f4 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 dc47804c05..91c8e2e6d3 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 0000000000..534f5d3668 --- /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 0000000000..0cbb8aab51 --- /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 0000000000..30c5b566d7 --- /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 0000000000..12e3c0a1b0 --- /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 0000000000..7602eff3f3 --- /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 0000000000..023a9888dd --- /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 0000000000..18fce39872 --- /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 0000000000..c40939e1bd --- /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 0000000000..d11d94e2d3 --- /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 0000000000..750a816a4b --- /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 0000000000..b6e9791276 --- /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 d745cb99c7..0de0acc9a9 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 172bc51d69..bf01a12f67 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 5993feb649..b020d5ddcf 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 66f518c1b9..4d07bd6ee4 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 fdccc13448..271d6496c6 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 f67e6dcc34..2cb2cdc3f7 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 6b245a1eb7..0e13d8cb2b 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 0000000000..1116ad640c --- /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 0000000000..1fbb20ec09 --- /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 0000000000..6fd1f1f87c --- /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 0000000000..30e0a4bdf1 --- /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 0000000000..0a007b0a2b --- /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 0000000000..1c3c1df3f8 --- /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 0000000000..3459b00524 --- /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 0000000000..66fd3b9d47 --- /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 0000000000..e7973dd864 --- /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 0000000000..7420c8bf63 --- /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 0000000000..ecb0714bc5 --- /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 65a200371b..0ceb16e18d 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 0000000000..c9fbf4706a --- /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 0000000000..8dd39b0d6b --- /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 0000000000..3c39f781b7 --- /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 0000000000..3913be0ce6 --- /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 0000000000..283f53b204 --- /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 0000000000..167c574c12 --- /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 0000000000..07c708859e --- /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 0000000000..87fa7935ff --- /dev/null +++ b/spec/models/keyword_subscribe_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe KeywordSubscribe, type: :model do +end -- GitLab