diff options
22 files changed, 238 insertions, 17 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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 050806939..8210ef70d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -858,7 +858,9 @@ en: scheduled_statuses: over_daily_limit: You have exceeded the limit of %{limit} scheduled roars for that day over_total_limit: You have exceeded the limit of %{limit} scheduled roars - too_soon: The scheduled date must be in the future + too_soon: The scheduled timeframe must be at least 5 minutes into the future + destructing_statuses: + too_soon: The destruction timeframe must be at least 5 minutes into the future sessions: activity: Last activity browser: Browser diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 21b0cd39a..544b1a03e 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -118,6 +118,7 @@ en: setting_default_content_type_x_bbcode_markdown: BBdown setting_default_language: Posting language setting_default_privacy: Post privacy + setting_roar_lifespan: Auto-delete new roars after setting_default_local: Default to Monsterpit-only roars (in Glitch flavour) setting_always_local: Don't send your roars outside Monsterpit setting_rawr_federated: Show raw world timeline (may contain offensive content!) @@ -156,19 +157,6 @@ en: setting_hide_public_profile: Hide your public profile from anonymous viewers setting_hide_public_outbox: Hide your public ActivityPub outbox (affects discoverability) setting_max_public_history: Limit history of roars on public profile to - setting_max_public_history_1: 1 day - setting_max_public_history_3: 3 days - setting_max_public_history_6: 6 days - setting_max_public_history_7: 1 week - setting_max_public_history_14: 2 weeks - setting_max_public_history_30: 6 weeks - setting_max_public_history_60: 2 months - setting_max_public_history_90: 3 months - setting_max_public_history_180: 6 months - setting_max_public_history_365: 1 year - setting_max_public_history_730: 2 years - setting_max_public_history_1095: 3 years - setting_max_public_history_2190: 6 years setting_noindex: Opt-out of search engine indexing setting_reduce_motion: Reduce motion in animations setting_show_application: Disclose application used to send roars @@ -180,6 +168,21 @@ en: username: Username username_or_email: Username or Email whole_word: Whole word + lifespan: + '0': No limit + 1: 1 day + 3: 3 days + 6: 6 days + 7: 1 week + 14: 2 weeks + 30: 6 weeks + 60: 2 months + 90: 3 months + 180: 6 months + 365: 1 year + 730: 2 years + 1095: 3 years + 2190: 6 years featured_tag: name: Hashtag interactions: diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 6ebe450b0..1ab523efb 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,6 +9,9 @@ scheduled_statuses_scheduler: every: '5m' class: Scheduler::ScheduledStatusesScheduler + destructing_statuses_scheduler: + every: '1m' + class: Scheduler::DestructingStatusesScheduler media_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::MediaCleanupScheduler diff --git a/db/migrate/20190723152514_create_destructing_statuses.rb b/db/migrate/20190723152514_create_destructing_statuses.rb new file mode 100644 index 000000000..baed9035a --- /dev/null +++ b/db/migrate/20190723152514_create_destructing_statuses.rb @@ -0,0 +1,9 @@ +class CreateDestructingStatuses < ActiveRecord::Migration[5.2] + def change + create_table :destructing_statuses do |t| + t.references :status, foreign_key: true + t.datetime :delete_after + end + add_index :destructing_statuses, :delete_after + end +end diff --git a/db/schema.rb b/db/schema.rb index 2238213a4..23ad7cf13 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: 2019_07_22_014444) do +ActiveRecord::Schema.define(version: 2019_07_23_152514) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -266,6 +266,13 @@ ActiveRecord::Schema.define(version: 2019_07_22_014444) do t.index ["account_id"], name: "index_custom_filters_on_account_id" end + create_table "destructing_statuses", force: :cascade do |t| + t.bigint "status_id" + t.datetime "delete_after" + t.index ["delete_after"], name: "index_destructing_statuses_on_delete_after" + t.index ["status_id"], name: "index_destructing_statuses_on_status_id" + end + create_table "domain_blocks", force: :cascade do |t| t.string "domain", default: "", null: false t.datetime "created_at", null: false @@ -817,6 +824,7 @@ ActiveRecord::Schema.define(version: 2019_07_22_014444) do add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade add_foreign_key "custom_filters", "accounts", on_delete: :cascade + add_foreign_key "destructing_statuses", "statuses" add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade add_foreign_key "featured_tags", "accounts", on_delete: :cascade diff --git a/spec/fabricators/destructing_status_fabricator.rb b/spec/fabricators/destructing_status_fabricator.rb new file mode 100644 index 000000000..7a538adb0 --- /dev/null +++ b/spec/fabricators/destructing_status_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:destructing_status) do + status nil + delete_after "2019-07-23 10:25:14" +end diff --git a/spec/models/destructing_status_spec.rb b/spec/models/destructing_status_spec.rb new file mode 100644 index 000000000..e68f39e62 --- /dev/null +++ b/spec/models/destructing_status_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe DestructingStatus, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end |