Merge pull request #642 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						32075fe27f
					
				
					 41 changed files with 350 additions and 88 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -28,6 +28,10 @@ module Admin | |||
|       @form         = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button)) | ||||
|       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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										17
									
								
								app/models/status_stat.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/models/status_stat.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|  | @ -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? | ||||
|  |  | |||
|  | @ -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! | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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? | ||||
|  |  | |||
|  | @ -8,17 +8,25 @@ | |||
|         %th= t('exports.storage') | ||||
|         %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') | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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! | ||||
|  |  | |||
							
								
								
									
										12
									
								
								db/migrate/20180812162710_create_status_stats.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20180812162710_create_status_stats.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
							
								
								
									
										19
									
								
								db/migrate/20180812173710_copy_status_stats.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								db/migrate/20180812173710_copy_status_stats.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
|  | @ -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 | ||||
							
								
								
									
										12
									
								
								db/post_migrate/20180813113448_copy_status_stats_cleanup.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/post_migrate/20180813113448_copy_status_stats_cleanup.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
							
								
								
									
										16
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										6
									
								
								spec/fabricators/status_stat_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/fabricators/status_stat_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| Fabricator(:status_stat) do | ||||
|   status_id        nil | ||||
|   replies_count    "" | ||||
|   reblogs_count    "" | ||||
|   favourites_count "" | ||||
| end | ||||
|  | @ -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) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										5
									
								
								spec/models/status_stat_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/status_stat_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe StatusStat, type: :model do | ||||
|   pending "add some examples to (or delete) #{__FILE__}" | ||||
| end | ||||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue