From 1486fd64cc73d1efb713ad3801cb8ae7acc0de1f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 12 Dec 2017 23:10:12 +0900 Subject: [PATCH 01/30] Move files for GitHub to .github directory (#5989) --- CODEOWNERS => .github/CODEOWNERS | 0 ISSUE_TEMPLATE.md => .github/ISSUE_TEMPLATE.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CODEOWNERS => .github/CODEOWNERS (100%) rename ISSUE_TEMPLATE.md => .github/ISSUE_TEMPLATE.md (100%) diff --git a/CODEOWNERS b/.github/CODEOWNERS similarity index 100% rename from CODEOWNERS rename to .github/CODEOWNERS diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md From fe180f18ff38a01007842ccff293a84a63336aae Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Tue, 12 Dec 2017 15:11:13 +0100 Subject: [PATCH 02/30] Change conditional to avoid nil into string error in sidekiq (#5987) * Change conditional to avoid nil into string error in sidekiq When obtaining information about users with mastodon in a different subdomain, sidekiq was giving out a 'no implicit conversion of nil into String' * Use presence instead of blank? with ternary. Following suggestion on PR --- app/services/fetch_remote_status_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 9c009335b6..9c3008035d 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService end def confirmed_domain?(domain, account) - account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero? + account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? end end From 19257d91bf1e613b48a7ac9de7ce6933405c9657 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Tue, 12 Dec 2017 15:12:09 +0100 Subject: [PATCH 03/30] Return false if object does not respond to url (#5988) Avoid error when the service returns a mostly valid oembed, but has no url in it, causing a MethodError: undefined method `url' for # --- app/services/fetch_link_card_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 7f4518ea7f..9f0c738582 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -87,6 +87,7 @@ class FetchLinkCardService < BaseService when 'link' @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url) when 'photo' + return false unless response.respond_to?(:url) @card.embed_url = response.url @card.width = response.width.presence || 0 @card.height = response.height.presence || 0 From cfea28216ffaec9c28ba2f57de868ada482c1779 Mon Sep 17 00:00:00 2001 From: nullkal Date: Tue, 12 Dec 2017 23:13:24 +0900 Subject: [PATCH 04/30] make it possible to stream public timelines without authorization (#5977) * make it possible to stream public timelines without authorization * Fix * Make eslint allow `value == null` * Remove redundant line * Improve style and revert .eslintrc.yml * Fix streamWsEnd * Show IP address instead of (anonymous user) * Add missing semicolon --- streaming/index.js | 99 +++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/streaming/index.js b/streaming/index.js index c79a58671d..31c597cf0c 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -97,6 +97,8 @@ const startWorker = (workerId) => { }; const app = express(); + app.set('trusted proxy', process.env.TRUSTED_PROXY_IP || 'loopback,uniquelocal'); + const pgPool = new pg.Pool(Object.assign(pgConfigs[env], dbUrlToConfig(process.env.DATABASE_URL))); const server = http.createServer(app); const redisNamespace = process.env.REDIS_NAMESPACE || null; @@ -177,6 +179,12 @@ const startWorker = (workerId) => { next(); }; + const setRemoteAddress = (req, res, next) => { + req.remoteAddress = req.connection.remoteAddress; + + next(); + }; + const accountFromToken = (token, req, next) => { pgPool.connect((err, client, done) => { if (err) { @@ -208,17 +216,22 @@ const startWorker = (workerId) => { }); }; - const accountFromRequest = (req, next) => { + const accountFromRequest = (req, next, required = true) => { const authorization = req.headers.authorization; const location = url.parse(req.url, true); const accessToken = location.query.access_token; if (!authorization && !accessToken) { - const err = new Error('Missing access token'); - err.statusCode = 401; + if (required) { + const err = new Error('Missing access token'); + err.statusCode = 401; - next(err); - return; + next(err); + return; + } else { + next(); + return; + } } const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken; @@ -226,7 +239,17 @@ const startWorker = (workerId) => { accountFromToken(token, req, next); }; + const PUBLIC_STREAMS = [ + 'public', + 'public:local', + 'hashtag', + 'hashtag:local', + ]; + const wsVerifyClient = (info, cb) => { + const location = url.parse(info.req.url, true); + const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream); + accountFromRequest(info.req, err => { if (!err) { cb(true, undefined, undefined); @@ -234,16 +257,24 @@ const startWorker = (workerId) => { log.error(info.req.requestId, err.toString()); cb(false, 401, 'Unauthorized'); } - }); + }, authRequired); }; + const PUBLIC_ENDPOINTS = [ + '/api/v1/streaming/public', + '/api/v1/streaming/public/local', + '/api/v1/streaming/hashtag', + '/api/v1/streaming/hashtag/local', + ]; + const authenticationMiddleware = (req, res, next) => { if (req.method === 'OPTIONS') { next(); return; } - accountFromRequest(req, next); + const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); + accountFromRequest(req, next, authRequired); }; const errorMiddleware = (err, req, res, {}) => { @@ -275,8 +306,10 @@ const startWorker = (workerId) => { }; const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => { + const accountId = req.accountId || req.remoteAddress; + const streamType = notificationOnly ? ' (notification)' : ''; - log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}${streamType}`); + log.verbose(req.requestId, `Starting stream from ${id} for ${accountId}${streamType}`); const listener = message => { const { event, payload, queued_at } = JSON.parse(message); @@ -286,7 +319,7 @@ const startWorker = (workerId) => { const delta = now - queued_at; const encodedPayload = typeof payload === 'object' ? JSON.stringify(payload) : payload; - log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); + log.silly(req.requestId, `Transmitting for ${accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); output(event, encodedPayload); }; @@ -313,26 +346,30 @@ const startWorker = (workerId) => { return; } - const queries = [ - client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)), - ]; + if (!req.accountId) { + const queries = [ + client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) OR (account_id = $2 AND target_account_id = $1) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, unpackedPayload.account.id].concat(targetAccountIds)), + ]; - if (accountDomain) { - queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); - } - - Promise.all(queries).then(values => { - done(); - - if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) { - return; + if (accountDomain) { + queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); } + Promise.all(queries).then(values => { + done(); + + if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) { + return; + } + + transmit(); + }).catch(err => { + done(); + log.error(err); + }); + } else { transmit(); - }).catch(err => { - done(); - log.error(err); - }); + } }); } else { transmit(); @@ -345,13 +382,15 @@ const startWorker = (workerId) => { // Setup stream output to HTTP const streamToHttp = (req, res) => { + const accountId = req.accountId || req.remoteAddress; + res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Transfer-Encoding', 'chunked'); const heartbeat = setInterval(() => res.write(':thump\n'), 15000); req.on('close', () => { - log.verbose(req.requestId, `Ending stream for ${req.accountId}`); + log.verbose(req.requestId, `Ending stream for ${accountId}`); clearInterval(heartbeat); }); @@ -383,8 +422,10 @@ const startWorker = (workerId) => { // Setup stream end for WebSockets const streamWsEnd = (req, ws, closeHandler = false) => (id, listener) => { + const accountId = req.accountId || req.remoteAddress; + ws.on('close', () => { - log.verbose(req.requestId, `Ending stream for ${req.accountId}`); + log.verbose(req.requestId, `Ending stream for ${accountId}`); unsubscribe(id, listener); if (closeHandler) { closeHandler(); @@ -392,7 +433,7 @@ const startWorker = (workerId) => { }); ws.on('error', () => { - log.verbose(req.requestId, `Ending stream for ${req.accountId}`); + log.verbose(req.requestId, `Ending stream for ${accountId}`); unsubscribe(id, listener); if (closeHandler) { closeHandler(); @@ -401,6 +442,7 @@ const startWorker = (workerId) => { }; app.use(setRequestId); + app.use(setRemoteAddress); app.use(allowCrossDomain); app.use(authenticationMiddleware); app.use(errorMiddleware); @@ -451,6 +493,7 @@ const startWorker = (workerId) => { const req = ws.upgradeReq; const location = url.parse(req.url, true); req.requestId = uuid.v4(); + req.remoteAddress = ws._socket.remoteAddress; ws.isAlive = true; From 2a61b9f000039a7d02315cdf6bca58138861a71c Mon Sep 17 00:00:00 2001 From: SerCom_KC Date: Tue, 12 Dec 2017 22:13:47 +0800 Subject: [PATCH 05/30] Update Chinese (Simplified) translations (#5991) * i18n: (zh-CN) Update translations for #5817 * i18n: (zh-CN) Add translation for #5985 * i18n: (zh-CN) Normalization --- config/locales/zh-CN.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 3ede5c4d56..0d0cac1b35 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -341,7 +341,7 @@ zh-CN: warning: 一定小心,千万不要把它分享给任何人! your_token: 你的访问令牌 auth: - agreement_html: 注册即表示你同意我们的使用条款隐私权政策。 + agreement_html: 注册即表示你同意遵守本实例的相关规定我们的使用条款。 change_password: 帐户安全 delete_account: 删除帐户 delete_account_html: 如果你想删除你的帐户,请点击这里继续。你需要确认你的操作。 @@ -591,6 +591,7 @@ zh-CN: private: 不能置顶非公开的嘟文 reblog: 不能置顶转嘟 show_more: 显示更多 + title: "%{name}:“%{quote}”" visibilities: private: 仅关注者 private_long: 只有关注你的用户能看到 From f9f6918148ab2471292bcc89e14be8471b42c992 Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Tue, 12 Dec 2017 23:54:38 +0900 Subject: [PATCH 06/30] Store preview image for embedded photo in preview cards (#5986) The preview image would be useful to embed in timeline. --- app/services/fetch_link_card_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 9f0c738582..09534d0ff0 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -89,6 +89,7 @@ class FetchLinkCardService < BaseService when 'photo' return false unless response.respond_to?(:url) @card.embed_url = response.url + @card.image = URI.parse(response.url) @card.width = response.width.presence || 0 @card.height = response.height.presence || 0 when 'video' From cfa3f55221733664004deb14f70764be4752b7bb Mon Sep 17 00:00:00 2001 From: abcang Date: Wed, 13 Dec 2017 01:38:42 +0900 Subject: [PATCH 07/30] Remove duplicate indexes in lists (#5990) --- .../20171212195226_remove_duplicate_indexes_in_lists.rb | 6 ++++++ db/schema.rb | 4 +--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb diff --git a/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb b/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb new file mode 100644 index 0000000000..03f2591a88 --- /dev/null +++ b/db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb @@ -0,0 +1,6 @@ +class RemoveDuplicateIndexesInLists < ActiveRecord::Migration[5.1] + def change + remove_index :list_accounts, name: "index_list_accounts_on_account_id" + remove_index :list_accounts, name: "index_list_accounts_on_list_id" + end +end diff --git a/db/schema.rb b/db/schema.rb index 7887f26a2e..c55020fa40 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171201000000) do +ActiveRecord::Schema.define(version: 20171212195226) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -202,10 +202,8 @@ ActiveRecord::Schema.define(version: 20171201000000) do t.bigint "account_id", null: false t.bigint "follow_id", null: false t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true - t.index ["account_id"], name: "index_list_accounts_on_account_id" t.index ["follow_id"], name: "index_list_accounts_on_follow_id" t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id" - t.index ["list_id"], name: "index_list_accounts_on_list_id" end create_table "lists", force: :cascade do |t| From 0c8b1eb577f11d33c27f28afdfdac807b7e27845 Mon Sep 17 00:00:00 2001 From: Neetshin Date: Tue, 12 Dec 2017 18:57:22 +0000 Subject: [PATCH 08/30] Make detect empty string before assign image description (#5994) * Add aria-autocomplete='list' in Textaria ref: https://www.w3.org/TR/wai-aria-1.1/#aria-autocomplete * Make detect empty string brefore assign upload description --- app/javascript/mastodon/features/compose/components/upload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/compose/components/upload.js b/app/javascript/mastodon/features/compose/components/upload.js index 6ab76492a9..3a3d177100 100644 --- a/app/javascript/mastodon/features/compose/components/upload.js +++ b/app/javascript/mastodon/features/compose/components/upload.js @@ -62,7 +62,7 @@ export default class Upload extends ImmutablePureComponent { render () { const { intl, media } = this.props; const active = this.state.hovered || this.state.focused; - const description = this.state.dirtyDescription || media.get('description') || ''; + const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''; return (
From c986218c3a98564e38d68689150b33a6aa6c4b3a Mon Sep 17 00:00:00 2001 From: erin Date: Tue, 12 Dec 2017 13:19:33 -0600 Subject: [PATCH 09/30] Improve error handling in streaming/index.js (#5968) On an unhandled worker exception, we should log the exception and exit with nonzero status, instead of letting workers silently fail and restarting them in an endless loop. Note: we previously tried to handle the `'error'` signal. That's not a signal Node fires; my patch traps `'uncaughtException'`, which is what the code was _trying_ to do. --- streaming/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/streaming/index.js b/streaming/index.js index 31c597cf0c..198eac1ae4 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -563,12 +563,14 @@ const startWorker = (workerId) => { const onError = (err) => { log.error(err); + server.close(); + process.exit(0); }; process.on('SIGINT', onExit); process.on('SIGTERM', onExit); process.on('exit', onExit); - process.on('error', onError); + process.on('uncaughtException', onError); }; throng({ From 0370ba7b0a18d41f688269370d0eb089261047a9 Mon Sep 17 00:00:00 2001 From: Quenty31 <33203663+Quenty31@users.noreply.github.com> Date: Tue, 12 Dec 2017 20:48:26 +0100 Subject: [PATCH 10/30] Update: #5985 and #5817 (#5996) --- config/locales/oc.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 0167e92716..60c9e0671e 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -343,7 +343,7 @@ oc: warning: Mèfi ! Agachatz de partejar aquela donada amb degun ! your_token: Vòstre geton d’accès auth: - agreement_html: En vos marcar acceptatz nòstres tèrmes de servici e politica de confidencialitat. + agreement_html: En vos marcar acceptatz las règlas de l’instància e politica de confidencialitat. change_password: Seguretat delete_account: Suprimir lo compte delete_account_html: Se volètz suprimir vòstre compte, podètz o far aquí. Vos demandarem que confirmetz. @@ -677,6 +677,7 @@ oc: private: Se pòt pas penjar los tuts pas publics reblog: Se pòt pas penjar un tut partejat show_more: Ne veire mai + title: '%{name} : "%{quote}"' visibilities: private: Seguidors solament private_long: Mostrar pas qu’als seguidors From 0128b86d3098042cdbc3a1629f74b70f665f8dfb Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 02:12:41 +0100 Subject: [PATCH 11/30] Use streaming API for standalone timelines on /about and /tag pages (#5998) --- .../features/standalone/hashtag_timeline/index.js | 12 +++++------- .../features/standalone/public_timeline/index.js | 12 +++++------- app/javascript/mastodon/stream.js | 8 +++++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js index f15fbb2f40..f14be2aafb 100644 --- a/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/standalone/hashtag_timeline/index.js @@ -8,6 +8,7 @@ import { } from '../../../actions/timelines'; import Column from '../../../components/column'; import ColumnHeader from '../../../components/column_header'; +import { connectHashtagStream } from '../../../actions/streaming'; @connect() export default class HashtagTimeline extends React.PureComponent { @@ -29,16 +30,13 @@ export default class HashtagTimeline extends React.PureComponent { const { dispatch, hashtag } = this.props; dispatch(refreshHashtagTimeline(hashtag)); - - this.polling = setInterval(() => { - dispatch(refreshHashtagTimeline(hashtag)); - }, 10000); + this.disconnect = dispatch(connectHashtagStream(hashtag)); } componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } } diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js index de4b5320a5..5805d1a105 100644 --- a/app/javascript/mastodon/features/standalone/public_timeline/index.js +++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js @@ -9,6 +9,7 @@ import { import Column from '../../../components/column'; import ColumnHeader from '../../../components/column_header'; import { defineMessages, injectIntl } from 'react-intl'; +import { connectPublicStream } from '../../../actions/streaming'; const messages = defineMessages({ title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, @@ -35,16 +36,13 @@ export default class PublicTimeline extends React.PureComponent { const { dispatch } = this.props; dispatch(refreshPublicTimeline()); - - this.polling = setInterval(() => { - dispatch(refreshPublicTimeline()); - }, 3000); + this.disconnect = dispatch(connectPublicStream()); } componentWillUnmount () { - if (typeof this.polling !== 'undefined') { - clearInterval(this.polling); - this.polling = null; + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; } } diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 36c68ffc5b..9a6f4f26d1 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -62,7 +62,13 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({ export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) { - const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`); + const params = [ `stream=${stream}` ]; + + if (accessToken !== null) { + params.push(`access_token=${accessToken}`); + } + + const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`); ws.onopen = connected; ws.onmessage = e => received(JSON.parse(e.data)); From 71965cbef2696e66be284c8ed11711ec46925603 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 02:40:32 +0100 Subject: [PATCH 12/30] Adjust empty list timeline message (#5997) --- app/javascript/mastodon/features/list_timeline/index.js | 2 +- app/javascript/mastodon/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 1dcd4de144..ae136e48f9 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -161,7 +161,7 @@ export default class ListTimeline extends React.PureComponent { scrollKey={`list_timeline-${columnId}`} timelineId={`list:${id}`} loadMore={this.handleLoadMore} - emptyMessage={} + emptyMessage={} /> ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3633025b8b..5c39bd682d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -91,7 +91,7 @@ "empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home.public_timeline": "the public timeline", - "empty_column.list": "There is nothing in this list yet.", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "follow_request.authorize": "Authorize", From 5706fe18c2803a33c5cd0beceb6a07ba4da0b5ba Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 04:12:38 +0100 Subject: [PATCH 13/30] Fix #5952 - NameError (regression from #5762) (#5999) * Fix #5952 - NameError (regression from #5762) * Fix --- app/services/follow_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 20579ca63a..ac0207a0ab 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -22,7 +22,7 @@ class FollowService < BaseService elsif source_account.requested?(target_account) # This isn't managed by a method in AccountInteractions, so we modify it # ourselves if necessary. - req = follow_requests.find_by(target_account: other_account) + req = source_account.follow_requests.find_by(target_account: target_account) req.update!(show_reblogs: reblogs) return end From 81923f88bac7d9df1ee92603d3d4d0838aeb740f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 07:42:22 +0100 Subject: [PATCH 14/30] Shorten English title for 2FA to avoid line-break (#6001) --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index ce6d7ee411..44c021bd5a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -588,7 +588,7 @@ en: notifications: Notifications preferences: Preferences settings: Settings - two_factor_authentication: Two-factor Authentication + two_factor_authentication: Two-factor Auth your_apps: Your applications statuses: open_in_web: Open in web From 155e211dd035992432623a33b809578f8315395f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 12:14:03 +0100 Subject: [PATCH 15/30] Fix GIF avatars not autoplaying when GIF autoplay is enabled (#6000) --- app/javascript/mastodon/components/avatar.js | 5 +++-- .../mastodon/components/avatar_overlay.js | 13 ++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/components/avatar.js b/app/javascript/mastodon/components/avatar.js index f7c484ee3a..570505833f 100644 --- a/app/javascript/mastodon/components/avatar.js +++ b/app/javascript/mastodon/components/avatar.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { autoPlayGif } from '../initial_state'; export default class Avatar extends React.PureComponent { @@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent { account: ImmutablePropTypes.map.isRequired, size: PropTypes.number.isRequired, style: PropTypes.object, - animate: PropTypes.bool, inline: PropTypes.bool, + animate: PropTypes.bool, }; static defaultProps = { - animate: false, + animate: autoPlayGif, size: 20, inline: false, }; diff --git a/app/javascript/mastodon/components/avatar_overlay.js b/app/javascript/mastodon/components/avatar_overlay.js index f5d67b34e8..3ec1d77304 100644 --- a/app/javascript/mastodon/components/avatar_overlay.js +++ b/app/javascript/mastodon/components/avatar_overlay.js @@ -1,22 +1,29 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { autoPlayGif } from '../initial_state'; export default class AvatarOverlay extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map.isRequired, + animate: PropTypes.bool, + }; + + static defaultProps = { + animate: autoPlayGif, }; render() { - const { account, friend } = this.props; + const { account, friend, animate } = this.props; const baseStyle = { - backgroundImage: `url(${account.get('avatar_static')})`, + backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`, }; const overlayStyle = { - backgroundImage: `url(${friend.get('avatar_static')})`, + backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`, }; return ( From 20a6584d2dd9d5ecaa19a45a0c0c5ffec5a100ff Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 12:15:10 +0100 Subject: [PATCH 16/30] Clean up admin UI for accounts (#6004) * Add staff filter to admin UI for accounts, remove obsolete columns * Only display OStatus section in admin UI for accounts when OStatus data --- app/controllers/admin/accounts_controller.rb | 3 ++- app/helpers/admin/filter_helper.rb | 2 +- app/models/account_filter.rb | 2 ++ app/views/admin/accounts/_account.html.haml | 17 +++-------------- app/views/admin/accounts/index.html.haml | 9 ++++++--- app/views/admin/accounts/show.html.haml | 3 ++- config/locales/en.yml | 1 + 7 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index e9a512e70c..7428c3f229 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -89,7 +89,8 @@ module Admin :username, :display_name, :email, - :ip + :ip, + :staff ) end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 9443934b30..7fe3def987 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Admin::FilterHelper - ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze + ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 1898723682..dc7a03039f 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -45,6 +45,8 @@ class AccountFilter else Account.default_scoped end + when 'staff' + accounts_with_users.merge User.staff else raise "Unknown filter: #{key}" end diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index 5265d77f66..598f6cddd0 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -4,22 +4,11 @@ %td.domain - unless account.local? = link_to account.domain, admin_accounts_path(by_domain: account.domain) - %td.protocol - - unless account.local? - = account.protocol.humanize - %td.confirmed + %td - if account.local? - - if account.user_confirmed? - %i.fa.fa-check - - else - %i.fa.fa-times - %td.subscribed - - if account.local? - = t('admin.accounts.location.local') - - elsif account.subscribed? - %i.fa.fa-check + = t("admin.accounts.roles.#{account.user&.role}") - else - %i.fa.fa-times + = account.protocol.humanize %td = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 27a0682d8b..6aa39a80a0 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -30,6 +30,11 @@ = filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'} - else = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' + .filter-subset + %strong= t('admin.accounts.role') + %ul + %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil + %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1' .filter-subset %strong= t('admin.accounts.order.title') %ul @@ -56,9 +61,7 @@ %tr %th= t('admin.accounts.username') %th= t('admin.accounts.domain') - %th= t('admin.accounts.protocol') - %th= t('admin.accounts.confirmed') - %th= fa_icon 'paper-plane-o' + %th %th %tbody = render @accounts diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index ddb1cf15d9..5f5d0995cd 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -104,7 +104,7 @@ - else = link_to t('admin.accounts.perform_full_suspension'), admin_account_suspension_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:suspend, @account) -- unless @account.local? +- if !@account.local? && @account.hub_url.present? %hr %h3 OStatus @@ -132,6 +132,7 @@ - if @account.subscribed? = link_to t('admin.accounts.unsubscribe'), unsubscribe_admin_account_path(@account.id), method: :post, class: 'button negative' if can?(:unsubscribe, @account) +- if !@account.local? && @account.inbox_url.present? %hr %h3 ActivityPub diff --git a/config/locales/en.yml b/config/locales/en.yml index 44c021bd5a..cc22d02ac9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -116,6 +116,7 @@ en: roles: admin: Administrator moderator: Moderator + staff: Staff user: User salmon_url: Salmon URL search: Search From a8deb6648bc348e64469cc3451040b46ea057b77 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Dec 2017 12:15:28 +0100 Subject: [PATCH 17/30] Fix redundant HTTP request in FetchLinkCardService (#6002) --- app/lib/provider_discovery.rb | 19 ++++++-- app/services/fetch_link_card_service.rb | 58 ++++++++++++------------- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb index bcc4ed500e..04ba381010 100644 --- a/app/lib/provider_discovery.rb +++ b/app/lib/provider_discovery.rb @@ -2,13 +2,26 @@ class ProviderDiscovery < OEmbed::ProviderDiscovery class << self + def get(url, **options) + provider = discover_provider(url, options) + + options.delete(:html) + + provider.get(url, options) + end + def discover_provider(url, **options) - res = Request.new(:get, url).perform format = options[:format] - raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html' + if options[:html] + html = Nokogiri::HTML(options[:html]) + else + res = Request.new(:get, url).perform - html = Nokogiri::HTML(res.to_s) + raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html' + + html = Nokogiri::HTML(res.to_s) + end if format.nil? || format == :json provider_endpoint ||= html.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 09534d0ff0..d0472a1d7f 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -40,6 +40,12 @@ class FetchLinkCardService < BaseService return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html') + @response = Request.new(:get, @url).perform + + return if @response.code != 200 || @response.mime_type != 'text/html' + + @html = @response.to_s + attempt_oembed || attempt_opengraph end @@ -70,32 +76,32 @@ class FetchLinkCardService < BaseService end def attempt_oembed - response = OEmbed::Providers.get(@url) + embed = OEmbed::Providers.get(@url, html: @html) - return false unless response.respond_to?(:type) + return false unless embed.respond_to?(:type) - @card.type = response.type - @card.title = response.respond_to?(:title) ? response.title : '' - @card.author_name = response.respond_to?(:author_name) ? response.author_name : '' - @card.author_url = response.respond_to?(:author_url) ? response.author_url : '' - @card.provider_name = response.respond_to?(:provider_name) ? response.provider_name : '' - @card.provider_url = response.respond_to?(:provider_url) ? response.provider_url : '' + @card.type = embed.type + @card.title = embed.respond_to?(:title) ? embed.title : '' + @card.author_name = embed.respond_to?(:author_name) ? embed.author_name : '' + @card.author_url = embed.respond_to?(:author_url) ? embed.author_url : '' + @card.provider_name = embed.respond_to?(:provider_name) ? embed.provider_name : '' + @card.provider_url = embed.respond_to?(:provider_url) ? embed.provider_url : '' @card.width = 0 @card.height = 0 case @card.type when 'link' - @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url) + @card.image = URI.parse(embed.thumbnail_url) if embed.respond_to?(:thumbnail_url) when 'photo' - return false unless response.respond_to?(:url) - @card.embed_url = response.url - @card.image = URI.parse(response.url) - @card.width = response.width.presence || 0 - @card.height = response.height.presence || 0 + return false unless embed.respond_to?(:url) + @card.embed_url = embed.url + @card.image = URI.parse(embed.url) + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 when 'video' - @card.width = response.width.presence || 0 - @card.height = response.height.presence || 0 - @card.html = Formatter.instance.sanitize(response.html, Sanitize::Config::MASTODON_OEMBED) + @card.width = embed.width.presence || 0 + @card.height = embed.height.presence || 0 + @card.html = Formatter.instance.sanitize(embed.html, Sanitize::Config::MASTODON_OEMBED) when 'rich' # Most providers rely on