Add antenna and bio-searchability support

This commit is contained in:
KMY 2023-04-24 10:06:25 +09:00
parent 7e125b276f
commit 2fef21664b
35 changed files with 775 additions and 6 deletions

View file

@ -0,0 +1,76 @@
# frozen_string_literal: true
class AntennasController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_antenna, only: [:edit, :update, :destroy]
before_action :set_lists, only: [:new, :edit]
before_action :set_body_classes
before_action :set_cache_headers
def index
@antennas = current_account.antennas.includes(:antenna_domains).includes(:antenna_tags).includes(:antenna_accounts)
end
def new
@antenna = current_account.antennas.build
@antenna.antenna_domains.build
@antenna.antenna_tags.build
@antenna.antenna_accounts.build
end
def create
@antenna = current_account.antennas.build(thin_resource_params)
saved = @antenna.save
saved = @antenna.update(resource_params) if saved
if saved
redirect_to antennas_path
else
render action: :new
end
end
def edit; end
def update
if @antenna.update(resource_params)
redirect_to antennas_path
else
render action: :edit
end
end
def destroy
@antenna.destroy
redirect_to antennas_path
end
private
def set_antenna
@antenna = current_account.antennas.find(params[:id])
end
def set_lists
@lists = current_account.owned_lists
end
def resource_params
params.require(:antenna).permit(:title, :list, :available, :expires_in, :keywords_raw, :exclude_keywords_raw, :domains_raw, :exclude_domains_raw, :accounts_raw, :exclude_accounts_raw, :tags_raw, :exclude_tags_raw)
end
def thin_resource_params
params.require(:antenna).permit(:title, :list)
end
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end

View file

@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
:bot,
:discoverable,
:searchability,
:dissubscribable,
:hide_collections,
fields_attributes: [:name, :value]
)

View file

@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :my_actor_type, :searchability, :group_allow_private_message, :discoverable, :hide_collections, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :my_actor_type, :searchability, :dissubscribable, :group_allow_private_message, :discoverable, :hide_collections, fields_attributes: [:name, :value])
end
def set_account

View file

@ -23,6 +23,7 @@ module ContextHelper
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
emoji_reactions: { 'fedibird' => 'http://fedibird.com/ns#', 'emojiReactions' => { '@id' => "fedibird:emojiReactions", '@type' => '@id' } },
searchable_by: { 'fedibird' => 'http://fedibird.com/ns#', 'searchableBy' => { '@id' => "fedibird:searchableBy", '@type' => '@id' } },
subscribable_by: { 'kmyblue' => 'http://kmy.blue/ns#', 'subscribableBy' => { '@id' => "kmyblue:subscribableBy", '@type' => '@id' } },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze

View file

@ -1062,6 +1062,13 @@ a.name-tag,
margin-bottom: 10px;
}
.listname {
color: $dark-text-color;
font-weight: bold;
font-size: 14px;
margin-left: 16px;
}
.expiration {
font-size: 13px;
}

View file

@ -449,11 +449,27 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end
end
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/.freeze
def searchability
searchability = searchability_from_audience
if searchability.nil?
note = @account&.note
return nil unless note.present?
searchability_bio = note.scan(SCAN_SEARCHABILITY_RE).first
return nil unless searchability_bio
searchability = searchability_bio[0]
return nil if searchability.nil?
searchability = :public if searchability == 'public'
searchability = :unlisted if searchability == 'followers'
searchability = :direct if searchability == 'private'
searchability = :private if searchability == 'reactors'
end
visibility = visibility_from_audience_with_silence
if searchability === visibility

View file

@ -186,6 +186,10 @@ class ActivityPub::TagManager
nil
end
def subscribable_by(account)
account.dissubscribable ? [] : [COLLECTIONS[:public]]
end
def searchable_by(status)
searchable_by =
case status.compute_searchability_activitypub

View file

@ -52,6 +52,7 @@
# requested_review_at :datetime
# group_allow_private_message :boolean
# searchability :integer default("private"), not null
# dissubscribable :boolean default(FALSE), not null
#
class Account < ApplicationRecord

203
app/models/antenna.rb Normal file
View file

