about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-07-23 16:48:08 -0500
committermultiple creatures <dev@multiple-creature.party>2019-07-23 16:48:08 -0500
commit3862f48c34a00691a12c6002abd88b088cf7c13e (patch)
treef1d5d2299de3d470816300221d3dba6ac8ecc30c /app
parent2a6ccce070277c8c278a2e8403f45394eec06f91 (diff)
add self-destructing roars & `live`/`lifespan` bangtags
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/v1/statuses_controller.rb2
-rw-r--r--app/controllers/settings/preferences_controller.rb1
-rw-r--r--app/javascript/flavours/glitch/components/status_icons.js3
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js20
-rw-r--r--app/lib/bangtags.rb55
-rw-r--r--app/lib/user_settings_decorator.rb5
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/destructing_status.rb20
-rw-r--r--app/models/status.rb13
-rw-r--r--app/models/user.rb5
-rw-r--r--app/serializers/rest/status_serializer.rb5
-rw-r--r--app/services/post_status_service.rb19
-rw-r--r--app/views/settings/preferences/show.html.haml7
-rw-r--r--app/workers/destruct_status_worker.rb16
-rw-r--r--app/workers/scheduler/destructing_statuses_scheduler.rb19
15 files changed, 189 insertions, 2 deletions
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index a9728f997..305e8d113 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -52,6 +52,7 @@ class Api::V1::StatusesController < Api::BaseController
                                          spoiler_text: status_params[:spoiler_text],
                                          visibility: status_params[:visibility],
                                          scheduled_at: status_params[:scheduled_at],
+                                         delete_after: status_params[:delete_after],
                                          sharekey: status_params[:sharekey],
                                          application: doorkeeper_token.application,
                                          poll: status_params[:poll],
@@ -92,6 +93,7 @@ class Api::V1::StatusesController < Api::BaseController
       :visibility,
       :sharekey,
       :scheduled_at,
+      :delete_after,
       :content_type,
       media_ids: [],
       poll: [
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index b9f3a803d..e7d2008d0 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -53,6 +53,7 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_hide_public_profile,
       :setting_hide_public_outbox,
       :setting_max_public_history,
+      :setting_roar_lifespan,
 
       :setting_default_privacy,
       :setting_default_sensitive,
diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js
index c9747650f..9a3b2b745 100644
--- a/app/javascript/flavours/glitch/components/status_icons.js
+++ b/app/javascript/flavours/glitch/components/status_icons.js
@@ -59,6 +59,9 @@ export default class StatusIcons extends React.PureComponent {
             aria-hidden='true'
           />
         ) : null}
+        {status.get('delete_after') ? (
+          <i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} aria-hidden='true' />
+        ) : null}
         {(
           <VisibilityIcon visibility={status.get('visibility')} />
         )}
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index e9bbcaa90..f8f5e053c 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -17,6 +17,15 @@ import classNames from 'classnames';
 import PollContainer from 'flavours/glitch/containers/poll_container';
 import { me } from 'flavours/glitch/util/initial_state';
 
+const dateFormatOptions = {
+  month: 'numeric',
+  day: 'numeric',
+  year: 'numeric',
+  hour12: false,
+  hour: '2-digit',
+  minute: '2-digit',
+};
+
 export default class DetailedStatus extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -119,6 +128,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
     let reblogIcon = 'repeat';
     let favouriteLink = '';
     let sharekeyLinks = '';
+    let destructIcon = '';
 
     if (this.props.measureHeight) {
       outerStyle.height = `${this.state.height}px`;
@@ -233,6 +243,14 @@ export default class DetailedStatus extends ImmutablePureComponent {
       );
     }
 
