Compare commits
	
		
			10 Commits
		
	
	
		
			c966d75600
			...
			82b9d4d535
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					82b9d4d535 | ||
| 
						 | 
					dc6d8f8825 | ||
| 
						 | 
					dd0647ca45 | ||
| 
						 | 
					70e2eb49df | ||
| 
						 | 
					bef28b2e51 | ||
| 
						 | 
					0b66bd591f | ||
| 
						 | 
					a94d7bf520 | ||
| 
						 | 
					c8551a3eca | ||
| 
						 | 
					06c2393805 | ||
| 
						 | 
					4e85b9073b | 
							
								
								
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,26 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
All notable changes to this project will be documented in this file.
 | 
					All notable changes to this project will be documented in this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [4.4.5] - 2025-09-23
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Update dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Add support for `has:quote` in search (#36217 by @ClearlyClaire)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Change quoted posts from silenced accounts to use a click-through rather than being hidden (#36166 and #36167 by @ClearlyClaire)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fix processing of out-of-order `Update` as implicit updates (#36190 by @ClearlyClaire)
 | 
				
			||||||
 | 
					- Fix getting `Create` and `Update` out of order (#36176 by @ClearlyClaire)
 | 
				
			||||||
 | 
					- Fix quotes with Content Warnings but no text being shown without Content Warnings (#36150 by @ClearlyClaire)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [4.4.4] - 2025-09-16
 | 
					## [4.4.4] - 2025-09-16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Security
 | 
					### Security
 | 
				
			||||||
@@ -18,7 +38,7 @@ All notable changes to this project will be documented in this file.
 | 
				
			|||||||
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
 | 
					- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
 | 
				
			||||||
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
 | 
					- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
 | 
				
			||||||
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
 | 
					- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
 | 
				
			||||||
- Fix HttpLog not being enabled with `RAILS_LOGÈ_LEVEL=debug` (#35833 by @mjankowski)
 | 
					- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
 | 
				
			||||||
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
 | 
					- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
 | 
				
			||||||
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
 | 
					- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
 | 
				
			||||||
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
 | 
					- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -725,7 +725,7 @@ GEM
 | 
				
			|||||||
    responders (3.1.1)
 | 
					    responders (3.1.1)
 | 
				
			||||||
      actionpack (>= 5.2)
 | 
					      actionpack (>= 5.2)
 | 
				
			||||||
      railties (>= 5.2)
 | 
					      railties (>= 5.2)
 | 
				
			||||||
    rexml (3.4.1)
 | 
					    rexml (3.4.4)
 | 
				
			||||||
    rotp (6.3.0)
 | 
					    rotp (6.3.0)
 | 
				
			||||||
    rouge (4.5.2)
 | 
					    rouge (4.5.2)
 | 
				
			||||||
    rpam2 (4.0.2)
 | 
					    rpam2 (4.0.2)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -81,7 +81,7 @@ export function normalizeStatus(status, normalOldStatus) {
 | 
				
			|||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // If the status has a CW but no contents, treat the CW as if it were the
 | 
					    // If the status has a CW but no contents, treat the CW as if it were the
 | 
				
			||||||
    // status' contents, to avoid having a CW toggle with seemingly no effect.
 | 
					    // status' contents, to avoid having a CW toggle with seemingly no effect.
 | 
				
			||||||
    if (normalStatus.spoiler_text && !normalStatus.content) {
 | 
					    if (normalStatus.spoiler_text && !normalStatus.content && !normalStatus.quote) {
 | 
				
			||||||
      normalStatus.content = normalStatus.spoiler_text;
 | 
					      normalStatus.content = normalStatus.spoiler_text;
 | 
				
			||||||
      normalStatus.spoiler_text = '';
 | 
					      normalStatus.spoiler_text = '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useEffect, useMemo } from 'react';
 | 
					import { useCallback, useEffect, useMemo } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,13 +11,16 @@ import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
 | 
				
			|||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 | 
					import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 | 
				
			||||||
import { Icon } from 'mastodon/components/icon';
 | 
					import { Icon } from 'mastodon/components/icon';
 | 
				
			||||||
import StatusContainer from 'mastodon/containers/status_container';
 | 
					import StatusContainer from 'mastodon/containers/status_container';
 | 
				
			||||||
 | 
					import { domain } from 'mastodon/initial_state';
 | 
				
			||||||
import type { Status } from 'mastodon/models/status';
 | 
					import type { Status } from 'mastodon/models/status';
 | 
				
			||||||
import type { RootState } from 'mastodon/store';
 | 
					import type { RootState } from 'mastodon/store';
 | 
				
			||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
					import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import QuoteIcon from '../../images/quote.svg?react';
 | 
					import QuoteIcon from '../../images/quote.svg?react';
 | 
				
			||||||
 | 
					import { revealAccount } from '../actions/accounts_typed';
 | 
				
			||||||
import { fetchStatus } from '../actions/statuses';
 | 
					import { fetchStatus } from '../actions/statuses';
 | 
				
			||||||
import { makeGetStatus } from '../selectors';
 | 
					import { makeGetStatus } from '../selectors';
 | 
				
			||||||
 | 
					import { getAccountHidden } from '../selectors/accounts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
 | 
					const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -73,6 +76,29 @@ type GetStatusSelector = (
 | 
				
			|||||||
  props: { id?: string | null; contextType?: string },
 | 
					  props: { id?: string | null; contextType?: string },
 | 
				
			||||||
) => Status | null;
 | 
					) => Status | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					  const reveal = useCallback(() => {
 | 
				
			||||||
 | 
					    dispatch(revealAccount({ id: accountId }));
 | 
				
			||||||
 | 
					  }, [dispatch, accountId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <FormattedMessage
 | 
				
			||||||
 | 
					        id='status.quote_error.limited_account_hint.title'
 | 
				
			||||||
 | 
					        defaultMessage='This account has been hidden by the moderators of {domain}.'
 | 
				
			||||||
 | 
					        values={{ domain }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <button onClick={reveal} className='link-button'>
 | 
				
			||||||
 | 
					        <FormattedMessage
 | 
				
			||||||
 | 
					          id='status.quote_error.limited_account_hint.action'
 | 
				
			||||||
 | 
					          defaultMessage='Show anyway'
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const QuotedStatus: React.FC<{
 | 
					export const QuotedStatus: React.FC<{
 | 
				
			||||||
  quote: QuoteMap;
 | 
					  quote: QuoteMap;
 | 
				
			||||||
  contextType?: string;
 | 
					  contextType?: string;
 | 
				
			||||||
@@ -100,6 +126,14 @@ export const QuotedStatus: React.FC<{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
 | 
					  const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const accountId: string | null = status?.get('account', null) as
 | 
				
			||||||
 | 
					    | string
 | 
				
			||||||
 | 
					    | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hiddenAccount = useAppSelector(
 | 
				
			||||||
 | 
					    (state) => accountId && getAccountHidden(state, accountId),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (shouldLoadQuote && quotedStatusId) {
 | 
					    if (shouldLoadQuote && quotedStatusId) {
 | 
				
			||||||
      dispatch(
 | 
					      dispatch(
 | 
				
			||||||
@@ -164,6 +198,8 @@ export const QuotedStatus: React.FC<{
 | 
				
			|||||||
        defaultMessage='This post cannot be displayed.'
 | 
					        defaultMessage='This post cannot be displayed.'
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (hiddenAccount && accountId) {
 | 
				
			||||||
 | 
					    quoteError = <LimitedAccountHint accountId={accountId} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (quoteError) {
 | 
					  if (quoteError) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -873,6 +873,8 @@
 | 
				
			|||||||
  "status.open": "Expand this post",
 | 
					  "status.open": "Expand this post",
 | 
				
			||||||
  "status.pin": "Pin on profile",
 | 
					  "status.pin": "Pin on profile",
 | 
				
			||||||
  "status.quote_error.filtered": "Hidden due to one of your filters",
 | 
					  "status.quote_error.filtered": "Hidden due to one of your filters",
 | 
				
			||||||
 | 
					  "status.quote_error.limited_account_hint.action": "Show anyway",
 | 
				
			||||||
 | 
					  "status.quote_error.limited_account_hint.title": "This account has been hidden by the moderators of {domain}.",
 | 
				
			||||||
  "status.quote_error.not_found": "This post cannot be displayed.",
 | 
					  "status.quote_error.not_found": "This post cannot be displayed.",
 | 
				
			||||||
  "status.quote_error.pending_approval": "This post is pending approval from the original author.",
 | 
					  "status.quote_error.pending_approval": "This post is pending approval from the original author.",
 | 
				
			||||||
  "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
 | 
					  "status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,9 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @status = Status.find_by(uri: object_uri, account_id: @account.id)
 | 
					    @status = Status.find_by(uri: object_uri, account_id: @account.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # We may be getting `Create` and `Update` out of order
 | 
				
			||||||
 | 
					    @status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return if @status.nil?
 | 
					    return if @status.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
 | 
					    ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -85,7 +85,7 @@ class StatusCacheHydrator
 | 
				
			|||||||
        if quote.quoted_status.nil?
 | 
					        if quote.quoted_status.nil?
 | 
				
			||||||
          payload[nested ? :quoted_status_id : :quoted_status] = nil
 | 
					          payload[nested ? :quoted_status_id : :quoted_status] = nil
 | 
				
			||||||
          payload[:state] = 'deleted'
 | 
					          payload[:state] = 'deleted'
 | 
				
			||||||
        elsif StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filtered?
 | 
					        elsif StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filtered_for_quote?
 | 
				
			||||||
          payload[nested ? :quoted_status_id : :quoted_status] = nil
 | 
					          payload[nested ? :quoted_status_id : :quoted_status] = nil
 | 
				
			||||||
          payload[:state] = 'unauthorized'
 | 
					          payload[:state] = 'unauthorized'
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,12 @@ class StatusFilter
 | 
				
			|||||||
    blocked_by_policy? || (account_present? && filtered_status?) || silenced_account?
 | 
					    blocked_by_policy? || (account_present? && filtered_status?) || silenced_account?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def filtered_for_quote?
 | 
				
			||||||
 | 
					    return false if !account.nil? && account.id == status.account_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    blocked_by_policy? || (account_present? && filtered_status?)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def account_present?
 | 
					  def account_present?
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,6 +43,7 @@ module Status::SearchConcern
 | 
				
			|||||||
      properties << 'embed' if preview_card&.video?
 | 
					      properties << 'embed' if preview_card&.video?
 | 
				
			||||||
      properties << 'sensitive' if sensitive?
 | 
					      properties << 'sensitive' if sensitive?
 | 
				
			||||||
      properties << 'reply' if reply?
 | 
					      properties << 'reply' if reply?
 | 
				
			||||||
 | 
					      properties << 'quote' if with_quote?
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,13 +8,13 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # Extra states when a status is unavailable
 | 
					    # Extra states when a status is unavailable
 | 
				
			||||||
    return 'deleted' if object.quoted_status.nil?
 | 
					    return 'deleted' if object.quoted_status.nil?
 | 
				
			||||||
    return 'unauthorized' if status_filter.filtered?
 | 
					    return 'unauthorized' if status_filter.filtered_for_quote?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    object.state
 | 
					    object.state
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def quoted_status
 | 
					  def quoted_status
 | 
				
			||||||
    object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered?
 | 
					    object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  attributes :domain, :title, :version, :source_url, :description,
 | 
					  attributes :domain, :title, :version, :source_url, :description,
 | 
				
			||||||
             :usage, :thumbnail, :icon, :languages, :configuration,
 | 
					             :usage, :thumbnail, :icon, :languages, :configuration,
 | 
				
			||||||
             :registrations, :api_versions
 | 
					             :registrations, :api_versions, :max_toot_chars
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :contact, serializer: ContactSerializer
 | 
					  has_one :contact, serializer: ContactSerializer
 | 
				
			||||||
  has_many :rules, serializer: REST::RuleSerializer
 | 
					  has_many :rules, serializer: REST::RuleSerializer
 | 
				
			||||||
@@ -119,6 +119,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 | 
				
			|||||||
    Mastodon::Version.api_versions
 | 
					    Mastodon::Version.api_versions
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def max_toot_chars
 | 
				
			||||||
 | 
					    StatusLengthValidator::MAX_CHARS
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def registrations_enabled?
 | 
					  def registrations_enabled?
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,9 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if @status_parser.edited_at.present? && (@status.edited_at.nil? || @status_parser.edited_at > @status.edited_at)
 | 
					    if @status_parser.edited_at.present? && (@status.edited_at.nil? || @status_parser.edited_at > @status.edited_at)
 | 
				
			||||||
      handle_explicit_update!
 | 
					      handle_explicit_update!
 | 
				
			||||||
 | 
					    elsif @status.edited_at.present? && (@status_parser.edited_at.nil? || @status_parser.edited_at < @status.edited_at)
 | 
				
			||||||
 | 
					      # This is an older update, reject it
 | 
				
			||||||
 | 
					      return @status
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      handle_implicit_update!
 | 
					      handle_implicit_update!
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StatusLengthValidator < ActiveModel::Validator
 | 
					class StatusLengthValidator < ActiveModel::Validator
 | 
				
			||||||
  MAX_CHARS = 500
 | 
					  MAX_CHARS = 1024
 | 
				
			||||||
  URL_PLACEHOLDER_CHARS = 23
 | 
					  URL_PLACEHOLDER_CHARS = 23
 | 
				
			||||||
  URL_PLACEHOLDER = 'x' * 23
 | 
					  URL_PLACEHOLDER = 'x' * 23
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ services:
 | 
				
			|||||||
  web:
 | 
					  web:
 | 
				
			||||||
    # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
 | 
					    # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
 | 
				
			||||||
    # build: .
 | 
					    # build: .
 | 
				
			||||||
    image: ghcr.io/mastodon/mastodon:v4.4.4
 | 
					    image: ghcr.io/mastodon/mastodon:v4.4.5
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    env_file: .env.production
 | 
					    env_file: .env.production
 | 
				
			||||||
    command: bundle exec puma -C config/puma.rb
 | 
					    command: bundle exec puma -C config/puma.rb
 | 
				
			||||||
@@ -83,7 +83,7 @@ services:
 | 
				
			|||||||
    # build:
 | 
					    # build:
 | 
				
			||||||
    #   dockerfile: ./streaming/Dockerfile
 | 
					    #   dockerfile: ./streaming/Dockerfile
 | 
				
			||||||
    #   context: .
 | 
					    #   context: .
 | 
				
			||||||
    image: ghcr.io/mastodon/mastodon-streaming:v4.4.4
 | 
					    image: ghcr.io/mastodon/mastodon-streaming:v4.4.5
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    env_file: .env.production
 | 
					    env_file: .env.production
 | 
				
			||||||
    command: node ./streaming/index.js
 | 
					    command: node ./streaming/index.js
 | 
				
			||||||
@@ -102,7 +102,7 @@ services:
 | 
				
			|||||||
  sidekiq:
 | 
					  sidekiq:
 | 
				
			||||||
    # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
 | 
					    # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
 | 
				
			||||||
    # build: .
 | 
					    # build: .
 | 
				
			||||||
    image: ghcr.io/mastodon/mastodon:v4.4.4
 | 
					    image: ghcr.io/mastodon/mastodon:v4.4.5
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
    env_file: .env.production
 | 
					    env_file: .env.production
 | 
				
			||||||
    command: bundle exec sidekiq
 | 
					    command: bundle exec sidekiq
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ module Mastodon
 | 
				
			|||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def patch
 | 
					    def patch
 | 
				
			||||||
      4
 | 
					      5
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def default_prerelease
 | 
					    def default_prerelease
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										111
									
								
								spec/lib/activitypub/activity_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								spec/lib/activitypub/activity_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe ActivityPub::Activity do
 | 
				
			||||||
 | 
					  describe 'processing a Create and an Update' do
 | 
				
			||||||
 | 
					    let(:sender) { Fabricate(:account, followers_url: 'http://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor') }
 | 
				
			||||||
 | 
					    let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
 | 
				
			||||||
 | 
					    let(:quoted_status) { Fabricate(:status, account: quoted_account) }
 | 
				
			||||||
 | 
					    let(:approval_uri) { 'https://quoted.example.com/approvals/1' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:approval_payload) do
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        '@context': [
 | 
				
			||||||
 | 
					          'https://www.w3.org/ns/activitystreams',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization',
 | 
				
			||||||
 | 
					            gts: 'https://gotosocial.org/ns#',
 | 
				
			||||||
 | 
					            interactingObject: {
 | 
				
			||||||
 | 
					              '@id': 'gts:interactingObject',
 | 
				
			||||||
 | 
					              '@type': '@id',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            interactionTarget: {
 | 
				
			||||||
 | 
					              '@id': 'gts:interactionTarget',
 | 
				
			||||||
 | 
					              '@type': '@id',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        type: 'QuoteAuthorization',
 | 
				
			||||||
 | 
					        id: approval_uri,
 | 
				
			||||||
 | 
					        attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
 | 
				
			||||||
 | 
					        interactingObject: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
				
			||||||
 | 
					        interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:create_json) do
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        '@context': [
 | 
				
			||||||
 | 
					          'https://www.w3.org/ns/activitystreams',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            quote: 'https://w3id.org/fep/044f#quote',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        id: [ActivityPub::TagManager.instance.uri_for(sender), '#create'].join,
 | 
				
			||||||
 | 
					        type: 'Create',
 | 
				
			||||||
 | 
					        actor: ActivityPub::TagManager.instance.uri_for(sender),
 | 
				
			||||||
 | 
					        object: {
 | 
				
			||||||
 | 
					          id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
				
			||||||
 | 
					          type: 'Note',
 | 
				
			||||||
 | 
					          to: [
 | 
				
			||||||
 | 
					            'https://www.w3.org/ns/activitystreams#Public',
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          content: 'foo',
 | 
				
			||||||
 | 
					          published: '2025-05-24T11:03:10Z',
 | 
				
			||||||
 | 
					          quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }.deep_stringify_keys
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let(:update_json) do
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        '@context': [
 | 
				
			||||||
 | 
					          'https://www.w3.org/ns/activitystreams',
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            quote: 'https://w3id.org/fep/044f#quote',
 | 
				
			||||||
 | 
					            quoteAuthorization: { '@id': 'https://w3id.org/fep/044f#quoteAuthorization', '@type': '@id' },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        id: [ActivityPub::TagManager.instance.uri_for(sender), '#update'].join,
 | 
				
			||||||
 | 
					        type: 'Update',
 | 
				
			||||||
 | 
					        actor: ActivityPub::TagManager.instance.uri_for(sender),
 | 
				
			||||||
 | 
					        object: {
 | 
				
			||||||
 | 
					          id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
 | 
				
			||||||
 | 
					          type: 'Note',
 | 
				
			||||||
 | 
					          to: [
 | 
				
			||||||
 | 
					            'https://www.w3.org/ns/activitystreams#Public',
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          content: 'foo',
 | 
				
			||||||
 | 
					          published: '2025-05-24T11:03:10Z',
 | 
				
			||||||
 | 
					          quote: ActivityPub::TagManager.instance.uri_for(quoted_status),
 | 
				
			||||||
 | 
					          quoteAuthorization: approval_uri,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }.deep_stringify_keys
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    before do
 | 
				
			||||||
 | 
					      sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump(approval_payload))
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when getting them in order' do
 | 
				
			||||||
 | 
					      it 'creates a status and approves the quote' do
 | 
				
			||||||
 | 
					        described_class.factory(create_json, sender).perform
 | 
				
			||||||
 | 
					        status = described_class.factory(update_json, sender).perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(status.quote.state).to eq 'accepted'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when getting them out of order' do
 | 
				
			||||||
 | 
					      it 'creates a status and approves the quote' do
 | 
				
			||||||
 | 
					        described_class.factory(update_json, sender).perform
 | 
				
			||||||
 | 
					        status = described_class.factory(create_json, sender).perform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(status.quote.state).to eq 'accepted'
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Reference in New Issue
	
	Block a user