@ -0,0 +1,203 @@
# == Schema Information
#
# Table name: antennas
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# list_id :bigint(8) not null
# title :string default(""), not null
# keywords :jsonb
# exclude_keywords :jsonb
# any_domains :boolean default(TRUE), not null
# any_tags :boolean default(TRUE), not null
# any_accounts :boolean default(TRUE), not null
# any_keywords :boolean default(TRUE), not null
# available :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# expires_at :datetime
# with_media_only :boolean default(FALSE), not null
#
class Antenna < ApplicationRecord
include Expireable
has_many :antenna_domains, inverse_of: :antenna, dependent: :destroy
has_many :antenna_tags, inverse_of: :antenna, dependent: :destroy
has_many :antenna_accounts, inverse_of: :antenna, dependent: :destroy
belongs_to :account
belongs_to :list
scope :all_keywords, -> { where(any_keywords: true) }
scope :all_domains, -> { where(any_domains: true) }
scope :all_accounts, -> { where(any_accounts: true) }
scope :all_tags, -> { where(any_tags: true) }
scope :availables, -> { where(available: true).where(Arel.sql('any_keywords = FALSE OR any_domains = FALSE OR any_accounts = FALSE OR any_tags = FALSE')) }
def enabled?
enabled_config? && !expired?
end
def enabled_config?
available && enabled_config_raws?
end
def enabled_config_raws?
!(any_keywords && any_domains && any_accounts && any_tags)
end
def expires_in
return @expires_in if defined?(@expires_in)
return nil if expires_at.nil?
[30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].find { |expires_in| expires_in.from_now >= expires_at }
end
def context
context = []
context << 'domain' if !any_domains
context << 'tag' if !any_tags
context << 'keyword' if !any_keywords
context << 'account' if !any_accounts
context
end
def list=(list_id)
list_id = list_id.to_i if list_id.is_a?(String)
if list_id.is_a?(Numeric)
self[:list_id] = list_id
else
self[:list] = list_id
end
end
def keywords_raw
return '' if !keywords.present?
keywords.join("\n")
end
def keywords_raw=(raw)
keywords = raw.split(/\R/).filter { |r| r.present? && r.length >= 2 }.uniq
self[:keywords] = keywords
self[:any_keywords] = !keywords.any? && !exclude_keywords&.any?
end
def exclude_keywords_raw
return '' if !exclude_keywords.present?
exclude_keywords.join("\n")
end
def exclude_keywords_raw=(raw)
exclude_keywords = raw.split(/\R/).filter { |r| r.present? }.uniq
self[:exclude_keywords] = exclude_keywords
self[:any_keywords] = !keywords&.any? && !exclude_keywords.any?
end
def tags_raw
antenna_tags.where(exclude: false).map(&:tag).map(&:name).join("\n")
end
def tags_raw=(raw)
return if tags_raw == raw
tag_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('#') ? r[1..-1] : r }.uniq
antenna_tags.where(exclude: false).destroy_all
Tag.find_or_create_by_names(tag_names).each do |tag|
antenna_tags.create!(tag: tag, exclude: false)
end
self[:any_tags] = !tag_names.any?
end
def exclude_tags_raw
antenna_tags.where(exclude: true).map(&:tag).map(&:name).join("\n")
end
def exclude_tags_raw=(raw)
return if exclude_tags_raw == raw
tag_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('#') ? r[1..-1] : r }.uniq
antenna_tags.where(exclude: true).destroy_all
Tag.find_or_create_by_names(tag_names).each do |tag|
antenna_tags.create!(tag: tag, exclude: true)
end
end
def domains_raw
antenna_domains.where(exclude: false).map(&:name).join("\n")
end
def domains_raw=(raw)
return if domains_raw == raw
domain_names = raw.split(/\R/).filter { |r| r.present? }.uniq
antenna_domains.where(exclude: false).destroy_all
domain_names.each do |domain|
antenna_domains.create!(name: domain, exclude: false)
end
self[:any_domains] = !domain_names.any?
end
def exclude_domains_raw
antenna_domains.where(exclude: true).map(&:name).join("\n")
end
def exclude_domains_raw=(raw)
return if exclude_domains_raw == raw
domain_names = raw.split(/\R/).filter { |r| r.present? }.uniq
antenna_domains.where(exclude: true).destroy_all
domain_names.each do |domain|
antenna_domains.create!(name: domain, exclude: true)
end
end
def accounts_raw
antenna_accounts.where(exclude: false).map(&:account).map { |account| account.domain ? "@#{account.username}@#{account.domain}" : "@#{account.username}" }.join("\n")
end
def accounts_raw=(raw)
return if accounts_raw == raw
account_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('@') ? r[1..-1] : r }.uniq
hit = false
antenna_accounts.where(exclude: false).destroy_all
account_names.each do |name|
username, domain = name.split('@')
account = Account.find_by(username: username, domain: domain)
if account.present?
antenna_accounts.create!(account: account, exclude: false)
hit = true
end
end
self[:any_accounts] = !hit
end
def exclude_accounts_raw
antenna_accounts.where(exclude: true).map(&:account).map { |account| account.domain ? "@#{account.username}@#{account.domain}" : "@#{account.username}" }.join("\n")
end
def exclude_accounts_raw=(raw)
return if exclude_accounts_raw == raw
account_names = raw.split(/\R/).filter { |r| r.present? }.map { |r| r.start_with?('@') ? r[1..-1] : r }.uniq
hit = false
antenna_accounts.where(exclude: true).destroy_all
account_names.each do |name|
username, domain = name.split('@')
account = Account.find_by(username: username, domain: domain)
if account.present?
antenna_accounts.create!(account: account, exclude: true)
hit = true
end
end
end
end