+    if (status.get('delete_after')) {
+      destructIcon = (
+        <span>
+          <i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} /> ·
+        </span>
+      )
+    }
+
     return (
       <div style={outerStyle}>
         <div ref={this.setRef} className={classNames('detailed-status', { compact })} data-status-by={status.getIn(['account', 'acct'])}>
@@ -254,7 +272,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
           />
 
           <div className='detailed-status__meta'>
-            {sharekeyLinks} {reblogLink} · {favouriteLink} · <VisibilityIcon visibility={status.get('visibility')} />
+            {sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} <VisibilityIcon visibility={status.get('visibility')} />
             <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
             </a>
diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb
index e1ad79be6..3ed56806a 100644
--- a/app/lib/bangtags.rb
+++ b/app/lib/bangtags.rb
@@ -22,6 +22,22 @@ class Bangtags
       ['media', 'stop'] => ['var', 'end'],
       ['media', 'endall'] => ['var', 'endall'],
       ['media', 'stopall'] => ['var', 'endall'],
+
+      ['admin', 'end'] => ['var', 'end'],
+      ['admin', 'stop'] => ['var', 'end'],
+      ['admin', 'endall'] => ['var', 'endall'],
+      ['admin', 'stopall'] => ['var', 'endall'],
+
+      ['parent', 'visibility'] => ['visibility', 'parent'],
+      ['parent', 'v'] => ['visibility', 'parent'],
+
+      ['parent', 'live'] => ['live', 'parent'],
+      ['parent', 'lifespan'] => ['lifespan', 'parent'],
+      ['parent', 'delete_in'] => ['delete_in', 'parent'],
+
+      ['all', 'live'] => ['live', 'all'],
+      ['all', 'lifespan'] => ['lifespan', 'all'],
+      ['all', 'delete_in'] => ['delete_in', 'all'],
     }
 
     # sections of the final status text
@@ -525,6 +541,45 @@ class Bangtags
               status.local_only = true
             end
           end
