{antenna.get('accounts_count')}
{antenna.get('domains_count')}
diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx
index 768acd983b..b46a8d11c9 100644
--- a/app/javascript/mastodon/features/list_timeline/index.jsx
+++ b/app/javascript/mastodon/features/list_timeline/index.jsx
@@ -146,7 +146,7 @@ class ListTimeline extends PureComponent {
handleEditAntennaClick = (e) => {
const id = e.currentTarget.getAttribute('data-id');
- window.open(`/antennas/${id}/edit`, '_blank');
+ this.context.router.history.push(`/antennasw/${id}/edit`);
}
handleRepliesPolicyChange = ({ target }) => {
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 39ae5956c4..fc9f28b425 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -48,6 +48,7 @@ import {
StatusReferences,
DirectTimeline,
HashtagTimeline,
+ AntennaTimeline,
Notifications,
FollowRequests,
FavouritedStatuses,
@@ -210,6 +211,7 @@ class SwitchingColumnsArea extends PureComponent {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 85e612a541..9fcfe73ed9 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -34,6 +34,10 @@ export function DirectTimeline() {
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
}
+export function AntennaTimeline () {
+ return import(/* webpackChunkName: "features/antenna_timeline" */'../../antenna_timeline');
+}
+
export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
}
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 7c06f3a6bc..7cf98f3e36 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -90,6 +90,14 @@ class FeedManager
true
end
+ def push_to_antenna(antenna, status, update: false)
+ return false unless add_to_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?)
+
+ trim(:antenna, antenna.id)
+ PushUpdateWorker.perform_async(antenna.account_id, status.id, "timeline:antenna:#{antenna.id}", { 'update' => update }) if push_update_required?("timeline:antenna:#{antenna.id}")
+ true
+ end
+
# Remove a status from a list feed and send a streaming API update
# @param [List] list
# @param [Status] status
@@ -102,6 +110,13 @@ class FeedManager
true
end
+ def unpush_from_antenna(antenna, status, update: false)
+ return false unless remove_from_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?)
+
+ redis.publish("timeline:antenna:#{antenna.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
+ true
+ end
+
# Fill a home feed with an account's statuses
# @param [Account] from_account
# @param [Account] into_account
diff --git a/app/models/antenna_feed.rb b/app/models/antenna_feed.rb
new file mode 100644
index 0000000000..6bef09a4d9
--- /dev/null
+++ b/app/models/antenna_feed.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AntennaFeed < Feed
+ def initialize(antenna)
+ super(:antenna, antenna.id)
+ end
+end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index a54d9e96ec..2d67276379 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -51,7 +51,7 @@ class FanOutOnWriteService < BaseService
when :public, :unlisted, :public_unlisted, :login, :private
deliver_to_all_followers!
deliver_to_lists!
- deliver_to_antennas! if [:public, :public_unlisted, :login].include?(@status.visibility.to_sym) && !@account.dissubscribable
+ deliver_to_antennas! unless @account.dissubscribable
deliver_to_stl_antennas!
when :limited
deliver_to_lists_mentioned_accounts_only!
@@ -159,9 +159,6 @@ class FanOutOnWriteService < BaseService
antennas = Antenna.availables
antennas = antennas.left_joins(:antenna_domains).where(any_domains: true).or(Antenna.left_joins(:antenna_domains).where(antenna_domains: { name: domain }))
- antennas = antennas.where(with_media_only: false) unless @status.with_media?
- antennas = antennas.where(ignore_reblog: false) unless @status.reblog?
- antennas = antennas.where(stl: false)
antennas = Antenna.where(id: antennas.select(:id))
antennas = antennas.left_joins(:antenna_accounts).where(any_accounts: true).or(Antenna.left_joins(:antenna_accounts).where(antenna_accounts: { account: @account }))
@@ -171,13 +168,17 @@ class FanOutOnWriteService < BaseService
antennas = antennas.left_joins(:antenna_tags).where(any_tags: true).or(Antenna.left_joins(:antenna_tags).where(antenna_tags: { tag_id: tag_ids }))
antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
+ antennas = antennas.where(account: @status.account.followers) if [:public, :public_unlisted, :login].exclude?(@status.visibility.to_sym)
+ antennas = antennas.where(with_media_only: false) unless @status.with_media?
+ antennas = antennas.where(ignore_reblog: false) unless @status.reblog?
+ antennas = antennas.where(stl: false)
collection = AntennaCollection.new(@status, @options[:update], false)
antennas.in_batches do |ans|
ans.each do |antenna|
next unless antenna.enabled?
- next if antenna.keywords.any? && antenna.keywords.none? { |keyword| @status.text.include?(keyword) }
+ next if antenna.keywords&.any? && antenna.keywords&.none? { |keyword| @status.text.include?(keyword) }
next if antenna.exclude_keywords&.any? { |keyword| @status.text.include?(keyword) }
next if antenna.exclude_accounts&.include?(@status.account_id)
next if antenna.exclude_domains&.include?(domain)
@@ -273,25 +274,25 @@ class FanOutOnWriteService < BaseService
def push(antenna)
if antenna.list_id.zero?
- @home_account_ids << antenna.account_id
- else
- @list_ids << antenna.list_id
+ @home_account_ids << { id: antenna.account_id, antenna_id: antenna.id } if @home_account_ids.none? { |id| id.id == antenna.account_id }
+ elsif @list_ids.none? { |id| id.id == antenna.list_id }
+ @list_ids << { id: antenna.list_id, antenna_id: antenna.id }
end
end
def deliver!
- lists = @list_ids.uniq
- homes = @home_account_ids.uniq
+ lists = @list_ids
+ homes = @home_account_ids
if lists.any?
FeedInsertWorker.push_bulk(lists) do |list|
- [@status.id, list, 'list', { 'update' => @update, 'stl_home' => @stl_home || false }]
+ [@status.id, list[:id], 'list', { 'update' => @update, 'stl_home' => @stl_home || false, 'antenna_id' => list[:antenna_id] }]
end
end
if homes.any?
FeedInsertWorker.push_bulk(homes) do |home|
- [@status.id, home, 'home', { 'update' => @update }]
+ [@status.id, home[:id], 'home', { 'update' => @update, 'antenna_id' => home[:antenna_id] }]
end
end
end
diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb
index e3e644c24f..c1b7edc90d 100644
--- a/app/workers/feed_insert_worker.rb
+++ b/app/workers/feed_insert_worker.rb
@@ -61,6 +61,11 @@ class FeedInsertWorker
when :list
FeedManager.instance.push_to_list(@list, @status, update: update?)
end
+
+ return if @options[:antenna_id].blank?
+
+ antenna = Antenna.find(@options[:antenna_id])
+ FeedManager.instance.push_to_antenna(antenna, @status, update: update?) if antenna.present?
end
def perform_unpush
@@ -70,6 +75,11 @@ class FeedInsertWorker
when :list
FeedManager.instance.unpush_from_list(@list, @status, update: true)
end
+
+ return if @options[:antenna_id].blank?
+
+ antenna = Antenna.find(@options[:antenna_id])
+ FeedManager.instance.unpush_from_antenna(antenna, @status, update: true) if antenna.present?
end
def perform_notify
diff --git a/config/routes.rb b/config/routes.rb
index 5042f7b165..0a801389e4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,6 +17,7 @@ Rails.application.routes.draw do
/conversations
/lists/(*any)
/antennasw/(*any)
+ /antennast/(*any)
/notifications
/favourites
/emoji_reactions
diff --git a/config/routes/api.rb b/config/routes/api.rb
index d959838326..30a86f56c6 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -49,6 +49,7 @@ namespace :api, format: false do
resource :public, only: :show, controller: :public
resources :tag, only: :show
resources :list, only: :show
+ resources :antenna, only: :show
end
get '/streaming', to: 'streaming#index'
diff --git a/streaming/index.js b/streaming/index.js
index cf08110803..7331e004ed 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -701,6 +701,33 @@ const startServer = async () => {
});
});
+ /**
+ * @param {string} antennaId
+ * @param {any} req
+ * @returns {Promise.}
+ */
+ const authorizeAntennaAccess = (antennaId, req) => new Promise((resolve, reject) => {
+ const { accountId } = req;
+
+ pgPool.connect((err, client, done) => {
+ if (err) {
+ reject();
+ return;
+ }
+
+ client.query('SELECT id, account_id FROM antennas WHERE id = $1 LIMIT 1', [antennaId], (err, result) => {
+ done();
+
+ if (err || result.rows.length === 0 || result.rows[0].account_id !== accountId) {
+ reject();
+ return;
+ }
+
+ resolve();
+ });
+ });
+ });
+
/**
* @param {string[]} ids
* @param {any} req
@@ -1214,6 +1241,17 @@ const startServer = async () => {
reject('Not authorized to stream this list');
});
+ break;
+ case 'antenna':
+ authorizeAntennaAccess(params.antenna, req).then(() => {
+ resolve({
+ channelIds: [`timeline:antenna:${params.antenna}`],
+ options: { needsFiltering: false },
+ });
+ }).catch(() => {
+ reject('Not authorized to stream this antenna');
+ });
+
break;
default:
reject('Unknown stream type');
@@ -1228,6 +1266,8 @@ const startServer = async () => {
const streamNameFromChannelName = (channelName, params) => {
if (channelName === 'list') {
return [channelName, params.list];
+ } else if (channelName === 'antenna') {
+ return [channelName, params.antenna];
} else if (['hashtag', 'hashtag:local'].includes(channelName)) {
return [channelName, params.tag];
} else {