View file

@ -0,0 +1,17 @@
# == Schema Information
#
# Table name: antenna_accounts
#
# id :bigint(8) not null, primary key
# antenna_id :bigint(8) not null
# account_id :bigint(8) not null
# exclude :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AntennaAccount < ApplicationRecord
belongs_to :antenna
belongs_to :account
end

View file

@ -0,0 +1,16 @@
# == Schema Information
#
# Table name: antenna_domains
#
# id :bigint(8) not null, primary key
# antenna_id :bigint(8) not null
# name :string
# exclude :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AntennaDomain < ApplicationRecord
belongs_to :antenna
end

17
app/models/antenna_tag.rb Normal file
View file

@ -0,0 +1,17 @@
# == Schema Information
#
# Table name: antenna_tags
#
# id :bigint(8) not null, primary key
# antenna_id :bigint(8) not null
# tag_id :bigint(8) not null
# exclude :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class AntennaTag < ApplicationRecord
belongs_to :antenna
belongs_to :tag
end

View file

@ -39,6 +39,8 @@ module AccountAssociations
has_many :report_notes, dependent: :destroy
has_many :custom_filters, inverse_of: :account, dependent: :destroy
has_many :antennas, inverse_of: :account, dependent: :destroy
has_many :antenna_accounts, inverse_of: :account, dependent: :destroy
# Moderation notes
has_many :account_moderation_notes, dependent: :destroy, inverse_of: :account

View file

@ -23,6 +23,7 @@ class List < ApplicationRecord
has_many :list_accounts, inverse_of: :list, dependent: :destroy
has_many :accounts, through: :list_accounts
has_many :antennas, inverse_of: :list, dependent: :destroy
validates :title, presence: true

View file

@ -27,6 +27,8 @@ class Tag < ApplicationRecord
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
has_many :followers, through: :passive_relationships, source: :account
has_one :antenna_tag, dependent: :destroy, inverse_of: :tag
HASHTAG_SEPARATORS = "_\u00B7\u30FB\u200c"
HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]"
HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]"

View file

@ -7,13 +7,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context :security
context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :discoverable, :olm, :suspended, :searchable_by
:moved_to, :property_value, :discoverable, :olm, :suspended, :searchable_by, :subscribable_by
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
:preferred_username, :name, :summary,
:url, :manually_approves_followers,
:discoverable, :published, :searchable_by
:discoverable, :published, :searchable_by, :subscribable_by
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
@ -166,6 +166,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
ActivityPub::TagManager.instance.account_searchable_by(object)
end
def subscribable_by
ActivityPub::TagManager.instance.subscribable_by(object)
end
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
end

View file

@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
include FormattingHelper
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static, :searchability,
:note, :url, :avatar, :avatar_static, :header, :header_static, :searchability, :dissubscribable,
:followers_count, :following_count, :statuses_count, :last_status_at
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

View file

