diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/api/v1/statuses_controller.rb | 2 | ||||
-rw-r--r-- | app/controllers/settings/preferences_controller.rb | 1 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/components/status_icons.js | 3 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/status/components/detailed_status.js | 20 | ||||
-rw-r--r-- | app/lib/bangtags.rb | 55 | ||||
-rw-r--r-- | app/lib/user_settings_decorator.rb | 5 | ||||
-rw-r--r-- | app/models/account.rb | 1 | ||||
-rw-r--r-- | app/models/destructing_status.rb | 20 | ||||
-rw-r--r-- | app/models/status.rb | 13 | ||||
-rw-r--r-- | app/models/user.rb | 5 | ||||
-rw-r--r-- | app/serializers/rest/status_serializer.rb | 5 | ||||
-rw-r--r-- | app/services/post_status_service.rb | 19 | ||||
-rw-r--r-- | app/views/settings/preferences/show.html.haml | 7 | ||||
-rw-r--r-- | app/workers/destruct_status_worker.rb | 16 | ||||
-rw-r--r-- | app/workers/scheduler/destructing_statuses_scheduler.rb | 19 |
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 |