Merge tag 'v2.8.4' into instance_only_statuses
This commit is contained in:
commit
1e5b17b88a
33
CHANGELOG.md
33
CHANGELOG.md
@ -3,6 +3,39 @@ Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.8.4] - 2019-05-24
|
||||
### Fixed
|
||||
|
||||
- Fix delivery not retrying on some inbox errors that should be retriable ([ThibG](https://github.com/tootsuite/mastodon/pull/10812))
|
||||
- Fix unnecessary 5 minute cooldowns on signature verifications in some cases ([ThibG](https://github.com/tootsuite/mastodon/pull/10813))
|
||||
- Fix possible race condition when processing statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10815))
|
||||
|
||||
### Security
|
||||
|
||||
- Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ThibG](https://github.com/tootsuite/mastodon/pull/10818))
|
||||
|
||||
## [2.8.3] - 2019-05-19
|
||||
### Added
|
||||
|
||||
- Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/tootsuite/mastodon/pull/10779))
|
||||
- Add clickable area below avatar in statuses in web UI ([Dar13](https://github.com/tootsuite/mastodon/pull/10766))
|
||||
- Add crossed-out eye icon on account gallery in web UI ([Kjwon15](https://github.com/tootsuite/mastodon/pull/10715))
|
||||
- Add media description tooltip to thumbnails in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10713))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change "mark as sensitive" button into a checkbox for clarity ([ThibG](https://github.com/tootsuite/mastodon/pull/10748))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bug allowing users to publicly boost their private statuses ([ThibG](https://github.com/tootsuite/mastodon/pull/10775), [ThibG](https://github.com/tootsuite/mastodon/pull/10783))
|
||||
- Fix performance in formatter by a little ([ThibG](https://github.com/tootsuite/mastodon/pull/10765))
|
||||
- Fix some colors in the light theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10754))
|
||||
- Fix some colors of the high contrast theme ([yuzulabo](https://github.com/tootsuite/mastodon/pull/10711))
|
||||
- Fix ambivalent active state of poll refresh button in web UI ([MaciekBaron](https://github.com/tootsuite/mastodon/pull/10720))
|
||||
- Fix duplicate posting being possible from web UI ([hinaloe](https://github.com/tootsuite/mastodon/pull/10785))
|
||||
- Fix "invited by" not showing up in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10791))
|
||||
|
||||
## [2.8.2] - 2019-05-05
|
||||
### Added
|
||||
|
||||
|
@ -43,13 +43,7 @@ module SignatureVerification
|
||||
return
|
||||
end
|
||||
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account_from_key_id(signature_params['keyId']) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
account = account_from_key_id(signature_params['keyId'])
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@ -62,13 +56,7 @@ module SignatureVerification
|
||||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
account_stoplight = Stoplight("source:#{request.ip}") { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
|
||||
account = account_stoplight.run
|
||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@ -136,14 +124,23 @@ module SignatureVerification
|
||||
|
||||
def account_from_key_id(key_id)
|
||||
if key_id.start_with?('acct:')
|
||||
ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
def stoplight_wrap_request(&block)
|
||||
Stoplight("source:#{request.remote_ip}", &block)
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
.run
|
||||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
|
@ -356,6 +356,7 @@ class Status extends ImmutablePureComponent {
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
@ -88,8 +89,10 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||
const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
|
||||
const height = width;
|
||||
const status = attachment.get('status');
|
||||
const title = status.get('spoiler_text') || attachment.get('description');
|
||||
|
||||
let thumbnail = '';
|
||||
let icon;
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
// Skip
|
||||
@ -131,11 +134,20 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
icon = (
|
||||
<span className='account-gallery__item__icons'>
|
||||
<Icon id='eye-slash' />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
{!visible && icon}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ const mapStateToProps = state => ({
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
|
||||
@ -38,9 +37,19 @@ class SensitiveButton extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
|
||||
<Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
|
||||
</button>
|
||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
|
||||
<input
|
||||
name='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<span className={classNames('checkbox', { active })} />
|
||||
|
||||
<FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -71,11 +71,7 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
|
||||
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||
const params = [ `stream=${stream}` ];
|
||||
|
||||
if (accessToken !== null) {
|
||||
params.push(`access_token=${accessToken}`);
|
||||
}
|
||||
|
||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`);
|
||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
||||
|
||||
ws.onopen = connected;
|
||||
ws.onmessage = e => received(JSON.parse(e.data));
|
||||
|
@ -67,3 +67,11 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nothing-here {
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.public-layout .public-account-header__tabs__tabs .counter.active::after {
|
||||
border-bottom: 4px solid $ui-highlight-color;
|
||||
}
|
||||
|
@ -162,7 +162,7 @@
|
||||
.actions-modal ul li:not(:empty) a:focus button,
|
||||
.actions-modal ul li:not(:empty) a:hover,
|
||||
.actions-modal ul li:not(:empty) a:hover button,
|
||||
.admin-wrapper .sidebar ul ul a.selected,
|
||||
.admin-wrapper .sidebar ul li a.selected,
|
||||
.simple_form .block-button,
|
||||
.simple_form .button,
|
||||
.simple_form button {
|
||||
@ -230,6 +230,7 @@
|
||||
.empty-column-indicator,
|
||||
.error-column {
|
||||
color: $primary-text-color;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
// Change the default colors used on some parts of the profile pages
|
||||
|
@ -268,9 +268,34 @@
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
|
||||
.icon-button {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.active {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 1px solid $ui-primary-color;
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
top: -1px;
|
||||
border-radius: 4px;
|
||||
vertical-align: middle;
|
||||
|
||||
&.active {
|
||||
border-color: $highlight-text-color;
|
||||
background: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1386,6 +1411,15 @@ a.account__display-name {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.status__expand {
|
||||
width: 68px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.muted {
|
||||
.status__content p,
|
||||
.status__content a {
|
||||
@ -4829,6 +4863,14 @@ a.status-card.compact:hover {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 2px;
|
||||
|
||||
&__icons {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.notification__filter-bar,
|
||||
|
@ -114,11 +114,14 @@
|
||||
text-decoration: underline;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgba($dark-text-color, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
|
@ -267,7 +267,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
def conversation_from_uri(uri)
|
||||
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)
|
||||
Conversation.find_by(uri: uri) || Conversation.create(uri: uri)
|
||||
begin
|
||||
Conversation.find_or_create_by!(uri: uri)
|
||||
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
def visibility_from_audience
|
||||
|
@ -187,7 +187,7 @@ class Formatter
|
||||
end
|
||||
|
||||
def rewrite(text, entities)
|
||||
chars = text.to_s.to_char_a
|
||||
text = text.to_s
|
||||
|
||||
# Sort by start index
|
||||
entities = entities.sort_by do |entity|
|
||||
@ -199,12 +199,12 @@ class Formatter
|
||||
|
||||
last_index = entities.reduce(0) do |index, entity|
|
||||
indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
|
||||
result << encode(chars[index...indices.first].join)
|
||||
result << encode(text[index...indices.first])
|
||||
result << yield(entity)
|
||||
indices.last
|
||||
end
|
||||
|
||||
result << encode(chars[last_index..-1].join)
|
||||
result << encode(text[last_index..-1])
|
||||
|
||||
result.flatten.join
|
||||
end
|
||||
@ -231,23 +231,14 @@ class Formatter
|
||||
# Note: I couldn't obtain list_slug with @user/list-name format
|
||||
# for mention so this requires additional check
|
||||
special = Extractor.extract_urls_with_indices(escaped, options).map do |extract|
|
||||
# exactly one of :url, :hashtag, :screen_name, :cashtag keys is present
|
||||
key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first
|
||||
|
||||
new_indices = [
|
||||
old_to_new_index.find_index(extract[:indices].first),
|
||||
old_to_new_index.find_index(extract[:indices].last),
|
||||
]
|
||||
|
||||
has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key)
|
||||
value_indices = [
|
||||
new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $
|
||||
new_indices.last - 1,
|
||||
]
|
||||
|
||||
next extract.merge(
|
||||
:indices => new_indices,
|
||||
key => text[value_indices.first..value_indices.last]
|
||||
indices: new_indices,
|
||||
url: text[new_indices.first..new_indices.last - 1]
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -114,6 +114,10 @@ class User < ApplicationRecord
|
||||
end
|
||||
|
||||
def invited?
|
||||
invite_id.present?
|
||||
end
|
||||
|
||||
def valid_invitation?
|
||||
invite_id.present? && invite.valid_for_use?
|
||||
end
|
||||
|
||||
@ -274,7 +278,7 @@ class User < ApplicationRecord
|
||||
private
|
||||
|
||||
def set_approved
|
||||
self.approved = open_registrations? || invited? || external?
|
||||
self.approved = open_registrations? || valid_invitation? || external?
|
||||
end
|
||||
|
||||
def open_registrations?
|
||||
|
@ -18,7 +18,9 @@ class ReblogService < BaseService
|
||||
|
||||
return reblog unless reblog.nil?
|
||||
|
||||
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: options[:visibility] || account.user&.setting_default_privacy)
|
||||
visibility = options[:visibility] || account.user&.setting_default_privacy
|
||||
visibility = reblogged_status.visibility if reblogged_status.hidden?
|
||||
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
|
||||
|
||||
DistributionWorker.perform_async(reblog.id)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class BlacklistedEmailValidator < ActiveModel::Validator
|
||||
def validate(user)
|
||||
return if user.invited?
|
||||
return if user.valid_invitation?
|
||||
|
||||
@email = user.email
|
||||
|
||||
|
@ -7,6 +7,8 @@
|
||||
- unless media.file.meta.nil?
|
||||
= opengraph 'og:image:width', media.file.meta.dig('original', 'width')
|
||||
= opengraph 'og:image:height', media.file.meta.dig('original', 'height')
|
||||
- if media.description.present?
|
||||
= opengraph 'og:image:alt', media.description
|
||||
- elsif media.video? || media.gifv?
|
||||
- player_card = true
|
||||
= opengraph 'og:image', full_asset_url(media.file.url(:small))
|
||||
|
@ -51,7 +51,7 @@ class ActivityPub::DeliveryWorker
|
||||
end
|
||||
|
||||
def response_error_unsalvageable?(response)
|
||||
(400...500).cover?(response.code) && response.code != 429
|
||||
(400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)
|
||||
end
|
||||
|
||||
def failure_tracker
|
||||
|
@ -0,0 +1,23 @@
|
||||
class RemoveBoostsWideningAudience < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
public_boosts = Status.find_by_sql(<<-SQL)
|
||||
SELECT boost.id
|
||||
FROM statuses AS boost
|
||||
LEFT JOIN statuses AS boosted ON boost.reblog_of_id = boosted.id
|
||||
WHERE
|
||||
boost.id > 101746055577600000
|
||||
AND (boost.local = TRUE OR boost.uri IS NULL)
|
||||
AND boost.visibility IN (0, 1)
|
||||
AND boost.reblog_of_id IS NOT NULL
|
||||
AND boosted.visibility = 2
|
||||
SQL
|
||||
|
||||
RemovalWorker.push_bulk(public_boosts.pluck(:id))
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_04_20_025523) do
|
||||
ActiveRecord::Schema.define(version: 2019_05_19_130537) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -13,7 +13,7 @@ module Mastodon
|
||||
end
|
||||
|
||||
def patch
|
||||
2
|
||||
4
|
||||
end
|
||||
|
||||
def pre
|
||||
|
@ -4,10 +4,9 @@ RSpec.describe ReblogService, type: :service do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
context 'creates a reblog with appropriate visibility' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
let(:visibility) { :public }
|
||||
let(:reblog_visibility) { :public }
|
||||
let(:status) { Fabricate(:status, account: bob, visibility: visibility) }
|
||||
let(:status) { Fabricate(:status, account: alice, visibility: visibility) }
|
||||
|
||||
subject { ReblogService.new }
|
||||
|
||||
@ -22,6 +21,15 @@ RSpec.describe ReblogService, type: :service do
|
||||
expect(status.reblogs.first.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'public reblogs of private toots should remain private' do
|
||||
let(:visibility) { :private }
|
||||
let(:reblog_visibility) { :public }
|
||||
|
||||
it 'reblogs privately' do
|
||||
expect(status.reblogs.first.visibility).to eq 'private'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'OStatus' do
|
||||
|
@ -8,7 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
|
||||
let(:errors) { double(add: nil) }
|
||||
|
||||
before do
|
||||
allow(user).to receive(:invited?) { false }
|
||||
allow(user).to receive(:valid_invitation?) { false }
|
||||
allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
|
||||
described_class.new.validate(user)
|
||||
end
|
||||
|
@ -195,14 +195,14 @@ const startWorker = (workerId) => {
|
||||
next();
|
||||
};
|
||||
|
||||
const accountFromToken = (token, req, next) => {
|
||||
const accountFromToken = (token, allowedScopes, req, next) => {
|
||||
pgPool.connect((err, client, done) => {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||
client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
|
||||
done();
|
||||
|
||||
if (err) {
|
||||
@ -218,18 +218,29 @@ const startWorker = (workerId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = result.rows[0].scopes.split(' ');
|
||||
|
||||
if (allowedScopes.size > 0 && !scopes.some(scope => allowedScopes.includes(scope))) {
|
||||
err = new Error('Access token does not cover required scopes');
|
||||
err.statusCode = 401;
|
||||
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
req.accountId = result.rows[0].account_id;
|
||||
req.chosenLanguages = result.rows[0].chosen_languages;
|
||||
req.allowNotifications = scopes.some(scope => ['read', 'read:notifications'].includes(scope));
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const accountFromRequest = (req, next, required = true) => {
|
||||
const accountFromRequest = (req, next, required = true, allowedScopes = ['read']) => {
|
||||
const authorization = req.headers.authorization;
|
||||
const location = url.parse(req.url, true);
|
||||
const accessToken = location.query.access_token;
|
||||
const accessToken = location.query.access_token || req.headers['sec-websocket-protocol'];
|
||||
|
||||
if (!authorization && !accessToken) {
|
||||
if (required) {
|
||||
@ -246,7 +257,7 @@ const startWorker = (workerId) => {
|
||||
|
||||
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
|
||||
|
||||
accountFromToken(token, req, next);
|
||||
accountFromToken(token, allowedScopes, req, next);
|
||||
};
|
||||
|
||||
const PUBLIC_STREAMS = [
|
||||
@ -261,6 +272,16 @@ const startWorker = (workerId) => {
|
||||
const wsVerifyClient = (info, cb) => {
|
||||
const location = url.parse(info.req.url, true);
|
||||
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
|
||||
const allowedScopes = [];
|
||||
|
||||
if (authRequired) {
|
||||
allowedScopes.push('read');
|
||||
if (location.query.stream === 'user:notification') {
|
||||
allowedScopes.push('read:notifications');
|
||||
} else {
|
||||
allowedScopes.push('read:statuses');
|
||||
}
|
||||
}
|
||||
|
||||
accountFromRequest(info.req, err => {
|
||||
if (!err) {
|
||||
@ -269,7 +290,7 @@ const startWorker = (workerId) => {
|
||||
log.error(info.req.requestId, err.toString());
|
||||
cb(false, 401, 'Unauthorized');
|
||||
}
|
||||
}, authRequired);
|
||||
}, authRequired, allowedScopes);
|
||||
};
|
||||
|
||||
const PUBLIC_ENDPOINTS = [
|
||||
@ -286,7 +307,18 @@ const startWorker = (workerId) => {
|
||||
}
|
||||
|
||||
const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
|
||||
accountFromRequest(req, next, authRequired);
|
||||
const allowedScopes = [];
|
||||
|
||||
if (authRequired) {
|
||||
allowedScopes.push('read');
|
||||
if (req.path === '/api/v1/streaming/user/notification') {
|
||||
allowedScopes.push('read:notifications');
|
||||
} else {
|
||||
allowedScopes.push('read:statuses');
|
||||
}
|
||||
}
|
||||
|
||||
accountFromRequest(req, next, authRequired, allowedScopes);
|
||||
};
|
||||
|
||||
const errorMiddleware = (err, req, res, {}) => {
|
||||
@ -339,6 +371,10 @@ const startWorker = (workerId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'notification' && !req.allowNotifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send local-only statuses to logged-in users
|
||||
if (payload.local_only && !req.accountId) {
|
||||
log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`);
|
||||
|
Loading…
Reference in New Issue
Block a user