about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb20
-rw-r--r--app/models/concerns/account_interactions.rb20
-rw-r--r--app/models/glitch.rb7
-rw-r--r--app/models/glitch/keyword_mute.rb66
-rw-r--r--app/models/media_attachment.rb29
-rw-r--r--app/models/mute.rb11
-rw-r--r--app/models/status.rb13
-rw-r--r--app/models/stream_entry.rb2
8 files changed, 154 insertions, 14 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 3dc2a95ab..85684c259 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -53,6 +53,8 @@ class Account < ApplicationRecord
   include Attachmentable
   include Remotable
 
+  MAX_NOTE_LENGTH = 500
+
   enum protocol: [:ostatus, :activitypub]
 
   # Local users
@@ -67,7 +69,7 @@ class Account < ApplicationRecord
   validates :username, format: { with: /\A[a-z0-9_]+\z/i }, uniqueness: { scope: :domain, case_sensitive: false }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }
   validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
   validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
-  validates :note, length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
+  validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? }
 
   # Timelines
   has_many :stream_entries, inverse_of: :account, dependent: :destroy
@@ -292,6 +294,22 @@ class Account < ApplicationRecord
     self.public_key  = keypair.public_key.to_pem
   end
 
+  YAML_START = "---\r\n"
+  YAML_END = "\r\n...\r\n"
+
+  def note_length_does_not_exceed_length_limit
+    note_without_metadata = note
+    if note.start_with? YAML_START
+      idx = note.index YAML_END
+      unless idx.nil?
+        note_without_metadata = note[(idx + YAML_END.length) .. -1]
+      end
+    end
+    if note_without_metadata.mb_chars.grapheme_length > MAX_NOTE_LENGTH
+      errors.add(:note, "can't be longer than 500 graphemes")
+    end
+  end
+
   def normalize_domain
     return if local?
 
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index b26520f5b..0afdebf89 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -17,7 +17,11 @@ module AccountInteractions
     end
 
     def muting_map(target_account_ids, account_id)
-      follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+      Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
+        mapping[mute.target_account_id] = {
+          notifications: mute.hide_notifications?
+        }
+      end
     end
 
     def requested_map(target_account_ids, account_id)
@@ -70,8 +74,14 @@ module AccountInteractions
     block_relationships.find_or_create_by!(target_account: other_account)
   end
 
-  def mute!(other_account)
-    mute_relationships.find_or_create_by!(target_account: other_account)
+  def mute!(other_account, notifications: nil)
+    notifications = true if notifications.nil?
+    mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
+    # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
+    if mute.hide_notifications? != notifications
+      mute.hide_notifications = notifications
+      mute.save!
+    end
   end
 
   def mute_conversation!(conversation)
@@ -127,6 +137,10 @@ module AccountInteractions
     conversation_mutes.where(conversation: conversation).exists?
   end
 
+  def muting_notifications?(other_account)
+    mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
+  end
+
   def requested?(other_account)
     follow_requests.where(target_account: other_account).exists?
   end
diff --git a/app/models/glitch.rb b/app/models/glitch.rb
new file mode 100644
index 000000000..0e497babc
--- /dev/null
+++ b/app/models/glitch.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Glitch
+  def self.table_name_prefix
+    'glitch_'
+  end
+end
diff --git a/app/models/glitch/keyword_mute.rb b/app/models/glitch/keyword_mute.rb
new file mode 100644
index 000000000..73de4d4b7
--- /dev/null
+++ b/app/models/glitch/keyword_mute.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: glitch_keyword_mutes
+#
+#  id         :integer          not null, primary key
+#  account_id :integer          not null
+#  keyword    :string           not null
+#  whole_word :boolean          default(TRUE), not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Glitch::KeywordMute < ApplicationRecord
+  belongs_to :account, required: true
+
+  validates_presence_of :keyword
+
+  after_commit :invalidate_cached_matcher
+
+  def self.matcher_for(account_id)
+    Matcher.new(account_id)
+  end
+
+  private
+
+  def invalidate_cached_matcher
+    Rails.cache.delete("keyword_mutes:regex:#{account_id}")
+  end
+
+  class Matcher
+    attr_reader :account_id
+    attr_reader :regex
+
+    def initialize(account_id)
+      @account_id = account_id
+      regex_text = Rails.cache.fetch("keyword_mutes:regex:#{account_id}") { regex_text_for_account }
+      @regex = /#{regex_text}/i
+    end
+
+    def =~(str)
+      regex =~ str
+    end
+
+    private
+
+    def keywords
+      Glitch::KeywordMute.where(account_id: account_id).select(:keyword, :id, :whole_word)
+    end
+
+    def regex_text_for_account
+      kws = keywords.find_each.with_object([]) do |kw, a|
+        a << (kw.whole_word ? boundary_regex_for_keyword(kw.keyword) : kw.keyword)
+      end
+
+      Regexp.union(kws).source
+    end
+
+    def boundary_regex_for_keyword(keyword)
+      sb = keyword =~ /\A[[:word:]]/ ? '\b' : ''
+      eb = keyword =~ /[[:word:]]\Z/ ? '\b' : ''
+
+      /#{sb}#{Regexp.escape(keyword)}#{eb}/
+    end
+  end
+end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 60380198b..f6c8879c5 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -24,15 +24,32 @@ require 'mime/types'
 class MediaAttachment < ApplicationRecord
   self.inheritance_column = nil
 
