add self-destructing roars & `live`/`lifespan` bangtags

staging
multiple creatures 2019-07-23 16:48:08 -05:00
parent 2a6ccce070
commit 3862f48c34
22 changed files with 238 additions and 17 deletions

View File

@ -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: [

View File

@ -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,

View File

@ -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')} />
)}

View File

@ -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>

View File

@ -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;',

View File

@ -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

View File

@ -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?,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
Fabricator(:destructing_status) do
status nil
delete_after "2019-07-23 10:25:14"
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe DestructingStatus, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end