about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorkibigo! <marrus-sh@users.noreply.github.com>2017-07-15 14:33:15 -0700
committerkibigo! <marrus-sh@users.noreply.github.com>2017-07-15 14:33:15 -0700
commit09cfc079b0958c42fe619e2d88c3f9fd1d7c7900 (patch)
tree156de790a5bec0fdf050e392bee8a64b220d3a9d /app/models
parent08d021916db9e350259b925d7e562aa13ba37422 (diff)
parent695439775eacea081c7257aabab39d0ec6b492dc (diff)
Merge upstream (#81)
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb12
-rw-r--r--app/models/concerns/remotable.rb3
-rw-r--r--app/models/domain_block.rb2
-rw-r--r--app/models/import.rb2
-rw-r--r--app/models/session_activation.rb12
-rw-r--r--app/models/status.rb16
-rw-r--r--app/models/subscription.rb6
-rw-r--r--app/models/tag.rb7
-rw-r--r--app/models/user.rb12
-rw-r--r--app/models/web/push_subscription.rb190
10 files changed, 246 insertions, 16 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index 49d2c88f6..9f8e22adf 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -47,6 +47,7 @@ class Account < ApplicationRecord
   include AccountInteractions
   include Attachmentable
   include Remotable
+  include EmojiHelper
 
   # Local users
   has_one :user, inverse_of: :account
@@ -129,7 +130,7 @@ class Account < ApplicationRecord
   end
 
   def subscription(webhook_url)
-    OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 86_400 * 30, webhook: webhook_url, hub: hub_url)
+    OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 30.days.seconds, webhook: webhook_url, hub: hub_url)
   end
 
   def save_with_optional_media!
@@ -240,9 +241,18 @@ class Account < ApplicationRecord
 
   before_create :generate_keys
   before_validation :normalize_domain
+  before_validation :prepare_contents, if: :local?
 
   private
 
+  def prepare_contents
+    display_name&.strip!
+    note&.strip!
+
+    self.display_name = emojify(display_name)
+    self.note         = emojify(note)
+  end
+
   def generate_keys
     return unless local?
 
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index b4f169649..1bd87a642 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -1,7 +1,6 @@
 # frozen_string_literal: true
 
 module Remotable
-  include HttpHelper
   extend ActiveSupport::Concern
 
   included do
@@ -20,7 +19,7 @@ module Remotable
         return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url
 
         begin
-          response = http_client.get(url)
+          response = Request.new(:get, url).perform
 
           return if response.code != 200
 
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 99dae9c1d..f26e8183f 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -8,7 +8,7 @@
 #  created_at   :datetime         not null
 #  updated_at   :datetime         not null
 #  severity     :integer          default("silence")
-#  reject_media :boolean
+#  reject_media :boolean          default(FALSE), not null
 #
 
 class DomainBlock < ApplicationRecord
diff --git a/app/models/import.rb b/app/models/import.rb
index 8c6253d49..815e02589 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -6,7 +6,7 @@
 #  id                :integer          not null, primary key
 #  account_id        :integer          not null
 #  type              :integer          not null
-#  approved          :boolean
+#  approved          :boolean          default(FALSE), not null
 #  created_at        :datetime         not null
 #  updated_at        :datetime         not null
 #  data_file_name    :string
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index 887e3e3bd..7eb16af8f 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -3,6 +3,17 @@
 #
 # Table name: session_activations
 #
+#  id                       :integer          not null, primary key
+#  user_id                  :integer          not null
+#  session_id               :string           not null
+#  created_at               :datetime         not null
+#  updated_at               :datetime         not null
+#  user_agent               :string           default(""), not null
+#  ip                       :inet
+#  access_token_id          :integer
+#  web_push_subscription_id :integer
+#
+
 #  id              :integer          not null, primary key
 #  user_id         :integer          not null
 #  session_id      :string           not null
@@ -15,6 +26,7 @@
 
 class SessionActivation < ApplicationRecord
   belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
+  belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy
 
   delegate :token,
            to: :access_token,
diff --git a/app/models/status.rb b/app/models/status.rb
index 791d96df1..24eaf7071 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -12,12 +12,12 @@
 #  in_reply_to_id         :integer
 #  reblog_of_id           :integer
 #  url                    :string
-#  sensitive              :boolean          default(FALSE)
+#  sensitive              :boolean          default(FALSE), not null
 #  visibility             :integer          default("public"), not null
 #  in_reply_to_account_id :integer
 #  application_id         :integer
 #  spoiler_text           :text             default(""), not null
-#  reply                  :boolean          default(FALSE)
+#  reply                  :boolean          default(FALSE), not null
 #  favourites_count       :integer          default(0), not null
 #  reblogs_count          :integer          default(0), not null
 #  language               :string