+        when 'live', 'lifespan', 'l', 'delete_in'
+          chunk = nil
+          next if cmd[1].nil?
+          case cmd[1].downcase
+          when 'parent'
+            next unless @parent_status.present? && @parent_status.account_id == @account.id
+            s = @parent_status
+            i = cmd[2].to_i
+            unit = cmd[3].present? ? cmd[3].downcase : 'minutes'
+          when 'all'
+            s = :all
+            i = cmd[2].to_i
+            unit = cmd[3].present? ? cmd[3].downcase : 'minutes'
+          else
+            s = @status
+            i = cmd[1].to_i
+            unit = cmd[2].present? ? cmd[2].downcase : 'minutes'
+          end
+          delete_after = case unit
+                         when 's', 'second', 'seconds'
+                           [60, i].max.seconds
+                         when 'm', 'minute', 'minutes'
+                           i.minutes
+                         when 'h', 'hour', 'hours'
+                           i.hours
+                         when 'd', 'day', 'days'
+                           i.days
+                         when 'w', 'week', 'weeks'
+                           i.weeks
+                         when 'm', 'month', 'months'
+                           i.months
+                         when 'y', 'year', 'years'
+                           i.years
+                         end
+          if s == :all
+            @account.statuses.find_each { |s| s.delete_after = delete_after }
+          else
+            s.delete_after = delete_after
+          end
         when 'keysmash'
           keyboard = [
             'asdf', 'jkl;',
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 4616142f8..50632ec44 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -37,6 +37,7 @@ class UserSettingsDecorator
     user.settings['hide_public_outbox']  = hide_public_outbox_preference if change?('setting_hide_public_outbox')
     user.settings['larger_emoji']        = larger_emoji_preference if change?('setting_larger_emoji')
     user.settings['max_public_history']  = max_public_history_preference if change?('setting_max_public_history')
+    user.settings['roar_lifespan']       = roar_lifespan_preference if change?('setting_roar_lifespan')
 
     user.settings['notification_emails'] = merged_notification_emails if change?('notification_emails')
     user.settings['interactions']        = merged_interactions if change?('interactions')
@@ -130,6 +131,10 @@ class UserSettingsDecorator
     settings['setting_max_public_history']
   end
 
+  def roar_lifespan_preference
+    settings['setting_roar_lifespan']
+  end
+
   def merged_notification_emails
     user.settings['notification_emails'].merge coerced_settings('notification_emails').to_h
   end
diff --git a/app/models/account.rb b/app/models/account.rb
index 1955b7aee..068b5e7a0 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -133,6 +133,7 @@ class Account < ApplicationRecord
            :defaults_to_local_only?,
            :always_local_only?,
            :max_public_history,
+           :roar_lifespan,
 
            :hides_public_profile?,
            :hides_public_outbox?,
diff --git a/app/models/destructing_status.rb b/app/models/destructing_status.rb
new file mode 100644
index 000000000..349e276cb
--- /dev/null
+++ b/app/models/destructing_status.rb
@@ -0,0 +1,20 @@
+# == Schema Information
+#
+# Table name: destructing_statuses
+#
+#  id           :bigint(8)        not null, primary key
+#  status_id    :bigint(8)
+#  delete_after :datetime
+#
+
+class DestructingStatus < ApplicationRecord
+  belongs_to :status, inverse_of: :destructing_status
+
+  validate :validate_future_date
+
+  private
+
+  def validate_future_date
+    errors.add(:delete_after, I18n.t('destructing_statuses.too_soon')) if delete_after.present? && delete_after < Time.now.utc + PostStatusService::MIN_DESTRUCT_OFFSET
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index ec492293f..946958758 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -79,6 +79,7 @@ class Status < ApplicationRecord
   has_one :stream_entry, as: :activity, inverse_of: :status
   has_one :status_stat, inverse_of: :status
   has_one :poll, inverse_of: :status, dependent: :destroy
+  has_one :destructing_status, inverse_of: :status, dependent: :destroy
 
   validates :uri, uniqueness: true, presence: true, unless: :local?
   validates :text, presence: true, unless: -> { with_media? || reblog? }
@@ -266,6 +267,18 @@ class Status < ApplicationRecord
     @chat_tags = tags.only_chat
   end
 
+  def delete_after
+    destructing_status&.delete_after
+  end
+
+  def delete_after=(value)
+    if destructing_status.nil?
+      DestructingStatus.create!(status_id: id, delete_after: Time.now.utc + value)
+    else
+      destructing_status.delete_after = Time.now.utc + value
+    end
+  end
+
   def mark_for_mass_destruction!
     @marked_for_mass_destruction = true
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1d06a43f8..2f01c2e5a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -125,6 +125,7 @@ class User < ApplicationRecord
     :hide_public_profile,
     :hide_public_outbox,
     :max_public_history,
+    :roar_lifespan,
 
     :auto_play_gif,
     :default_sensitive,
@@ -299,6 +300,10 @@ class User < ApplicationRecord
     @_max_public_history ||= (settings.max_public_history || 6)
   end
 
+  def roar_lifespan
+    @_roar_lifespan ||= (settings.roar_lifespan || 0)
+  end
+
   def defaults_to_local_only?
     @defaults_to_local_only ||= (settings.default_local || false)
   end
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 64578713a..284e634df 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -13,6 +13,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
   attribute :pinned, if: :pinnable?
   attribute :local_only if :local?
   attribute :sharekey, if: :owner?
+  attribute :delete_after, if: :current_user?
 
   attribute :content, unless: :source_requested?
   attribute :text, if: :source_requested?
@@ -135,6 +136,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
     object.active_mentions.to_a.sort_by(&:id)
   end
 
+  def delete_after
+    object.delete_after
+  end
+
   class ApplicationSerializer < ActiveModel::Serializer
     attributes :name, :website
   end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 3bb580508..65f0c88ce 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -4,6 +4,8 @@ class PostStatusService < BaseService
   include Redisable
 
   MIN_SCHEDULE_OFFSET = 5.minutes.freeze
+  MIN_DESTRUCT_OFFSET = 30.seconds.freeze
+
   VISIBILITY_RANK = {
     'public'    => 0,
     'unlisted'  => 1,
@@ -28,6 +30,7 @@ class PostStatusService < BaseService
   # @option [String] :spoiler_text
   # @option [String] :language
   # @option [String] :scheduled_at
+  # @option [String] :delete_after
   # @option [Hash] :poll Optional poll to attach
   # @option [Enumerable] :media_ids Optional array of media IDs to attach
   # @option [Doorkeeper::Application] :application
@@ -134,6 +137,19 @@ class PostStatusService < BaseService
 
     @scheduled_at = @options[:scheduled_at]&.to_datetime
     @scheduled_at = nil if scheduled_in_the_past?
+
+    case @options[:delete_after].class
+    when NilClass
+      @delete_after = @account.user.setting_roar_lifespan.to_i.days
+    when ActiveSupport::Duration
+      @delete_after = @options[:delete_after]
+    when Integer
+      @delete_after = @options[:delete_after].minutes
+    when Float
+      @delete_after = @options[:delete_after].minutes
+    end
+    @delete_after = nil if @delete_after.present? && (@delete_after < MIN_DESTRUCT_OFFSET)
+
   rescue ArgumentError
     raise ActiveRecord::RecordInvalid
   end
@@ -179,6 +195,8 @@ class PostStatusService < BaseService
     end
 
     PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
+
+    @status.delete_after = @delete_after unless @delete_after.nil?
   end
 
   def validate_media!
@@ -250,6 +268,7 @@ class PostStatusService < BaseService
       spoiler_text: @options[:spoiler_text] || '',
       visibility: @visibility,
       local_only: @local_only,
+      delete_after: @delete_after,
       sharekey: @sharekey,
       language: language_from_option(@options[:language]) || @account.user_default_language&.presence || 'en',
       application: @options[:application],
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 0f65bc327..aeacd2830 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -36,6 +36,8 @@
   .fields-group
     = f.input :setting_rawr_federated, as: :boolean, wrapper: :with_label
 
+  %hr/
+
   .fields-group
     = f.input :setting_hide_network, as: :boolean, wrapper: :with_label
     = f.input :setting_hide_interactions, as: :boolean, wrapper: :with_label
@@ -43,8 +45,11 @@
     = f.input :setting_show_application, as: :boolean, wrapper: :with_label
     = f.input :setting_noindex, as: :boolean, wrapper: :with_label
 
+  %hr/
+
   .fields-group
-    = f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_max_public_history_#{item}")]) }, selected: current_user.max_public_history.to_i
+    = f.input :setting_max_public_history, collection: [1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.max_public_history.to_i
+    = f.input :setting_roar_lifespan, collection: [0, 1, 3, 6, 7, 14, 30, 60, 90, 180, 365, 730, 1095, 2190], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.lifespan.#{item}")]) }, selected: current_user.roar_lifespan.to_i
     = f.input :setting_hide_public_profile, as: :boolean, wrapper: :with_label
     = f.input :setting_hide_public_outbox, as: :boolean, wrapper: :with_label
 
diff --git a/app/workers/destruct_status_worker.rb b/app/workers/destruct_status_worker.rb
new file mode 100644
index 000000000..6a3971220
--- /dev/null
+++ b/app/workers/destruct_status_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class DestructStatusWorker
+  include Sidekiq::Worker
+
+  sidekiq_options unique: :until_executed
+
+  def perform(destructing_status_id)
+    destructing_status = DestructingStatus.find(destructing_status_id)
+    destructing_status.destroy!
+
+    RemoveStatusService.new.call(destructing_status.status)
+  rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
+    true
+  end
+end
diff --git a/app/workers/scheduler/destructing_statuses_scheduler.rb b/app/workers/scheduler/destructing_statuses_scheduler.rb
new file mode 100644
index 000000000..d79f97c33
--- /dev/null
+++ b/app/workers/scheduler/destructing_statuses_scheduler.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Scheduler::DestructingStatusesScheduler
+  include Sidekiq::Worker
+
+  sidekiq_options unique: :until_executed, retry: 0
+
+  def perform
+    due_statuses.find_each do |destructing_status|
+      DestructStatusWorker.perform_at(destructing_status.delete_after, destructing_status.id)
+    end
+  end
+
+  private
+
+  def due_statuses
+    DestructingStatus.where('delete_after <= ?', Time.now.utc + PostStatusService::MIN_DESTRUCT_OFFSET)
+  end
+end