about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDarius Kazemi <darius.kazemi@gmail.com>2019-04-30 15:29:28 -0700
committerFire Demon <firedemon@creature.cafe>2020-08-30 05:45:15 -0500
commit6378feffa8935238bdb5f1f1c01fcb102440fe30 (patch)
treeb1eea210ace7eb72a00ab4604c3ec10c5ae4db4b
parent4eb49257fc618219709b357fb68f4d6156cab249 (diff)
[Feature, Federation, Port: hometown@b3e6597] Support locally cached inline images [+ Monsterfork additions]
Changes added by Monsterfork:
- Do not limit to only Articles
- Reuse existing media; retroactively using more-detailed descriptions
- Also scrub carrige returns between tags
- Handle download failures
- Attach to statuses and keep track of inlined media
- Handle local edits

Co-authored-by: Fire Demon <firedemon@creature.cafe>
-rw-r--r--app/helpers/img_proxy_helper.rb128
-rw-r--r--app/javascript/flavours/glitch/styles/monsterfork/components/status.scss10
-rw-r--r--app/lib/activitypub/activity.rb4
-rw-r--r--app/lib/activitypub/activity/create.rb6
-rw-r--r--app/lib/command_tag/processor.rb2
-rw-r--r--app/lib/formatter.rb9
-rw-r--r--app/lib/img_tag_handler.rb30
-rw-r--r--app/lib/sanitize_config.rb23
-rw-r--r--app/models/inline_media_attachment.rb14
-rw-r--r--app/models/media_attachment.rb12
-rw-r--r--app/models/status.rb1
-rw-r--r--app/services/post_status_service.rb1
-rw-r--r--app/services/update_status_service.rb14
-rw-r--r--app/workers/redownload_media_worker.rb19
-rw-r--r--db/migrate/20200724035808_add_inline_to_media_attachments.rb7
-rw-r--r--db/migrate/20200724045955_create_inline_media_attachments.rb12
-rw-r--r--db/schema.rb13
17 files changed, 272 insertions, 33 deletions
diff --git a/app/helpers/img_proxy_helper.rb b/app/helpers/img_proxy_helper.rb
new file mode 100644
index 000000000..6ef3fe84d
--- /dev/null
+++ b/app/helpers/img_proxy_helper.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+#                  .~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.                  #
+###################              Cthulhu Code!              ###################
+#                  `~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`                  #
+# - Has a high complexity level and needs tests.                              #
+# - Makes many assumptions the environment it's included into.                #
+# - Incurs a high performance penalty.                                        #
+#                                                                             #
+###############################################################################
+
+module ImgProxyHelper
+  def process_inline_images!
+    raise NameError('@status must be defined by the instance this method is being called from.') unless defined?(@status)
+    return if @status.text&.strip.blank? || @status.content_type == 'text/plain'
+
+    replace_markdown_images_with_html!
+
+    handler = ImgTagHandler.new
+    Ox.sax_parse(handler, StringIO.new(@status.text, 'r'))
+    return if handler.srcs.blank?
+
+    @skip_download_from = { @status.account.domain => DomainBlock.reject_media?(@status.account.domain) }
+    @redownload_attachment_ids = Set[]
+
+    handler.srcs.each do |src|
+      alt                   = handler.alts[src]
+      normalized_src_parts  = begin
+                                Addressable::URI.parse(src&.strip).normalize
+                              rescue Addressable::URI::InvalidURIError
+                                nil
+                              end
+      normalized_src        = normalized_src_parts.to_s
+
+      next replace_text!(src) if normalized_src.blank? || skip_download_from?(normalized_src_parts.host)
+
+      file_name             = normalized_src_parts.path.split('/').last
+      media_attachment      = find_media_attachment(normalized_src, file_name)
+
+      if media_attachment.present?
+        media_attachment.update(description: alt) if alt_more_descriptive?(alt, media_attachment.description)
+      elsif normalized_src_parts.scheme.blank? || !file_name.match?(/\S\.\w{3,}/)
+        next replace_text!(src)
+      else
+        media_attachment = create_media_attachment!(normalized_src)
+      end
+
+      next replace_text!(src) if media_attachment.blank? || media_attachment.destroyed?
+
+      if media_attachment.needs_redownload?
+        replace_text!(src, "#{media_attachment.file.url(:small)}##{media_attachment.id}")
+      else
+        replace_text!(src, media_attachment.file.url(:small))
+      end
+    end
+  end
+
+  private
+
+  def skip_download_from?(domain)
+    return true if @skip_download_from[@status.account.domain]
+    return @skip_download_from[domain] if @skip_download_from[domain]
+
+    @skip_download_from[domain] = DomainBlock.reject_media?(domain)
+  end
+
+  def unsupported_media_type?(mime_type)
+    mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type)
+  end
+
+  def html_entities
+    @html_entities ||= HTMLEntities.new
+  end
+
+  def replace_markdown_images_with_html!
+    return unless @status.content_type == 'text/markdown'
+
+    @status.text.gsub!(/!\[(\S+)\]\(\s*(\S+)\s*\)/) do
+      begin
+        alt = html_entities.encode(Regexp.last_match(1).strip)
+        url = Addressable::URI.parse(Regexp.last_match(2)).normalize.to_s
+        "<img title=\"#{alt}\" alt=\"#{alt}\" src=\"#{url}\" />"
+      rescue Addressable::URI::InvalidURIError
+        ''
+      end
+    end
+  end
+
+  def replace_text!(text, replacement = '')
+    @status.text.gsub!(text, replacement)
+  end
+
+  def alt_more_descriptive?(alt, description)
+    return false unless alt.present? && description != alt
+    return true if description.blank? || alt.split(/[\s\n\r]+/).count > description.split(/[\s\n\r]+/).count
+  end
+
+  def find_media_attachment(src, file_name)
+    media_attachment = src.start_with?('http') ? MediaAttachment.find_by(account: @account, remote_url: src, inline: true) : nil
+    return media_attachment if media_attachment.present?
+
+    MediaAttachment.where(account: @status.account, file_file_name: file_name, inline: true)
+                   .find { |m| [m.file.url(:small), m.file.url(:original)].include?(src) || m.status_id == @status.id }
+  end
+
+  def create_media_attachment!(src, alt)
+    media_attachment = MediaAttachment.create!(account: @status.account, remote_url: src, description: alt, focus: nil, inline: true)
+    media_attachment = process_media_attachment!(media_attachment)
+    return if media_attachment.destroyed?
+
+    @status.inlined_attachments.first_or_create!(media_attachment: media_attachment)
+    media_attachment
+  end
+
+  def process_media_attachment!(media_attachment)
+    media_attachment.download_file!
+    media_attachment.download_thumbnail!
+    media_attachment.save!
+    media_attachment.destroy! if unsupported_media_type?(media_attachment.file.content_type)
+    media_attachment
+  rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
+    return if @redownload_attachment_ids.include?(media_attachment.id)
+
+    RedownloadMediaWorker.perform_in(rand(30..60).seconds, media_attachment.id)
+    @redownload_attachment_ids << media_attachment.id
+    media_attachment
+  end
+end
diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss
index 33601b8bf..2df7f1aac 100644
--- a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss
@@ -35,3 +35,13 @@
 div[data-nest-deep="true"] {
   border-left: 75px dashed darken($ui-base-color, 8%);
 }