@ -78,6 +78,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.suspension_origin = :local if auto_suspend?
@account.silenced_at = domain_block.created_at if auto_silence?
@account.searchability = :private # not null
@account.dissubscribable = false # not null
@account.save
end
@ -115,6 +116,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.discoverable = @json['discoverable'] || false
@account.searchability = searchability_from_audience
@account.dissubscribable = !subscribable(@account.note)
end
def set_fetchable_key!
@ -249,6 +251,20 @@ class ActivityPub::ProcessAccountService < BaseService
end
end
def subscribable_by
return nil if @json['subscribableBy'].nil?
@subscribable_by = as_array(@json['subscribableBy']).map { |x| value_or_id(x) }
end
def subscribable(note)
if subscribable_by.nil?
!note.include?('[subscribable:no]')
else
subscribable_by.any? { |uri| ActivityPub::TagManager.instance.public_collection?(uri) }
end
end
def property_values
return unless @json['attachment'].is_a?(Array)

View file

@ -8,6 +8,7 @@ class DeleteAccountService < BaseService
account_pins
active_relationships
aliases
antennas
block_relationships
blocked_by_relationships
conversation_mutes

View file

@ -49,6 +49,7 @@ class FanOutOnWriteService < BaseService
when :public, :unlisted, :public_unlisted, :private
deliver_to_all_followers!
deliver_to_lists!
deliver_to_antennas! if [:public, :public_unlisted].include?(@status.visibility.to_sym) && !@status.account.dissubscribable
when :limited
deliver_to_mentioned_followers!
else
@ -115,6 +116,34 @@ class FanOutOnWriteService < BaseService
end
end
def deliver_to_antennas!
lists = []
antennas = Antenna.availables
antennas = antennas.left_joins(:antenna_accounts).where(any_accounts: true).or(Antenna.availables.left_joins(:antenna_accounts) .where(antenna_accounts: { exclude: false, account: @status.account }))
antennas = antennas.left_joins(:antenna_domains) .where(any_domains: true) .or(Antenna.availables.left_joins(:antenna_accounts).left_joins(:antenna_domains) .where(antenna_domains: { exclude: false, name: @status.account.domain }))
antennas = antennas.left_joins(:antenna_tags) .where(any_tags: true) .or(Antenna.availables.left_joins(:antenna_accounts).left_joins(:antenna_domains).left_joins(:antenna_tags).where(antenna_tags: { exclude: false, tag: @status.tags }))
antennas = antennas.where(account: @status.account.followers) if @status.visibility.to_sym == :unlisted
antennas.in_batches do |ans|
ans.each do |antenna|
next if !antenna.enabled?
next if antenna.keywords.any? && !([nil, :public].include?(@status.searchability&.to_sym))
next if antenna.keywords.any? && !antenna.keywords.any? { |keyword| @status.text.include?(keyword) }
next if antenna.exclude_keywords.any? && antenna.exclude_keywords.any? { |keyword| @status.text.include?(keyword) }
next if antenna.antenna_accounts.where(exclude: true, account: @status.account).any?
next if antenna.antenna_domains.where(exclude: true, name: @status.account.domain).any?
next if antenna.antenna_tags.where(exclude: true, tag: @status.tags).any?
lists << antenna.list
end
end
lists = lists.uniq
if lists.any?
FeedInsertWorker.push_bulk(lists) do |list|
[@status.id, list.id, 'list', { 'update' => update? }]
end
end
end
def deliver_to_mentioned_followers!
@status.mentions.joins(:account).merge(@account.followers_for_local_distribution).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
FeedInsertWorker.push_bulk(mentions) do |mention|

View file

