Merge branch 'kb_development' into kb_migration
This commit is contained in:
commit
3742720b5d
23 changed files with 386 additions and 59 deletions
65
README.md
65
README.md
|
@ -2,36 +2,36 @@
|
|||
|
||||
[](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml)
|
||||
|
||||
kmyblue は[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のための Mastodon を目指して開発しました。
|
||||
kmyblueは[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のためのMastodonを目指して開発しました。
|
||||
|
||||
kmyblue はフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしての kmyblue をさします。
|
||||
kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。
|
||||
|
||||
kmyblue は AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。また ActivityPub に参加することもできます。サーバーkmyblueは創作作家向けのものですが、フォークとしてのkmyblueは作者の嫌いな政治に関する過激な話を取り扱うコミュニティ、創作活動の一部(エロ関係含む)または全体を否定するコミュニティなども平等にお使いいただけますし、サーバーkmyblueのルールを適用する必要もありません。
|
||||
ただし kmyblue においてテストコードは飾りでしかないため、不具合が発生しても自己責任になります。既知のバグもいくつかありますし、直す予定のないものも含まれます。
|
||||
kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。また ActivityPub に参加することもできます。サーバーkmyblueは創作作家向けのものですが、フォークとしてのkmyblueは作者の嫌いな政治に関する過激な話を取り扱うコミュニティ、創作活動の一部(エロ関係含む)または全体を否定するコミュニティなども平等にお使いいただけますし、サーバーkmyblueのルールを適用する必要もありません。
|
||||
ただしkmyblueにおいてテストコードは飾りでしかないため、不具合が発生しても自己責任になります。既知のバグもいくつかありますし、直す予定のないものも含まれます。
|
||||
|
||||
テストコード、Lint どちらも動いています。
|
||||
|
||||
## kmyblue の強み
|
||||
## kmyblueの強み
|
||||
|
||||
### 本家 Mastodon への積極的追従
|
||||
### 本家Mastodonへの積極的追従
|
||||
|
||||
kmyblue は、いくつかのフォークと異なり、追加機能を控えめにする代わりに本家 Mastodon に積極的に追従を行います。バージョン 4 には 4 のよさがありますが、技術的に可能である限り、バージョン 5 へのアップグレードもやぶさかではありません。
|
||||
kmyblue の追加機能そのままに、Mastodon の新機能も利用できるよう調整を行います。
|
||||
kmyblueは、いくつかのフォークと異なり、追加機能を控えめにする代わりに本家Mastodonに積極的に追従を行います。バージョン 4 には 4 のよさがありますが、技術的に可能である限り、バージョン 5 へのアップグレードもやぶさかではありません。
|
||||
kmyblueの追加機能そのままに、Mastodonの新機能も利用できるよう調整を行います。
|
||||
|
||||
### 絵文字リアクション対応
|
||||
|
||||
kmyblue は絵文字リアクションに対応しているフォークの1つです。絵文字リアクションは Misskey 標準搭載の機能で、需要が高い機能である割には、サーバーに負荷がかかるため本家 Mastodon には搭載されていません。
|
||||
kmyblueは絵文字リアクションに対応しているフォークの1つです。絵文字リアクションは Misskey 標準搭載の機能で、需要が高い機能である割には、サーバーに負荷がかかるため本家Mastodonには搭載されていません。
|
||||
|
||||
## kmyblue のブランチ
|
||||
## kmyblueのブランチ
|
||||
|
||||
- **main** - 現在はメンテナンスされていません
|
||||
- **kb_development** - 現在 kmyblue 本体で使われているソースコードです
|
||||
- **kb_migration** - 本家 Mastodon への追従を目的としたブランチです。`kb_development`上で開発を進めているときに利用します
|
||||
- **kb_migration_development** - 本家 Mastodon へ追従し、かつその上で開発するときに使うブランチです。最新の本家コードでリファクタリングが行われ、`kb_development`と`kb_migration`の互換性の維持が困難になったときに利用します。ここで追加された機能は原則、本家 Mastodon のバージョンアップと同時に`kb_development`に反映されます
|
||||
- **kb_development** - 現在kmyblue本体で使われているソースコードです
|
||||
- **kb_migration** - 本家Mastodonへの追従を目的としたブランチです。`kb_development`上で開発を進めているときに利用します
|
||||
- **kb_migration_development** - 本家Mastodonへ追従し、かつその上で開発するときに使うブランチです。最新の本家コードでリファクタリングが行われ、`kb_development`と`kb_migration`の互換性の維持が困難になったときに利用します。ここで追加された機能は原則、本家Mastodonのバージョンアップと同時に`kb_development`に反映されます
|
||||
|
||||
## 本家 Mastodon からの追加機能
|
||||
## 本家Mastodonからの追加機能
|
||||
|
||||
kmyblue は、本家 Mastodon にいくつかの改造を加えています。以下に示します。
|
||||
kmyblueは、本家Mastodonにいくつかの改造を加えています。以下に示します。
|
||||
|
||||
### ローカル公開
|
||||
|
||||
|
@ -39,24 +39,35 @@ kmyblue は、本家 Mastodon にいくつかの改造を加えています。
|
|||
|
||||
### スタンプ(絵文字リアクション)
|
||||
|
||||
kmyblue 内での呼称はスタンプですが、一般には絵文字リアクションと呼ばれる機能です。自分や他人の投稿に絵文字をつけることができます。kmyblue のスタンプは Fedibird の絵文字リアクションと互換性のある API を持っているため、Fedibird 対応アプリで kmyblue のスタンプ機能を利用できる場合があります。
|
||||
kmyblue内での呼称はスタンプですが、一般には絵文字リアクションと呼ばれる機能です。自分や他人の投稿に絵文字をつけることができます。kmyblueのスタンプは Fedibird の絵文字リアクションと互換性のある API を持っているため、Fedibird 対応アプリでkmyblueのスタンプ機能を利用できる場合があります。
|
||||
|
||||
kmyblue は、1つのアカウントが1つの投稿に複数のスタンプ(絵文字リアクション)を最大3個までつけることが可能です。また、下記機能にも対応しています。
|
||||
kmyblueは、1つのアカウントが1つの投稿に複数のスタンプ(絵文字リアクション)を最大3個までつけることが可能です。また、下記機能にも対応しています。
|
||||
|
||||
- 他のサーバーのアカウントがつけたスタンプに相乗りする
|
||||
- 自分がスタンプを付けた投稿一覧を見る
|
||||
- トレンド投稿の選定条件にスタンプを付けたアカウントの数を考慮する
|
||||
- 投稿の自動削除で削除条件にスタンプの数を指定する
|
||||
|
||||
kmyblue は、他のサーバーの投稿にスタンプをつけることで、相手サーバーに情報を送信します。ただしスタンプに対応していないサーバーにおいては、通知されることはありません。
|
||||
kmyblueは、他のサーバーの投稿にスタンプをつけることで、相手サーバーに情報を送信します。ただしスタンプに対応していないサーバーにおいては、通知されることはありません。
|
||||
|
||||
### アンテナ
|
||||
|
||||
「自分はフォローしていないが連合タイムラインに流れているアカウント」の投稿を購読することが可能です。アンテナはドメイン、アカウント、ハッシュタグ、キーワードの4種類の絞り込み条件を持ち、複合指定することで AND 条件として働きます。アンテナによって検出された投稿は、指定されたリスト、またはホームタイムラインに追加されます。
|
||||
「自分はフォローしていないが連合タイムラインに流れているアカウント」の投稿を購読することが可能です。アンテナはドメイン、アカウント、ハッシュタグ、キーワードの4種類の絞り込み条件を持ち、複合指定することで AND 条件として働きます。アンテナによって検出された投稿は、指定されたリスト、またはホームタイムラインに追加されます。
|
||||
アンテナ専用のタイムラインも存在し、ここではアンテナで拾った投稿が流れます。これはリストやホームに配置しなくても容易に確認できます。
|
||||
|
||||
ドメイン購読において、自分自身のドメインを指定できることも特長の1つです。また、STL(ソーシャルタイムライン)モードにも対応しています。
|
||||
|
||||
自分の投稿がアンテナに検出されるのを拒否することもできます。この拒否設定は、ActivityPub で他サーバーにも共有されますが、対応するかはそれぞれの判断に委ねられます。
|
||||
絞り込まれた投稿は、さらにドメイン、アカウント、ハッシュタグ、キーワードの4種類の条件を指定して除外することができます。これはOR条件として働きます。
|
||||
|
||||
アンテナの条件指定は複雑ですが、Webクライアントに搭載された編集画面では、事前の説明がなくても条件指定の落とし穴に気づきやすいようにしています。
|
||||
|
||||
自分の投稿がアンテナに検出されるのを拒否することもできます。この拒否設定は、ActivityPubで他サーバーにも共有されますが、対応するかはそれぞれの判断に委ねられます。
|
||||
|
||||
### サークル
|
||||
|
||||
自分のフォロワーの中でも特に対象を絞ってサークルという単位にまとめ、対象アカウントのみが閲覧可能な投稿を送信できます。ただしこれはMastodonサーバーとしか共有できません。(4.2.0-beta2現在、本家Mastodonではバグのため正常に受信できません)
|
||||
|
||||
相互フォロー限定投稿にも対応しています。
|
||||
|
||||
### 期間限定投稿
|
||||
|
||||
|
@ -66,7 +77,7 @@ kmyblue は、他のサーバーの投稿にスタンプをつけることで、
|
|||
|
||||
### グループ
|
||||
|
||||
kmyblue はグループ機能に対応しています。グループのフォロワーからグループアカウントへのメンションはすべてブーストされ、グループのフォロワー全員に届きます。なおこれは本家 Mastodon でも今後実装予定の機能です。
|
||||
kmyblueはグループ機能に対応しています。グループのフォロワーからグループアカウントへのメンションはすべてブーストされ、グループのフォロワー全員に届きます。なおこれは本家Mastodonでも今後実装予定の機能です。
|
||||
|
||||
### ドメインブロックの拡張
|
||||
|
||||
|
@ -81,7 +92,7 @@ kmyblue はグループ機能に対応しています。グループのフォロ
|
|||
|
||||
#### Misskey 対策
|
||||
|
||||
Misskey およびそのフォーク(Calckey など)は、**フォローしていないアカウントの未収載投稿**を自由に検索・購読することができます。これは Mastodon の設計とは根本的に異なる仕様です。kmyblue では、別途手動でドメインブロックデータにフラグを付けたサーバーに限り、そのサーバーに未収載投稿を送信するときに「フォロワーのみ」に変更します。他のサーバーには未収載として送信されます。この動作には、管理者だけでなくユーザー各自の設定も必要になります。
|
||||
Misskey およびそのフォーク(Calckey など)は、**フォローしていないアカウントの未収載投稿**を自由に検索・購読することができます。これはMastodonの設計とは根本的に異なる仕様です。kmyblueでは、そのサーバーに未収載投稿を送信するときに「フォロワーのみ」に変更します。他のサーバーには未収載として送信されます。この動作は新規登録したばかりのユーザーにおいてはデフォルトではオフとなっており、ユーザー各自の設定が必要になります。
|
||||
|
||||
### モデレーションの拡張
|
||||
|
||||
|
@ -109,11 +120,11 @@ Misskey およびそのフォーク(Calckey など)では、MFM を利用す
|
|||
|
||||
### 投票項目数の拡張
|
||||
|
||||
投票について、本家 Mastodon では4項目までですが、kmyblue では8個までに拡張しています。
|
||||
投票について、本家Mastodonでは4項目までですが、kmyblueでは8個までに拡張しています。
|
||||
|
||||
### 連合から送られてくる投稿の添付画像最大数の拡張
|
||||
|
||||
本家 Mastodon では4個までですが、kmyblue では8個までに拡張しています。ただし Web クライアントでの表示には、各自ユーザーによる設定が必要です。kmyblue ローカルから投稿できる画像の枚数に変更はありません。
|
||||
本家Mastodonでは4個までですが、kmyblueでは8個までに拡張しています。ただし Web クライアントでの表示には、各自ユーザーによる設定が必要です。kmyblueローカルから投稿できる画像の枚数に変更はありません。
|
||||
|
||||
### 検索許可
|
||||
|
||||
|
@ -121,11 +132,13 @@ Misskey およびそのフォーク(Calckey など)では、MFM を利用す
|
|||
|
||||
これは Fedibird の「検索範囲」機能に対応します。API に互換性はありませんが、ActivityPub 仕様は共通しています。
|
||||
|
||||
なおMisskeyからの投稿は、検索許可が自動的に「全て」になります。
|
||||
|
||||
### トレンドの拡張
|
||||
|
||||
本家マストドンでは、センシティブフラグのついた全ての投稿がトレンドに掲載されません。kmyblue はその中でも、「センシティブフラグはついているが、画像が添付されておらず CW 付きでもない」投稿をトレンドに掲載します。
|
||||
本家マストドンでは、センシティブフラグのついた全ての投稿がトレンドに掲載されません。kmyblueはその中でも、「センシティブフラグはついているが、画像が添付されておらず CW 付きでもない」投稿をトレンドに掲載します。
|
||||
|
||||
本来このような投稿はトレンドに掲載すべきでありませんが、本家 Mastodon の Web クライアントでは文章だけの投稿のセンシティブフラグを自由に操作できないことを理由とした独自対応となります。
|
||||
本来このような投稿はトレンドに掲載すべきでありませんが、本家Mastodonの Web クライアントでは文章だけの投稿のセンシティブフラグを自由に操作できないことを理由とした独自対応となります。
|
||||
|
||||
### Sidekiq ヘルスチェック
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::ExcludeAntennasController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
def index
|
||||
@antennas = @account.suspended? ? [] : current_account.antennas.where('exclude_accounts @> \'[?]\'', @account.id)
|
||||
render json: @antennas, each_serializer: REST::AntennaSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
|
@ -122,6 +122,10 @@ export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETC
|
|||
export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS';
|
||||
export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL';
|
||||
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST';
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS';
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL';
|
||||
|
||||
export const fetchAntenna = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['antennas', id])) {
|
||||
return;
|
||||
|
@ -905,6 +909,15 @@ export const setupAntennaAdder = accountId => (dispatch, getState) => {
|
|||
dispatch(fetchAccountAntennas(accountId));
|
||||
};
|
||||
|
||||
export const setupExcludeAntennaAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ANTENNA_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchAntennas());
|
||||
dispatch(fetchExcludeAccountAntennas(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountAntennas = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountAntennasRequest(accountId));
|
||||
|
||||
|
@ -930,6 +943,31 @@ export const fetchAccountAntennasFail = (id, err) => ({
|
|||
err,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennas = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchExcludeAccountAntennasRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/exclude_antennas`)
|
||||
.then(({ data }) => dispatch(fetchExcludeAccountAntennasSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchExcludeAccountAntennasFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchExcludeAccountAntennasRequest = id => ({
|
||||
type:ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennasSuccess = (id, antennas) => ({
|
||||
type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS,
|
||||
id,
|
||||
antennas,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennasFail = (id, err) => ({
|
||||
type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
@ -938,3 +976,11 @@ export const removeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
|||
dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const addExcludeToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addExcludeToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeExcludeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(removeExcludeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ const messages = defineMessages({
|
|||
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||
add_or_remove_from_antenna: { id: 'account.add_or_remove_from_antenna', defaultMessage: 'Add or Remove from antennas' },
|
||||
add_or_remove_from_exclude_antenna: { id: 'account.add_or_remove_from_exclude_antenna', defaultMessage: 'Add or Remove from antennas as exclusion' },
|
||||
add_or_remove_from_circle: { id: 'account.add_or_remove_from_circle', defaultMessage: 'Add or Remove from circles' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
||||
|
@ -106,6 +107,7 @@ class Header extends ImmutablePureComponent {
|
|||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
onAddToAntenna: PropTypes.func.isRequired,
|
||||
onAddToExcludeAntenna: PropTypes.func.isRequired,
|
||||
onAddToCircle: PropTypes.func.isRequired,
|
||||
onEditAccountNote: PropTypes.func.isRequired,
|
||||
onChangeLanguages: PropTypes.func.isRequired,
|
||||
|
@ -332,6 +334,7 @@ class Header extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
}
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_antenna), action: this.props.onAddToAntenna });
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_exclude_antenna), action: this.props.onAddToExcludeAntenna });
|
||||
if (account.getIn(['relationship', 'followed_by'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_circle), action: this.props.onAddToCircle });
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
onAddToAntenna: PropTypes.func.isRequired,
|
||||
onAddToExcludeAntenna: PropTypes.func.isRequired,
|
||||
onAddToCircle: PropTypes.func.isRequired,
|
||||
onChangeLanguages: PropTypes.func.isRequired,
|
||||
onInteractionModal: PropTypes.func.isRequired,
|
||||
|
@ -102,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onAddToAntenna(this.props.account);
|
||||
};
|
||||
|
||||
handleAddToExcludeAntenna = () => {
|
||||
this.props.onAddToExcludeAntenna(this.props.account);
|
||||
};
|
||||
|
||||
handleAddToCircle = () => {
|
||||
this.props.onAddToCircle(this.props.account);
|
||||
};
|
||||
|
@ -149,6 +154,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
onEndorseToggle={this.handleEndorseToggle}
|
||||
onAddToList={this.handleAddToList}
|
||||
onAddToAntenna={this.handleAddToAntenna}
|
||||
onAddToExcludeAntenna={this.handleAddToExcludeAntenna}
|
||||
onAddToCircle={this.handleAddToCircle}
|
||||
onEditAccountNote={this.handleEditAccountNote}
|
||||
onChangeLanguages={this.handleChangeLanguages}
|
||||
|
|
|
@ -169,6 +169,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
modalType: 'ANTENNA_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.get('id'),
|
||||
isExclude: false,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onAddToExcludeAntenna (account) {
|
||||
dispatch(openModal({
|
||||
modalType: 'ANTENNA_ADDER',
|
||||
modalProps: {
|
||||
accountId: account.get('id'),
|
||||
isExclude: true,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromAntennaAdder, addToAntennaAdder } from '../../../actions/antennas';
|
||||
import { removeFromAntennaAdder, addToAntennaAdder, removeExcludeFromAntennaAdder, addExcludeToAntennaAdder } from '../../../actions/antennas';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -24,15 +24,20 @@ const MapStateToProps = (state, { antennaId, added }) => ({
|
|||
const mapDispatchToProps = (dispatch, { antennaId }) => ({
|
||||
onRemove: () => dispatch(removeFromAntennaAdder(antennaId)),
|
||||
onAdd: () => dispatch(addToAntennaAdder(antennaId)),
|
||||
onExcludeRemove: () => dispatch(removeExcludeFromAntennaAdder(antennaId)),
|
||||
onExcludeAdd: () => dispatch(addExcludeToAntennaAdder(antennaId)),
|
||||
});
|
||||
|
||||
class Antenna extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
antenna: ImmutablePropTypes.map.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
onExcludeRemove: PropTypes.func.isRequired,
|
||||
onExcludeAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -40,15 +45,31 @@ class Antenna extends ImmutablePureComponent {
|
|||
added: false,
|
||||
};
|
||||
|
||||
handleRemove = () => {
|
||||
if (this.props.isExclude) {
|
||||
this.props.onExcludeRemove();
|
||||
} else {
|
||||
this.props.onRemove();
|
||||
}
|
||||
};
|
||||
|
||||
handleAdd = () => {
|
||||
if (this.props.isExclude) {
|
||||
this.props.onExcludeAdd();
|
||||
} else {
|
||||
this.props.onAdd();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { antenna, intl, onRemove, onAdd, added } = this.props;
|
||||
const { antenna, intl, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={this.handleRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={this.handleAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,7 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { setupAntennaAdder, resetAntennaAdder } from '../../actions/antennas';
|
||||
import { setupAntennaAdder, resetAntennaAdder, setupExcludeAntennaAdder } from '../../actions/antennas';
|
||||
import NewAntennaForm from '../antennas/components/new_antenna_form';
|
||||
import Account from '../list_adder/components/account';
|
||||
|
||||
|
@ -28,6 +28,7 @@ const mapStateToProps = state => ({
|
|||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupAntennaAdder(accountId)),
|
||||
onExcludeInitialize: accountId => dispatch(setupExcludeAntennaAdder(accountId)),
|
||||
onReset: () => dispatch(resetAntennaAdder()),
|
||||
});
|
||||
|
||||
|
@ -35,16 +36,22 @@ class AntennaAdder extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onExcludeInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
antennaIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
const { isExclude, onInitialize, onExcludeInitialize, accountId } = this.props;
|
||||
if (isExclude) {
|
||||
onExcludeInitialize(accountId);
|
||||
} else {
|
||||
onInitialize(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -53,7 +60,7 @@ class AntennaAdder extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { accountId, antennaIds } = this.props;
|
||||
const { accountId, antennaIds, isExclude } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
|
@ -65,7 +72,7 @@ class AntennaAdder extends ImmutablePureComponent {
|
|||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{antennaIds.map(antennaId => <Antenna key={antennaId} antennaId={antennaId} />)}
|
||||
{antennaIds.map(antennaId => <Antenna key={antennaId} antennaId={antennaId} isExclude={isExclude} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,9 +20,9 @@ const messages = defineMessages({
|
|||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added, isExclude }) => ({
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['antennaEditor', isExclude ? 'excludeAccounts' : 'accounts', 'items']).includes(accountId) : added,
|
||||
added: typeof added === 'undefined' ? state.getIn(['antennaEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -15,8 +15,8 @@ import Account from './components/account';
|
|||
import EditAntennaForm from './components/edit_antenna_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = (state, { isExclude }) => ({
|
||||
accountIds: state.getIn(['antennaEditor', isExclude ? 'excludeAccounts' : 'accounts', 'items']),
|
||||
const mapStateToProps = (state) => ({
|
||||
accountIds: state.getIn(['antennaEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['antennaEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Icon } from 'mastodon/components/icon';
|
|||
import { fetchCircleSuggestions, clearCircleSuggestions, changeCircleSuggestions } from '../../../actions/circles';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'circles.search', defaultMessage: 'Search among people you follow' },
|
||||
search: { id: 'circles.search', defaultMessage: 'Search among people follow you' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
"about.rules": "サーバーのルール",
|
||||
"account.account_note_header": "メモ",
|
||||
"account.add_or_remove_from_antenna": "アンテナから追加または外す",
|
||||
"account.add_or_remove_from_exclude_antenna": "アンテナ除外条件から追加または外す",
|
||||
"account.add_or_remove_from_circle": "サークルから追加または外す",
|
||||
"account.add_or_remove_from_list": "リストから追加または外す",
|
||||
"account.badges.bot": "Bot",
|
||||
"account.badges.group": "Group",
|
||||
|
@ -95,6 +97,8 @@
|
|||
"antennas.add_domain_placeholder": "新しいドメイン名",
|
||||
"antennas.add_keyword": "新規キーワード",
|
||||
"antennas.add_keyword_placeholder": "新しいキーワード",
|
||||
"antennas.add_tag": "新規タグ",
|
||||
"antennas.add_tag_placeholder": "新しいタグ",
|
||||
"antennas.delete": "アンテナを削除",
|
||||
"antennas.domains": "{count} のドメイン",
|
||||
"antennas.edit": "アンテナを編集",
|
||||
|
@ -104,6 +108,7 @@
|
|||
"antennas.exclude_accounts": "除外するアカウント",
|
||||
"antennas.exclude_domains": "除外するドメイン",
|
||||
"antennas.exclude_keywords": "除外するキーワード",
|
||||
"antennas.exclude_tags": "除外するタグ",
|
||||
"antennas.filter": "絞り込み条件",
|
||||
"antennas.filter_not": "絞り込み条件の例外",
|
||||
"antennas.go_timeline": "タイムラインを見る",
|
||||
|
@ -141,6 +146,13 @@
|
|||
"bundle_modal_error.close": "閉じる",
|
||||
"bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。",
|
||||
"bundle_modal_error.retry": "再試行",
|
||||
"circles.account.add": "おはぎに追加",
|
||||
"circles.account.remove": "おはぎから外す",
|
||||
"circles.edit.submit": "タイトルを変更",
|
||||
"circles.new.create": "サークルを作成",
|
||||
"circles.new.title_placeholder": "新規サークル名",
|
||||
"circles.search": "フォロワーの中から検索",
|
||||
"circles.subheading": "あなたのサークル",
|
||||
"closed_registrations.other_server_instructions": "Mastodonは分散型なので他のサーバーにアカウントを作ってもこのサーバーとやり取りできます。",
|
||||
"closed_registrations_modal.description": "現在{domain}でアカウント作成はできませんがMastodonは{domain}のアカウントでなくても利用できます。",
|
||||
"closed_registrations_modal.find_another_server": "別のサーバーを探す",
|
||||
|
@ -150,6 +162,7 @@
|
|||
"column.antennas": "アンテナ",
|
||||
"column.blocks": "ブロックしたユーザー",
|
||||
"column.bookmarks": "ブックマーク",
|
||||
"column.circles": "サークル",
|
||||
"column.community": "ローカルタイムライン",
|
||||
"column.direct": "非公開の返信",
|
||||
"column.directory": "ディレクトリ",
|
||||
|
@ -215,6 +228,8 @@
|
|||
"confirmations.cancel_follow_request.message": "{name}に対するフォローリクエストを取り消しますか?",
|
||||
"confirmations.delete.confirm": "削除",
|
||||
"confirmations.delete.message": "本当に削除しますか?",
|
||||
"confirmations.delete_circle.confirm": "削除",
|
||||
"confirmations.delete_circle.message": "本当にこのサークルを完全に削除しますか?",
|
||||
"confirmations.delete_list.confirm": "削除",
|
||||
"confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?",
|
||||
"confirmations.delete_antenna.confirm": "削除",
|
||||
|
@ -451,6 +466,7 @@
|
|||
"navigation_bar.antennas": "アンテナ",
|
||||
"navigation_bar.blocks": "ブロックしたユーザー",
|
||||
"navigation_bar.bookmarks": "ブックマーク",
|
||||
"navigation_bar.circles": "サークル",
|
||||
"navigation_bar.community_timeline": "ローカルタイムライン",
|
||||
"navigation_bar.compose": "投稿の新規作成",
|
||||
"navigation_bar.direct": "非公開の返信",
|
||||
|
@ -570,6 +586,8 @@
|
|||
"poll_button.add_poll": "アンケートを追加",
|
||||
"poll_button.remove_poll": "アンケートを削除",
|
||||
"privacy.change": "公開範囲を変更",
|
||||
"privacy.circle.long": "サークルメンバーのみ",
|
||||
"privacy.circle.short": "サークル",
|
||||
"privacy.direct.long": "指定された相手のみ閲覧可",
|
||||
"privacy.direct.short": "指定された相手のみ",
|
||||
"privacy.limited.short": "限定投稿",
|
||||
|
|
|
@ -6,8 +6,13 @@ import {
|
|||
ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
|
||||
ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
|
||||
ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
|
||||
ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST,
|
||||
ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS,
|
||||
ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL,
|
||||
ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS,
|
||||
} from '../actions/antennas';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
|
@ -29,18 +34,23 @@ export default function antennaAdderReducer(state = initialState, action) {
|
|||
map.set('accountId', action.account.get('id'));
|
||||
});
|
||||
case ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST:
|
||||
case ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST:
|
||||
return state.setIn(['antennas', 'isLoading'], true);
|
||||
case ANTENNA_ADDER_ANTENNAS_FETCH_FAIL:
|
||||
return state.setIn(['antennas', 'isLoading'], false);
|
||||
case ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL:
|
||||
return state.setIn(['antennas', 'isLoading'], false);
|
||||
case ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS:
|
||||
return state.update('antennas', antennas => antennas.withMutations(map => {
|
||||
case ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS:
|
||||
return state.update('antennas', antennas => antennas.withMutations(map => {
|
||||
map.set('isLoading', false);
|
||||
map.set('loaded', true);
|
||||
map.set('items', ImmutableList(action.antennas.map(item => item.id)));
|
||||
}));
|
||||
case ANTENNA_EDITOR_ADD_SUCCESS:
|
||||
case ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS:
|
||||
return state.updateIn(['antennas', 'items'], antenna => antenna.unshift(action.antennaId));
|
||||
case ANTENNA_EDITOR_REMOVE_SUCCESS:
|
||||
case ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS:
|
||||
return state.updateIn(['antennas', 'items'], antenna => antenna.filterNot(item => item === action.antennaId));
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -38,12 +38,6 @@ const initialState = ImmutableMap({
|
|||
isLoading: false,
|
||||
}),
|
||||
|
||||
excludeAccounts: ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
suggestions: ImmutableMap({
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
|
@ -92,11 +86,11 @@ export default function antennaEditorReducer(state = initialState, action) {
|
|||
map.set('items', ImmutableList(action.accounts.map(item => item.id)));
|
||||
}));
|
||||
case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST:
|
||||
return state.setIn(['excludeAccounts', 'isLoading'], true);
|
||||
return state.setIn(['accounts', 'isLoading'], true);
|
||||
case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL:
|
||||
return state.setIn(['excludeAccounts', 'isLoading'], false);
|
||||
return state.setIn(['accounts', 'isLoading'], false);
|
||||
case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS:
|
||||
return state.update('excludeAccounts', accounts => accounts.withMutations(map => {
|
||||
return state.update('accounts', accounts => accounts.withMutations(map => {
|
||||
map.set('isLoading', false);
|
||||
map.set('loaded', true);
|
||||
map.set('items', ImmutableList(action.accounts.map(item => item.id)));
|
||||
|
@ -111,13 +105,11 @@ export default function antennaEditorReducer(state = initialState, action) {
|
|||
map.set('value', '');
|
||||
}));
|
||||
case ANTENNA_EDITOR_ADD_SUCCESS:
|
||||
case ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS:
|
||||
return state.updateIn(['accounts', 'items'], antenna => antenna.unshift(action.accountId));
|
||||
case ANTENNA_EDITOR_REMOVE_SUCCESS:
|
||||
return state.updateIn(['accounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId));
|
||||
case ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS:
|
||||
return state.updateIn(['excludeAccounts', 'items'], antenna => antenna.unshift(action.accountId));
|
||||
case ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS:
|
||||
return state.updateIn(['excludeAccounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId));
|
||||
return state.updateIn(['accounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,35 @@
|
|||
input,
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
input[type='number'],
|
||||
input:not([type]),
|
||||
textarea {
|
||||
background: $ui-base-color !important;
|
||||
color: $primary-text-color !important;
|
||||
|
||||
&.setting-text {
|
||||
border-color: $darker-text-color;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: lighten($dark-text-color, 36%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.column-content-select__control,
|
||||
.button.button-secondary {
|
||||
border-color: $darker-text-color !important;
|
||||
}
|
||||
|
||||
.emoji-mart-category-label {
|
||||
color: $lighter-text-color !important;
|
||||
color: $primary-text-color !important;
|
||||
}
|
||||
|
||||
.emoji-mart-bar:first-child {
|
||||
background: lighten($classic-base-color, 36%);
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-selected {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
.compose-form .compose-form__warning {
|
||||
|
|
|
@ -6,9 +6,12 @@ class ActivityPub::FetchInstanceInfoWorker
|
|||
include Redisable
|
||||
include Lockable
|
||||
|
||||
sidekiq_options queue: 'push', retry: 2
|
||||
|
||||
class Error < StandardError; end
|
||||
class GoneError < Error; end
|
||||
class RequestError < Error; end
|
||||
class DeadError < Error; end
|
||||
|
||||
SUPPORTED_NOTEINFO_RELS = ['http://nodeinfo.diaspora.software/ns/schema/2.0', 'http://nodeinfo.diaspora.software/ns/schema/2.1'].freeze
|
||||
|
||||
|
@ -22,6 +25,8 @@ class ActivityPub::FetchInstanceInfoWorker
|
|||
|
||||
update_info!(link)
|
||||
end
|
||||
rescue ActivityPub::FetchInstanceInfoWorker::DeadError
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -65,6 +70,8 @@ class ActivityPub::FetchInstanceInfoWorker
|
|||
body_to_json(response.body_with_limit)
|
||||
elsif response.code == 410
|
||||
raise ActivityPub::FetchInstanceInfoWorker::GoneError, "#{@instance.domain} is gone from the server"
|
||||
elsif response.code == 404
|
||||
raise ActivityPub::FetchInstanceInfoWorker::DeadError, "Request for #{@instance.domain} returned HTTP #{response.code}"
|
||||
else
|
||||
raise ActivityPub::FetchInstanceInfoWorker::RequestError, "Request for #{@instance.domain} returned HTTP #{response.code}"
|
||||
end
|
||||
|
|
|
@ -249,6 +249,7 @@ ja:
|
|||
setting_hide_statuses_count: 投稿数を隠す
|
||||
setting_stay_privacy: 投稿時に公開範囲を保存する
|
||||
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
||||
setting_public_post_to_unlisted: サードパーティから公開範囲「公開」で投稿した場合、「ローカル公開」に変更する
|
||||
setting_reduce_motion: アニメーションの動きを減らす
|
||||
setting_reject_public_unlisted_subscription: Misskey系サーバーに「ローカル公開」投稿を「フォロワーのみ」に変換して配送する
|
||||
setting_reject_unlisted_subscription: Misskey系サーバーに「未収載」投稿を「フォロワーのみ」に変換して配送する
|
||||
|
|
|
@ -180,6 +180,7 @@ namespace :api, format: false do
|
|||
resources :following, only: :index, controller: 'accounts/following_accounts'
|
||||
resources :lists, only: :index, controller: 'accounts/lists'
|
||||
resources :antennas, only: :index, controller: 'accounts/antennas'
|
||||
resources :exclude_antennas, only: :index, controller: 'accounts/exclude_antennas'
|
||||
resources :circles, only: :index, controller: 'accounts/circles'
|
||||
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
|
||||
resources :featured_tags, only: :index, controller: 'accounts/featured_tags'
|
||||
|
|
|
@ -21,7 +21,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def suffix
|
||||
ENV.fetch('MASTODON_VERSION_SUFFIX', '')
|
||||
ENV.fetch('MASTODON_VERSION_SUFFIX', '+kmyblue')
|
||||
end
|
||||
|
||||
def to_a
|
||||
|
|
|
@ -1188,5 +1188,54 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||
expect(sender.statuses.count).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when bearcaps' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/statuses/1234567890')
|
||||
.with(headers: { 'Authorization' => 'Bearer test_ohagi_token' })
|
||||
.to_return(status: 200, body: Oj.dump(object_json), headers: {})
|
||||
|
||||
subject.perform
|
||||
end
|
||||
|
||||
let!(:recipient) { Fabricate(:account) }
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://example.com/statuses/1234567890',
|
||||
type: 'Note',
|
||||
content: 'Lorem ipsum',
|
||||
to: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
attachment: [
|
||||
{
|
||||
type: 'Document',
|
||||
mediaType: 'image/png',
|
||||
url: 'http://example.com/attachment.png',
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||
type: 'Create',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: "bear:?#{{ u: 'https://example.com/statuses/1234567890', t: 'test_ohagi_token' }.to_query}",
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'creates status' do
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status).to_not be_nil
|
||||
expect(status.text).to eq 'Lorem ipsum'
|
||||
expect(status.mentions.map(&:account)).to include(recipient)
|
||||
expect(status.mentions.count).to eq 1
|
||||
expect(status.visibility).to eq 'limited'
|
||||
expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,59 @@ describe StatusReachFinder do
|
|||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let(:status) { Fabricate(:status, account: alice, thread: parent_status, visibility: visibility) }
|
||||
|
||||
context 'with a simple case' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
||||
|
||||
context 'with follower' do
|
||||
before do
|
||||
bob.follow!(alice)
|
||||
end
|
||||
|
||||
it 'send status' do
|
||||
expect(subject.inboxes).to include 'https://foo.bar/inbox'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-follower' do
|
||||
it 'send status' do
|
||||
expect(subject.inboxes).to_not include 'https://foo.bar/inbox'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when misskey case with unlisted post' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
||||
let(:sender_software) { 'mastodon' }
|
||||
let(:visibility) { :unlisted }
|
||||
|
||||
before do
|
||||
Fabricate(:instance_info, domain: 'foo.bar', software: sender_software)
|
||||
bob.follow!(alice)
|
||||
end
|
||||
|
||||
context 'when mastodon' do
|
||||
it 'send status' do
|
||||
expect(subject.inboxes).to include 'https://foo.bar/inbox'
|
||||
expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when misskey' do
|
||||
let(:sender_software) { 'misskey' }
|
||||
|
||||
it 'send status without setting' do
|
||||
expect(subject.inboxes).to include 'https://foo.bar/inbox'
|
||||
expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox'
|
||||
end
|
||||
|
||||
it 'send status with setting' do
|
||||
alice.user.settings.update(reject_unlisted_subscription: 'true')
|
||||
expect(subject.inboxes).to_not include 'https://foo.bar/inbox'
|
||||
expect(subject.inboxes_for_misskey).to include 'https://foo.bar/inbox'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it contains mentions of remote accounts' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') }
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe ActivityPub::NoteForMisskeySerializer do
|
||||
subject { JSON.parse(serialization.to_json) }
|
||||
|
||||
let(:serialization) { ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter) }
|
||||
let!(:account) { Fabricate(:account) }
|
||||
let!(:other) { Fabricate(:account) }
|
||||
let!(:parent) { Fabricate(:status, account: account, visibility: :unlisted) }
|
||||
let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
|
||||
let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
|
||||
let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) }
|
||||
let!(:reply_by_account_third) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
|
||||
let!(:reply_by_account_visibility_direct) { Fabricate(:status, account: account, thread: parent, visibility: :direct) }
|
||||
|
||||
before do
|
||||
account.user.settings.update(reject_unlisted_subscription: 'true')
|
||||
end
|
||||
|
||||
it 'has a Note type' do
|
||||
expect(subject['type']).to eql('Note')
|
||||
end
|
||||
|
||||
it 'has a replies collection' do
|
||||
expect(subject['replies']['type']).to eql('Collection')
|
||||
end
|
||||
|
||||
it 'has a replies collection with a first Page' do
|
||||
expect(subject['replies']['first']['type']).to eql('CollectionPage')
|
||||
end
|
||||
|
||||
it 'includes public self-replies in its replies collection' do
|
||||
expect(subject['replies']['first']['items']).to include(reply_by_account_first.uri, reply_by_account_next.uri, reply_by_account_third.uri)
|
||||
end
|
||||
|
||||
it 'does not include replies from others in its replies collection' do
|
||||
expect(subject['replies']['first']['items']).to_not include(reply_by_other_first.uri)
|
||||
end
|
||||
|
||||
it 'does not include replies with direct visibility in its replies collection' do
|
||||
expect(subject['replies']['first']['items']).to_not include(reply_by_account_visibility_direct.uri)
|
||||
end
|
||||
|
||||
it 'has private visibility' do
|
||||
expect(subject['to']).to_not include('https://www.w3.org/ns/activitystreams#Public')
|
||||
expect(subject['to'].any? { |to| to.end_with?("#{account.username}/followers") }).to be true
|
||||
expect(subject['cc']).to_not include('https://www.w3.org/ns/activitystreams#Public')
|
||||
end
|
||||
end
|
|
@ -88,7 +88,7 @@ describe ActivityPub::FetchInstanceInfoWorker do
|
|||
end
|
||||
|
||||
it 'performs a mastodon instance' do
|
||||
expect { subject.perform('example.com') }.to raise_error(ActivityPub::FetchInstanceInfoWorker::RequestError, 'Request for example.com returned HTTP 404')
|
||||
expect(subject.perform('example.com')).to be true
|
||||
|
||||
info = InstanceInfo.find_by(domain: 'example.com')
|
||||
expect(info).to be_nil
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue