diff --git a/INSTALL.md b/INSTALL.md index 91729e336c..0311120436 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -33,58 +33,10 @@ Sudachiインストール終了後、追加で`/etc/elasticsearch/sudachi/config } ``` -## 新規インストールの場合 +## インストール手順 -1. 本家Mastodonとセットアップ手順はほとんど一緒です。kmyblueが独自に必須ソフトウェアを追加したわけではありません。ただしkmyblueはMastodonの開発中コードを取り込んでいるので、Rubyなどのバージョンアップ作業が必要になる場合があります。Mastodon公式のセットアップ手順を盲信せず、画面の指示に従ってインストールを進めてください。CloudFlareを組み合わせてセットアップしたとき、サーバーに接続すると400が出るなどのトラブルが出ることがありますが、大抵はMastodon本家由来のトラブルだと思われるので基本サポートはしません -2. ただひとつ差異があります。Gitリポジトリはこのkmyblueに向けてください。`kb_development`ブランチの最新コミットではなく、`kb`で始まる最新のタグを取り込むことを強くおすすめします +[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください -## 本家Mastodonからのマイグレーションの場合 +## アップデート手順 -kmyblueから本家Mastodonに戻りたい場合もあると思いますので、**必ずデータベースのバックアップをとってください**。 - -1. kmyblueのリリースノートに、kmyblueバージョンに対応した本家Mastodonのバージョンが記載されています。それを参照して、まず本家Mastodonをそのバージョンまでバージョンアップしてください -2. Gitのリモートにkmyblueを追加して、そのままチェックアウトしてください -3. データベースのマイグレーションなどを行ってください - -``` -sudo systemctl stop mastodon-* - -bundle install -yarn install -RAILS_ENV=production bin/rails db:migrate -RAILS_ENV=production bin/rails assets:clobber -RAILS_ENV=production bin/rails assets:precompile - -# ElasticSearchを使用する場合 -RAILS_ENV=production bin/tootctl search deploy - -RAILS_ENV=production bin/tootctl cache clear -sudo systemctl start mastodon-web mastodon-streaming mastodon-sidekiq -``` - -## kmyblueのバージョンをアップデートする - -リリースノートを参照して、自分に必要な作業を特定してください。面倒な場合は毎回全部実行してしまっても問題ありません。(プリコンパイルが失敗する可能性があるのでご注意ください) - -``` -# Rubyパッケージアップデート -bundle intall - -# JSパッケージアップデート -yarn install - -# DBマイグレーション -RAILS_ENV=production bin/rails db:migrate - -# プリコンパイル -# うまくいかない場合(エラーは出ないのにWeb表示が崩れる)はclobberしてからprecompile -# それでもうまくいかない場合はsudo systemctl stop mastodon-webしてから試す -# それでもうまくいかない場合はサーバーOSを再起動してから試す -RAILS_ENV=production bin/rails assets:clobber # プリコンパイルがうまくいかない場合 -RAILS_ENV=production bin/rails assets:precompile - -# サーバー再起動 -sudo systemctl restart mastodon-web -sudo systemctl restart mastodon-streaming -sudo systemctl restart mastodon-sidekiq -``` +[Wiki](https://github.com/kmycode/mastodon/wiki/Updation)を参照してください diff --git a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb index 92cd5228cc..9c2fb3d4a5 100644 --- a/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactioned_by_accounts_controller.rb @@ -33,7 +33,7 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll def paginated_emoji_reactions EmojiReaction.paginate_by_max_id( - limit_param(1000), # limit_param(DEFAULT_ACCOUNTS_LIMIT), + limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id] ) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 3ea159f019..1f682d1321 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -785,11 +785,12 @@ export function insertExpirationCompose(position, data) { }; } -export function insertReferenceCompose(position, url) { +export function insertReferenceCompose(position, url, attributeType) { return { type: COMPOSE_REFERENCE_INSERT, position, url, + attributeType, }; } diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index a5f6729f41..b361809309 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -51,6 +51,10 @@ export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST'; export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS'; export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL'; +export const EMOJI_REACTIONS_EXPAND_REQUEST = 'EMOJI_REACTIONS_EXPAND_REQUEST'; +export const EMOJI_REACTIONS_EXPAND_SUCCESS = 'EMOJI_REACTIONS_EXPAND_SUCCESS'; +export const EMOJI_REACTIONS_EXPAND_FAIL = 'EMOJI_REACTIONS_EXPAND_FAIL'; + export const PIN_REQUEST = 'PIN_REQUEST'; export const PIN_SUCCESS = 'PIN_SUCCESS'; export const PIN_FAIL = 'PIN_FAIL'; @@ -547,8 +551,9 @@ export function fetchEmojiReactions(id) { dispatch(fetchEmojiReactionsRequest(id)); api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data.map((er) => er.account))); - dispatch(fetchEmojiReactionsSuccess(id, response.data)); + dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); }).catch(error => { dispatch(fetchEmojiReactionsFail(id, error)); }); @@ -562,11 +567,12 @@ export function fetchEmojiReactionsRequest(id) { }; } -export function fetchEmojiReactionsSuccess(id, accounts) { +export function fetchEmojiReactionsSuccess(id, accounts, next) { return { type: EMOJI_REACTIONS_FETCH_SUCCESS, id, accounts, + next, }; } @@ -577,6 +583,48 @@ export function fetchEmojiReactionsFail(id, error) { }; } +export function expandEmojiReactions(id) { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next']); + if (url === null) { + return; + } + + dispatch(expandEmojiReactionsRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map((er) => er.account))); + dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null)); + }).catch(error => dispatch(expandEmojiReactionsFail(id, error))); + }; +} + +export function expandEmojiReactionsRequest(id) { + return { + type: EMOJI_REACTIONS_EXPAND_REQUEST, + id, + }; +} + +export function expandEmojiReactionsSuccess(id, accounts, next) { + return { + type: EMOJI_REACTIONS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +} + +export function expandEmojiReactionsFail(id, error) { + return { + type: EMOJI_REACTIONS_EXPAND_FAIL, + id, + error, + }; +} + export function fetchStatusReferences(id) { return (dispatch, getState) => { dispatch(fetchStatusReferencesRequest(id)); diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index d689b1ed03..02a42a92dd 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -52,6 +52,7 @@ const messages = defineMessages({ admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, reference: { id: 'status.reference', defaultMessage: 'Add reference' }, + quote: { id: 'status.quote', defaultMessage: 'Add ref (quote in other servers)' }, hide: { id: 'status.hide', defaultMessage: 'Hide post' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, @@ -97,6 +98,8 @@ class StatusActionBar extends ImmutablePureComponent { onBookmarkCategoryAdder: PropTypes.func, onFilter: PropTypes.func, onAddFilter: PropTypes.func, + onReference: PropTypes.func, + onQuote: PropTypes.func, onInteractionModal: PropTypes.func, withDismiss: PropTypes.bool, withCounters: PropTypes.bool, @@ -271,6 +274,10 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onReference(this.props.status); }; + handleQuote = () => { + this.props.onQuote(this.props.status); + }; + handleHideClick = () => { this.props.onFilter(); }; @@ -316,6 +323,7 @@ class StatusActionBar extends ImmutablePureComponent { if (publicStatus) { menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); + menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote }); } menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClickOriginal }); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 5023b7ef03..da3058334b 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -203,7 +203,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onReference (status) { - dispatch(insertReferenceCompose(0, status.get('url'))); + dispatch(insertReferenceCompose(0, status.get('url'), 'BT')); + }, + + onQuote (status) { + dispatch(insertReferenceCompose(0, status.get('url'), 'QT')); }, onTranslate (status) { diff --git a/app/javascript/mastodon/features/emoji_reactions/index.jsx b/app/javascript/mastodon/features/emoji_reactions/index.jsx index 78d088e947..5ab2a34958 100644 --- a/app/javascript/mastodon/features/emoji_reactions/index.jsx +++ b/app/javascript/mastodon/features/emoji_reactions/index.jsx @@ -8,7 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { fetchEmojiReactions } from 'mastodon/actions/interactions'; +import { debounce } from 'lodash'; + +import { fetchEmojiReactions, expandEmojiReactions } from 'mastodon/actions/interactions'; import ColumnHeader from 'mastodon/components/column_header'; import { Icon } from 'mastodon/components/icon'; import ScrollableList from 'mastodon/components/scrollable_list'; @@ -25,7 +27,9 @@ const messages = defineMessages({ const mapStateToProps = (state, props) => { return { - accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId]), + accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']), + hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']), + isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true), }; }; @@ -35,6 +39,8 @@ class EmojiReactions extends ImmutablePureComponent { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, + hasMore: PropTypes.bool, + isLoading: PropTypes.bool, multiColumn: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -45,18 +51,16 @@ class EmojiReactions extends ImmutablePureComponent { } } - componentWillReceiveProps (nextProps) { - if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { - this.props.dispatch(fetchEmojiReactions(nextProps.params.statusId)); - } - } - handleRefresh = () => { this.props.dispatch(fetchEmojiReactions(this.props.params.statusId)); }; + handleLoadMore = debounce(() => { + this.props.dispatch(expandEmojiReactions(this.props.params.statusId)); + }, 300, { leading: true }); + render () { - const { intl, accountIds, multiColumn } = this.props; + const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props; if (!accountIds) { return ( @@ -68,7 +72,7 @@ class EmojiReactions extends ImmutablePureComponent { let groups = {}; for (const emoji_reaction of accountIds) { - const key = emoji_reaction.account.id; + const key = emoji_reaction.account_id; const value = emoji_reaction; if (!groups[key]) groups[key] = [value]; else groups[key].push(value); @@ -82,12 +86,15 @@ class EmojiReactions extends ImmutablePureComponent { showBackButton multiColumn={multiColumn} extraButton={( - + )} /> diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index e80a83c907..56086075fc 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -46,6 +46,7 @@ const messages = defineMessages({ admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, reference: { id: 'status.reference', defaultMessage: 'Add reference' }, + quote: { id: 'status.quote', defaultMessage: 'Add ref (quote in other servers)' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, @@ -74,6 +75,7 @@ class ActionBar extends PureComponent { onFavourite: PropTypes.func.isRequired, onEmojiReact: PropTypes.func.isRequired, onReference: PropTypes.func.isRequired, + onQuote: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, onBookmarkCategoryAdder: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, @@ -208,6 +210,10 @@ class ActionBar extends PureComponent { this.props.onReference(this.props.status); }; + handleQuote = () => { + this.props.onQuote(this.props.status); + }; + handleEmojiPick = (data) => { this.props.onEmojiReact(this.props.status, data); }; @@ -248,6 +254,7 @@ class ActionBar extends PureComponent { if (publicStatus) { menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference }); + menu.push({ text: intl.formatMessage(messages.quote), action: this.handleQuote }); } menu.push({ text: intl.formatMessage(messages.bookmark_category), action: this.handleBookmarkCategoryAdderClick }); diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index cf4c2bd6ce..03a721d9c8 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -363,7 +363,11 @@ class Status extends ImmutablePureComponent { }; handleReference = (status) => { - this.props.dispatch(insertReferenceCompose(0, status.get('url'))); + this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'BT')); + }; + + handleQuote = (status) => { + this.props.dispatch(insertReferenceCompose(0, status.get('url'), 'QT')); }; handleBookmarkClick = (status) => { @@ -750,6 +754,7 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onReblogForceModal={this.handleReblogForceModalClick} onReference={this.handleReference} + onQuote={this.handleQuote} onBookmark={this.handleBookmarkClick} onBookmarkCategoryAdder={this.handleBookmarkCategoryAdderClick} onDelete={this.handleDeleteClick} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9de9b72162..b986b45b30 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -683,6 +683,7 @@ "status.open": "Expand this post", "status.pin": "Pin on profile", "status.pinned": "Pinned post", + "status.quote": "Ref (quote in other servers)", "status.read_more": "Read more", "status.reblog": "Boost", "status.reblog_private": "Boost with original visibility", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index d294d85e0b..e598512178 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -769,6 +769,7 @@ "status.open": "詳細を表示", "status.pin": "プロフィールに固定表示", "status.pinned": "固定された投稿", + "status.quote": "参照 (他サーバーで引用扱い)", "status.read_more": "もっと見る", "status.reblog": "ブースト", "status.reblog_private": "ブースト", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 59be1ae6da..612ee01d9b 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -252,10 +252,11 @@ const insertExpiration = (state, position, data) => { }); }; -const insertReference = (state, url) => { +const insertReference = (state, url, attributeType) => { const oldText = state.get('text'); + const attribute = attributeType || 'BT'; - if (oldText.indexOf(`BT ${url}`) >= 0) { + if (oldText.indexOf(`${attribute} ${url}`) >= 0) { return state; } @@ -271,12 +272,12 @@ const insertReference = (state, url) => { if (oldText.length > 0) { const lastLine = oldText.slice(oldText.lastIndexOf('\n') + 1, oldText.length - 1); - if (lastLine.startsWith('BT ')) { + if (lastLine.startsWith(`${attribute} `)) { newLine = '\n'; } } - const referenceText = `${newLine}BT ${url}`; + const referenceText = `${newLine}${attribute} ${url}`; const text = `${oldText}${referenceText}`; return state.merge({ @@ -526,7 +527,7 @@ export default function compose(state = initialState, action) { case COMPOSE_EXPIRATION_INSERT: return insertExpiration(state, action.position, action.data); case COMPOSE_REFERENCE_INSERT: - return insertReference(state, action.url); + return insertReference(state, action.url, action.attributeType); case COMPOSE_UPLOAD_CHANGE_SUCCESS: return state .set('is_changing_upload', false) diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index edc83bd3d4..2cb41cd03d 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -57,7 +57,12 @@ import { FAVOURITES_EXPAND_REQUEST, FAVOURITES_EXPAND_SUCCESS, FAVOURITES_EXPAND_FAIL, + EMOJI_REACTIONS_FETCH_REQUEST, EMOJI_REACTIONS_FETCH_SUCCESS, + EMOJI_REACTIONS_FETCH_FAIL, + EMOJI_REACTIONS_EXPAND_REQUEST, + EMOJI_REACTIONS_EXPAND_SUCCESS, + EMOJI_REACTIONS_EXPAND_FAIL, STATUS_REFERENCES_FETCH_SUCCESS, } from '../actions/interactions'; import { @@ -101,12 +106,33 @@ const normalizeList = (state, path, accounts, next) => { })); }; +const normalizeEmojiReactionList = (state, path, rows, next) => { + return state.setIn(path, ImmutableMap({ + next, + items: ImmutableList(rows.map(normalizeEmojiReactionRow)), + isLoading: false, + })); +}; + const appendToList = (state, path, accounts, next) => { return state.updateIn(path, map => { return map.set('next', next).set('isLoading', false).update('items', list => list.concat(accounts.map(item => item.id))); }); }; +const appendToEmojiReactionList = (state, path, rows, next) => { + return state.updateIn(path, map => { + return map.set('next', next).set('isLoading', false).update('items', list => list.concat(rows.map(normalizeEmojiReactionRow))); + }); +}; + +const normalizeEmojiReactionRow = (row) => { + const accountId = row.account ? row.account.id : 0; + delete row.account; + row.account_id = accountId; + return row; +}; + const normalizeFollowRequest = (state, notification) => { return state.updateIn(['follow_requests', 'items'], list => { return list.filterNot(item => item === notification.account.id).unshift(notification.account.id); @@ -167,8 +193,16 @@ export default function userLists(state = initialState, action) { case FAVOURITES_FETCH_FAIL: case FAVOURITES_EXPAND_FAIL: return state.setIn(['favourited_by', action.id, 'isLoading'], false); + case EMOJI_REACTIONS_FETCH_REQUEST: + case EMOJI_REACTIONS_EXPAND_REQUEST: + return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], true); + case EMOJI_REACTIONS_FETCH_FAIL: + case EMOJI_REACTIONS_EXPAND_FAIL: + return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], false); case EMOJI_REACTIONS_FETCH_SUCCESS: - return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts)); + return normalizeEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next); + case EMOJI_REACTIONS_EXPAND_SUCCESS: + return appendToEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next); case STATUS_REFERENCES_FETCH_SUCCESS: return state.setIn(['referred_by', action.id], ImmutableList(action.statuses.map(item => item.id))); case NOTIFICATIONS_UPDATE: diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 57cd9ca415..08b546561d 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -46,6 +46,7 @@ class Form::AdminSettings receive_other_servers_emoji_reaction streaming_other_servers_emoji_reaction enable_emoji_reaction + check_lts_version_only ).freeze INTEGER_KEYS = %i( @@ -72,6 +73,7 @@ class Form::AdminSettings receive_other_servers_emoji_reaction streaming_other_servers_emoji_reaction enable_emoji_reaction + check_lts_version_only ).freeze UPLOAD_KEYS = %i( diff --git a/app/models/software_update.rb b/app/models/software_update.rb index cb3a6df2ae..4aeb7e665b 100644 --- a/app/models/software_update.rb +++ b/app/models/software_update.rb @@ -36,5 +36,13 @@ class SoftwareUpdate < ApplicationRecord def urgent_pending? pending_to_a.any?(&:urgent?) end + + def major_pending? + pending_to_a.any?(&:major_type?) + end + + def patch_pending? + pending_to_a.any?(&:patch_type?) + end end end diff --git a/app/models/status.rb b/app/models/status.rb index f9587b3c71..9f0299a511 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -226,6 +226,10 @@ class Status < ApplicationRecord !reblog_of_id.nil? end + def quote + reference_objects.where(attribute_type: 'QT').first&.target_status + end + def within_realtime_window? created_at >= REAL_TIME_WINDOW.ago end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 109f8115df..d8f7a328ed 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -17,7 +17,6 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :quote_uri, if: :quote? attribute :misskey_quote, key: :_misskey_quote, if: :quote? - attribute :misskey_content, key: :_misskey_content, if: :quote? has_many :virtual_attachments, key: :attachment has_many :virtual_tags, key: :tag @@ -172,21 +171,21 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def quote? - object.references.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote') + @quote ||= (object.reference_objects.count == 1 && object.account.user&.settings&.[]('single_ref_to_quote')) || object.reference_objects.where(attribute_type: 'QT').count == 1 + end + + def quote_post + @quote_post ||= object.quote || object.references.first end def quote_uri - ActivityPub::TagManager.instance.uri_for(object.references.first) + ActivityPub::TagManager.instance.uri_for(quote_post) end def misskey_quote quote_uri end - def misskey_content - object.text - end - def poll_options object.preloadable_poll.loaded_options end diff --git a/app/services/delivery_antenna_service.rb b/app/services/delivery_antenna_service.rb index e494321917..53bcc226e7 100644 --- a/app/services/delivery_antenna_service.rb +++ b/app/services/delivery_antenna_service.rb @@ -47,8 +47,8 @@ class DeliveryAntennaService end antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)) - antennas = antennas.where(account: @status.account.followers) if [:public, :public_unlisted, :login, :limited].exclude?(@status.visibility.to_sym) && !@status.public_searchability? - antennas = antennas.where(account: @status.mentioned_accounts) if @status.visibility.to_sym == :limited + antennas = antennas.where(account: @status.account.followers) if followers_only? + antennas = antennas.where(account: @status.mentioned_accounts) if mentioned_users_only? antennas = antennas.where(with_media_only: false) unless @status.with_media? antennas = antennas.where(ignore_reblog: false) if @status.reblog? antennas = antennas.where(stl: false, ltl: false) @@ -116,6 +116,26 @@ class DeliveryAntennaService collection.deliver! end + def followers_only? + case @status.visibility.to_sym + when :public, :public_unlisted, :login, :limited + false + when :unlisted + if @status.local? + !@status.public_searchability? + else + info = InstanceInfo.find_by(domain: @status.account.domain) + info&.software == 'firefish' || !@status.public_searchability? + end + else + true + end + end + + def mentioned_users_only? + @status.visibility.to_sym == :limited + end + class AntennaCollection def initialize(status, update, stl_home = false) # rubocop:disable Style/OptionalBooleanParameter @status = status diff --git a/app/services/process_references_service.rb b/app/services/process_references_service.rb index 05aa5ef345..faf5965a7a 100644 --- a/app/services/process_references_service.rb +++ b/app/services/process_references_service.rb @@ -58,11 +58,11 @@ class ProcessReferencesService < BaseService private def references - @references = @reference_parameters + scan_text! + @references ||= @reference_parameters + scan_text! end def old_references - @old_references = @status.references.pluck(:id) + @old_references ||= @status.references.pluck(:id) end def added_references @@ -112,7 +112,7 @@ class ProcessReferencesService < BaseService def create_notifications! return if @added_objects.blank? - local_reference_objects = @added_objects.filter { |ref| ref.target_status.account.local? } + local_reference_objects = @added_objects.filter { |ref| ref.target_status.account.local? && StatusPolicy.new(ref.target_status.account, ref.status).show? } return if local_reference_objects.empty? LocalNotificationWorker.push_bulk(local_reference_objects) do |ref| diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb index 49b92f104d..b20fb8611b 100644 --- a/app/services/software_update_check_service.rb +++ b/app/services/software_update_check_service.rb @@ -27,11 +27,18 @@ class SoftwareUpdateCheckService < BaseService end def api_url - ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check') + ENV.fetch('UPDATE_CHECK_URL', 'https://kmy.blue/update-check') end def version - @version ||= Mastodon::Version.to_s.split('+')[0] + if ENV.fetch('UPDATE_CHECK_SOURCE', 'kmyblue') == 'kmyblue' + @version = "#{Mastodon::Version.kmyblue_major}.#{Mastodon::Version.kmyblue_minor}" + @version += '-lts' if Setting.check_lts_version_only + else + @version = Mastodon::Version.to_s.split('+')[0] + end + + @version end def process_update_notices!(update_notices) diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index 60443e6877..6ea9e4fb4b 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -55,6 +55,9 @@ .fields-group = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil + .fields-group + = f.input :check_lts_version_only, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false + %h4= t('admin.settings.discovery.follow_recommendations') .fields-group diff --git a/app/views/settings/preferences/reaching/show.html.haml b/app/views/settings/preferences/reaching/show.html.haml index b3d22e8052..cfeaeff68e 100644 --- a/app/views/settings/preferences/reaching/show.html.haml +++ b/app/views/settings/preferences/reaching/show.html.haml @@ -31,7 +31,7 @@ .fields-row .fields-group.fields-row__column.fields-row__column-12 - = ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability') + = ff.input :default_searchability, collection: Status.selectable_searchabilities, wrapper: :with_label, kmyblue: true, include_blank: false, label_method: lambda { |searchability| safe_join([I18n.t("statuses.searchabilities.#{searchability}"), I18n.t("statuses.searchabilities.#{searchability}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_default_searchability') .fields-group = ff.input :disallow_unlisted_public_searchability, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_disallow_unlisted_public_searchability'), hint: I18n.t('simple_form.hints.defaults.setting_disallow_unlisted_public_searchability') diff --git a/config/locales/en.yml b/config/locales/en.yml index d9d0bb084a..4e477ba146 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -997,6 +997,9 @@ en: other: Used by %{count} people over the last week title: Trends trending: Trending + update-pendings: + major: Major update pending + patch: Patch update pending warning_presets: add_new: Add new delete: Delete diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 286e19d42f..4d6d4e4c6a 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -991,6 +991,9 @@ ja: other: 週間%{count}人に使用されました title: トレンド trending: トレンド + update_pendings: + major: メジャーアップデートあり + patch: パッチアップデートあり warning_presets: add_new: 追加 delete: 削除 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 738e5f39c2..aa17a42816 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -62,6 +62,7 @@ en: setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon setting_bookmark_category_needed: When removing from all category, unbookmarked automatically + setting_default_searchability: kmyblue・Fedibirdでは検索許可設定に基づき検索されます。Misskeyでは当設定に関係なく、全ての公開・ローカル公開・未収載投稿が検索されます。Mastodon・Firefishでは検索許可の代わりにプロフィール設定の「公開投稿を他のサーバーで自由に検索できるようにする」設定が適用されます setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media @@ -315,6 +316,7 @@ en: activity_api_enabled: Publish aggregate statistics about user activity in the API backups_retention_period: User archive retention period bootstrap_timeline_accounts: Always recommend these accounts to new users + check_lts_version_only: Check kmyblue LTS version only when update check closed_registrations_message: Custom message when sign-ups are not available content_cache_retention_period: Content cache retention period custom_css: Custom CSS diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 5c72720a33..e255c43cd4 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -65,6 +65,7 @@ ja: setting_always_send_emails: 通常、Mastodon からメール通知は行われません。 setting_bookmark_category_needed: すべてのカテゴリから削除したとき、ブックマークが自動で外れるようになります setting_boost_modal: ブーストの公開範囲が指定できるようになります + setting_default_searchability: kmyblue・Fedibirdでは検索許可設定に基づき検索されます。Misskeyでは当設定に関係なく、全ての公開・ローカル公開・未収載投稿が検索されます。Mastodon・Firefishでは検索許可の代わりにプロフィール設定の「公開投稿を他のサーバーで自由に検索できるようにする」設定が適用されます setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります setting_disallow_unlisted_public_searchability: この設定を有効にすると、未収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります setting_display_media_default: 閲覧注意としてマークされたメディアは隠す @@ -330,6 +331,7 @@ ja: activity_api_enabled: APIでユーザーアクティビティに関する集計統計を公開する backups_retention_period: ユーザーアーカイブの保持期間 bootstrap_timeline_accounts: おすすめユーザーに常に表示するアカウント + check_lts_version_only: 更新チェックの時、LTSバージョンのみ確認する closed_registrations_message: アカウント作成を停止している時のカスタムメッセージ content_cache_retention_period: コンテンツキャッシュの保持期間 custom_css: カスタムCSS diff --git a/config/navigation.rb b/config/navigation.rb index ce8c44d542..bdb86f06a7 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -4,7 +4,11 @@ SimpleNavigation::Configuration.run do |navigation| navigation.items do |n| n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path - n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' } + if ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) + n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { SoftwareUpdate.urgent_pending? }, html: { class: 'warning' } + n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.update_pendings.major')]), admin_software_updates_path, if: -> { !SoftwareUpdate.urgent_pending? && SoftwareUpdate.major_pending? }, html: { class: 'warning' } + n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.update_pendings.patch')]), admin_software_updates_path, if: -> { !SoftwareUpdate.urgent_pending? && SoftwareUpdate.patch_pending? }, html: { class: 'warning' } + end n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy} diff --git a/config/settings.yml b/config/settings.yml index 0042b5822e..69b3ed1ee3 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -41,6 +41,7 @@ defaults: &defaults receive_other_servers_emoji_reaction: false streaming_other_servers_emoji_reaction: false enable_emoji_reaction: true + check_lts_version_only: true development: <<: *defaults diff --git a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb index 3e9ab134b7..d29d7847c5 100644 --- a/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb +++ b/db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb @@ -18,7 +18,7 @@ class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1] def supports_concurrent_reindex? @supports_concurrent_reindex ||= begin version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i - version >= 12_000 + version >= 120_000 end end diff --git a/lib/mastodon/migration_helpers.rb b/lib/mastodon/migration_helpers.rb index 4a43f67c27..c382b5fbd5 100644 --- a/lib/mastodon/migration_helpers.rb +++ b/lib/mastodon/migration_helpers.rb @@ -202,7 +202,7 @@ module Mastodon def supports_add_column_with_default? version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i - version >= 11_000 + version >= 110_000 end # Adds a foreign key with only minimal locking on the tables involved. diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 0086555a4e..e5d7b0ae8f 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -62,7 +62,11 @@ module Mastodon end def gem_version - @gem_version ||= Gem::Version.new(to_s.split('+')[0]) + @gem_version ||= if ENV.fetch('UPDATE_CHECK_SOURCE', 'kmyblue') == 'kmyblue' + Gem::Version.new("#{kmyblue_major}.#{kmyblue_minor}") + else + Gem::Version.new(to_s.split('+')[0]) + end end def repository diff --git a/public/robots.txt b/public/robots.txt index 6672eeba1f..3bd45adaef 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -3,6 +3,9 @@ User-agent: GPTBot Disallow: / +User-agent: CCBot +Disallow: / + User-agent: * Disallow: /media_proxy/ Disallow: /interact/ diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index f4248e548e..0425e2e66b 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -73,7 +73,6 @@ describe ActivityPub::NoteSerializer do expect(subject['quoteUri']).to_not be_nil expect(subject['quoteUri']).to eq referred.uri expect(subject['_misskey_quote']).to eq referred.uri - expect(subject['_misskey_content']).to eq referred.text expect(subject['references']['first']['items']).to include referred.uri end end diff --git a/spec/services/delivery_antenna_service_spec.rb b/spec/services/delivery_antenna_service_spec.rb index 26c23eb930..e9620dc083 100644 --- a/spec/services/delivery_antenna_service_spec.rb +++ b/spec/services/delivery_antenna_service_spec.rb @@ -12,6 +12,7 @@ RSpec.describe DeliveryAntennaService, type: :service do let(:domain) { nil } let(:spoiler_text) { '' } let(:tags) { Tag.find_or_create_by_names(['hoge']) } + let(:software) { nil } let(:status) do url = domain.present? ? 'https://example.com/status' : nil status = Fabricate(:status, account: alice, spoiler_text: spoiler_text, visibility: visibility, searchability: searchability, text: 'Hello my body #hoge', url: url) @@ -30,6 +31,8 @@ RSpec.describe DeliveryAntennaService, type: :service do let(:mode) { :home } before do + Fabricate(:instance_info, domain: domain, software: software) if domain.present? && software.present? + bob.follow!(alice) alice.block!(ohagi) @@ -359,4 +362,42 @@ RSpec.describe DeliveryAntennaService, type: :service do expect(antenna_feed_of(antenna)).to include status.id end end + + context 'with federated unlisted post' do + let(:visibility) { :unlisted } + let(:searchability) { :public } + let(:domain) { 'fast.example.com' } + let!(:antenna) { antenna_with_keyword(bob, 'body') } + let!(:empty_antenna) { antenna_with_keyword(tom, 'body') } + + context 'when unknown domain' do + let(:software) { nil } + + it 'detecting antenna' do + expect(antenna_feed_of(antenna)).to include status.id + expect(antenna_feed_of(empty_antenna)).to include status.id + end + end + + context 'when misskey domain' do + let(:software) { 'misskey' } + + it 'detecting antenna' do + expect(antenna_feed_of(antenna)).to include status.id + expect(antenna_feed_of(empty_antenna)).to include status.id + end + end + + context 'when firefish domain' do + let(:software) { 'firefish' } + + it 'detecting antenna' do + expect(antenna_feed_of(antenna)).to include status.id + end + + it 'not detecting antenna' do + expect(antenna_feed_of(empty_antenna)).to_not include status.id + end + end + end end diff --git a/spec/services/process_references_service_spec.rb b/spec/services/process_references_service_spec.rb index e9099fb935..c41144a2aa 100644 --- a/spec/services/process_references_service_spec.rb +++ b/spec/services/process_references_service_spec.rb @@ -6,10 +6,16 @@ RSpec.describe ProcessReferencesService, type: :service do let(:text) { 'Hello' } let(:account) { Fabricate(:user).account } let(:visibility) { :public } + let(:target_status_visibility) { :public } let(:status) { Fabricate(:status, account: account, text: text, visibility: visibility) } - let(:target_status) { Fabricate(:status, account: Fabricate(:user).account) } + let(:target_status) { Fabricate(:status, account: Fabricate(:user).account, visibility: target_status_visibility) } let(:target_status_uri) { ActivityPub::TagManager.instance.uri_for(target_status) } + def notify?(target_status_id = nil) + target_status_id ||= target_status.id + StatusReference.exists?(id: Notification.where(type: 'status_reference').select(:activity_id), target_status_id: target_status_id) + end + describe 'posting new status' do subject do described_class.new.call(status, reference_parameters, urls: urls, fetch_remote: fetch_remote) @@ -27,6 +33,7 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 1 expect(subject.pluck(0)).to include target_status.id expect(subject.pluck(1)).to include 'RT' + expect(notify?).to be true end end @@ -39,6 +46,59 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 2 expect(subject).to include [target_status.id, 'RT'] expect(subject).to include [target_status2.id, 'BT'] + expect(notify?).to be true + expect(notify?(target_status2.id)).to be true + end + end + + context 'when private post' do + let(:text) { "Hello RT #{target_status_uri}" } + let(:visibility) { :private } + + it 'post status' do + expect(subject.size).to eq 1 + expect(subject.pluck(0)).to include target_status.id + expect(subject.pluck(1)).to include 'RT' + expect(notify?).to be false + end + end + + context 'when cannot show private post' do + let(:text) { "Hello RT #{target_status_uri}" } + let(:target_status_visibility) { :private } + + it 'post status' do + expect(subject.size).to eq 0 + expect(notify?).to be false + end + end + + context 'with quote' do + let(:text) { "Hello QT #{target_status_uri}" } + + it 'post status' do + expect(subject.size).to eq 1 + expect(subject.pluck(0)).to include target_status.id + expect(subject.pluck(1)).to include 'QT' + expect(status.quote).to_not be_nil + expect(status.quote.id).to eq target_status.id + expect(notify?).to be true + end + end + + context 'with quote and reference' do + let(:target_status2) { Fabricate(:status) } + let(:target_status2_uri) { ActivityPub::TagManager.instance.uri_for(target_status2) } + let(:text) { "Hello QT #{target_status_uri}\nBT #{target_status2_uri}" } + + it 'post status' do + expect(subject.size).to eq 2 + expect(subject).to include [target_status.id, 'QT'] + expect(subject).to include [target_status2.id, 'BT'] + expect(status.quote).to_not be_nil + expect(status.quote.id).to eq target_status.id + expect(notify?).to be true + expect(notify?(target_status2.id)).to be true end end @@ -47,6 +107,7 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status' do expect(subject.size).to eq 0 + expect(notify?).to be false end end @@ -143,6 +204,7 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status' do expect(subject.size).to eq 1 expect(subject).to include target_status.id + expect(notify?).to be true end end @@ -154,6 +216,7 @@ RSpec.describe ProcessReferencesService, type: :service do expect(subject.size).to eq 2 expect(subject).to include target_status.id expect(subject).to include target_status2.id + expect(notify?(target_status2.id)).to be true end end @@ -173,6 +236,7 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status' do expect(subject.size).to eq 0 + expect(notify?).to be false end end @@ -183,6 +247,7 @@ RSpec.describe ProcessReferencesService, type: :service do it 'post status' do expect(subject.size).to eq 1 expect(subject).to include target_status2.id + expect(notify?(target_status2.id)).to be true end end end diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb index c8821348ac..7cc99bb09c 100644 --- a/spec/services/software_update_check_service_spec.rb +++ b/spec/services/software_update_check_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe SoftwareUpdateCheckService, type: :service do subject { described_class.new } shared_examples 'when the feature is enabled' do - let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" } + let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.kmyblue_major}.#{Mastodon::Version.kmyblue_minor}-lts" } let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) } let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } @@ -139,7 +139,7 @@ RSpec.describe SoftwareUpdateCheckService, type: :service do end context 'when using the default update checking API' do - let(:update_check_url) { 'https://api.joinmastodon.org/update-check' } + let(:update_check_url) { 'https://kmy.blue/update-check' } it_behaves_like 'when the feature is enabled' end