@ -0,0 +1,74 @@
.filters-list__item{ class: [(antenna.expired? || !antenna.enabled_config?) && 'expired'] }
= link_to edit_antenna_path(antenna), class: 'filters-list__item__title' do
= antenna.title
- if !antenna.enabled_config?
.expiration{ title: t('antennas.index.disabled') }
= t('antennas.index.disabled')
- elsif antenna.expires?
.expiration{ title: t('antennas.index.expires_on', date: l(antenna.expires_at)) }
- if antenna.expired?
= t('invites.expired')
- else
= t('antennas.index.expires_in', distance: distance_of_time_in_words_to_now(antenna.expires_at))
.listname
= antenna.list.title
.filters-list__item__permissions
%ul.permissions-list
- unless antenna.antenna_domains.empty?
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('sitemap')
.permissions-list__item__text
.permissions-list__item__text__title
= t('antennas.index.domains', count: antenna.antenna_domains.size)
.permissions-list__item__text__type
- domains = antenna.antenna_domains.map { |domain| domain.name }
- domains = domains.take(5) + ['…'] if domains.size > 5 # TODO
= domains.join(', ')
- unless antenna.antenna_accounts.empty?
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('users')
.permissions-list__item__text
.permissions-list__item__text__title
= t('antennas.index.accounts', count: antenna.antenna_accounts.size)
.permissions-list__item__text__type
- accounts = antenna.antenna_accounts.map { |account| account.account.domain ? "@#{account.account.username}@#{account.account.domain}" : "@#{account.account.username}" }
- accounts = accounts.take(5) + ['…'] if accounts.size > 5 # TODO
= accounts.join(', ')
- unless antenna.keywords.nil? || antenna.keywords.empty?
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('paragraph')
.permissions-list__item__text
.permissions-list__item__text__title
= t('antennas.index.keywords', count: antenna.keywords.size)
.permissions-list__item__text__type
- keywords = antenna.keywords
- keywords = keywords.take(5) + ['…'] if keywords.size > 5 # TODO
= keywords.join(', ')
- unless antenna.antenna_tags.empty?
%li.permissions-list__item
.permissions-list__item__icon
= fa_icon('hashtag')
.permissions-list__item__text
.permissions-list__item__text__title
= t('antennas.index.tags', count: antenna.antenna_tags.size)
.permissions-list__item__text__type
- tags = antenna.antenna_tags.map { |tag| tag.tag.name }
- tags = keywords.take(5) + ['…'] if tags.size > 5 # TODO
= tags.join(', ')
.announcements-list__item__action-bar
.announcements-list__item__meta
- if antenna.enabled_config_raws?
= t('antennas.index.contexts', contexts: antenna.context.map { |context| I18n.t("antennas.contexts.#{context}") }.join(', '))
- else
= t('antennas.errors.empty_contexts')
%div
= table_link_to 'pencil', t('antennas.edit.title'), edit_antenna_path(antenna)
= table_link_to 'times', t('antennas.index.delete'), antenna_path(antenna), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View file

@ -0,0 +1,51 @@
%p= t 'antennas.edit.description'
%hr.spacer/
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :title, as: :string, wrapper: :with_label, hint: false
.fields-row__column.fields-row__column-6.fields-group
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
.fields-row
.fields-group.fields-row__column.fields-row__column-6
= f.input :list, collection: lists, wrapper: :with_label, label_method: lambda { |list| list.title }, label: t('antennas.edit.list'), selected: f.object.list&.id, hint: false
.fields-group.fields-row__column.fields-row__column-6
= f.input :available, wrapper: :with_label, label: t('antennas.edit.available'), hint: false
%hr.spacer/
%p.hint= t 'antennas.edit.hint'
%hr.spacer/
%h4= t('antennas.contexts.domain')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :domains_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.domains_raw')
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_domains_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.exclude_domains_raw')
%h4= t('antennas.contexts.account')
%p.hint= t 'antennas.edit.accounts_hint'
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :accounts_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.accounts_raw')
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_accounts_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.exclude_accounts_raw')
%h4= t('antennas.contexts.tag')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :tags_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.tags_raw')
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_tags_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.exclude_tags_raw')
%h4= t('antennas.contexts.keyword')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :keywords_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.keywords_raw')
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_keywords_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }, label: t('antennas.edit.exclude_keywords_raw')

View file

@ -0,0 +1,8 @@
- content_for :page_title do
= t('antennas.edit.title')
= simple_form_for @antenna, url: antenna_path(@antenna), method: :put do |f|
= render 'antenna_fields', f: f, lists: @lists
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1,14 @@
- content_for :page_title do
= t('antennas.index.title')
- content_for :heading_actions do
= link_to t('antennas.new.title'), new_antenna_path, class: 'button'
.flash-message.alert
%strong= t('antennas.beta')
- if @antennas.empty?
.muted-hint.center-text= t 'antennas.index.empty'
- else
.applications-list
= render partial: 'antenna', collection: @antennas

View file

