mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-04 03:25:14 +00:00
Merge tag 'v4.5.6'
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
CodeQL / Analyze-1 (push) Waiting to run
CodeQL / Analyze-2 (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (push) Blocked by required conditions
Ruby Testing / ImageMagick tests-1 (push) Blocked by required conditions
Ruby Testing / ImageMagick tests-2 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / End to End testing-1 (push) Blocked by required conditions
Ruby Testing / End to End testing-2 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-1 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-2 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-3 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-4 (push) Blocked by required conditions
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
CodeQL / Analyze-1 (push) Waiting to run
CodeQL / Analyze-2 (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (push) Blocked by required conditions
Ruby Testing / ImageMagick tests-1 (push) Blocked by required conditions
Ruby Testing / ImageMagick tests-2 (push) Blocked by required conditions
Ruby Testing / End to End testing (push) Blocked by required conditions
Ruby Testing / End to End testing-1 (push) Blocked by required conditions
Ruby Testing / End to End testing-2 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-1 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-2 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-3 (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing-4 (push) Blocked by required conditions
This commit is contained in:
commit
9261623563
17 changed files with 109 additions and 35 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
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.5.6] - 2026-02-03
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix ActivityPub collection caching logic for pinned posts and featured tags not checking blocked accounts ([GHSA-ccpr-m53r-mfwr](https://github.com/mastodon/mastodon/security/advisories/GHSA-ccpr-m53r-mfwr))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Shorten caching of quote posts pending approval (#37570 and #37592 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix relationship cache not being cleared when handling account migrations (#37664 by @ClearlyClaire)
|
||||||
|
- Fix quote cancel button not appearing after edit then delete-and-redraft (#37066 by @PGrayCS)
|
||||||
|
- Fix followers with profile subscription (bell icon) being notified of post edits (#37646 by @ClearlyClaire)
|
||||||
|
- Fix error when encountering invalid tag in updated object (#37635 by @ClearlyClaire)
|
||||||
|
- Fix cross-server conversation tracking (#37559 by @ClearlyClaire)
|
||||||
|
- Fix recycled connections not being immediately closed (#37335 and #37674 by @ClearlyClaire and @shleeable)
|
||||||
|
|
||||||
## [4.5.5] - 2026-01-20
|
## [4.5.5] - 2026-01-20
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,31 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||||
|
|
||||||
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||||
|
before_action :check_authorization
|
||||||
before_action :set_items
|
before_action :set_items
|
||||||
before_action :set_size
|
before_action :set_size
|
||||||
before_action :set_type
|
before_action :set_type
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 3.minutes, public: public_fetch_mode?
|
expires_in 3.minutes, public: public_fetch_mode?
|
||||||
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
|
||||||
|
if @unauthorized
|
||||||
|
render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||||
|
else
|
||||||
|
render_with_cache json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
# Because in public fetch mode we cache the response, there would be no
|
||||||
|
# benefit from performing the check below, since a blocked account or domain
|
||||||
|
# would likely be served the cache from the reverse proxy anyway
|
||||||
|
|
||||||
|
@unauthorized = authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||||
|
end
|
||||||
|
|
||||||
def set_items
|
def set_items
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
|
|
@ -57,11 +71,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_signed_account
|
def for_signed_account
|
||||||
# Because in public fetch mode we cache the response, there would be no
|
if @unauthorized
|
||||||
# benefit from performing the check below, since a blocked account or domain
|
|
||||||
# would likely be served the cache from the reverse proxy anyway
|
|
||||||
|
|
||||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
yield
|
yield
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class StatusesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode?
|
expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode?
|
||||||
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
|
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ class Status extends ImmutablePureComponent {
|
||||||
'hidden',
|
'hidden',
|
||||||
'unread',
|
'unread',
|
||||||
'pictureInPicture',
|
'pictureInPicture',
|
||||||
|
'onQuoteCancel',
|
||||||
];
|
];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
def conversation_from_uri(uri)
|
def conversation_from_uri(uri)
|
||||||
return nil if uri.nil?
|
return nil if uri.nil?
|
||||||
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
|
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
|
||||||
|
return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
Conversation.find_or_create_by!(uri: uri)
|
Conversation.find_or_create_by!(uri: uri)
|
||||||
|
|
|
||||||
|
|
@ -241,12 +241,6 @@ class ActivityPub::TagManager
|
||||||
!host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host))
|
!host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host))
|
||||||
end
|
end
|
||||||
|
|
||||||
def uri_to_local_id(uri, param = :id)
|
|
||||||
path_params = Rails.application.routes.recognize_path(uri)
|
|
||||||
path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors'
|
|
||||||
path_params[param]
|
|
||||||
end
|
|
||||||
|
|
||||||
def uris_to_local_accounts(uris)
|
def uris_to_local_accounts(uris)
|
||||||
usernames = []
|
usernames = []
|
||||||
ids = []
|
ids = []
|
||||||
|
|
@ -264,6 +258,14 @@ class ActivityPub::TagManager
|
||||||
uri_to_resource(uri, Account)
|
uri_to_resource(uri, Account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def uri_to_local_conversation(uri)
|
||||||
|
path_params = Rails.application.routes.recognize_path(uri)
|
||||||
|
return unless path_params[:controller] == 'activitypub/contexts'
|
||||||
|
|
||||||
|
account_id, conversation_id = path_params[:id].split('-')
|
||||||
|
Conversation.find_by(parent_account_id: account_id, id: conversation_id)
|
||||||
|
end
|
||||||
|
|
||||||
def uri_to_resource(uri, klass)
|
def uri_to_resource(uri, klass)
|
||||||
return if uri.nil?
|
return if uri.nil?
|
||||||
|
|
||||||
|
|
@ -271,6 +273,8 @@ class ActivityPub::TagManager
|
||||||
case klass.name
|
case klass.name
|
||||||
when 'Account'
|
when 'Account'
|
||||||
uris_to_local_accounts([uri]).first
|
uris_to_local_accounts([uri]).first
|
||||||
|
when 'Conversation'
|
||||||
|
uri_to_local_conversation(uri)
|
||||||
else
|
else
|
||||||
StatusFinder.new(uri).status
|
StatusFinder.new(uri).status
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ class ConnectionPool::SharedTimedStack
|
||||||
if @created == @max && !@queue.empty?
|
if @created == @max && !@queue.empty?
|
||||||
throw_away_connection = @queue.pop
|
throw_away_connection = @queue.pop
|
||||||
@tagged_queue[throw_away_connection.site].delete(throw_away_connection)
|
@tagged_queue[throw_away_connection.site].delete(throw_away_connection)
|
||||||
|
throw_away_connection.close
|
||||||
@create_block.call(preferred_tag)
|
@create_block.call(preferred_tag)
|
||||||
elsif @created != @max
|
elsif @created != @max
|
||||||
connection = @create_block.call(preferred_tag)
|
connection = @create_block.call(preferred_tag)
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,12 @@ class OStatus::TagManager
|
||||||
def unique_tag_to_local_id(tag, expected_type)
|
def unique_tag_to_local_id(tag, expected_type)
|
||||||
return nil unless local_id?(tag)
|
return nil unless local_id?(tag)
|
||||||
|
|
||||||
if ActivityPub::TagManager.instance.local_uri?(tag)
|
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
|
||||||
ActivityPub::TagManager.instance.uri_to_local_id(tag)
|
matches[1] unless matches.nil?
|
||||||
else
|
|
||||||
matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag)
|
|
||||||
matches[1] unless matches.nil?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_id?(id)
|
def local_id?(id)
|
||||||
id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id)
|
id.start_with?("tag:#{Rails.configuration.x.local_domain}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def uri_for(target)
|
def uri_for(target)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class Quote < ApplicationRecord
|
||||||
|
|
||||||
def accept!
|
def accept!
|
||||||
update!(state: :accepted)
|
update!(state: :accepted)
|
||||||
|
|
||||||
|
reset_parent_cache! if attribute_previously_changed?(:state)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject!
|
def reject!
|
||||||
|
|
@ -75,6 +77,15 @@ class Quote < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def reset_parent_cache!
|
||||||
|
return if status_id.nil?
|
||||||
|
|
||||||
|
Rails.cache.delete("v3:statuses/#{status_id}")
|
||||||
|
|
||||||
|
# This clears the web cache for the ActivityPub representation
|
||||||
|
Rails.cache.delete("statuses/show:v3:statuses/#{status_id}")
|
||||||
|
end
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
self.account = status.account
|
self.account = status.account
|
||||||
self.quoted_account = quoted_status&.account
|
self.quoted_account = quoted_status&.account
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,11 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||||
|
|
||||||
def update_tags!
|
def update_tags!
|
||||||
previous_tags = @status.tags.to_a
|
previous_tags = @status.tags.to_a
|
||||||
current_tags = @status.tags = Tag.find_or_create_by_names(@raw_tags)
|
current_tags = @status.tags = @raw_tags.flat_map do |tag|
|
||||||
|
Tag.find_or_create_by_names([tag]).filter(&:valid?)
|
||||||
|
rescue ActiveRecord::RecordInvalid
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
return unless @status.distributable?
|
return unless @status.distributable?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ class FeedInsertWorker
|
||||||
|
|
||||||
def notify?(filter_result)
|
def notify?(filter_result)
|
||||||
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) ||
|
return false if @type != :home || @status.reblog? || (@status.reply? && @status.in_reply_to_account_id != @status.account_id) ||
|
||||||
filter_result == :filter
|
update? || filter_result == :filter
|
||||||
|
|
||||||
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
|
Follow.find_by(account: @follower, target_account: @status.account)&.notify?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,16 @@ class MoveWorker
|
||||||
.in_batches do |follows|
|
.in_batches do |follows|
|
||||||
ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
|
ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
|
||||||
num_moved += follows.update_all(target_account_id: @target_account.id)
|
num_moved += follows.update_all(target_account_id: @target_account.id)
|
||||||
|
|
||||||
|
# Clear any relationship cache, since callbacks are not called
|
||||||
|
Rails.cache.delete_multi(follows.flat_map do |follow|
|
||||||
|
[
|
||||||
|
['relationship', follow.account_id, follow.target_account_id],
|
||||||
|
['relationship', follow.target_account_id, follow.account_id],
|
||||||
|
['relationship', follow.account_id, @target_account.id],
|
||||||
|
['relationship', @target_account.id, follow.account_id],
|
||||||
|
]
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
num_moved
|
num_moved
|
||||||
|
|
|
||||||
|
|
@ -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.5.5
|
image: ghcr.io/mastodon/mastodon:v4.5.6
|
||||||
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.5.5
|
image: ghcr.io/mastodon/mastodon-streaming:v4.5.6
|
||||||
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.5.5
|
image: ghcr.io/mastodon/mastodon:v4.5.6
|
||||||
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
|
||||||
5
|
6
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
|
|
|
||||||
|
|
@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a reply' do
|
context 'with a reply without explicitly setting a conversation' do
|
||||||
let(:original_status) { Fabricate(:status) }
|
let(:original_status) { Fabricate(:status) }
|
||||||
|
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
|
|
@ -493,6 +493,30 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a reply explicitly setting a conversation' do
|
||||||
|
let(:original_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
let(:object_json) do
|
||||||
|
build_object(
|
||||||
|
inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
|
||||||
|
conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation),
|
||||||
|
context: ActivityPub::TagManager.instance.uri_for(original_status.conversation)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status' do
|
||||||
|
expect { subject.perform }.to change(sender.statuses, :count).by(1)
|
||||||
|
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.thread).to eq original_status
|
||||||
|
expect(status.reply?).to be true
|
||||||
|
expect(status.in_reply_to_account).to eq original_status.account
|
||||||
|
expect(status.conversation).to eq original_status.conversation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with mentions' do
|
context 'with mentions' do
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -612,14 +612,6 @@ RSpec.describe ActivityPub::TagManager do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#uri_to_local_id' do
|
|
||||||
let(:account) { Fabricate(:account, id_scheme: :username_ap_id) }
|
|
||||||
|
|
||||||
it 'returns the local ID' do
|
|
||||||
expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#uris_to_local_accounts' do
|
describe '#uris_to_local_accounts' do
|
||||||
it 'returns the expected local accounts' do
|
it 'returns the expected local accounts' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,7 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||||
tag: [
|
tag: [
|
||||||
{ type: 'Hashtag', name: 'foo' },
|
{ type: 'Hashtag', name: 'foo' },
|
||||||
{ type: 'Hashtag', name: 'bar' },
|
{ type: 'Hashtag', name: 'bar' },
|
||||||
|
{ type: 'Hashtag', name: '#2024' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue