diff options
46 files changed, 324 insertions, 116 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index f9bd616e4..03c07c50b 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -29,7 +29,7 @@ class AccountsController < ApplicationController return end - @pinned_statuses = cache_collection(@account.pinned_statuses.not_local_only, Status) if show_pinned_statuses? + @pinned_statuses = cached_filtered_status_pins if show_pinned_statuses? @statuses = cached_filtered_status_page @rss_url = rss_url @@ -65,6 +65,10 @@ class AccountsController < ApplicationController [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end + def filtered_pinned_statuses + @account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted]) + end + def filtered_statuses default_statuses.tap do |statuses| statuses.merge!(hashtag_scope) if tag_requested? @@ -143,6 +147,13 @@ class AccountsController < ApplicationController request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end + def cached_filtered_status_pins + cache_collection( + filtered_pinned_statuses, + Status + ) + end + def cached_filtered_status_page cache_collection_paginated_by_id( filtered_statuses, diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 00f3d3cba..ac7ab8a0b 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -21,6 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController case params[:id] when 'featured' @items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) } + @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } when 'tags' @items = for_signed_account { @account.featured_tags } when 'devices' diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 92ccb8061..2c027ea76 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -46,9 +46,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def pinned_scope - return Status.none if @account.blocking?(current_account) - - @account.pinned_statuses + @account.pinned_statuses.permitted_for(@account, current_account) end def no_replies_scope diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index 63cc521ed..9b8f2fb05 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -94,7 +94,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController private def set_accounts - @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite, :ips]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end def set_account diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 2636b4718..ddc87adff 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -153,7 +153,7 @@ class Auth::SessionsController < Devise::SessionsController clear_attempt_from_session - user.update_sign_in!(request, new_sign_in: true) + user.update_sign_in!(new_sign_in: true) sign_in(user) flash.delete(:notice) diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb index efda37fae..45f3aab0d 100644 --- a/app/controllers/concerns/user_tracking_concern.rb +++ b/app/controllers/concerns/user_tracking_concern.rb @@ -3,7 +3,7 @@ module UserTrackingConcern extend ActiveSupport::Concern - UPDATE_SIGN_IN_HOURS = 24 + UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze included do before_action :update_user_sign_in @@ -12,10 +12,10 @@ module UserTrackingConcern private def update_user_sign_in - current_user.update_sign_in!(request) if user_needs_sign_in_update? + current_user.update_sign_in! if user_needs_sign_in_update? end def user_needs_sign_in_update? - user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago) + user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago) end end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index 32aaf9f5e..c21d41341 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -2,17 +2,17 @@ module Admin::DashboardHelper def relevant_account_ip(account, ip_query) - default_ip = [account.user_current_sign_in_ip || account.user_sign_up_ip] + ips = account.user.present? ? account.user.ips.to_a : [] matched_ip = begin ip_query_addr = IPAddr.new(ip_query) - account.user.recent_ips.find { |(_, ip)| ip_query_addr.include?(ip) } || default_ip + ips.find { |ip| ip_query_addr.include?(ip.ip) } || ips.first rescue IPAddr::Error - default_ip - end.last + ips.first + end if matched_ip - link_to matched_ip, admin_accounts_path(ip: matched_ip) + link_to matched_ip.ip, admin_accounts_path(ip: matched_ip.ip) else '-' end diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 9af8b6d20..261c72b2a 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -286,12 +286,15 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, f)); } else if (status === 202) { + let tryCount = 1; const poll = () => { api(getState).get(`/api/v1/media/${data.id}`).then(response => { if (response.status === 200) { dispatch(uploadComposeSuccess(response.data, f)); } else if (response.status === 206) { - setTimeout(() => poll(), 1000); + let retryAfter = (Math.log2(tryCount) || 1) * 1000; + tryCount += 1; + setTimeout(() => poll(), retryAfter); } }).catch(error => dispatch(uploadComposeFail(error))); }; diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index d63c6b142..650b33b62 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -196,6 +196,7 @@ class StatusActionBar extends ImmutablePureComponent { const anonymousAccess = !me; const mutingConversation = status.get('muted'); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const writtenByMe = status.getIn(['account', 'id']) === me; let menu = []; @@ -212,7 +213,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push(null); - if (writtenByMe && publicStatus) { + if (writtenByMe && pinnableStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push(null); } diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 6ed5f3865..eb4583026 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -146,6 +146,7 @@ class ActionBar extends React.PureComponent { const { status, intl } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -158,7 +159,7 @@ class ActionBar extends React.PureComponent { } if (writtenByMe) { - if (publicStatus) { + if (pinnableStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push(null); } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index afd42bdef..7c3bbcbd8 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -254,12 +254,15 @@ export function uploadCompose(files) { if (status === 200) { dispatch(uploadComposeSuccess(data, f)); } else if (status === 202) { + let tryCount = 1; const poll = () => { api(getState).get(`/api/v1/media/${data.id}`).then(response => { if (response.status === 200) { dispatch(uploadComposeSuccess(response.data, f)); } else if (response.status === 206) { - setTimeout(() => poll(), 1000); + let retryAfter = (Math.log2(tryCount) || 1) * 1000; + tryCount += 1; + setTimeout(() => poll(), retryAfter); } }).catch(error => dispatch(uploadComposeFail(error))); }; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 85c76edee..d125359e9 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -225,6 +225,7 @@ class StatusActionBar extends ImmutablePureComponent { const anonymousAccess = !me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -242,7 +243,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); - if (writtenByMe && publicStatus) { + if (writtenByMe && pinnableStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); } diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index ffa2510c0..e60119bc4 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -188,6 +188,7 @@ class ActionBar extends React.PureComponent { const { status, relationship, intl } = this.props; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -201,7 +202,7 @@ class ActionBar extends React.PureComponent { } if (writtenByMe) { - if (publicStatus) { + if (pinnableStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push(null); } diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 7010ff43e..5126e23c6 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -3,7 +3,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def perform return accept_follow_for_relay if relay_follow? - return follow_request_from_object.authorize! unless follow_request_from_object.nil? + return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? case @object['type'] when 'Follow' @@ -19,7 +19,16 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity return if target_account.nil? || !target_account.local? follow_request = FollowRequest.find_by(account: target_account, target_account: @account) - follow_request&.authorize! + accept_follow!(follow_request) + end + + def accept_follow!(request) + return if request.nil? + + is_first_follow = !request.target_account.followers.local.exists? + request.authorize! + + RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow end def accept_follow_for_relay diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb index 688ab00b3..845eeaef7 100644 --- a/app/lib/activitypub/activity/add.rb +++ b/app/lib/activitypub/activity/add.rb @@ -4,8 +4,7 @@ class ActivityPub::Activity::Add < ActivityPub::Activity def perform return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url - status = status_from_uri(object_uri) - status ||= fetch_remote_original_status + status = status_from_object return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) diff --git a/app/models/account.rb b/app/models/account.rb index 5476272f9..a044da8de 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -127,7 +127,6 @@ class Account < ApplicationRecord delegate :email, :unconfirmed_email, - :current_sign_in_ip, :current_sign_in_at, :created_at, :sign_up_ip, diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index defd531ac..dcb174122 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -21,7 +21,7 @@ class AccountFilter end def results - scope = Account.includes(:account_stat, user: [:session_activations, :invite_request]).without_instance_actor.reorder(nil) + scope = Account.includes(:account_stat, user: [:ips, :invite_request]).without_instance_actor.reorder(nil) params.each do |key, value| scope.merge!(scope_for(key, value.to_s.strip)) if value.present? diff --git a/app/models/user.rb b/app/models/user.rb index 7de579286..6673b3d2b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,8 +14,6 @@ # sign_in_count :integer default(0), not null # current_sign_in_at :datetime # last_sign_in_at :datetime -# current_sign_in_ip :inet -# last_sign_in_ip :inet # admin :boolean default(FALSE), not null # confirmation_token :string # confirmed_at :datetime @@ -81,6 +79,7 @@ class User < ApplicationRecord has_many :invites, inverse_of: :user has_many :markers, inverse_of: :user, dependent: :destroy has_many :webauthn_credentials, dependent: :destroy + has_many :ips, class_name: 'UserIp', inverse_of: :user has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text } @@ -107,7 +106,7 @@ class User < ApplicationRecord scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } - scope :matches_ip, ->(value) { where('current_sign_in_ip <<= ?', value).or(where('users.sign_up_ip <<= ?', value)).or(where('users.last_sign_in_ip <<= ?', value)).or(where(id: SessionActivation.select(:user_id).where('ip <<= ?', value))) } + scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value) } scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) } before_validation :sanitize_languages @@ -174,15 +173,11 @@ class User < ApplicationRecord prepare_new_user! if new_user && approved? end - def update_sign_in!(request, new_sign_in: false) + def update_sign_in!(new_sign_in: false) old_current, new_current = current_sign_in_at, Time.now.utc self.last_sign_in_at = old_current || new_current self.current_sign_in_at = new_current - old_current, new_current = current_sign_in_ip, request.remote_ip - self.last_sign_in_ip = old_current || new_current - self.current_sign_in_ip = new_current - if new_sign_in self.sign_in_count ||= 0 self.sign_in_count += 1 @@ -201,7 +196,7 @@ class User < ApplicationRecord end def suspicious_sign_in?(ip) - !otp_required_for_login? && !skip_sign_in_token? && current_sign_in_at.present? && !recent_ip?(ip) + !otp_required_for_login? && !skip_sign_in_token? && current_sign_in_at.present? && !ips.where(ip: ip).exists? end def functional? @@ -277,31 +272,28 @@ class User < ApplicationRecord @shows_application ||= settings.show_application end - # rubocop:disable Naming/MethodParameterName - def token_for_app(a) - return nil if a.nil? || a.owner != self - Doorkeeper::AccessToken.find_or_create_by(application_id: a.id, resource_owner_id: id) do |t| - t.scopes = a.scopes - t.expires_in = Doorkeeper.configuration.access_token_expires_in + def token_for_app(app) + return nil if app.nil? || app.owner != self + + Doorkeeper::AccessToken.find_or_create_by(application_id: app.id, resource_owner_id: id) do |t| + t.scopes = app.scopes + t.expires_in = Doorkeeper.configuration.access_token_expires_in t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled? end end - # rubocop:enable Naming/MethodParameterName def activate_session(request) - session_activations.activate(session_id: SecureRandom.hex, - user_agent: request.user_agent, - ip: request.remote_ip).session_id + session_activations.activate( + session_id: SecureRandom.hex, + user_agent: request.user_agent, + ip: request.remote_ip + ).session_id end def clear_other_sessions(id) session_activations.exclusive(id) end - def session_active?(id) - session_activations.active? id - end - def web_push_subscription(session) session.web_push_subscription.nil? ? nil : session.web_push_subscription end @@ -364,22 +356,6 @@ class User < ApplicationRecord setting_display_media == 'hide_all' end - def recent_ips - @recent_ips ||= begin - arr = [] - - session_activations.each do |session_activation| - arr << [session_activation.updated_at, session_activation.ip] - end - - arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present? - arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present? - arr << [created_at, sign_up_ip] if sign_up_ip.present? - - arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse! - end - end - def sign_in_token_expired? sign_in_token_sent_at.nil? || sign_in_token_sent_at < 5.minutes.ago end @@ -410,10 +386,6 @@ class User < ApplicationRecord private - def recent_ip?(ip) - recent_ips.any? { |(_, recent_ip)| recent_ip == ip } - end - def send_pending_devise_notifications pending_devise_notifications.each do |notification, args, kwargs| render_and_send_devise_message(notification, *args, **kwargs) diff --git a/app/models/user_ip.rb b/app/models/user_ip.rb new file mode 100644 index 000000000..a8e802e13 --- /dev/null +++ b/app/models/user_ip.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: user_ips +# +# user_id :bigint(8) primary key +# ip :inet +# used_at :datetime +# + +class UserIp < ApplicationRecord + self.primary_key = :user_id + + belongs_to :user, foreign_key: :user_id + + def readonly? + true + end +end diff --git a/app/serializers/rest/admin/account_serializer.rb b/app/serializers/rest/admin/account_serializer.rb index f579d3302..3480e8c5a 100644 --- a/app/serializers/rest/admin/account_serializer.rb +++ b/app/serializers/rest/admin/account_serializer.rb @@ -9,6 +9,7 @@ class REST::Admin::AccountSerializer < ActiveModel::Serializer attribute :created_by_application_id, if: :created_by_application? attribute :invited_by_account_id, if: :invited? + has_many :ips, serializer: REST::Admin::IpSerializer has_one :account, serializer: REST::AccountSerializer def id @@ -19,10 +20,6 @@ class REST::Admin::AccountSerializer < ActiveModel::Serializer object.user_email end - def ip - object.user_current_sign_in_ip.to_s.presence - end - def role object.user_role end @@ -74,4 +71,12 @@ class REST::Admin::AccountSerializer < ActiveModel::Serializer def created_by_application? object.user&.created_by_application_id&.present? end + + def ips + object.user&.ips + end + + def ip + ips&.first + end end diff --git a/app/serializers/rest/admin/ip_serializer.rb b/app/serializers/rest/admin/ip_serializer.rb new file mode 100644 index 000000000..d11699dc4 --- /dev/null +++ b/app/serializers/rest/admin/ip_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::Admin::IpSerializer < ActiveModel::Serializer + attributes :ip, :used_at +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index b5dcf6208..45ecd392b 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -124,7 +124,7 @@ class REST::StatusSerializer < ActiveModel::Serializer current_user? && current_user.account_id == object.account_id && !object.reblog? && - %w(public unlisted).include?(object.visibility) + %w(public unlisted private).include?(object.visibility) end def source_requested? diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 72352aca6..9fce478c1 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -23,7 +23,7 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService def process_items(items) status_ids = items.map { |item| value_or_id(item) } - .filter_map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) unless ActivityPub::TagManager.instance.local_uri?(uri) } + .filter_map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower) unless ActivityPub::TagManager.instance.local_uri?(uri) } .filter_map { |status| status.id if status.account_id == @account.id } to_remove = [] to_add = status_ids @@ -46,4 +46,8 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService def supported_context? super(@json) end + + def local_follower + @local_follower ||= account.followers.local.without_suspended.first + end end diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb index 16353066c..35a101f1d 100644 --- a/app/validators/status_pin_validator.rb +++ b/app/validators/status_pin_validator.rb @@ -6,7 +6,7 @@ class StatusPinValidator < ActiveModel::Validator def validate(pin) pin.errors.add(:base, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog? pin.errors.add(:base, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id - pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility) + pin.errors.add(:base, I18n.t('statuses.pin_errors.direct')) if pin.status.direct_visibility? pin.errors.add(:base, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count >= MAX_PINNED && pin.account.local? end end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 64cfc9a77..3867d1b19 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -156,12 +156,14 @@ %time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at %td - - @account.user.recent_ips.each_with_index do |(_, ip), i| + - recent_ips = @account.user.ips.order(used_at: :desc).to_a + + - recent_ips.each_with_index do |recent_ip, i| %tr - if i.zero? - %th{ rowspan: @account.user.recent_ips.size }= t('admin.accounts.most_recent_ip') - %td= ip - %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: ip) + %th{ rowspan: recent_ips.size }= t('admin.accounts.most_recent_ip') + %td= recent_ip.ip + %td= table_link_to 'search', t('admin.accounts.search_same_ip'), admin_accounts_path(ip: recent_ip.ip) %tr %th= t('admin.accounts.most_recent_activity') diff --git a/app/views/admin_mailer/new_pending_account.text.erb b/app/views/admin_mailer/new_pending_account.text.erb index bcc251819..a8a2a35fa 100644 --- a/app/views/admin_mailer/new_pending_account.text.erb +++ b/app/views/admin_mailer/new_pending_account.text.erb @@ -3,7 +3,7 @@ <%= raw t('admin_mailer.new_pending_account.body') %> <%= @account.user_email %> (@<%= @account.username %>) -<%= @account.user_current_sign_in_ip %> +<%= @account.user_sign_up_ip %> <% if @account.user&.invite_request&.text.present? %> <%= quote_wrap(@account.user&.invite_request&.text) %> diff --git a/app/workers/remote_account_refresh_worker.rb b/app/workers/remote_account_refresh_worker.rb new file mode 100644 index 000000000..9632936b5 --- /dev/null +++ b/app/workers/remote_account_refresh_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class RemoteAccountRefreshWorker + include Sidekiq::Worker + include ExponentialBackoff + include JsonLdHelper + + sidekiq_options queue: 'pull', retry: 3 + + def perform(id) + account = Account.find_by(id: id) + return if account.nil? || account.local? + + ActivityPub::FetchRemoteAccountService.new.call(account.uri) + rescue Mastodon::UnexpectedResponseError => e + response = e.response + + if response_error_unsalvageable?(response) + # Give up + else + raise e + end + end +end diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb index 918c10ac9..adc99c605 100644 --- a/app/workers/scheduler/ip_cleanup_scheduler.rb +++ b/app/workers/scheduler/ip_cleanup_scheduler.rb @@ -16,7 +16,7 @@ class Scheduler::IpCleanupScheduler def clean_ip_columns! SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all - User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_sign_in_ip: nil, current_sign_in_ip: nil, sign_up_ip: nil) + User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(sign_up_ip: nil) LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all end diff --git a/chart/Chart.yaml b/chart/Chart.yaml index a09ae8e8a..24676d749 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.1.1 +version: 1.2.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/chart/values.yaml b/chart/values.yaml index 4fc4084d4..bbe55410e 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -110,10 +110,8 @@ elasticsearch: # RAILS_ENV=production bundle exec rake chewy:sync # (https://docs.joinmastodon.org/admin/optional/elasticsearch/) enabled: true - # may be removed once https://github.com/tootsuite/mastodon/pull/13828 is part - # of a tagged release image: - tag: 6 + tag: 7 # https://github.com/bitnami/charts/tree/master/bitnami/postgresql#parameters postgresql: diff --git a/config/environments/production.rb b/config/environments/production.rb index 417d53a8f..b72d1b342 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -107,9 +107,9 @@ Rails.application.configure do :authentication => ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain, :ca_file => ENV['SMTP_CA_FILE'].presence || '/etc/ssl/certs/ca-certificates.crt', :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'], - :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true, - :tls => ENV['SMTP_TLS'].presence, - :ssl => ENV['SMTP_SSL'].presence, + :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false', + :tls => ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', + :ssl => ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', } config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5232e6cfd..b434c68fa 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,11 +1,8 @@ require 'devise/strategies/authenticatable' Warden::Manager.after_set_user except: :fetch do |user, warden| - if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']) - session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'] - else - session_id = user.activate_session(warden.request) - end + session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'] + session_id = user.activate_session(warden.request) unless user.session_activations.active?(session_id) warden.cookies.signed['_session_id'] = { value: session_id, @@ -17,9 +14,13 @@ Warden::Manager.after_set_user except: :fetch do |user, warden| end Warden::Manager.after_fetch do |user, warden| - if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']) + session_id = warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'] + + if session_id && (session = user.session_activations.find_by(session_id: session_id)) + session.update(ip: warden.request.remote_ip) if session.ip != warden.request.remote_ip + warden.cookies.signed['_session_id'] = { - value: warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'], + value: session_id, expires: 1.year.from_now, httponly: true, secure: (Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'), diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 2a6cca7dc..964d4266d 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -55,7 +55,7 @@ class Rack::Attack end throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req| - req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media') + req.authenticated_user_id if req.post? && req.path.match?('^/api/v\d+/media') end throttle('throttle_media_proxy', limit: 30, period: 10.minutes) do |req| diff --git a/config/locales/en.yml b/config/locales/en.yml index 32b48dbff..693a7b400 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1300,9 +1300,9 @@ en: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded pin_errors: + direct: Posts that are only visible to mentioned users cannot be pinned limit: You have already pinned the maximum number of posts ownership: Someone else's post cannot be pinned - private: Non-public posts cannot be pinned reblog: A boost cannot be pinned poll: total_people: diff --git a/db/migrate/20210616214526_create_user_ips.rb b/db/migrate/20210616214526_create_user_ips.rb new file mode 100644 index 000000000..68e81a9d8 --- /dev/null +++ b/db/migrate/20210616214526_create_user_ips.rb @@ -0,0 +1,5 @@ +class CreateUserIps < ActiveRecord::Migration[6.1] + def change + create_view :user_ips + end +end diff --git a/db/post_migrate/20210616214135_remove_current_sign_in_ip_from_users.rb b/db/post_migrate/20210616214135_remove_current_sign_in_ip_from_users.rb new file mode 100644 index 000000000..b53b247f2 --- /dev/null +++ b/db/post_migrate/20210616214135_remove_current_sign_in_ip_from_users.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class RemoveCurrentSignInIpFromUsers < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + safety_assured do + remove_column :users, :current_sign_in_ip, :inet + remove_column :users, :last_sign_in_ip, :inet + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d3337ac04..d357512b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -925,8 +925,6 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do t.integer "sign_in_count", default: 0, null: false t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.inet "current_sign_in_ip" - t.inet "last_sign_in_ip" t.boolean "admin", default: false, null: false t.string "confirmation_token" t.datetime "confirmed_at" @@ -1122,6 +1120,28 @@ ActiveRecord::Schema.define(version: 2021_12_13_040746) do SQL add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true + create_view "user_ips", sql_definition: <<-SQL + SELECT t0.user_id, + t0.ip, + max(t0.used_at) AS used_at + FROM ( SELECT users.id AS user_id, + users.sign_up_ip AS ip, + users.created_at AS used_at + FROM users + WHERE (users.sign_up_ip IS NOT NULL) + UNION ALL + SELECT session_activations.user_id, + session_activations.ip, + session_activations.updated_at + FROM session_activations + UNION ALL + SELECT login_activities.user_id, + login_activities.ip, + login_activities.created_at + FROM login_activities + WHERE (login_activities.success = true)) t0 + GROUP BY t0.user_id, t0.ip; + SQL create_view "account_summaries", materialized: true, sql_definition: <<-SQL SELECT accounts.id AS account_id, mode() WITHIN GROUP (ORDER BY t0.language) AS language, diff --git a/db/views/user_ips_v01.sql b/db/views/user_ips_v01.sql new file mode 100644 index 000000000..50a8201cd --- /dev/null +++ b/db/views/user_ips_v01.sql @@ -0,0 +1,26 @@ +SELECT + user_id, + ip, + max(used_at) AS used_at +FROM ( + SELECT + id AS user_id, + sign_up_ip AS ip, + created_at AS used_at + FROM users + WHERE sign_up_ip IS NOT NULL + UNION ALL + SELECT + user_id, + ip, + updated_at + FROM session_activations + UNION ALL + SELECT + user_id, + ip, + created_at + FROM login_activities + WHERE success = 't' +) AS t0 +GROUP BY user_id, ip diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index ac426b01e..7c5ba8754 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -35,6 +35,7 @@ RSpec.describe AccountsController, type: :controller do before do status_media.media_attachments << Fabricate(:media_attachment, account: account, type: :image) account.pinned_statuses << status_pinned + account.pinned_statuses << status_private end shared_examples 'preliminary checks' do diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb index d584136ff..21a033945 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/controllers/activitypub/collections_controller_spec.rb @@ -4,6 +4,7 @@ require 'rails_helper' RSpec.describe ActivityPub::CollectionsController, type: :controller do let!(:account) { Fabricate(:account) } + let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } shared_examples 'cachable response' do @@ -27,6 +28,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do Fabricate(:status_pin, account: account) Fabricate(:status_pin, account: account) + Fabricate(:status_pin, account: account, status: private_pinned) Fabricate(:status, account: account, visibility: :private) end @@ -50,7 +52,15 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do it 'returns orderedItems with pinned statuses' do expect(body[:orderedItems]).to be_an Array - expect(body[:orderedItems].size).to eq 2 + expect(body[:orderedItems].size).to eq 3 + end + + it 'includes URI of private pinned status' do + expect(body[:orderedItems]).to include(ActivityPub::TagManager.instance.uri_for(private_pinned)) + end + + it 'does not include contents of private pinned status' do + expect(response.body).not_to include(private_pinned.text) end context 'when account is permanently suspended' do @@ -96,7 +106,16 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do it 'returns orderedItems with pinned statuses' do json = body_as_json expect(json[:orderedItems]).to be_an Array - expect(json[:orderedItems].size).to eq 2 + expect(json[:orderedItems].size).to eq 3 + end + + it 'includes URI of private pinned status' do + json = body_as_json + expect(json[:orderedItems]).to include(ActivityPub::TagManager.instance.uri_for(private_pinned)) + end + + it 'does not include contents of private pinned status' do + expect(response.body).not_to include(private_pinned.text) end end diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb index 693cd1ac6..0a18ddcbd 100644 --- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb @@ -39,7 +39,7 @@ describe Api::V1::Accounts::StatusesController do end end - context 'with only pinned' do + context 'with only own pinned' do before do Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account)) end @@ -50,5 +50,38 @@ describe Api::V1::Accounts::StatusesController do expect(response).to have_http_status(200) end end + + context "with someone else's pinned statuses" do + let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com') } + let(:status) { Fabricate(:status, account: account) } + let(:private_status) { Fabricate(:status, account: account, visibility: :private) } + let!(:pin) { Fabricate(:status_pin, account: account, status: status) } + let!(:private_pin) { Fabricate(:status_pin, account: account, status: private_status) } + + it 'returns http success' do + get :index, params: { account_id: account.id, pinned: true } + expect(response).to have_http_status(200) + end + + context 'when user does not follow account' do + it 'lists the public status only' do + get :index, params: { account_id: account.id, pinned: true } + json = body_as_json + expect(json.map { |item| item[:id].to_i }).to eq [status.id] + end + end + + context 'when user follows account' do + before do + user.account.follow!(account) + end + + it 'lists both the public and the private statuses' do + get :index, params: { account_id: account.id, pinned: true } + json = body_as_json + expect(json.map { |item| item[:id].to_i }.sort).to eq [status.id, private_status.id].sort + end + end + end end end diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index f718f5dd9..2368cc2bf 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -400,7 +400,7 @@ RSpec.describe Auth::SessionsController, type: :controller do end context 'when 2FA is disabled and IP is unfamiliar' do - let!(:user) { Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', current_sign_in_at: 3.weeks.ago, current_sign_in_ip: '0.0.0.0') } + let!(:user) { Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', current_sign_in_at: 3.weeks.ago) } before do request.remote_ip = '10.10.10.10' diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index 883bab6ac..304cf2208 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -23,6 +23,7 @@ RSpec.describe ActivityPub::Activity::Accept do subject { described_class.new(json, sender) } before do + allow(RemoteAccountRefreshWorker).to receive(:perform_async) Fabricate(:follow_request, account: recipient, target_account: sender) subject.perform end @@ -34,6 +35,10 @@ RSpec.describe ActivityPub::Activity::Accept do it 'removes the follow request' do expect(recipient.requested?(sender)).to be false end + + it 'queues a refresh' do + expect(RemoteAccountRefreshWorker).to have_received(:perform_async).with(sender.id) + end end context 'given a relay' do diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb index 16db71c88..e6408b610 100644 --- a/spec/lib/activitypub/activity/add_spec.rb +++ b/spec/lib/activitypub/activity/add_spec.rb @@ -1,8 +1,8 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Add do - let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') } - let(:status) { Fabricate(:status, account: sender) } + let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured', domain: 'example.com') } + let(:status) { Fabricate(:status, account: sender, visibility: :private) } let(:json) do { @@ -24,6 +24,8 @@ RSpec.describe ActivityPub::Activity::Add do end context 'when status was not known before' do + let(:service_stub) { double } + let(:json) do { '@context': 'https://www.w3.org/ns/activitystreams', @@ -36,12 +38,40 @@ RSpec.describe ActivityPub::Activity::Add do end before do - stub_request(:get, 'https://example.com/unknown').to_return(status: 410) + allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service_stub) + end + + context 'when there is a local follower' do + before do + account = Fabricate(:account) + account.follow!(sender) + end + + it 'fetches the status and pins it' do + allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil| + expect(uri).to eq 'https://example.com/unknown' + expect(id).to eq true + expect(on_behalf_of&.following?(sender)).to eq true + status + end + subject.perform + expect(service_stub).to have_received(:call) + expect(sender.pinned?(status)).to be true + end end - it 'fetches the status' do - subject.perform - expect(a_request(:get, 'https://example.com/unknown')).to have_been_made.at_least_once + context 'when there is no local follower' do + it 'tries to fetch the status' do + allow(service_stub).to receive(:call) do |uri, id: true, on_behalf_of: nil| + expect(uri).to eq 'https://example.com/unknown' + expect(id).to eq true + expect(on_behalf_of).to eq nil + nil + end + subject.perform + expect(service_stub).to have_received(:call) + expect(sender.pinned?(status)).to be false + end end end end diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb index 6f0b2feb8..c18faca78 100644 --- a/spec/models/status_pin_spec.rb +++ b/spec/models/status_pin_spec.rb @@ -24,11 +24,11 @@ RSpec.describe StatusPin, type: :model do expect(StatusPin.new(account: account, status: reblog).save).to be false end - it 'does not allow pins of private statuses' do + it 'does allow pins of direct statuses' do account = Fabricate(:account) status = Fabricate(:status, account: account, visibility: :private) - expect(StatusPin.new(account: account, status: status).save).to be false + expect(StatusPin.new(account: account, status: status).save).to be true end it 'does not allow pins of direct statuses' do diff --git a/spec/validators/status_pin_validator_spec.rb b/spec/validators/status_pin_validator_spec.rb index 06532e5b3..d5bd0d1b8 100644 --- a/spec/validators/status_pin_validator_spec.rb +++ b/spec/validators/status_pin_validator_spec.rb @@ -9,7 +9,7 @@ RSpec.describe StatusPinValidator, type: :validator do end let(:pin) { double(account: account, errors: errors, status: status, account_id: pin_account_id) } - let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility) } + let(:status) { double(reblog?: reblog, account_id: status_account_id, visibility: visibility, direct_visibility?: visibility == 'direct') } let(:account) { double(status_pins: status_pins, local?: local) } let(:status_pins) { double(count: count) } let(:errors) { double(add: nil) } @@ -37,11 +37,11 @@ RSpec.describe StatusPinValidator, type: :validator do end end - context 'unless %w(public unlisted).include?(pin.status.visibility)' do - let(:visibility) { '' } + context 'if pin.status.direct_visibility?' do + let(:visibility) { 'direct' } it 'calls errors.add' do - expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.private')) + expect(errors).to have_received(:add).with(:base, I18n.t('statuses.pin_errors.direct')) end end |