@ -0,0 +1,8 @@
- content_for :page_title do
= t('antennas.new.title')
= simple_form_for @antenna, url: antennas_path do |f|
= render 'antenna_fields', f: f, lists: @lists
.actions
= f.button :button, t('antennas.new.save'), type: :submit

View file

@ -38,6 +38,9 @@
.fields-group
= f.input :hide_collections, as: :boolean, wrapper: :with_label, label: t('simple_form.labels.defaults.setting_hide_network'), hint: t('simple_form.hints.defaults.setting_hide_network')
.fields-group
= f.input :dissubscribable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.dissubscribable')
%hr.spacer/
.fields-row

View file

@ -964,6 +964,43 @@ en:
empty: You have no aliases.
hint_html: If you want to move from another account to this one, here you can create an alias, which is required before you can proceed with moving followers from the old account to this one. This action by itself is <strong>harmless and reversible</strong>. <strong>The account migration is initiated from the old account</strong>.
remove: Unlink alias
antennas:
beta: This function is in beta.
contexts:
account: Accounts
domain: Domains
keyword: Keywords
tag: Tags
edit:
accounts_hint: \@askyq or @askyq@example.com
accounts_raw: Account list
available: Available
description: アンテナは、サーバーが認識した全ての公開・ローカル公開投稿のうち、検索許可が「公開」または明示的に設定されていないもの(検索許可システムに対応していないサーバーからの投稿)、かつ購読を拒否していないすべてのアカウントからの投稿が対象です。検出された投稿は、指定したリストに追加されます。
domains_raw: Domain list
exclude_accounts_raw: Excluding account list
exclude_domains_raw: Excluding domain list
exclude_keywords_raw: Excluding keyword list
exclude_tags_raw: Excluding hashtag list
hint: 下のリストに、絞り込み条件・除外条件を入力します。条件は複数指定することができます。1行につき1つずつ入力してください。空行、コメント、重複を含めることはできません。
keywords_raw: Keyword list
list: Destination list
tags_raw: Hashtag list
title: Edit antenna
errors:
deprecated_api_multiple_keywords: These parameters cannot be changed from this application because they apply to more than one filter keyword. Use a more recent application or the web interface.
empty_contexts: No contexts! You must set any context filters
invalid_context: None or invalid context supplied
index:
contexts: Antennas in %{contexts}
delete: Delete
disabled: Disabled
empty: You have no antennas.
expires_in: Expires in %{distance}
expires_on: Expires on %{date}
title: Antennas
new:
save: Save new antenna
title: Add new antenna
appearance:
advanced_web_interface: Advanced web interface
advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.'

View file

@ -940,6 +940,49 @@ ja:
empty: エイリアスがありません。
hint_html: 他のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。エイリアス自体は<strong>無害で、取り消す</strong>ことができます。<strong>引っ越しは以前のアカウント側から開始する必要があります</strong>。
remove: エイリアスを削除
antennas:
beta: アンテナ機能はベータ版です。今後、予告なく全データリセット・機能削除を行う場合があります。この機能の存在は外部に積極的に宣伝しないよう、ご協力をお願いします。
contexts:
account: アカウント
domain: ドメイン
keyword: キーワード
tag: ハッシュタグ
errors:
empty_contexts: 絞り込み条件が1つも指定されていないため無効です除外条件はカウントされません
edit:
accounts_hint: ローカルアカウントの場合は「@info」、リモートアカウントの場合は「@info@example.com」の形式で指定します。サーバーが認識していないアカウントは保存時に自動的に削除されます。
accounts_raw: 絞り込むアカウント
available: 有効
description: アンテナは、サーバーが認識した全ての公開・ローカル公開投稿のうち、検索許可が「公開」または明示的に設定されていないもの(検索許可システムに対応していないサーバーからの投稿)、かつ購読を拒否していないすべてのアカウントからの投稿が対象です。検出された投稿は、指定したリストに追加されます。
domains_raw: 絞り込むドメイン
exclude_accounts_raw: 除外するアカウント
exclude_domains_raw: 除外するドメイン
exclude_keywords_raw: 除外するキーワード
exclude_tags_raw: 除外するハッシュタグ
hint: 下のリストに、絞り込み条件・除外条件を入力します。条件は複数指定することができます。1行につき1つずつ入力してください。空行、コメント、重複を含めることはできません。絞り込み条件除外条件ではないは最低1つ設定しなければいけません。
keywords_raw: 絞り込むキーワード
list: 投稿配置先リスト
tags_raw: 絞り込むハッシュタグ
title: アンテナを編集
index:
accounts:
other: "%{count}件のアカウント"
contexts: "%{contexts}のアンテナ"
delete: 削除
disabled: 無効
domains:
other: "%{count}件のドメイン"
empty: アンテナはありません。
expires_in: "%{distance}で期限切れ"
expires_on: 有効期限 %{date}
keywords:
other: "%{count}件のキーワード"
tags:
other: "%{count}件のタグ"
title: アンテナ
new:
save: 新規アンテナを保存
title: 新規アンテナを追加
appearance:
advanced_web_interface: 上級者向けUI
advanced_web_interface_hint: ディスプレイを幅いっぱいまで活用したい場合、上級者向け UI をおすすめします。ホーム、通知、連合タイムライン、更にはリストやハッシュタグなど、様々な異なるカラムから望む限りの情報を一度に受け取れるような設定が可能になります。

