diff --git a/app/models/quote.rb b/app/models/quote.rb new file mode 100644 index 00000000000..7e7e8d8917f --- /dev/null +++ b/app/models/quote.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: quotes +# +# id :bigint(8) not null, primary key +# activity_uri :string +# approval_uri :string +# state :integer default("pending"), not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint(8) +# quoted_account_id :bigint(8) +# quoted_status_id :bigint(8) +# status_id :bigint(8) not null +# +class Quote < ApplicationRecord + enum :state, + { pending: 0, accepted: 1, rejected: 2, revoked: 3 }, + validate: true + + belongs_to :status + belongs_to :quoted_status, class_name: 'Status', optional: true + + belongs_to :account + belongs_to :quoted_account, class_name: 'Account', optional: true + + before_validation :set_accounts! + + validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } + validate :validate_visibility! + + private + + def set_accounts! + self.account = status.account + self.quoted_account = quoted_status&.account + end + + def validate_visibility! + return if account_id == quoted_account_id || quoted_status.nil? || quoted_status.distributable? + + errors.add(:quoted_status_id, :visibility_mismatch) + end +end diff --git a/app/models/status.rb b/app/models/status.rb index cdff5a2ac3d..17375a30605 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -93,6 +93,7 @@ class Status < ApplicationRecord has_one :status_stat, inverse_of: :status, dependent: nil has_one :poll, inverse_of: :status, dependent: :destroy has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil + has_one :quote, inverse_of: :status, dependent: :destroy validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } @@ -154,6 +155,7 @@ class Status < ApplicationRecord :status_stat, :tags, :preloadable_poll, + quote: { status: { account: [:account_stat, user: :role] } }, preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, account: [:account_stat, user: :role], active_mentions: :account, @@ -164,6 +166,7 @@ class Status < ApplicationRecord :conversation, :status_stat, :preloadable_poll, + :quote, preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, account: [:account_stat, user: :role], active_mentions: :account, diff --git a/db/migrate/20250411094808_create_quotes.rb b/db/migrate/20250411094808_create_quotes.rb new file mode 100644 index 00000000000..3eb850b5794 --- /dev/null +++ b/db/migrate/20250411094808_create_quotes.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateQuotes < ActiveRecord::Migration[8.0] + def change + create_table :quotes do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade }, index: false + t.belongs_to :status, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false + t.belongs_to :quoted_status, foreign_key: { to_table: :statuses, on_delete: :nullify }, null: true + t.belongs_to :quoted_account, foreign_key: { to_table: :accounts, on_delete: :nullify }, null: true + t.integer :state, null: false, default: 0 + t.string :approval_uri, index: { where: 'approval_uri IS NOT NULL' } + t.string :activity_uri, index: { unique: true, where: 'activity_uri IS NOT NULL' } + + t.timestamps + end + + add_index :quotes, [:account_id, :quoted_account_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index b09360ff438..e1b5e60078a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do +ActiveRecord::Schema[8.0].define(version: 2025_04_11_094808) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -871,6 +871,24 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do t.string "url" end + create_table "quotes", force: :cascade do |t| + t.bigint "account_id" + t.bigint "status_id", null: false + t.bigint "quoted_status_id" + t.bigint "quoted_account_id" + t.integer "state", default: 0, null: false + t.string "approval_uri" + t.string "activity_uri" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "quoted_account_id"], name: "index_quotes_on_account_id_and_quoted_account_id" + t.index ["activity_uri"], name: "index_quotes_on_activity_uri", unique: true, where: "(activity_uri IS NOT NULL)" + t.index ["approval_uri"], name: "index_quotes_on_approval_uri", where: "(approval_uri IS NOT NULL)" + t.index ["quoted_account_id"], name: "index_quotes_on_quoted_account_id" + t.index ["quoted_status_id"], name: "index_quotes_on_quoted_status_id" + t.index ["status_id"], name: "index_quotes_on_status_id", unique: true + end + create_table "relationship_severance_events", force: :cascade do |t| t.integer "type", null: false t.string "target_name", null: false @@ -1350,6 +1368,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do add_foreign_key "polls", "statuses", on_delete: :cascade add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade add_foreign_key "preview_cards", "accounts", column: "author_account_id", on_delete: :nullify + add_foreign_key "quotes", "accounts", column: "quoted_account_id", on_delete: :nullify + add_foreign_key "quotes", "accounts", on_delete: :cascade + add_foreign_key "quotes", "statuses", column: "quoted_status_id", on_delete: :nullify + add_foreign_key "quotes", "statuses", on_delete: :cascade add_foreign_key "report_notes", "accounts", on_delete: :cascade add_foreign_key "report_notes", "reports", on_delete: :cascade add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify diff --git a/spec/fabricators/quote_fabricator.rb b/spec/fabricators/quote_fabricator.rb new file mode 100644 index 00000000000..c420d2720c9 --- /dev/null +++ b/spec/fabricators/quote_fabricator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Fabricator(:quote) do + status { Fabricate.build(:status) } + quoted_status { Fabricate.build(:status) } + state :pending +end