@@ -29,6 +29,7 @@ class Status < ApplicationRecord
   include Streamable
   include Cacheable
   include StatusThreadingConcern
+  include EmojiHelper
 
   enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
 
@@ -120,10 +121,11 @@ class Status < ApplicationRecord
     !sensitive? && media_attachments.any?
   end
 
-  before_validation :prepare_contents
+  before_validation :prepare_contents, if: :local?
   before_validation :set_reblog
   before_validation :set_visibility
   before_validation :set_conversation
+  before_validation :set_sensitivity
 
   class << self
     def not_in_filtered_languages(account)
@@ -240,6 +242,9 @@ class Status < ApplicationRecord
   def prepare_contents
     text&.strip!
     spoiler_text&.strip!
+
+    self.text         = emojify(text)
+    self.spoiler_text = emojify(spoiler_text)
   end
 
   def set_reblog
@@ -248,6 +253,11 @@ class Status < ApplicationRecord
 
   def set_visibility
     self.visibility = (account.locked? ? :private : :public) if visibility.nil?
+    self.sensitive  = false if sensitive.nil?
+  end
+
+  def set_sensitivity
+    self.sensitive = sensitive || spoiler_text.present?
   end
 
   def set_conversation
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 35a228df0..bf643c1f9 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,5 +1,4 @@
 # frozen_string_literal: true
-
 # == Schema Information
 #
 # Table name: subscriptions
@@ -13,11 +12,12 @@
 #  created_at                  :datetime         not null
 #  updated_at                  :datetime         not null
 #  last_successful_delivery_at :datetime
+#  domain                      :string
 #
 
 class Subscription < ApplicationRecord
-  MIN_EXPIRATION = 7.days.seconds.to_i
-  MAX_EXPIRATION = 30.days.seconds.to_i
+  MIN_EXPIRATION = 1.day.to_i
+  MAX_EXPIRATION = 30.days.to_i
 
   belongs_to :account, required: true
 
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 08e3c1b03..0fa08e157 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -12,9 +12,10 @@
 class Tag < ApplicationRecord
   has_and_belongs_to_many :statuses
 
-  HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i
+  HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_][[:word:]_]*'
+  HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
 
-  validates :name, presence: true, uniqueness: true
+  validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
 
   def to_param
     name
@@ -23,7 +24,7 @@ class Tag < ApplicationRecord
   class << self
     def search_for(term, limit = 5)
       pattern = sanitize_sql_like(term) + '%'
-      Tag.where('name like ?', pattern).order(:name).limit(limit)
+      Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
     end
   end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index c80115a08..becf0018f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -17,7 +17,7 @@
 #  last_sign_in_at           :datetime
 #  current_sign_in_ip        :inet
 #  last_sign_in_ip           :inet
-#  admin                     :boolean          default(FALSE)
+#  admin                     :boolean          default(FALSE), not null
 #  confirmation_token        :string
 #  confirmed_at              :datetime
 #  confirmation_sent_at      :datetime
@@ -27,7 +27,7 @@
 #  encrypted_otp_secret_iv   :string
 #  encrypted_otp_secret_salt :string
 #  consumed_timestep         :integer
-#  otp_required_for_login    :boolean
+#  otp_required_for_login    :boolean          default(FALSE), not null
 #  last_emailed_at           :datetime
 #  otp_backup_codes          :string           is an Array
 #  filtered_languages        :string           default([]), not null, is an Array
@@ -99,6 +99,10 @@ class User < ApplicationRecord
     settings.system_font_ui
   end
 