View file

@ -38,6 +38,7 @@ en:
current_username: To confirm, please enter the username of the current account
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features
dissubscribable: Your post is not picked by antenna
email: You will be sent a confirmation e-mail
fields: You can have up to 4 items displayed as a table on your profile
group: Reps sent to this account will be automatically BT'd and distributed to all accounts you follow!
@ -179,6 +180,7 @@ en:
data: Data
discoverable: Suggest account to others
display_name: Display name
dissubscribable: Reject any subscriptions
email: E-mail address
expires_in: Expire after
fields: Profile metadata

View file

@ -38,6 +38,7 @@ ja:
current_username: 確認のため、現在のアカウントのユーザー名を入力してください
digest: 長期間使用していない場合と不在時に返信を受けた場合のみ送信されます
discoverable: レコメンド、トレンド、その他の機能により、あなたのアカウントを他の人から見つけられるようにします
dissubscribable: あなたの投稿はすべてのアンテナに掲載されなくなります。Fedibirdからの購読やMisskeyのアンテナを拒否することはできません
email: 確認のメールが送信されます
fields: プロフィールに表として4つまでの項目を表示することができます
group: このアカウントに送られたメンションは自動でBTされ、フォローしている全てのアカウントに配信されます
@ -181,6 +182,7 @@ ja:
data: データ
discoverable: ディレクトリに掲載する
display_name: 表示名
dissubscribable: 購読を拒否する
email: メールアドレス
expires_in: 有効期限
fields: プロフィール補足情報

View file

@ -17,6 +17,7 @@ SimpleNavigation::Configuration.run do |navigation|
n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? }
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 :antennas, safe_join([fa_icon('wifi fw'), t('antennas.index.title')]), antennas_path, highlights_on: %r{/antennas}, if: -> { current_user.functional? }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_path do |s|

View file

@ -216,6 +216,7 @@ Rails.application.routes.draw do
end
end
end
resources :antennas, except: [:show]
resource :relationships, only: [:show, :update]
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]

View file

@ -0,0 +1,40 @@
class CreateAntennas < ActiveRecord::Migration[6.1]
def change
create_table :antennas do |t|
t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }
t.belongs_to :list, null: false, foreign_key: { on_delete: :cascade }
t.string :title, null: false, default: ''
t.jsonb :keywords
t.jsonb :exclude_keywords
t.boolean :any_domains, null: false, default: true, index: true
t.boolean :any_tags, null: false, default: true, index: true
t.boolean :any_accounts, null: false, default: true, index: true
t.boolean :any_keywords, null: false, default: true, index: true
t.boolean :available, null: false, default: true, index: true
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
t.datetime :expires_at
end
create_table :antenna_domains do |t|
t.belongs_to :antenna, null: false, foreign_key: { on_delete: :cascade }
t.string :name, index: true
t.boolean :exclude, null: false, default: false, index: true
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
end
create_table :antenna_tags do |t|
t.belongs_to :antenna, null: false, foreign_key: { on_delete: :cascade }
t.belongs_to :tag, null: false, foreign_key: { on_delete: :cascade }
t.boolean :exclude, null: false, default: false, index: true
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
end
create_table :antenna_accounts do |t|
t.belongs_to :antenna, null: false, foreign_key: { on_delete: :cascade }
t.belongs_to :account, null: false, foreign_key: { on_delete: :cascade }
t.boolean :exclude, null: false, default: false, index: true
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
end
end
end