+
+.status__content {
+  .status__content__text,
+  .e-content {
+    img {
+      max-width: 100%;
+      margin: 1em auto;
+    }
+  }
+}
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index cff6b569a..9b58fabed 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -4,8 +4,8 @@ class ActivityPub::Activity
   include JsonLdHelper
   include Redisable
 
-  SUPPORTED_TYPES = %w(Note Question).freeze
-  CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
+  SUPPORTED_TYPES = %w(Note Question Article).freeze
+  CONVERTED_TYPES = %w(Image Audio Video Page Event).freeze
 
   def initialize(json, account, **options)
     @json    = json
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index fbb4624e3..10a0a9498 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class ActivityPub::Activity::Create < ActivityPub::Activity
+  include ImgProxyHelper
+
   def perform
     dereference_object!
 
@@ -94,6 +96,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 
     ApplicationRecord.transaction do
       @status = Status.create!(@params)
+      process_inline_images!
       attach_tags(@status)
     end
 
@@ -138,6 +141,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
         text: text_from_content || '',
         language: detected_language,
         spoiler_text: converted_object_type? ? '' : (text_from_summary || ''),
+        title: text_from_title,
         sensitive: @object['sensitive'] || false,
         visibility: visibility_from_audience,
         media_attachment_ids: process_attachments.take(4).map(&:id),
@@ -433,7 +437,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return Formatter.instance.linkify([[text_from_name, text_from_summary.presence].compact.join("\n\n"), object_url || @object['id']].join(' ')) if converted_object_type?
 
     if @object['content'].present?
-      @object['content']
+      @object['type'] == 'Article' ? Formatter.instance.format_article(@object['content']) : @object['content']
     elsif content_language_map?
       @object['contentMap'].values.first
     end