+  def setting_noindex
+    settings.noindex
+  end
+
   def activate_session(request)
     session_activations.activate(session_id: SecureRandom.hex,
                                  user_agent: request.user_agent,
@@ -113,6 +117,10 @@ class User < ApplicationRecord
     session_activations.active? id
   end
 
+  def web_push_subscription(session)
+    session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
+  end
+
   protected
 
   def send_devise_notification(notification, *args)
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
new file mode 100644
index 000000000..4440706a6
--- /dev/null
+++ b/app/models/web/push_subscription.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: web_push_subscriptions
+#
+#  id         :integer          not null, primary key
+#  endpoint   :string           not null
+#  key_p256dh :string           not null
+#  key_auth   :string           not null
+#  data       :json
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class Web::PushSubscription < ApplicationRecord
+  include RoutingHelper
+  include StreamEntriesHelper
+  include ActionView::Helpers::TranslationHelper
+  include ActionView::Helpers::SanitizeHelper
+
+  has_one :session_activation
+
+  before_create :send_welcome_notification
+
+  def push(notification)
+    return unless pushable? notification
+
+    name = display_name notification.from_account
+    title = title_str(name, notification)
+    body = body_str notification
+    dir = dir_str body
+    url = url_str notification
+    image = image_str notification
+    actions = actions_arr notification
+
+    access_token = actions.empty? ? nil : find_or_create_access_token(notification).token
+    nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text
+
+    # TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge
+    # TODO: Queue the requests - Webpush::TooManyRequests
+    Webpush.payload_send(
+      message: JSON.generate(
+        title: title,
+        dir: dir,
+        image: image,
+        badge: full_asset_url('badge.png'),
+        tag: notification.id,
+        timestamp: notification.created_at,
+        icon: notification.from_account.avatar_static_url,
+        data: {
+          content: decoder.decode(strip_tags(body)),
+          nsfw: nsfw.nil? ? nil : decoder.decode(strip_tags(nsfw)),
+          url: url,
+          actions: actions,
+          access_token: access_token,
+        }
+      ),
+      endpoint: endpoint,
+      p256dh: key_p256dh,
+      auth: key_auth,
+      vapid: {
+        # subject: "mailto:#{Setting.site_contact_email}",
+        private_key: Rails.configuration.x.vapid_private_key,
+        public_key: Rails.configuration.x.vapid_public_key,
+      },
+      ttl: 40 * 60 * 60 # 48 hours
+    )
+  end
+
+  def as_payload
+    payload = {
+      id: id,
+      endpoint: endpoint,
+    }
+
+    payload[:alerts] = data['alerts'] if data && data.key?('alerts')
+
+    payload
+  end
+
+  private
+
+  def title_str(name, notification)
+    case notification.type
+    when :mention then translate('push_notifications.mention.title', name: name)
+    when :follow then translate('push_notifications.follow.title', name: name)
+    when :favourite then translate('push_notifications.favourite.title', name: name)
+    when :reblog then translate('push_notifications.reblog.title', name: name)
+    end
+  end
+
+  def body_str(notification)
+    case notification.type
+    when :mention then notification.target_status.text
+    when :follow then notification.from_account.note
+    when :favourite then notification.target_status.text
+    when :reblog then notification.target_status.text
+    end
+  end
+
+  def url_str(notification)
+    case notification.type
+    when :mention then web_url("statuses/#{notification.target_status.id}")
+    when :follow then web_url("accounts/#{notification.from_account.id}")
+    when :favourite then web_url("statuses/#{notification.target_status.id}")
+    when :reblog then web_url("statuses/#{notification.target_status.id}")
+    end
+  end
+
+  def actions_arr(notification)
+    actions =
+      case notification.type
+      when :mention then [
+        {
+          title: translate('push_notifications.mention.action_favourite'),
+          icon: full_asset_url('emoji/2764.png'),
+          todo: 'request',
+          method: 'POST',
+          action: "/api/v1/statuses/#{notification.target_status.id}/favourite",
+        },
+      ]
+      else []
+      end
+
+    should_hide = notification.type.equal?(:mention) && !notification.target_status.nil? && (notification.target_status.sensitive || !notification.target_status.spoiler_text.empty?)
+    can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden?
+
+    if should_hide
+      actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('emoji/1f441.png'), todo: 'expand', action: 'expand')
+    end
+
+    if can_boost
+      actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" }
+    end
+
+    actions
+  end
+
+  def image_str(notification)
+    return nil if notification.target_status.nil? || notification.target_status.media_attachments.empty?
+
+    full_asset_url(notification.target_status.media_attachments.first.file.url(:small))
+  end
+
+  def dir_str(body)
+    rtl?(body) ? 'rtl' : 'ltr'
+  end
+
+  def pushable?(notification)
+    data && data.key?('alerts') && data['alerts'][notification.type.to_s]
+  end
+
+  def send_welcome_notification
+    Webpush.payload_send(
+      message: JSON.generate(
+        title: translate('push_notifications.subscribed.title'),
+        icon: full_asset_url('android-chrome-192x192.png'),
+        badge: full_asset_url('badge.png'),
+        data: {
+          content: translate('push_notifications.subscribed.body'),
+          actions: [],
+          url: web_url('notifications'),
+        }
+      ),
+      endpoint: endpoint,
+      p256dh: key_p256dh,
+      auth: key_auth,
+      vapid: {
+        # subject: "mailto:#{Setting.site_contact_email}",
+        private_key: Rails.configuration.x.vapid_private_key,
+        public_key: Rails.configuration.x.vapid_public_key,
+      },
+      ttl: 5 * 60 # 5 minutes
+    )
+  end
+
+  def find_or_create_access_token(notification)
+    Doorkeeper::AccessToken.find_or_create_for(
+      Doorkeeper::Application.find_by(superapp: true),
+      notification.account.user.id,
+      Doorkeeper::OAuth::Scopes.from_string('read write follow'),
+      Doorkeeper.configuration.access_token_expires_in,
+      Doorkeeper.configuration.refresh_token_enabled?
+    )
+  end
+
+  def decoder
+    @decoder ||= HTMLEntities.new
+  end
+end