-  enum type: [:image, :gifv, :video, :unknown]
+  enum type: [:image, :gifv, :video, :audio, :unknown]
 
   IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze
   VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v'].freeze
+  AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze
 
   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
   VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze
+  AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze
 
   IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze
+  AUDIO_STYLES = {
+    original: {
+      format: 'mp4',
+      convert_options: {
+        output: {
+          filter_complex: '"[0:a]compand,showwaves=s=640x360:mode=line,format=yuv420p[v]"',
+          map: '"[v]" -map 0:a', 
+          threads: 2,
+          vcodec: 'libx264',
+          acodec: 'aac',
+          movflags: '+faststart',
+        },
+      },
+    },
+  }.freeze
   VIDEO_STYLES = {
     small: {
       convert_options: {
@@ -55,7 +72,7 @@ class MediaAttachment < ApplicationRecord
 
   include Remotable
 
-  validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
+  validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES
   validates_attachment_size :file, less_than: 8.megabytes
 
   validates :account, presence: true
@@ -110,6 +127,8 @@ class MediaAttachment < ApplicationRecord
         }
       elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type
         IMAGE_STYLES
+      elsif AUDIO_MIME_TYPES.include? f.instance.file_content_type
+        AUDIO_STYLES
       else
         VIDEO_STYLES
       end
@@ -120,6 +139,8 @@ class MediaAttachment < ApplicationRecord
         [:gif_transcoder]
       elsif VIDEO_MIME_TYPES.include? f.file_content_type
         [:video_transcoder]
+      elsif AUDIO_MIME_TYPES.include? f.file_content_type
+        [:audio_transcoder]
       else
         [:thumbnail]
       end
@@ -144,8 +165,8 @@ class MediaAttachment < ApplicationRecord
   end
 
   def set_type_and_extension
-    self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
-    extension = appropriate_extension
+    self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : AUDIO_MIME_TYPES.include?(file_content_type) ? :audio : :image
+    extension = AUDIO_MIME_TYPES.include?(file_content_type) ? '.mp4' : appropriate_extension
     basename  = Paperclip::Interpolations.basename(file, :original)
     file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
   end
diff --git a/app/models/mute.rb b/app/models/mute.rb
index 6e64848c7..bcd3d247c 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,11 +3,12 @@
 #
 # Table name: mutes
 #
-#  created_at        :datetime         not null
-#  updated_at        :datetime         not null
-#  account_id        :integer          not null
-#  id                :integer          not null, primary key
-#  target_account_id :integer          not null
+#  created_at         :datetime         not null
+#  updated_at         :datetime         not null
+#  account_id         :integer          not null
+#  id                 :integer          not null, primary key
+#  target_account_id  :integer          not null
+#  hide_notifications :boolean          default(TRUE), not null
 #
 
 class Mute < ApplicationRecord
diff --git a/app/models/status.rb b/app/models/status.rb
index 5a7245613..d78a921b5 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -154,6 +154,14 @@ class Status < ApplicationRecord
       where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
     end
 
+    def as_direct_timeline(account)
+      query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
+              .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}")
+              .where(visibility: [:direct])
+
+      apply_timeline_filters(query, account, false)
+    end
+
     def as_public_timeline(account = nil, local_only = false)
       query = timeline_scope(local_only).without_replies
 
@@ -261,6 +269,11 @@ class Status < ApplicationRecord
     end
   end
 
+  def local_only?
+    # match both with and without U+FE0F (the emoji variation selector)
+    /👁\ufe0f?\z/.match?(content)
+  end
+
   private
 
   def store_uri
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index b51fe9ad7..720cd518c 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -27,7 +27,7 @@ class StreamEntry < ApplicationRecord
   scope :recent, -> { reorder(id: :desc) }
   scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
 
-  delegate :target, :title, :content, :thread,
+  delegate :target, :title, :content, :thread, :local_only?,
            to: :status,
            allow_nil: true