diff --git a/app/lib/command_tag/processor.rb b/app/lib/command_tag/processor.rb
index d9e49c84a..118affee4 100644
--- a/app/lib/command_tag/processor.rb
+++ b/app/lib/command_tag/processor.rb
@@ -39,7 +39,7 @@ class CommandTag::Processor
       end
     elsif @status.destroyed?
       %w(after_destroy once_after_destroy).each { |suffix| execute_statements(suffix) }
-    elsif @status.update(text: @text)
+    elsif @status.update(text: process_inline_images(@text))
       %w(after_save once_after_save).each { |suffix| execute_statements(suffix) }
     else
       %w(after_save_fail once_after_save_fail).each { |suffix| execute_statements(suffix) }
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index d5408a30b..39c42c8db 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -24,6 +24,7 @@ class HTMLRenderer < Redcarpet::Render::HTML
   end
 end
 
+# rubocop:disable Metrics/ClassLength
 class Formatter
   include Singleton
   include RoutingHelper
@@ -75,6 +76,11 @@ class Formatter
     html.delete("\r").delete("\n")
   end
 
+  def format_article(text)
+    text = text.gsub(/>[\r\n]+</, "><")
+    text.html_safe # rubocop:disable Rails/OutputSafety
+  end
+
   def reformat(html, outgoing = false)
     sanitize(html, Sanitize::Config::MASTODON_STRICT.merge(outgoing: outgoing))
   rescue ArgumentError
