Wip: antenna

This commit is contained in:
KMY 2023-04-23 14:20:07 +09:00
parent 07ea091320
commit 2eb5ffb9b3
19 changed files with 610 additions and 1 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

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
#
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) }
def enabled?
available && !(any_keywords && any_domains && any_accounts && any_tags) && !expires?
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 expires?
expires_at.present? && expires_at < Time.now.utc
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? }
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? }
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? }
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? && !exclude_tags&.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? }
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
self[:any_tags] = !tag_names.any? && !tags&.any?
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? }
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? && !exclude_domains&.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? }
antenna_domains.where(exclude: true).destroy_all!
domain_names.each do |domain|
antenna_domains.create!(name: domain, exclude: true)
end
self[:any_domains] = !domain_names.any? && !domains&.any?
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? }
hit = false
antenna_accounts.where(exclude: false).destroy_all!
account_names.each do |name|
name = name[1..-1] if name.start_with?('@')
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 && !exclude_accounts&.any?
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? }
hit = false
antenna_accounts.where(exclude: true).destroy_all!
account_names.each do |name|
name = name[1..-1] if name.start_with?('@')
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
self[:any_accounts] = !hit && !accounts&.any?
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

@ -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)
when :limited
deliver_to_mentioned_followers!
else
@ -115,6 +116,30 @@ class FanOutOnWriteService < BaseService
end
end
def deliver_to_antennas!
lists = []
antennas = Antenna.availables
p '=========================== DEBUG A ' + antennas.size.to_s
antennas = antennas.merge!(Antenna.where(any_accounts: true).or(Antenna.joins(:antenna_accounts).where(antenna_accounts: { account: @status.account }).map(&:antenna)))
p '=========================== DEBUG B ' + antennas.size.to_s
p '=========================== DEBUG C ' + antennas.size.to_s
p '=========================== DEBUG D ' + antennas.size.to_s
antennas.in_batches do |ans|
ans.each do |antenna|
next if !antenna.enabled?
next if antenna.keywords.any? && !@status.text.include?(antenna.keywords)
next if antenna.exclude_keywords.any? && @status.text.include?(antenna.exclude_keywords)
lists << antenna.list
end
end
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,65 @@
.filters-list__item{ class: [antenna.expired? && 'expired'] }
= link_to edit_antenna_path(antenna), class: 'filters-list__item__title' do
= antenna.title
- if 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))
.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
- 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
= t('antenna.index.contexts', contexts: antenna.context.map { |context| I18n.t("antenna.contexts.#{context}") }.join(', '))
%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,45 @@
.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 }, selected: f.object.list&.id, hint: false
.fields-group.fields-row__column.fields-row__column-6
= f.input :available, wrapper: :with_label, hint: false
%hr.spacer/
%h4= t('antennas.edit.domains')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :domains_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_domains_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
%h4= t('antennas.edit.accounts')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :accounts_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_accounts_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
%h4= t('antennas.edit.tags')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :tags_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_tags_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
%h4= t('antennas.edit.keywords')
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :keywords_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }
.fields-row__column.fields-row__column-6.fields-group
= f.input :exclude_keywords_raw, wrapper: :with_label, as: :text, input_html: { rows: 5 }

View file

@ -0,0 +1,8 @@
%tr.nested-fields
%td= f.input :keyword, as: :string
%td
.label_input__wrapper= f.input_field :whole_word
%td
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
= link_to_remove_association(f, class: 'table-action-link') do
= safe_join([fa_icon('times'), t('antennas.index.delete')])

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,11 @@
- 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'
- 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

@ -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
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

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_04_20_081634) do
ActiveRecord::Schema.define(version: 2023_04_23_002728) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -253,6 +253,62 @@ ActiveRecord::Schema.define(version: 2023_04_20_081634) 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.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
@ -1175,6 +1231,13 @@ ActiveRecord::Schema.define(version: 2023_04_20_081634) 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