diff options
41 files changed, 350 insertions, 88 deletions
diff --git a/.env.production.sample b/.env.production.sample index 1f952be06..235b19207 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -165,6 +165,7 @@ STREAMING_CLUSTER_NUM=1 # LDAP_BIND_DN= # LDAP_PASSWORD= # LDAP_UID=cn +# LDAP_SEARCH_FILTER="%{uid}=%{email}" # PAM authentication (optional) # PAM authentication uses for the email generation the "email" pam variable diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 382bfc4a2..a69f12084 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -29,6 +29,10 @@ module Admin flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save redirect_to admin_account_statuses_path(@account.id, current_params) + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.statuses.no_status_selected') + + redirect_to admin_account_statuses_path(@account.id, current_params) end private diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 4c663211e..85bd30304 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -34,7 +34,11 @@ module Admin::ActionLogsHelper link_to attributes['domain'], "https://#{attributes['domain']}" when 'Status' tmp_status = Status.new(attributes) - link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status) + if tmp_status.account + link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id) + else + I18n.t('admin.action_logs.deleted_status') + end end end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 3302454ab..28f27fbc6 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -1,9 +1,6 @@ // This file will be loaded on admin pages, regardless of theme. import { delegate } from 'rails-ujs'; -import { start } from '../mastodon/common'; - -start(); function handleDeleteStatus(event) { const [data] = event.detail; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0ae21e3f0..c799d4e98 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -32,6 +32,16 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, }); +const obfuscatedCount = count => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + @injectIntl export default class StatusActionBar extends ImmutablePureComponent { @@ -194,7 +204,7 @@ export default class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> - <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> + <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index 43b4811e1..bc6f86628 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -147,17 +147,17 @@ export default class ActionBar extends React.PureComponent { <div className='account__action-bar'> <div className='account__action-bar-links'> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> <FormattedMessage id='account.posts' defaultMessage='Toots' /> <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> </Link> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> <FormattedMessage id='account.follows' defaultMessage='Follows' /> <strong>{shortNumberFormat(account.get('following_count'))}</strong> </Link> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> + <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <FormattedMessage id='account.followers' defaultMessage='Followers' /> <strong>{shortNumberFormat(account.get('followers_count'))}</strong> </Link> diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 89387ca43..0ffeaa4dc 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -355,7 +355,9 @@ export default class Status extends ImmutablePureComponent { if (status && ancestorsIds && ancestorsIds.size > 0) { const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1]; - element.scrollIntoView(true); + window.requestAnimationFrame(() => { + element.scrollIntoView(true); + }); this._scrolledIntoView = true; } } diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 47a165e16..55ea32acb 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -158,6 +158,9 @@ export default class Video extends React.PureComponent { this.setState({ dragging: true }); this.video.pause(); this.handleMouseMove(e); + + e.preventDefault(); + e.stopPropagation(); } handleMouseUp = () => { @@ -174,8 +177,10 @@ export default class Video extends React.PureComponent { const { x } = getPointerPosition(this.seek, e); const currentTime = Math.floor(this.video.duration * x); - this.video.currentTime = currentTime; - this.setState({ currentTime }); + if (!isNaN(currentTime)) { + this.video.currentTime = currentTime; + this.setState({ currentTime }); + } }, 60); togglePlay = () => { @@ -281,6 +286,15 @@ export default class Video extends React.PureComponent { playerStyle.height = height; } + let preload; + if (startTime || fullscreen || dragging) { + preload = 'auto'; + } else if (detailed) { + preload = 'metadata'; + } else { + preload = 'none'; + } + return ( <div role='menuitem' @@ -296,7 +310,7 @@ export default class Video extends React.PureComponent { ref={this.setVideoRef} src={src} poster={preview} - preload={startTime ? 'auto' : 'none'} + preload={preload} loop role='button' tabIndex='0' diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8067b80bb..547bcfd1e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -921,15 +921,31 @@ align-items: center; display: flex; margin-top: 8px; + + &__counter { + display: inline-flex; + margin-right: 11px; + align-items: center; + + .status__action-bar-button { + margin-right: 4px; + } + + &__label { + display: inline-block; + width: 14px; + font-size: 12px; + font-weight: 500; + color: $action-button-color; + } + } } .status__action-bar-button { - float: left; margin-right: 18px; } .status__action-bar-dropdown { - float: left; height: 23.15px; width: 23.15px; } diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index e54c55947..c2206cf55 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -1,9 +1,3 @@ -@keyframes Swag { - 0% { background-position: 0% 0%; } - 50% { background-position: 100% 0%; } - 100% { background-position: 200% 0%; } -} - .table { width: 100%; max-width: 100%; @@ -191,14 +185,12 @@ a.table-action-link { .status__content { padding-top: 0; + summary { + display: list-item; + } + strong { font-weight: 700; - background: linear-gradient(to right, orange , yellow, green, cyan, blue, violet,orange , yellow, green, cyan, blue, violet); - background-size: 200% 100%; - -webkit-background-clip: text; - background-clip: text; - color: transparent; - animation: Swag 2s linear 0s infinite; } } } diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 826dcf18e..c45832648 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -13,7 +13,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity # Fast-forward repeat follow requests if @account.following?(target_account) - AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true) + AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id']) return end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index cbed417c4..64c2be7d9 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -5,6 +5,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity case @object['type'] when 'Announce' undo_announce + when 'Accept' + undo_accept when 'Follow' undo_follow when 'Like' @@ -27,6 +29,10 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity end end + def undo_accept + ::Follow.find_by(target_account: @account, uri: target_uri)&.revoke_request! + end + def undo_follow target_account = account_from_uri(target_uri) diff --git a/app/models/export.rb b/app/models/export.rb index f0d5dd255..0eeac0dc0 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -24,8 +24,16 @@ class Export account.media_attachments.sum(:file_file_size) end + def total_statuses + account.statuses_count + end + def total_follows - account.following.count + account.following_count + end + + def total_followers + account.followers_count end def total_blocks diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 0fce82f6f..ce7a6a336 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -32,20 +32,11 @@ class Favourite < ApplicationRecord private def increment_cache_counters - if association(:status).loaded? - status.update_attribute(:favourites_count, status.favourites_count + 1) - else - Status.where(id: status_id).update_all('favourites_count = COALESCE(favourites_count, 0) + 1') - end + status.increment_count!(:favourites_count) end def decrement_cache_counters return if association(:status).loaded? && (status.marked_for_destruction? || status.marked_for_mass_destruction?) - - if association(:status).loaded? - status.update_attribute(:favourites_count, [status.favourites_count - 1, 0].max) - else - Status.where(id: status_id).update_all('favourites_count = GREATEST(COALESCE(favourites_count, 0) - 1, 0)') - end + status.decrement_count!(:favourites_count) end end diff --git a/app/models/follow.rb b/app/models/follow.rb index 3fce14b9a..714f4e898 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -32,6 +32,11 @@ class Follow < ApplicationRecord false # Force uri_for to use uri attribute end + def revoke_request! + FollowRequest.create!(account: account, target_account: target_account, show_reblogs: show_reblogs, uri: uri) + destroy! + end + before_validation :set_uri, only: :create after_destroy :remove_endorsements diff --git a/app/models/status.rb b/app/models/status.rb index 398462302..01615c876 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -15,8 +15,6 @@ # visibility :integer default("public"), not null # spoiler_text :text default(""), not null # reply :boolean default(FALSE), not null -# favourites_count :integer default(0), not null -# reblogs_count :integer default(0), not null # language :string # conversation_id :bigint(8) # local :boolean @@ -28,6 +26,8 @@ # class Status < ApplicationRecord + self.cache_versioning = false + include Paginable include Streamable include Cacheable @@ -62,6 +62,7 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity, inverse_of: :status + has_one :status_stat, inverse_of: :status validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } @@ -86,7 +87,25 @@ class Status < ApplicationRecord scope :not_local_only, -> { where(local_only: [false, nil]) } - cache_associated :account, :application, :media_attachments, :conversation, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, :conversation, mentions: :account], thread: :account + cache_associated :account, + :application, + :media_attachments, + :conversation, + :status_stat, + :tags, + :stream_entry, + mentions: :account, + reblog: [ + :account, + :application, + :stream_entry, + :tags, + :media_attachments, + :conversation, + :status_stat, + mentions: :account, + ], + thread: :account delegate :domain, to: :account, prefix: true @@ -180,6 +199,26 @@ class Status < ApplicationRecord @marked_for_mass_destruction end + def replies_count + status_stat&.replies_count || 0 + end + + def reblogs_count + status_stat&.reblogs_count || 0 + end + + def favourites_count + status_stat&.favourites_count || 0 + end + + def increment_count!(key) + update_status_stat!(key => public_send(key) + 1) + end + + def decrement_count!(key) + update_status_stat!(key => [public_send(key) - 1, 0].max) + end + after_create :increment_counter_caches after_destroy :decrement_counter_caches @@ -197,6 +236,10 @@ class Status < ApplicationRecord before_validation :set_local class << self + def cache_ids + left_outer_joins(:status_stat).select('statuses.id, greatest(statuses.updated_at, status_stats.updated_at) AS updated_at') + end + def in_chosen_languages(account) where(language: nil).or where(language: account.chosen_languages) end @@ -372,6 +415,11 @@ class Status < ApplicationRecord private + def update_status_stat!(attrs) + record = status_stat || build_status_stat + record.update(attrs) + end + def store_uri update_attribute(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil? end @@ -434,13 +482,8 @@ class Status < ApplicationRecord Account.where(id: account_id).update_all('statuses_count = COALESCE(statuses_count, 0) + 1') end - return unless reblog? - - if association(:reblog).loaded? - reblog.update_attribute(:reblogs_count, reblog.reblogs_count + 1) - else - Status.where(id: reblog_of_id).update_all('reblogs_count = COALESCE(reblogs_count, 0) + 1') - end + reblog.increment_count!(:reblogs_count) if reblog? + thread.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) end def decrement_counter_caches @@ -452,12 +495,7 @@ class Status < ApplicationRecord Account.where(id: account_id).update_all('statuses_count = GREATEST(COALESCE(statuses_count, 0) - 1, 0)') end - return unless reblog? - - if association(:reblog).loaded? - reblog.update_attribute(:reblogs_count, [reblog.reblogs_count - 1, 0].max) - else - Status.where(id: reblog_of_id).update_all('reblogs_count = GREATEST(COALESCE(reblogs_count, 0) - 1, 0)') - end + reblog.decrement_count!(:reblogs_count) if reblog? + thread.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) end end diff --git a/app/models/status_stat.rb b/app/models/status_stat.rb new file mode 100644 index 000000000..9d358776b --- /dev/null +++ b/app/models/status_stat.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: status_stats +# +# id :bigint(8) not null, primary key +# status_id :bigint(8) not null +# replies_count :bigint(8) default(0), not null +# reblogs_count :bigint(8) default(0), not null +# favourites_count :bigint(8) default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class StatusStat < ApplicationRecord + belongs_to :status, inverse_of: :status_stat +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index c8fffcf68..89776b1fc 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -3,7 +3,8 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility, :language, - :uri, :content, :url, :reblogs_count, :favourites_count + :uri, :content, :url, :replies_count, :reblogs_count, + :favourites_count attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index f47d488f1..1674239df 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -3,7 +3,7 @@ class AuthorizeFollowService < BaseService def call(source_account, target_account, **options) if options[:skip_follow_request] - follow_request = FollowRequest.new(account: source_account, target_account: target_account) + follow_request = FollowRequest.new(account: source_account, target_account: target_account, uri: options[:follow_request_uri]) else follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.authorize! diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index a068c1ed8..1db1917e2 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -2,11 +2,13 @@ class ResolveURLService < BaseService include JsonLdHelper + include Authorization attr_reader :url - def call(url) + def call(url, on_behalf_of: nil) @url = url + @on_behalf_of = on_behalf_of return process_local_url if local_url? @@ -84,6 +86,10 @@ class ResolveURLService < BaseService def check_local_status(status) return if status.nil? - status if status.public_visibility? || status.unlisted_visibility? + authorize_with @on_behalf_of, status, :show? + status + rescue Mastodon::NotPermittedError + # Do not disclose the existence of status the user is not authorized to see + nil end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 5bb395942..cc1fcb52f 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -53,7 +53,7 @@ class SearchService < BaseService end def url_resource - @_url_resource ||= ResolveURLService.new.call(query) + @_url_resource ||= ResolveURLService.new.call(query, on_behalf_of: @account) end def url_resource_symbol diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index d3b9893c4..caf03bd7c 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -14,17 +14,17 @@ .public-account-header__tabs__tabs .details-counters .counter{ class: active_nav_class(short_account_url(account)) } - = link_to short_account_url(account), class: 'u-url u-uid' do + = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do %span.counter-number= number_to_human account.statuses_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.posts') .counter{ class: active_nav_class(account_following_index_url(account)) } - = link_to account_following_index_url(account) do + = link_to account_following_index_url(account), title: number_with_delimiter(account.following_count) do %span.counter-number= number_to_human account.following_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.following') .counter{ class: active_nav_class(account_followers_url(account)) } - = link_to account_followers_url(account) do + = link_to account_followers_url(account), title: number_with_delimiter(account.followers_count) do %span.counter-number= number_to_human account.followers_count, strip_insignificant_zeros: true %span.counter-label= t('accounts.followers') .spacer diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 5e174f312..5b410ec84 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -3,11 +3,13 @@ = f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id .batch-table__row__content .status__content>< - - unless status.proper.spoiler_text.blank? - %p>< - %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)} - - = Formatter.instance.format(status.proper, custom_emojify: true) + - if status.proper.spoiler_text.blank? + = Formatter.instance.format(status.proper, custom_emojify: true) + - else + %details< + %summary>< + %strong> Content warning: #{Formatter.instance.format_spoiler(status.proper)} + = Formatter.instance.format(status.proper, custom_emojify: true) - unless status.proper.media_attachments.empty? - if status.proper.media_attachments.first.video? diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index 30cd26914..ef2d2b894 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -9,16 +9,24 @@ %td= number_to_human_size @export.total_storage %td %tr + %th= t('accounts.statuses') + %td= number_with_delimiter @export.total_statuses + %td + %tr %th= t('exports.follows') - %td= number_to_human @export.total_follows + %td= number_with_delimiter @export.total_follows %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %tr + %th= t('accounts.followers') + %td= number_with_delimiter @export.total_followers + %td + %tr %th= t('exports.blocks') - %td= number_to_human @export.total_blocks + %td= number_with_delimiter @export.total_blocks %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %tr %th= t('exports.mutes') - %td= number_to_human @export.total_mutes + %td= number_with_delimiter @export.total_mutes %td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv) %p.muted-hint= t('exports.archive_takeout.hint_html') diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 991dd4e94..2b43cb134 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -1,11 +1,14 @@ - content_for :page_title do = t('settings.import') -%p.hint= t('imports.preface') - = simple_form_for @import, url: settings_import_path do |f| - = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' - = f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data') + %p.hint= t('imports.preface') + + .field-group + = f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + + .field-group + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') .actions = f.button :button, t('imports.upload'), type: :submit diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 8532c9d9a..cd9bacf68 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -59,6 +59,8 @@ module Devise @@ldap_password = nil mattr_accessor :ldap_tls_no_verify @@ldap_tls_no_verify = false + mattr_accessor :ldap_search_filter + @@ldap_search_filter = nil class Strategies::PamAuthenticatable def valid? @@ -362,5 +364,6 @@ Devise.setup do |config| config.ldap_password = ENV.fetch('LDAP_PASSWORD') config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true' + config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}') end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4ed868c8b..ecabd9a36 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -184,6 +184,7 @@ en: unsuspend_account: "%{name} unsuspended %{target}'s account" update_custom_emoji: "%{name} updated emoji %{target}" update_status: "%{name} updated status by %{target}" + deleted_status: "(deleted status)" title: Audit log custom_emojis: by_domain: Domain @@ -401,6 +402,7 @@ en: media: title: Media no_media: No media + no_status_selected: No statuses were changed as none were selected title: Account statuses with_media: With media subscriptions: diff --git a/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb b/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb index 120f74402..3369e3b9e 100644 --- a/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb +++ b/db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb @@ -1,5 +1,3 @@ -require Rails.root.join('lib', 'mastodon', 'migration_helpers') - class ChangeAccountIdNonnullableInLists < ActiveRecord::Migration[5.1] def change change_column_null :lists, :account_id, false diff --git a/db/migrate/20180528141303_fix_accounts_unique_index.rb b/db/migrate/20180528141303_fix_accounts_unique_index.rb index 624ea229e..bd4e158b7 100644 --- a/db/migrate/20180528141303_fix_accounts_unique_index.rb +++ b/db/migrate/20180528141303_fix_accounts_unique_index.rb @@ -6,6 +6,10 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2] def local? domain.nil? end + + def acct + local? ? username : "#{username}@#{domain}" + end end disable_ddl_transaction! diff --git a/db/migrate/20180812162710_create_status_stats.rb b/db/migrate/20180812162710_create_status_stats.rb new file mode 100644 index 000000000..d4da36fe7 --- /dev/null +++ b/db/migrate/20180812162710_create_status_stats.rb @@ -0,0 +1,12 @@ +class CreateStatusStats < ActiveRecord::Migration[5.2] + def change + create_table :status_stats do |t| + t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } + t.bigint :replies_count, null: false, default: 0 + t.bigint :reblogs_count, null: false, default: 0 + t.bigint :favourites_count, null: false, default: 0 + + t.timestamps + end + end +end diff --git a/db/migrate/20180812173710_copy_status_stats.rb b/db/migrate/20180812173710_copy_status_stats.rb new file mode 100644 index 000000000..64a564ca0 --- /dev/null +++ b/db/migrate/20180812173710_copy_status_stats.rb @@ -0,0 +1,19 @@ +class CopyStatusStats < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + safety_assured do + execute <<-SQL.squish + INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at) + SELECT id, reblogs_count, favourites_count, created_at, updated_at + FROM statuses + ON CONFLICT (status_id) DO UPDATE + SET reblogs_count = EXCLUDED.reblogs_count, favourites_count = EXCLUDED.favourites_count + SQL + end + end + + def down + # Nothing + end +end diff --git a/db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb b/db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb new file mode 100644 index 000000000..7077a4e65 --- /dev/null +++ b/db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb @@ -0,0 +1,23 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration[5.2] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default( + :oauth_applications, + :confidential, + :boolean, + allow_null: false, + default: true # maintaining backwards compatibility: require secrets + ) + end + end + + def down + remove_column :oauth_applications, :confidential + end +end diff --git a/db/post_migrate/20180813113448_copy_status_stats_cleanup.rb b/db/post_migrate/20180813113448_copy_status_stats_cleanup.rb new file mode 100644 index 000000000..f3ae772c7 --- /dev/null +++ b/db/post_migrate/20180813113448_copy_status_stats_cleanup.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CopyStatusStatsCleanup < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + safety_assured do + remove_column :statuses, :reblogs_count, :integer, default: 0, null: false + remove_column :statuses, :favourites_count, :integer, default: 0, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c2a313ffa..bac85869a 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: 2018_08_13_160548) do +ActiveRecord::Schema.define(version: 2018_08_14_171349) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -359,6 +359,7 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do t.string "website" t.string "owner_type" t.bigint "owner_id" + t.boolean "confidential", default: true, null: false t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type" t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end @@ -466,6 +467,16 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true end + create_table "status_stats", force: :cascade do |t| + t.bigint "status_id", null: false + t.bigint "replies_count", default: 0, null: false + t.bigint "reblogs_count", default: 0, null: false + t.bigint "favourites_count", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status_id"], name: "index_status_stats_on_status_id", unique: true + end + create_table "statuses", id: :bigint, default: -> { "timestamp_id('statuses'::text)" }, force: :cascade do |t| t.string "uri" t.text "text", default: "", null: false @@ -478,8 +489,6 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do t.integer "visibility", default: 0, null: false t.text "spoiler_text", default: "", null: false t.boolean "reply", default: false, null: false - t.integer "favourites_count", default: 0, null: false - t.integer "reblogs_count", default: 0, null: false t.string "language" t.bigint "conversation_id" t.boolean "local" @@ -643,6 +652,7 @@ ActiveRecord::Schema.define(version: 2018_08_13_160548) do add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "statuses", on_delete: :cascade + add_foreign_key "status_stats", "statuses", on_delete: :cascade add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", name: "fk_c7fa917661", on_delete: :nullify add_foreign_key "statuses", "accounts", name: "fk_9bda1543f7", on_delete: :cascade add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify diff --git a/lib/devise/ldap_authenticatable.rb b/lib/devise/ldap_authenticatable.rb index ef786fbb7..534c7a851 100644 --- a/lib/devise/ldap_authenticatable.rb +++ b/lib/devise/ldap_authenticatable.rb @@ -24,7 +24,8 @@ module Devise connect_timeout: 10 ) - if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: "(#{Devise.ldap_uid}=#{email})", password: password)) + filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, email: email) + if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: password)) user = User.ldap_get_user(user_info.first) success!(user) else diff --git a/spec/fabricators/status_stat_fabricator.rb b/spec/fabricators/status_stat_fabricator.rb new file mode 100644 index 000000000..9c67fd404 --- /dev/null +++ b/spec/fabricators/status_stat_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:status_stat) do + status_id nil + replies_count "" + reblogs_count "" + favourites_count "" +end diff --git a/spec/lib/activitypub/activity/undo_spec.rb b/spec/lib/activitypub/activity/undo_spec.rb index e01c5e03e..9545e1f46 100644 --- a/spec/lib/activitypub/activity/undo_spec.rb +++ b/spec/lib/activitypub/activity/undo_spec.rb @@ -52,6 +52,32 @@ RSpec.describe ActivityPub::Activity::Undo do end end + context 'with Accept' do + let(:recipient) { Fabricate(:account) } + let(:object_json) do + { + id: 'bar', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'follow-to-revoke', + } + end + + before do + recipient.follow!(sender, uri: 'follow-to-revoke') + end + + it 'deletes follow from recipient to sender' do + subject.perform + expect(recipient.following?(sender)).to be false + end + + it 'creates a follow request from recipient to sender' do + subject.perform + expect(recipient.requested?(sender)).to be true + end + end + context 'with Block' do let(:recipient) { Fabricate(:account) } diff --git a/spec/models/export_spec.rb b/spec/models/export_spec.rb index 6daa46145..277dcc526 100644 --- a/spec/models/export_spec.rb +++ b/spec/models/export_spec.rb @@ -48,17 +48,17 @@ describe Export do describe 'total_follows' do it 'returns the total number of the followed accounts' do target_accounts.each(&account.method(:follow!)) - expect(Export.new(account).total_follows).to eq 2 + expect(Export.new(account.reload).total_follows).to eq 2 end it 'returns the total number of the blocked accounts' do target_accounts.each(&account.method(:block!)) - expect(Export.new(account).total_blocks).to eq 2 + expect(Export.new(account.reload).total_blocks).to eq 2 end it 'returns the total number of the muted accounts' do target_accounts.each(&account.method(:mute!)) - expect(Export.new(account).total_mutes).to eq 2 + expect(Export.new(account.reload).total_mutes).to eq 2 end end end diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb index 43175d852..f221973b6 100644 --- a/spec/models/follow_spec.rb +++ b/spec/models/follow_spec.rb @@ -37,4 +37,20 @@ RSpec.describe Follow, type: :model do expect(a[1]).to eq follow0 end end + + describe 'revoke_request!' do + let(:follow) { Fabricate(:follow, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + + it 'revokes the follow relation' do + follow.revoke_request! + expect(account.following?(target_account)).to be false + end + + it 'creates a follow request' do + follow.revoke_request! + expect(account.requested?(target_account)).to be true + end + end end diff --git a/spec/models/status_stat_spec.rb b/spec/models/status_stat_spec.rb new file mode 100644 index 000000000..5e9351aff --- /dev/null +++ b/spec/models/status_stat_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe StatusStat, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 673de5233..671080f1d 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -29,7 +29,7 @@ describe SearchService, type: :service do allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, 10) - expect(service).to have_received(:call).with(@query) + expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results end end @@ -41,7 +41,7 @@ describe SearchService, type: :service do allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, 10) - expect(service).to have_received(:call).with(@query) + expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(accounts: [account]) end end @@ -53,7 +53,7 @@ describe SearchService, type: :service do allow(ResolveURLService).to receive(:new).and_return(service) results = subject.call(@query, 10) - expect(service).to have_received(:call).with(@query) + expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(statuses: [status]) end end |