Compare commits

...

1 commit

Author SHA1 Message Date
Essem
55b25a777b
Initial bubble timeline support 2024-01-14 01:22:06 -06:00
16 changed files with 275 additions and 34 deletions

View file

@ -3,7 +3,7 @@
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
before_action :require_user!, only: [:show], if: :require_auth? before_action :require_user!, only: [:show], if: :require_auth?
PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze PERMITTED_PARAMS = %i(local remote bubble limit only_media allow_local_only).freeze
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@ -38,6 +38,7 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
PublicFeed.new( PublicFeed.new(
current_account, current_account,
local: truthy_param?(:local), local: truthy_param?(:local),
bubble: truthy_param?(:bubble),
remote: truthy_param?(:remote), remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media), only_media: truthy_param?(:only_media),
allow_local_only: truthy_param?(:allow_local_only), allow_local_only: truthy_param?(:allow_local_only),

View file

@ -22,6 +22,7 @@ import {
fillPublicTimelineGaps, fillPublicTimelineGaps,
fillCommunityTimelineGaps, fillCommunityTimelineGaps,
fillListTimelineGaps, fillListTimelineGaps,
fillBubbleTimelineGaps,
} from './timelines'; } from './timelines';
/** /**
@ -150,6 +151,14 @@ export const connectUserStream = () =>
export const connectCommunityStream = ({ onlyMedia } = {}) => export const connectCommunityStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
/**
* @param {Object} options
* @param {boolean} [options.onlyMedia]
* @returns {function(): void}
*/
export const connectBubbleStream = ({ onlyMedia } = {}) =>
connectTimelineStream(`bubble${onlyMedia ? ':media' : ''}`, `public:bubble${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillBubbleTimelineGaps({ onlyMedia })) });
/** /**
* @param {Object} options * @param {Object} options
* @param {boolean} [options.onlyMedia] * @param {boolean} [options.onlyMedia]

View file

@ -156,6 +156,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandBubbleTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
@ -174,6 +175,7 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done); export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done); export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done); export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
export const fillBubbleTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, only_media: !!onlyMedia }, done);
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done); export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
export function expandTimelineRequest(timeline, isLoadingMore) { export function expandTimelineRequest(timeline, isLoadingMore) {

View file

@ -8,8 +8,8 @@ import { NavLink } from 'react-router-dom';
import { addColumn } from 'flavours/glitch/actions/columns'; import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings'; import { changeSetting } from 'flavours/glitch/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming'; import { connectPublicStream, connectCommunityStream, connectBubbleStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { expandPublicTimeline, expandCommunityTimeline, expandBubbleTimeline } from 'flavours/glitch/actions/timelines';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import SettingText from 'flavours/glitch/components/setting_text'; import SettingText from 'flavours/glitch/components/setting_text';
import initialState, { domain } from 'flavours/glitch/initial_state'; import initialState, { domain } from 'flavours/glitch/initial_state';
@ -91,6 +91,9 @@ const Firehose = ({ feedType, multiColumn }) => {
case 'public': case 'public':
dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } })); dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } }));
break; break;
case 'bubble':
dispatch(addColumn('BUBBLE', { other: { onlyMedia }, regex: { body: regex } }));
break;
case 'public:remote': case 'public:remote':
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } })); dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } }));
break; break;
@ -105,6 +108,9 @@ const Firehose = ({ feedType, multiColumn }) => {
case 'community': case 'community':
dispatch(expandCommunityTimeline({ maxId, onlyMedia })); dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
break; break;
case 'bubble':
dispatch(expandBubbleTimeline({ maxId, onlyMedia }));
break;
case 'public': case 'public':
dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly })); dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
break; break;
@ -128,6 +134,12 @@ const Firehose = ({ feedType, multiColumn }) => {
disconnect = dispatch(connectCommunityStream({ onlyMedia })); disconnect = dispatch(connectCommunityStream({ onlyMedia }));
} }
break; break;
case 'bubble':
dispatch(expandBubbleTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectBubbleStream({ onlyMedia }));
}
break;
case 'public': case 'public':
dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly })); dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
if (signedIn) { if (signedIn) {
@ -145,35 +157,58 @@ const Firehose = ({ feedType, multiColumn }) => {
return () => disconnect?.(); return () => disconnect?.();
}, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]); }, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);
const prependBanner = feedType === 'community' ? ( let prependBanner;
<DismissableBanner id='community_timeline'> let emptyMessage;
<FormattedMessage
id='dismissable_banner.community_timeline'
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
values={{ domain }}
/>
</DismissableBanner>
) : (
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
values={{ domain }}
/>
</DismissableBanner>
);
const emptyMessage = feedType === 'community' ? ( if (feedType === 'community') {
<FormattedMessage prependBanner = (
id='empty_column.community' <DismissableBanner id='community_timeline'>
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' <FormattedMessage
/> id='dismissable_banner.community_timeline'
) : ( defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
<FormattedMessage values={{ domain }}
id='empty_column.public' />
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' </DismissableBanner>
/> );
); emptyMessage = (
<FormattedMessage
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
);
} else if (feedType === 'bubble') {
prependBanner = (
<DismissableBanner id='bubble_timeline'>
<FormattedMessage
id='dismissable_banner.bubble_timeline'
defaultMessage='These are the most recent public posts from people on the social web whose accounts are on other servers selected by {domain}.'
values={{ domain }}
/>
</DismissableBanner>
);
emptyMessage = (
<FormattedMessage
id='empty_column.bubble'
defaultMessage='The bubble timeline is currently empty, but something might show up here soon!'
/>
);
} else {
prependBanner = (
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
values={{ domain }}
/>
</DismissableBanner>
);
emptyMessage = (
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
);
}
return ( return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}> <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
@ -193,6 +228,10 @@ const Firehose = ({ feedType, multiColumn }) => {
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' /> <FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
</NavLink> </NavLink>
<NavLink exact to='/public/bubble'>
<FormattedMessage tagName='div' id='firehose.bubble' defaultMessage='Bubble servers' />
</NavLink>
<NavLink exact to='/public/remote'> <NavLink exact to='/public/remote'>
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' /> <FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
</NavLink> </NavLink>

View file

@ -208,6 +208,7 @@ class SwitchingColumnsArea extends PureComponent {
<Redirect from='/timelines/public/local' to='/public/local' exact /> <Redirect from='/timelines/public/local' to='/public/local' exact />
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} /> <WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} /> <WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
<WrappedRoute path='/public/bubble' exact component={Firehose} componentParams={{ feedType: 'bubble' }} content={children} />
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} /> <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} /> <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} /> <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />

View file

@ -43,8 +43,11 @@
"confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}",
"content-type.change": "Content type", "content-type.change": "Content type",
"direct.group_by_conversations": "Group by conversation", "direct.group_by_conversations": "Group by conversation",
"dismissable_banner.bubble_timeline": "These are the most recent public posts from people on the social web whose accounts are on other servers selected by {domain}.",
"empty_column.bubble": "The bubble timeline is currently empty, but something might show up here soon!",
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
"favourite_modal.combo": "You can press {combo} to skip this next time", "favourite_modal.combo": "You can press {combo} to skip this next time",
"firehose.bubble": "Bubble servers",
"firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"", "firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"",
"home.column_settings.advanced": "Advanced", "home.column_settings.advanced": "Advanced",
"home.column_settings.filter_regex": "Filter out by regular expressions", "home.column_settings.filter_regex": "Filter out by regular expressions",

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: bubble_domains
#
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class BubbleDomain < ApplicationRecord
include Paginable
include DomainNormalizable
include DomainMaterializable
validates :domain, presence: true, uniqueness: true, domain: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
def to_log_human_identifier
domain
end
class << self
def in_bubble?(domain)
!rule_for(domain).nil?
end
def bubble_domains
select(:domain)
end
def rule_for(domain)
return if domain.blank?
uri = Addressable::URI.new.tap { |u| u.host = domain.delete('/') }
find_by(domain: uri.normalized_host)
end
end
end

View file

@ -6,6 +6,7 @@ class PublicFeed
# @option [Boolean] :with_replies # @option [Boolean] :with_replies
# @option [Boolean] :with_reblogs # @option [Boolean] :with_reblogs
# @option [Boolean] :local # @option [Boolean] :local
# @option [Boolean] :bubble
# @option [Boolean] :remote # @option [Boolean] :remote
# @option [Boolean] :only_media # @option [Boolean] :only_media
# @option [Boolean] :allow_local_only # @option [Boolean] :allow_local_only
@ -26,6 +27,7 @@ class PublicFeed
scope.merge!(without_replies_scope) unless with_replies? scope.merge!(without_replies_scope) unless with_replies?
scope.merge!(without_reblogs_scope) unless with_reblogs? scope.merge!(without_reblogs_scope) unless with_reblogs?
scope.merge!(local_only_scope) if local_only? scope.merge!(local_only_scope) if local_only?
scope.merge!(bubble_only_scope) if bubble_only?
scope.merge!(remote_only_scope) if remote_only? scope.merge!(remote_only_scope) if remote_only?
scope.merge!(account_filters_scope) if account? scope.merge!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only? scope.merge!(media_only_scope) if media_only?
@ -51,11 +53,15 @@ class PublicFeed
end end
def local_only? def local_only?
options[:local] && !options[:remote] options[:local] && !options[:remote] && !options[:bubble]
end
def bubble_only?
options[:bubble] && !options[:local] && !options[:remote]
end end
def remote_only? def remote_only?
options[:remote] && !options[:local] options[:remote] && !options[:local] && !options[:bubble]
end end
def account? def account?
@ -78,6 +84,10 @@ class PublicFeed
Status.local Status.local
end end
def bubble_only_scope
Status.bubble
end
def remote_only_scope def remote_only_scope
Status.remote Status.remote
end end

View file

@ -127,6 +127,8 @@ class Status < ApplicationRecord
scope :not_local_only, -> { where(local_only: [false, nil]) } scope :not_local_only, -> { where(local_only: [false, nil]) }
scope :bubble, -> { left_outer_joins(:account).where(accounts: { domain: BubbleDomain.bubble_domains }) }
after_create_commit :trigger_create_webhooks after_create_commit :trigger_create_webhooks
after_update_commit :trigger_update_webhooks after_update_commit :trigger_update_webhooks

View file

@ -30,6 +30,7 @@ class TagFeed < PublicFeed
scope.merge!(tagged_with_all_scope) scope.merge!(tagged_with_all_scope)
scope.merge!(tagged_with_none_scope) scope.merge!(tagged_with_none_scope)
scope.merge!(local_only_scope) if local_only? scope.merge!(local_only_scope) if local_only?
scope.merge!(bubble_only_scope) if bubble_only?
scope.merge!(remote_only_scope) if remote_only? scope.merge!(remote_only_scope) if remote_only?
scope.merge!(account_filters_scope) if account? scope.merge!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only? scope.merge!(media_only_scope) if media_only?

View file

@ -25,6 +25,7 @@ Rails.application.routes.draw do
/home /home
/public /public
/public/local /public/local
/public/bubble
/public/remote /public/remote
/conversations /conversations
/lists/(*any) /lists/(*any)

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class CreateBubbleDomains < ActiveRecord::Migration[7.1]
def change
create_table :bubble_domains do |t|
t.string :domain, default: '', null: false
t.timestamps
end
add_index :bubble_domains, :domain, name: :index_bubble_domains_on_domain, unique: true
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do ActiveRecord::Schema[7.1].define(version: 2024_01_14_042123) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -296,6 +296,13 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
t.index ["status_id"], name: "index_bookmarks_on_status_id" t.index ["status_id"], name: "index_bookmarks_on_status_id"
end end
create_table "bubble_domains", force: :cascade do |t|
t.string "domain", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["domain"], name: "index_bubble_domains_on_domain", unique: true
end
create_table "bulk_import_rows", force: :cascade do |t| create_table "bulk_import_rows", force: :cascade do |t|
t.bigint "bulk_import_id", null: false t.bigint "bulk_import_id", null: false
t.jsonb "data" t.jsonb "data"

View file

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'concurrent'
require_relative 'base'
module Mastodon::CLI
class BubbleDomains < Base
desc 'list', 'List domains in the bubble'
def list
BubbleDomain.find_each do |entry|
say(entry.domain.to_s, :white)
end
end
desc 'add [DOMAIN...]', 'Add domains to the bubble'
def add(*domains)
if domains.empty?
say('No domain(s) given', :red)
exit(1)
end
domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
skipped = 0
processed = 0
domains.each do |domain|
if BubbleDomain.exists?(domain: domain)
say("#{domain} is already in the bubble.", :yellow)
skipped += 1
next
end
bubble_domain = BubbleDomain.new(domain: domain)
bubble_domain.save!
processed += 1
end
say("Added #{processed}, skipped #{skipped}", color(processed, 0))
end
desc 'remove DOMAIN...', 'Remove domain from the bubble'
def remove(*domains)
if domains.empty?
say('No domain(s) given', :red)
exit(1)
end
skipped = 0
processed = 0
failed = 0
domains.each do |domain|
entry = BubbleDomain.find_by(domain: domain)
if entry.nil?
say("#{domain} is not in the bubble.", :yellow)
skipped += 1
next
end
result = entry.destroy
if result
processed += 1
else
say("#{domain} could not be removed.", :red)
failed += 1
end
end
say("Removed #{processed}, skipped #{skipped}, failed #{failed}", color(processed, failed))
end
private
def color(processed, failed)
if !processed.zero? && failed.zero?
:green
elsif failed.zero?
:yellow
else
:red
end
end
end
end

View file

@ -3,6 +3,7 @@
require_relative 'base' require_relative 'base'
require_relative 'accounts' require_relative 'accounts'
require_relative 'bubble_domains'
require_relative 'cache' require_relative 'cache'
require_relative 'canonical_email_blocks' require_relative 'canonical_email_blocks'
require_relative 'domains' require_relative 'domains'
@ -63,6 +64,9 @@ module Mastodon::CLI
desc 'canonical_email_blocks SUBCOMMAND ...ARGS', 'Manage canonical e-mail blocks' desc 'canonical_email_blocks SUBCOMMAND ...ARGS', 'Manage canonical e-mail blocks'
subcommand 'canonical_email_blocks', CanonicalEmailBlocks subcommand 'canonical_email_blocks', CanonicalEmailBlocks
desc 'bubble_domains SUBCOMMAND ...ARGS', 'Manage bubble domains'
subcommand 'bubble_domains', BubbleDomains
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities' desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
subcommand 'maintenance', Maintenance subcommand 'maintenance', Maintenance

View file

@ -164,6 +164,8 @@ const PUBLIC_CHANNELS = [
'public:media', 'public:media',
'public:local', 'public:local',
'public:local:media', 'public:local:media',
'public:bubble',
'public:bubble:media',
'public:remote', 'public:remote',
'public:remote:media', 'public:remote:media',
'hashtag', 'hashtag',
@ -438,6 +440,8 @@ const startServer = async () => {
return onlyMedia ? 'public:media' : 'public'; return onlyMedia ? 'public:media' : 'public';
case '/api/v1/streaming/public/local': case '/api/v1/streaming/public/local':
return onlyMedia ? 'public:local:media' : 'public:local'; return onlyMedia ? 'public:local:media' : 'public:local';
case '/api/v1/streaming/public/bubble':
return onlyMedia ? 'public:bubble:media' : 'public:bubble';
case '/api/v1/streaming/public/remote': case '/api/v1/streaming/public/remote':
return onlyMedia ? 'public:remote:media' : 'public:remote'; return onlyMedia ? 'public:remote:media' : 'public:remote';
case '/api/v1/streaming/hashtag': case '/api/v1/streaming/hashtag':
@ -1096,6 +1100,13 @@ const startServer = async () => {
options: { needsFiltering: true, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break;
case 'public:bubble':
resolve({
channelIds: ['timeline:public:bubble'],
options: { needsFiltering: true, allowLocalOnly: false },
});
break; break;
case 'public:remote': case 'public:remote':
resolve({ resolve({
@ -1124,6 +1135,13 @@ const startServer = async () => {
options: { needsFiltering: true, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break;
case 'public:bubble:media':
resolve({
channelIds: ['timeline:public:bubble:media'],
options: { needsFiltering: true, allowLocalOnly: false },
});
break; break;
case 'public:remote:media': case 'public:remote:media':
resolve({ resolve({