From 29fd0cde7afd262543fb6c2424b81be280f100af Mon Sep 17 00:00:00 2001 From: noellabo <noel.yoshiba@gmail.com> Date: Thu, 14 Nov 2019 07:42:56 +0900 Subject: [PATCH] Add a remote timeline --- .../api/v1/timelines/public_controller.rb | 8 +- app/javascript/mastodon/actions/streaming.js | 1 + app/javascript/mastodon/actions/timelines.js | 1 + .../mastodon/components/status_action_bar.js | 9 ++ .../containers/column_settings_container.js | 28 ++++ .../features/domain_timeline/index.js | 138 ++++++++++++++++++ .../features/status/components/action_bar.js | 9 ++ .../features/ui/components/columns_area.js | 2 + app/javascript/mastodon/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + app/javascript/mastodon/locales/en.json | 1 + app/javascript/mastodon/locales/ja.json | 1 + app/javascript/mastodon/reducers/settings.js | 6 + app/models/status.rb | 10 ++ app/services/batched_remove_status_service.rb | 8 + app/services/fan_out_on_write_service.rb | 2 + app/services/remove_status_service.rb | 2 + streaming/index.js | 32 ++++ 18 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 app/javascript/mastodon/features/domain_timeline/containers/column_settings_container.js create mode 100644 app/javascript/mastodon/features/domain_timeline/index.js diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index ccc10f966c..a59d9ecb75 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -41,7 +41,11 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_timeline_statuses - Status.as_public_timeline(current_account, truthy_param?(:local)) + if params[:domain].present? + Status.as_domain_timeline(current_account, params[:domain]) + else + Status.as_public_timeline(current_account, truthy_param?(:local)) + end end def insert_pagination_headers @@ -49,7 +53,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) + params.slice(:local, :domain, :limit, :only_media).permit(:local, :domain, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index c678e93932..1e6687d550 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -56,6 +56,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); +export const connectDomainStream = (domain, { onlyMedia } = {}) => connectTimelineStream(`domain${onlyMedia ? ':media' : ''}:${domain}`, `public:remote${onlyMedia ? ':media' : ''}&domain=${domain}`); export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index bc2ac5e823..ee54a082da 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -109,6 +109,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { 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 expandDomainTimeline = (domain, { maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`domain:${domain}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 9acb8f9403..32b6ff2eb2 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -39,6 +39,7 @@ const messages = defineMessages({ copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + openDomainTimeline: { id: 'account.open_domain_timeline', defaultMessage: 'Open {domain} timeline' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); @@ -199,6 +200,13 @@ class StatusActionBar extends ImmutablePureComponent { onUnblockDomain(account.get('acct').split('@')[1]); } + handleOpenDomainTimeline = () => { + const { status } = this.props; + const account = status.get('account'); + + this.context.router.history.push(`/timelines/public/remote/${account.get('acct').split('@')[1]}`); + } + handleOpen = () => { this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); } @@ -302,6 +310,7 @@ class StatusActionBar extends ImmutablePureComponent { } else { menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); } + menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline }); } if (isStaff) { diff --git a/app/javascript/mastodon/features/domain_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/domain_timeline/containers/column_settings_container.js new file mode 100644 index 0000000000..a83d090306 --- /dev/null +++ b/app/javascript/mastodon/features/domain_timeline/containers/column_settings_container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../../community_timeline/components/column_settings'; +import { changeSetting } from '../../../actions/settings'; +import { changeColumnParams } from '../../../actions/columns'; + +const mapStateToProps = (state, { columnId }) => { + const uuid = columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + + return { + settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'domain']), + }; +}; + +const mapDispatchToProps = (dispatch, { columnId }) => { + return { + onChange (key, checked) { + if (columnId) { + dispatch(changeColumnParams(columnId, key, checked)); + } else { + dispatch(changeSetting(['domain', ...key], checked)); + } + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/mastodon/features/domain_timeline/index.js b/app/javascript/mastodon/features/domain_timeline/index.js new file mode 100644 index 0000000000..b5eb5d6ba6 --- /dev/null +++ b/app/javascript/mastodon/features/domain_timeline/index.js @@ -0,0 +1,138 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import { expandDomainTimeline } from '../../actions/timelines'; +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { connectDomainStream } from '../../actions/streaming'; + +const mapStateToProps = (state, props) => { + const uuid = props.columnId; + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === uuid); + const onlyMedia = (props.columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'domain', 'other', 'onlyMedia']); + const timelineState = state.getIn(['timelines', `domain:${domain}${onlyMedia ? ':media' : ''}`]); + const domain = props.params.domain; + + return { + hasUnread: !!timelineState && timelineState.get('unread') > 0, + onlyMedia, + domain: domain, + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class DomainTimeline extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static defaultProps = { + onlyMedia: false, + }; + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + shouldUpdateScroll: PropTypes.func, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + hasUnread: PropTypes.bool, + multiColumn: PropTypes.bool, + onlyMedia: PropTypes.bool, + domain: PropTypes.string, + }; + + handlePin = () => { + const { columnId, dispatch, onlyMedia, domain } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('DOMAIN', { domain, other: { onlyMedia } })); + } + } + + handleMove = (dir) => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch, onlyMedia, domain } = this.props; + + dispatch(expandDomainTimeline(domain, { onlyMedia })); + this.disconnect = dispatch(connectDomainStream(domain, { onlyMedia })); + } + + componentDidUpdate (prevProps) { + if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.domain !== this.props.domain) { + const { dispatch, onlyMedia, domain } = this.props; + + this.disconnect(); + dispatch(expandDomainTimeline(domain, { onlyMedia })); + this.disconnect = dispatch(connectDomainStream(domain, { onlyMedia })); + } + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + setRef = c => { + this.column = c; + } + + handleLoadMore = maxId => { + const { dispatch, onlyMedia, domain } = this.props; + + dispatch(expandDomainTimeline(domain, { maxId, onlyMedia })); + } + + render () { + const { shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia, domain } = this.props; + const pinned = !!columnId; + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={domain}> + <ColumnHeader + icon='users' + active={hasUnread} + title={domain} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + showBackButton + > + <ColumnSettingsContainer columnId={columnId} /> + </ColumnHeader> + + <StatusListContainer + trackScroll={!pinned} + scrollKey={`domain_timeline-${columnId}`} + timelineId={`domain:${domain}${onlyMedia ? ':media' : ''}`} + onLoadMore={this.handleLoadMore} + emptyMessage={<FormattedMessage id='empty_column.domain' defaultMessage='There is nothing here! Manually follow users from other servers to fill it up' />} + shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} + /> + </Column> + ); + } + +} diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index b841ad3f9f..bcd8e343b4 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -34,6 +34,7 @@ const messages = defineMessages({ copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + openDomainTimeline: { id: 'account.open_domain_timeline', defaultMessage: 'Open {domain} timeline' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, }); @@ -146,6 +147,13 @@ class ActionBar extends React.PureComponent { onUnblockDomain(account.get('acct').split('@')[1]); } + handleOpenDomainTimeline = () => { + const { status } = this.props; + const account = status.get('account'); + + this.context.router.history.push(`/timelines/public/remote/${account.get('acct').split('@')[1]}`); + } + handleConversationMuteClick = () => { this.props.onMuteConversation(this.props.status); } @@ -246,6 +254,7 @@ class ActionBar extends React.PureComponent { } else { menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain }); } + menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline }); } if (isStaff) { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index e7ff17fcf3..bcbd8cd858 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -18,6 +18,7 @@ import { HomeTimeline, CommunityTimeline, PublicTimeline, + DomainTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, @@ -38,6 +39,7 @@ const componentMap = { 'NOTIFICATIONS': Notifications, 'PUBLIC': PublicTimeline, 'COMMUNITY': CommunityTimeline, + 'DOMAIN': DomainTimeline, 'HASHTAG': HashtagTimeline, 'DIRECT': DirectTimeline, 'FAVOURITES': FavouritedStatuses, diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 4acf9ee066..f0bcde9dff 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -28,6 +28,7 @@ import { KeyboardShortcuts, PublicTimeline, CommunityTimeline, + DomainTimeline, AccountTimeline, AccountGallery, HomeTimeline, @@ -184,6 +185,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> + <WrappedRoute path='/timelines/public/remote/:domain' exact component={DomainTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 986efda1ec..bf9e87e174 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -22,6 +22,10 @@ export function CommunityTimeline () { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); } +export function DomainTimeline () { + return import(/* webpackChunkName: "features/domain_timeline" */'../../domain_timeline'); +} + export function HashtagTimeline () { return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 7d9bafd626..138b67f6fb 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -27,6 +27,7 @@ "account.mute_notifications": "Mute notifications from @{name}", "account.muted": "Muted", "account.never_active": "Never", + "account.open_domain_timeline": "Open {domain} timeline", "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.report": "Report @{name}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 0252a5525a..86a3a9d7a8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -27,6 +27,7 @@ "account.mute_notifications": "@{name}ã•ã‚“ã‹ã‚‰ã®é€šçŸ¥ã‚’å—ã‘å–らãªã„", "account.muted": "ミュート済ã¿", "account.never_active": "活動ãªã—", + "account.open_domain_timeline": "{domain}タイムラインを表示", "account.posts": "投稿", "account.posts_with_replies": "投稿ã¨è¿”ä¿¡", "account.report": "@{name}ã•ã‚“ã‚’é€šå ±", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index efef2ad9a5..f0461eedd1 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -68,6 +68,12 @@ const initialState = ImmutableMap({ }), }), + domain: ImmutableMap({ + regex: ImmutableMap({ + body: '', + }), + }), + public: ImmutableMap({ regex: ImmutableMap({ body: '', diff --git a/app/models/status.rb b/app/models/status.rb index f7c551e811..aeb6bf0ca5 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -307,6 +307,16 @@ class Status < ApplicationRecord apply_timeline_filters(query, account, local_only) end + def as_domain_timeline(account = nil, domain) + query = Status.includes(:account) + .where(accounts: {domain: domain}).select('statuses.*, accounts.*') + .with_public_visibility + .without_reblogs + .without_replies + + apply_timeline_filters(query, account, false) + end + def as_tag_timeline(tag, account = nil, local_only = false) query = timeline_scope(local_only).tagged_with(tag) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 3638134be7..fcf20d1c5f 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -16,6 +16,7 @@ class BatchedRemoveStatusService < BaseService @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } + @domains = statuses.each_with_object({}) { |s, h| h[s.id] = s.account.domain unless s.local? } @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } @@ -74,9 +75,16 @@ class BatchedRemoveStatusService < BaseService redis.publish('timeline:public', payload) redis.publish('timeline:public:local', payload) if status.local? + @domains[status.id].each do |domain| + redis.publish("timeline:public:remote:#{domain.mb_chars.downcase}", payload) + end + if status.media_attachments.any? redis.publish('timeline:public:media', payload) redis.publish('timeline:public:local:media', payload) if status.local? + @domains[status.id].each do |domain| + redis.publish("timeline:public:remote:media:#{domain.mb_chars.downcase}", payload) + end end @tags[status.id].each do |hashtag| diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 77b11a80df..db4a37e454 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -167,12 +167,14 @@ class FanOutOnWriteService < BaseService Rails.logger.debug "Delivering status #{status.id} to public timeline" Redis.current.publish('timeline:public', @payload) + Redis.current.publish("timeline:public:remote:#{status.account.domain.mb_chars.downcase}", @payload) unless status.local? end def deliver_to_media(status) Rails.logger.debug "Delivering status #{status.id} to media timeline" Redis.current.publish('timeline:public:media', @payload) + Redis.current.publish("timeline:public:remote:media:#{status.account.domain.mb_chars.downcase}", @payload) unless status.local? end def deliver_to_own_conversation(status) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index f9352ed3d3..887566b9e8 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -141,6 +141,7 @@ class RemoveStatusService < BaseService redis.publish('timeline:public', @payload) redis.publish('timeline:public:local', @payload) if @status.local? + redis.publish("timeline:public:remote:#{@account.domain.mb_chars.downcase}", @payload) unless @status.local? end def remove_from_media @@ -148,6 +149,7 @@ class RemoveStatusService < BaseService redis.publish('timeline:public:media', @payload) redis.publish('timeline:public:local:media', @payload) if @status.local? + redis.publish("timeline:public:remote:media:#{@account.domain.mb_chars.downcase}", @payload) unless @status.local? end def remove_media diff --git a/streaming/index.js b/streaming/index.js index 304e7e0465..2ac0f8b27f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -266,6 +266,8 @@ const startWorker = (workerId) => { 'public:media', 'public:local', 'public:local:media', + 'public:remote', + 'public:remote:media', 'hashtag', 'hashtag:local', ]; @@ -297,6 +299,7 @@ const startWorker = (workerId) => { const PUBLIC_ENDPOINTS = [ '/api/v1/streaming/public', '/api/v1/streaming/public/local', + '/api/v1/streaming/public/remote', '/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag/local', ]; @@ -532,6 +535,19 @@ const startWorker = (workerId) => { streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); + app.get('/api/v1/streaming/public/remote', (req, res) => { + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:remote:media' : 'timeline:public:remote'; + const { domain } = req.query; + + if (!domain || domain.length === 0) { + httpNotFound(res); + return; + } + + streamFrom(`${channel}:${domain.toLowerCase()}`, req, streamToHttp(req, res), streamHttpEnd(req), true); + }); + app.get('/api/v1/streaming/direct', (req, res) => { const channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); @@ -596,12 +612,28 @@ const startWorker = (workerId) => { case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote': + if (!location.query.domain || location.query.domain.length === 0) { + ws.close(); + return; + } + + streamFrom(`timeline:public:remote:${location.query.domain.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'public:media': streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:local:media': streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote:media': + if (!location.query.domain || location.query.domain.length === 0) { + ws.close(); + return; + } + + streamFrom(`timeline:public:remote:media:${location.query.domain.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'direct': channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); -- GitLab