@@ -154,7 +160,7 @@ class Formatter
     renderer = HTMLRenderer.new({
       filter_html: false,
       escape_html: false,
-      no_images: true,
+      no_images: false,
       no_styles: true,
       safe_links_only: true,
       hard_wrap: true,
@@ -389,3 +395,4 @@ class Formatter
     "<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
   end
 end
+# rubocop:enable Metrics/ClassLength
diff --git a/app/lib/img_tag_handler.rb b/app/lib/img_tag_handler.rb
new file mode 100644
index 000000000..0263e1cbd
--- /dev/null
+++ b/app/lib/img_tag_handler.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class ImgTagHandler < ::Ox::Sax
+  attr_reader :srcs
+  attr_reader :alts
+
+  def initialize
+    @stack = []
+    @srcs = []
+    @alts = {}
+  end
+
+  def start_element(element_name)
+    @stack << [element_name, {}]
+  end
+
+  def end_element(_)
+    self_name, self_attributes = @stack[-1]
+    if self_name == :img && !self_attributes[:src].nil?
+      @srcs << self_attributes[:src]
+      @alts[self_attributes[:src]] = self_attributes[:alt]&.strip
+    end
+    @stack.pop
+  end
+
+  def attr(attribute_name, attribute_value)
+    _name, attributes = @stack.last
+    attributes[attribute_name] = attribute_value&.strip
+  end
+end
diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb
index ccc3f4642..05131b674 100644
--- a/app/lib/sanitize_config.rb
+++ b/app/lib/sanitize_config.rb
@@ -35,25 +35,6 @@ class Sanitize
       node['class'] = class_list.join(' ')
     end
 
-    IMG_TAG_TRANSFORMER = lambda do |env|
-      node = env[:node]
-
-      return unless env[:node_name] == 'img'
-
-      node.name = 'a'
-
-      node['href'] = node['src']
-      if node['alt'].present?
-        node.content = "[🖼  #{node['alt']}]"
-      else
-        url = node['href']
-        prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
-        text   = url[prefix.length, 30]
-        text   = text + "…" if url[prefix.length..-1].length > 30
-        node.content = "[🖼  #{text}]"
-      end
-    end
-
     LINK_REL_TRANSFORMER = lambda do |env|
       return unless env[:node_name] == 'a' and env[:node]['href']
 
@@ -83,7 +64,7 @@ class Sanitize
     end
 
     MASTODON_STRICT ||= freeze_config(
-      elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li),
+      elements: %w(p br span a abbr del pre blockquote code b strong u sub sup i em h1 h2 h3 h4 h5 ul ol li img),
 
       attributes: {
         'a'          => %w(href rel class title),
@@ -92,6 +73,7 @@ class Sanitize
         'blockquote' => %w(cite),
         'ol'         => %w(start reversed),
         'li'         => %w(value),
+        'img'        => %w(src alt title),
       },
 
       add_attributes: {
@@ -107,7 +89,6 @@ class Sanitize
 
       transformers: [
         CLASS_WHITELIST_TRANSFORMER,
-        IMG_TAG_TRANSFORMER,
         UNSUPPORTED_HREF_TRANSFORMER,
         LINK_REL_TRANSFORMER,
       ]
diff --git a/app/models/inline_media_attachment.rb b/app/models/inline_media_attachment.rb
new file mode 100644
index 000000000..ac4388a6d
--- /dev/null
+++ b/app/models/inline_media_attachment.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: inline_media_attachments
+#
+#  id                  :bigint(8)        not null, primary key
+#  status_id           :bigint(8)
+#  media_attachment_id :bigint(8)
+#
+
+class InlineMediaAttachment < ApplicationRecord
+  belongs_to :status, inverse_of: :inlined_attachments
+  belongs_to :media_attachment, inverse_of: :inlines
+end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index cfdd95b22..324bd0305 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -26,6 +26,7 @@
 #  thumbnail_file_size         :integer
 #  thumbnail_updated_at        :datetime
 #  thumbnail_remote_url        :string
+#  inline                      :boolean          default(FALSE), not null
 #
 
 class MediaAttachment < ApplicationRecord
@@ -160,6 +161,8 @@ class MediaAttachment < ApplicationRecord
   belongs_to :status,           inverse_of: :media_attachments, optional: true
   belongs_to :scheduled_status, inverse_of: :media_attachments, optional: true
 
+  has_many :inlines, class_name: 'InlineMediaAttachment', inverse_of: :media_attachment, dependent: :destroy
+
   has_attached_file :file,
                     styles: ->(f) { file_styles f },
                     processors: ->(f) { file_processors f },
@@ -189,13 +192,16 @@ class MediaAttachment < ApplicationRecord
   validates :file, presence: true, if: :local?
   validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
 
-  scope :attached,   -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
-  scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
+  scope :attached,   -> { all_media.where.not(status_id: nil).or(all_media.where.not(scheduled_status_id: nil)) }
+  scope :unattached, -> { all_media.where(status_id: nil, scheduled_status_id: nil) }
+  scope :uninlined,  -> { where(inline: false) }
+  scope :inlined,    -> { rewhere(inline: true) }
+  scope :all_media,  -> { unscope(where: :inline) }
   scope :local,      -> { where(remote_url: '') }
   scope :remote,     -> { where.not(remote_url: '') }
   scope :cached,     -> { remote.where.not(file_file_name: nil) }
 
-  default_scope { order(id: :asc) }
+  default_scope { uninlined.order(id: :asc) }
 
   def local?
     remote_url.blank?
diff --git a/app/models/status.rb b/app/models/status.rb
index 164c6cb8e..46361280d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -70,6 +70,7 @@ class Status < ApplicationRecord
   has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
   has_many :media_attachments, dependent: :nullify
 
+  has_many :inlined_attachments, class_name: 'InlineMediaAttachment', inverse_of: :status, dependent: :destroy
   has_many :mutes, class_name: 'StatusMute', inverse_of: :status, dependent: :destroy
   belongs_to :conversation_mute, primary_key: 'conversation_id', foreign_key: 'conversation_id', inverse_of: :conversation, dependent: :destroy, optional: true
 
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index eb60c1022..aef630f71 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -2,6 +2,7 @@
 
 class PostStatusService < BaseService
   include Redisable
+  include ImgProxyHelper
 
   MIN_SCHEDULE_OFFSET = 5.minutes.freeze
 
diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb
index 795e43d01..f59f26a25 100644
--- a/app/services/update_status_service.rb
+++ b/app/services/update_status_service.rb
@@ -2,6 +2,7 @@
 
 class UpdateStatusService < BaseService
   include Redisable
+  include ImgProxyHelper
 
   ALLOWED_ATTRIBUTES = %i(
     spoiler_text
@@ -42,11 +43,19 @@ class UpdateStatusService < BaseService
     @deleted_tag_ids        = @status.tag_ids - @tags.pluck(:id)
     @deleted_tag_names      = @status.tags.pluck(:name) - @tags.pluck(:name)
     @deleted_attachment_ids = @status.media_attachment_ids - (@params[:media_attachment_ids] || @params[:media_attachments]&.pluck(:id) || [])
-    @new_mention_ids        = @mentions.pluck(:id) - @status.mention_ids
+
+    @new_mention_ids = @mentions.pluck(:id) - @status.mention_ids
 
     ApplicationRecord.transaction do
       @status.update!(@params)
-      ProcessCommandTagsService.new.call(@account, @status) if @account.local?
+
+      if @account.local?
+        ProcessCommandTagsService.new.call(@account, @status)
+      else
+        process_inline_images!
+        @status.save!
+      end
+
       detach_deleted_tags
       attach_updated_tags
     end
@@ -64,6 +73,7 @@ class UpdateStatusService < BaseService
   private
 
   def prune_attachments
+    @new_inline_ids = @status.inlined_attachments.pluck(:media_attachment_id)
     RemoveMediaAttachmentsWorker.perform_async(@deleted_attachment_ids) if @deleted_attachment_ids.present?
   end
 
diff --git a/app/workers/redownload_media_worker.rb b/app/workers/redownload_media_worker.rb
index 0638cd0f0..0ead9a7a8 100644
--- a/app/workers/redownload_media_worker.rb
+++ b/app/workers/redownload_media_worker.rb
@@ -11,10 +11,27 @@ class RedownloadMediaWorker
 
     return if media_attachment.remote_url.blank?
 
+    orig_small_url = media_attachment.file.url(:small)
+
     media_attachment.download_file!
     media_attachment.download_thumbnail!
-    media_attachment.save
+
+    if media_attachment.save && media_attachment.inline? && media_attachment.status.present?
+      if unsupported_media_type?(media_attachment.file.content_type)
+        media_attachment.destroy
+        true
+      else
+        media_attachment.status.text.gsub!("#{orig_small_url}##{media_attachment.id}", media_attachment.file.url(:small))
+        media_attachment.status.save
+      end
+    end
   rescue ActiveRecord::RecordNotFound
     true
   end
+
+  private
+
+  def unsupported_media_type?(mime_type)
+    mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type)
+  end
 end
diff --git a/db/migrate/20200724035808_add_inline_to_media_attachments.rb b/db/migrate/20200724035808_add_inline_to_media_attachments.rb
new file mode 100644
index 000000000..171eca4b5
--- /dev/null
+++ b/db/migrate/20200724035808_add_inline_to_media_attachments.rb
@@ -0,0 +1,7 @@
+class AddInlineToMediaAttachments < ActiveRecord::Migration[5.2]
+  def change
+    safety_assured do
+      add_column :media_attachments, :inline, :boolean, default: false, null: false
+    end
+  end
+end
diff --git a/db/migrate/20200724045955_create_inline_media_attachments.rb b/db/migrate/20200724045955_create_inline_media_attachments.rb
new file mode 100644
index 000000000..a894c3868
--- /dev/null
+++ b/db/migrate/20200724045955_create_inline_media_attachments.rb
@@ -0,0 +1,12 @@
+class CreateInlineMediaAttachments < ActiveRecord::Migration[5.2]
+  disable_ddl_transaction!
+
+  def change
+    create_table :inline_media_attachments do |t|
+      t.references :status, index: { algorithm: :concurrently }, foreign_key: { on_delete: :cascade }
+      t.references :media_attachment, index: { algorithm: :concurrently }, foreign_key: { on_delete: :cascade }
+    end
+
+    add_index :inline_media_attachments, [:status_id, :media_attachment_id], unique: true, algorithm: :concurrently, name: 'uniq_index_on_status_and_attachment'
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 901c13c76..3cbbd7732 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.define(version: 2020_07_21_221659) do
+ActiveRecord::Schema.define(version: 2020_07_24_045955) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -448,6 +448,14 @@ ActiveRecord::Schema.define(version: 2020_07_21_221659) do
     t.boolean "overwrite", default: false, null: false
   end
 
+  create_table "inline_media_attachments", force: :cascade do |t|
+    t.bigint "status_id"
+    t.bigint "media_attachment_id"
+    t.index ["media_attachment_id"], name: "index_inline_media_attachments_on_media_attachment_id"
+    t.index ["status_id", "media_attachment_id"], name: "uniq_index_on_status_and_attachment", unique: true
+    t.index ["status_id"], name: "index_inline_media_attachments_on_status_id"
+  end
+
   create_table "invites", force: :cascade do |t|
     t.bigint "user_id", null: false
     t.string "code", default: "", null: false
@@ -513,6 +521,7 @@ ActiveRecord::Schema.define(version: 2020_07_21_221659) do
     t.integer "thumbnail_file_size"
     t.datetime "thumbnail_updated_at"
     t.string "thumbnail_remote_url"
+    t.boolean "inline", default: false, null: false
     t.index ["account_id"], name: "index_media_attachments_on_account_id"
     t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
     t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
@@ -984,6 +993,8 @@ ActiveRecord::Schema.define(version: 2020_07_21_221659) do
   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 "inline_media_attachments", "media_attachments", on_delete: :cascade
+  add_foreign_key "inline_media_attachments", "statuses", on_delete: :cascade
   add_foreign_key "invites", "users", on_delete: :cascade
   add_foreign_key "list_accounts", "accounts", on_delete: :cascade
   add_foreign_key "list_accounts", "follows", on_delete: :cascade