View file

@ -0,0 +1,6 @@
class AddDissubscribableToAccounts < ActiveRecord::Migration[6.1]
def change
add_column :antennas, :with_media_only, :boolean, null: false, default: false, index: true
add_column :accounts, :dissubscribable, :boolean, null: false, default: false
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_04_14_010523) do
ActiveRecord::Schema.define(version: 2023_04_23_233429) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 2023_04_14_010523) do
t.datetime "requested_review_at"
t.boolean "group_allow_private_message"
t.integer "searchability", default: 2, null: false
t.boolean "dissubscribable", default: false, null: false
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id", where: "(moved_to_account_id IS NOT NULL)"
@ -251,6 +252,63 @@ ActiveRecord::Schema.define(version: 2023_04_14_010523) do
t.bigint "status_ids", array: true
end
create_table "antenna_accounts", force: :cascade do |t|
t.bigint "antenna_id", null: false
t.bigint "account_id", null: false
t.boolean "exclude", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_antenna_accounts_on_account_id"
t.index ["antenna_id"], name: "index_antenna_accounts_on_antenna_id"
t.index ["exclude"], name: "index_antenna_accounts_on_exclude"
end
create_table "antenna_domains", force: :cascade do |t|
t.bigint "antenna_id", null: false
t.string "name"
t.boolean "exclude", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["antenna_id"], name: "index_antenna_domains_on_antenna_id"
t.index ["exclude"], name: "index_antenna_domains_on_exclude"
t.index ["name"], name: "index_antenna_domains_on_name"
end
create_table "antenna_tags", force: :cascade do |t|
t.bigint "antenna_id", null: false
t.bigint "tag_id", null: false
t.boolean "exclude", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["antenna_id"], name: "index_antenna_tags_on_antenna_id"
t.index ["exclude"], name: "index_antenna_tags_on_exclude"
t.index ["tag_id"], name: "index_antenna_tags_on_tag_id"
end
create_table "antennas", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "list_id", null: false
t.string "title", default: "", null: false
t.jsonb "keywords"
t.jsonb "exclude_keywords"
t.boolean "any_domains", default: true, null: false
t.boolean "any_tags", default: true, null: false
t.boolean "any_accounts", default: true, null: false
t.boolean "any_keywords", default: true, null: false
t.boolean "available", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "expires_at"
t.boolean "with_media_only", default: false, null: false
t.index ["account_id"], name: "index_antennas_on_account_id"
t.index ["any_accounts"], name: "index_antennas_on_any_accounts"
t.index ["any_domains"], name: "index_antennas_on_any_domains"
t.index ["any_keywords"], name: "index_antennas_on_any_keywords"
t.index ["any_tags"], name: "index_antennas_on_any_tags"
t.index ["available"], name: "index_antennas_on_available"
t.index ["list_id"], name: "index_antennas_on_list_id"
end
create_table "appeals", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "account_warning_id", null: false
@ -1173,6 +1231,13 @@ ActiveRecord::Schema.define(version: 2023_04_14_010523) do
add_foreign_key "announcement_reactions", "accounts", on_delete: :cascade
add_foreign_key "announcement_reactions", "announcements", on_delete: :cascade
add_foreign_key "announcement_reactions", "custom_emojis", on_delete: :cascade
add_foreign_key "antenna_accounts", "accounts", on_delete: :cascade
add_foreign_key "antenna_accounts", "antennas", on_delete: :cascade
add_foreign_key "antenna_domains", "antennas", on_delete: :cascade
add_foreign_key "antenna_tags", "antennas", on_delete: :cascade
add_foreign_key "antenna_tags", "tags", on_delete: :cascade
add_foreign_key "antennas", "accounts", on_delete: :cascade
add_foreign_key "antennas", "lists", on_delete: :cascade
add_foreign_key "appeals", "account_warnings", on_delete: :cascade
add_foreign_key "appeals", "accounts", column: "approved_by_account_id", on_delete: :nullify
add_foreign_key "appeals", "accounts", column: "rejected_by_account_id", on_delete: :nullify