1
0
Fork 0
forked from gitea/nas

Merge branch 'kb_development' into kb_migration

This commit is contained in:
KMY 2023-09-12 09:09:46 +09:00
commit bf41ee1574
61 changed files with 561 additions and 93 deletions

View file

@ -3,3 +3,8 @@ NODE_ENV=tests
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
# Elasticsearch
ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
ES_PREFIX=test

View file

@ -113,6 +113,7 @@ jobs:
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test'
CI_JOBS: ${{ matrix.ci_job }}/4
ES_ENABLED: false
strategy:
fail-fast: false
@ -192,6 +193,7 @@ jobs:
DISABLE_SIMPLECOV: true
RAILS_ENV: test
BUNDLE_WITH: test
ES_ENABLED: false
strategy:
fail-fast: false

View file

@ -1,46 +1,36 @@
# Contributing
# CONTRIBUTING
Thank you for considering contributing to Mastodon 🐘
kmyblueは、コミュニティの意見も聞くには聞きますが導入する・しないは管理人が決定します。
You can contribute in the following ways:
## バグ報告
- Finding and reporting bugs
- Translating the Mastodon interface into various languages
- Contributing code to Mastodon by fixing bugs or implementing features
- Improving the documentation
バグについて、最新よりも過去のバージョンへの対応は特別な場合以外は行いません。
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
以下のいずれかの方法で報告してください。
## Bug reports
- [GitHub Issues](https://github.com/kmycode/mastodon/issues)
- [kmyblue開発者への連絡](https://kmy.blue/@askyq)
- [kmyblue開発者へのメール](https://kmy.blue/about)
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## 翻訳、プルリクエスト
## Translations
新しい機能や既存機能の修正については、プルリクエストのためにコードを作成する前に、まずGitHub Issuesで機能の提案を行いkmyblue開発者の考えを聞くことをおすすめします。バグ修正、翻訳、テストコードなどは基本受け入れますが、依存モジュールのバージョンアップについては本家Mastodonよりも先に行かないようにしてください。
You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase.
プルリクエストのタイトルには、プルリクエストの内容が明確になるようなものを設定してください。
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)](https://crowdin.com/project/mastodon)
### kmyblueの開発方針
## Pull requests
下記のものに矛盾がなければ、あとは管理人の意向次第です。
**Please use clean, concise titles for your pull requests.** Unless the pull request is about refactoring code, updating dependencies or other internal tasks, assume that the person reading the pull request title is not a programmer or Mastodon developer, but instead a Mastodon user or server administrator, and **try to describe your change or fix from their perspective**. We use commit squashing, so the final commit in the main branch will carry the title of the pull request, and commits from the main branch are fed into the changelog. The changelog is separated into [keepachangelog.com categories](https://keepachangelog.com/en/1.0.0/), and while that spec does not prescribe how the entries ought to be named, for easier sorting, start your pull request titles using one of the verbs "Add", "Change", "Deprecate", "Remove", or "Fix" (present tense).
- **自分の投稿を見せたくない人に見せない**
- **他人の見たくない投稿を見ない**
- ただし本家Mastodonで上記原則に矛盾した機能が追加された場合は従う
- 画面を騒がしくするような機能(絵文字を大きく表示するなど)は追加しないか、控えめにする。ただし他のソフトウェアにも導入され利用者が多くいる場合などは別途判断して、オプトアウト可能な設定項目とともに追加する
- 負荷を著しく上げるような機能はできるだけ追加しない
Example:
kmyblueが意図的に実装していない機能は、例えば以下のものがあります。詳しい理由が知りたい場合は[この記事を参照するか](https://note.com/kmycode/n/n463410b5e03c)、別途お問い合わせください。もちろん明確な根拠がある場合、あなたはこれに抗議する権利を有しますが、あなたがこのkmyblueをフォークして新しいリポジトリを作るほうがより自由でしょう。
| Not ideal | Better |
| ------------------------------------ | ------------------------------------------------------------- |
| Fixed NoMethodError in RemovalWorker | Fix nil error when removing statuses caused by race condition |
It is not always possible to phrase every change in such a manner, but it is desired.
**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable.
**Pull requests that do not pass automated checks may not be reviewed**. In particular, you need to keep in mind:
- Unit and integration tests (rspec, jest)
- Code style rules (rubocop, eslint)
- Normalization of locale files (i18n-tasks)
## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).
- 引用機能
- お気に入り一覧の公開
- ブックマーク分類の公開
- 他のサーバーの投稿に対して他のサーバーのアカウントが行った絵文字リアクションの受け入れ

60
INSTALL.md Normal file
View file

@ -0,0 +1,60 @@
# kmyblueインストール手順
## 共通の注意事項
### 必須ソフトウェアのバージョン
Ruby、ElasticSearch、ImageMagick、PostgreSQLなど必須ソフトウェアのバージョンは、本家Mastodonに準じます。リリースートに対応する本家Mastodonバージョンが記載されていますので、本家Mastodonのリリースートから対応するバージョンを探して調べてください。
### 一般的な注意事項
kmyblueは頻繁にバージョンアップを行います。
- 本家Mastodonの開発中のバージョンを平然と取り込みます
- バグが含まれていることがあります
- 特に最新コミットでは、デバッグ用コードや、`kmy.blue`本番サーバーで動作確認を行うためのコードが含まれている場合があります。ブランチの最新コミットではなく最新タグを取り込むことを強くおすすめします
### ElasticSearchを使用する場合
kmyblueでは、sudachiの使用を前提としています。
下記URLより、ElasticSearchにSudachiプラグインを追加してください。
ただし辞書ファイルsudachi dictionary archiveは手順書で指示されたパスではなく`/etc/elasticsearch/sudachi`に格納してください。
https://github.com/WorksApplications/elasticsearch-sudachi
Sudachiインストール終了後、追加で`/etc/elasticsearch/sudachi/config.json`に下記を記述して保存してください。`system_full.dic`を使用する場合は適宜`systemDict`プロパティの内容を置き換えてください。
```json
{
"systemDict": "system_core.dic"
}
```
## 新規インストールの場合
1. 本家Mastodonとセットアップ手順はほとんど一緒です。kmyblueが独自に必須ソフトウェアを追加したわけではありません。ただしkmyblueはMastodonの開発中コードを取り込んでいるので、Rubyなどのバージョンアップ作業が必要になる場合があります。Mastodon公式のセットアップ手順を盲信せず、画面の指示に従ってインストールを進めてください。CloudFlareを組み合わせてセットアップしたとき、サーバーに接続すると400が出るなどのトラブルが出ることがありますが、大抵はMastodon本家由来のトラブルだと思われるので基本サポートはしません
2. ただひとつ差異があります。Gitリポジトリはこのkmyblueに向けてください。`kb_development`ブランチの最新コミットではなく、`kb`で始まる最新のタグを取り込むことを強くおすすめします
## 本家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
sudo systemctl start mastodon-web mastodon-streaming@4000 mastodon-sidekiq
```

View file

@ -6,11 +6,19 @@ kmyblueは[Mastodon](https://github.com/mastodon/mastodon)のフォークです
kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。
kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。また ActivityPub に参加することもできます。サーバーkmyblueは創作作家向けのものですが、フォークとしてのkmyblueは作者の嫌いな政治に関する過激な話を取り扱うコミュニティ、創作活動の一部(エロ関係含む)または全体を否定するコミュニティなども平等にお使いいただけますし、サーバーkmyblueのルールを適用する必要もありません
ただしkmyblueにおいてテストコードは飾りでしかないため、不具合が発生しても自己責任になります。既知のバグもいくつかありますし、直す予定のないものも含まれます。
kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。また ActivityPub に参加することもできます。確かにサーバーkmyblueは創作作家向けのものですが、フォークとしてのkmyblueは作者と政治的に対立するコミュニティ、創作活動の一部(エロ関係含む)または全体を否定するコミュニティなどにも平等にお使いいただけます。いかなるコミュニティがkmyblueフォークを使用しても、それ自体に作者が抗議することもありません。サーバーkmyblueのルールを適用する必要もなく、「Anyone But Kmyblue」なルールを設定することすら許容されます
ただしkmyblueにおいて**テストコードは飾り**でしかありません。独自機能のテストを記述するだけでなく、本家のテストコードの補強も行っておりますが、確認漏れは必ず発生するものです。不具合が発生しても自己責任になります。既知のバグもいくつかありますし、直す予定のないものも含まれます。
テストコード、Lint どちらも動いています。
## インストール方法
INSTALL.mdを参照してください。
## 開発への参加方法
CONTRIBUTING.mdを参照してください。
## kmyblueの強み
### 本家Mastodonへの積極的追従

View file

@ -1,7 +1,72 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
settings index: index_preset(refresh_interval: '30s'), analysis: {
DEVELOPMENT_SETTINGS = {
filter: {
english_stop: {
type: 'stop',
stopwords: '_english_',
},
english_stemmer: {
type: 'stemmer',
language: 'english',
},
english_possessive_stemmer: {
type: 'stemmer',
language: 'possessive_english',
},
},
analyzer: {
natural: {
tokenizer: 'standard',
filter: %w(
lowercase
asciifolding
cjk_width
elision
english_possessive_stemmer
english_stop
english_stemmer
),
},
sudachi_analyzer: {
tokenizer: 'standard',
filter: %w(
lowercase
asciifolding
cjk_width
elision
english_possessive_stemmer
english_stop
english_stemmer
),
},
verbatim: {
tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width),
},
edge_ngram: {
tokenizer: 'edge_ngram',
filter: %w(lowercase asciifolding cjk_width),
},
},
tokenizer: {
edge_ngram: {
type: 'edge_ngram',
min_gram: 1,
max_gram: 15,
},
},
}.freeze
PRODUCTION_SETTINGS = {
filter: {
english_stop: {
type: 'stop',
@ -77,7 +142,9 @@ class AccountsIndex < Chewy::Index
discard_punctuation: 'true',
},
},
}
}.freeze
settings index: index_preset(refresh_interval: '30s'), analysis: Rails.env.test? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS
index_scope ::Account.searchable.includes(:account_stat)

View file

@ -38,6 +38,19 @@ class PublicStatusesIndex < Chewy::Index
),
},
sudachi_analyzer: {
tokenizer: 'standard',
filter: %w(
lowercase
asciifolding
cjk_width
elision
english_possessive_stemmer
english_stop
english_stemmer
),
},
hashtag: {
tokenizer: 'keyword',
filter: %w(
@ -126,7 +139,7 @@ class PublicStatusesIndex < Chewy::Index
},
}.freeze
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: PRODUCTION_SETTINGS
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: Rails.env.test? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS
index_scope ::Status.unscoped
.kept

View file

@ -37,6 +37,19 @@ class StatusesIndex < Chewy::Index
),
},
sudachi_analyzer: {
tokenizer: 'standard',
filter: %w(
lowercase
asciifolding
cjk_width
elision
english_possessive_stemmer
english_stop
english_stemmer
),
},
hashtag: {
tokenizer: 'keyword',
filter: %w(
@ -129,7 +142,7 @@ class StatusesIndex < Chewy::Index
},
}.freeze
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: PRODUCTION_SETTINGS
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: Rails.env.test? ? DEVELOPMENT_SETTINGS : PRODUCTION_SETTINGS
index_scope ::Status.unscoped.kept.without_reblogs.includes(
:media_attachments,

View file

@ -42,6 +42,6 @@ class Api::V1::AntennasController < Api::BaseController
end
def antenna_params
params.permit(:title, :list_id, :insert_feeds, :stl, :with_media_only, :ignore_reblog)
params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog)
end
end

View file

@ -1,6 +1,13 @@
# frozen_string_literal: true
class Settings::Preferences::OtherController < Settings::Preferences::BaseController
include DtlHelper
def show
@dtl_enabled = DTL_ENABLED
@dtl_tag = DTL_TAG
end
private
def after_update_redirect_path

View file

@ -0,0 +1,6 @@
# frozen_string_literal: true
module DtlHelper
DTL_ENABLED = ENV.fetch('DTL_ENABLED', 'false') == 'true'
DTL_TAG = ENV.fetch('DTL_TAG', 'kmyblue')
end

BIN
app/javascript/icons/android-chrome-144x144.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-192x192.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-256x256.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-36x36.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-384x384.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-48x48.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-512x512.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-72x72.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

BIN
app/javascript/icons/android-chrome-96x96.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-1024x1024.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-114x114.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-120x120.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-144x144.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-152x152.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-167x167.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-180x180.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-57x57.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-60x60.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-72x72.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

BIN
app/javascript/icons/apple-touch-icon-76x76.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

BIN
app/javascript/icons/favicon-16x16.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 986 B

Before After
Before After

BIN
app/javascript/icons/favicon-32x32.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

BIN
app/javascript/icons/favicon-48x48.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

@ -236,10 +236,10 @@ export const createAntennaFail = error => ({
error,
});
export const updateAntenna = (id, title, shouldReset, list_id, stl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
dispatch(updateAntennaRequest(id));
api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => {
api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => {
dispatch(updateAntennaSuccess(data));
if (shouldReset) {

View file

@ -198,31 +198,37 @@ class AntennaSetting extends PureComponent {
onStlToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined, undefined, undefined));
dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined, undefined, undefined, undefined));
};
onLtlToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked, undefined, undefined, undefined));
};
onMediaOnlyToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked, undefined, undefined));
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, target.checked, undefined, undefined));
};
onIgnoreReblogToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, target.checked, undefined));
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, target.checked, undefined));
};
onNoInsertFeedsToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, target.checked));
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, undefined, target.checked));
};
onSelect = value => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateAntenna(id, undefined, false, value.value, undefined, undefined, undefined, undefined));
dispatch(updateAntenna(id, undefined, false, value.value, undefined, undefined, undefined, undefined, undefined));
};
onHomeSelect = () => this.onSelect({ value: '0' });
@ -293,6 +299,7 @@ class AntennaSetting extends PureComponent {
const pinned = !!columnId;
const title = antenna ? antenna.get('title') : id;
const isStl = antenna ? antenna.get('stl') : undefined;
const isLtl = antenna ? antenna.get('ltl') : undefined;
const isMediaOnly = antenna ? antenna.get('with_media_only') : undefined;
const isIgnoreReblog = antenna ? antenna.get('ignore_reblog') : undefined;
const isInsertFeeds = antenna ? antenna.get('insert_feeds') : undefined;
@ -312,7 +319,7 @@ class AntennaSetting extends PureComponent {
}
let columnSettings;
if (!isStl) {
if (!isStl && !isLtl) {
columnSettings = (
<>
<div className='setting-toggle'>
@ -339,6 +346,12 @@ class AntennaSetting extends PureComponent {
<p><FormattedMessage id='antennas.in_stl_mode' defaultMessage='This antenna is in STL mode.' /></p>
</div>
);
} else if (isLtl) {
stlAlert = (
<div className='antenna-setting'>
<p><FormattedMessage id='antennas.in_ltl_mode' defaultMessage='This antenna is in LTL mode.' /></p>
</div>
);
}
const rangeRadioValues = ImmutableList([
@ -384,12 +397,23 @@ class AntennaSetting extends PureComponent {
</button>
</div>
<div className='setting-toggle'>
<Toggle id={`antenna-${id}-stl`} defaultChecked={isStl} onChange={this.onStlToggle} />
<label htmlFor={`antenna-${id}-stl`} className='setting-toggle__label'>
<FormattedMessage id='antennas.stl' defaultMessage='STL mode' />
</label>
</div>
{!isLtl && (
<div className='setting-toggle'>
<Toggle id={`antenna-${id}-stl`} defaultChecked={isStl} onChange={this.onStlToggle} />
<label htmlFor={`antenna-${id}-stl`} className='setting-toggle__label'>
<FormattedMessage id='antennas.stl' defaultMessage='STL mode' />
</label>
</div>
)}
{!isStl && (
<div className='setting-toggle'>
<Toggle id={`antenna-${id}-ltl`} defaultChecked={isLtl} onChange={this.onLtlToggle} />
<label htmlFor={`antenna-${id}-ltl`} className='setting-toggle__label'>
<FormattedMessage id='antennas.ltl' defaultMessage='LTL mode' />
</label>
</div>
)}
<div className='setting-toggle'>
<Toggle id={`antenna-${id}-noinsertfeeds`} defaultChecked={isInsertFeeds} onChange={this.onNoInsertFeedsToggle} />
@ -429,7 +453,7 @@ class AntennaSetting extends PureComponent {
</>
)}
{!isStl && (
{!isStl && !isLtl && (
<>
<h2><FormattedMessage id='antennas.filter' defaultMessage='Filter' /></h2>
<RadioPanel values={rangeRadioValues} value={rangeRadioValue} onChange={this.onRangeRadioChanged} />

View file

@ -22,6 +22,7 @@ import NewListForm from './components/new_list_form';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
with_antenna: { id: 'lists.with_antenna', defaultMessage: 'Antenna' },
});
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
@ -76,7 +77,8 @@ class Lists extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{lists.map(list =>
<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
(<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')}
badge={(list.get('antennas') && list.get('antennas').size > 0) ? intl.formatMessage(messages.with_antenna) : undefined} />),
)}
</ScrollableList>

View file

@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal';
import { enableDtlMenu, timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import { enableDtlMenu, timelinePreview, trendsEnabled, dtlTag } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import ColumnLink from './column_link';
@ -93,8 +93,8 @@ class NavigationPanel extends Component {
</>
)}
{signedIn && enableDtlMenu && (
<ColumnLink transparent to='/tags/kmyblue' icon='users' text={intl.formatMessage(messages.deepLocal)} />
{signedIn && enableDtlMenu && dtlTag && (
<ColumnLink transparent to={`/tags/${dtlTag}`} icon='users' text={intl.formatMessage(messages.deepLocal)} />
)}
{!signedIn && explorer}

View file

@ -58,6 +58,7 @@
* @property {string} display_media
* @property {boolean} display_media_expand
* @property {string} domain
* @property {string} dtl_tag
* @property {boolean} enable_login_privacy
* @property {boolean} enable_dtl_menu
* @property {boolean=} expand_spoilers
@ -124,6 +125,7 @@ export const disabledAccountId = getMeta('disabled_account_id');
export const displayMedia = getMeta('display_media');
export const displayMediaExpand = getMeta('display_media_expand');
export const domain = getMeta('domain');
export const dtlTag = getMeta('dtl_tag');
export const enableLoginPrivacy = getMeta('enable_login_privacy');
export const enableDtlMenu = getMeta('enable_dtl_menu');
export const expandSpoilers = getMeta('expand_spoilers');

View file

@ -206,7 +206,7 @@
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
"compose_form.limited_post_warning": "限定投稿は現状、ごく一部のMastodonサーバーにしか届きません2023年8月時点でFedibird、kmyblue、Pawooなど一部のみです)",
"compose_form.limited_post_warning": "限定投稿は現状、ごく一部のMastodonサーバーにしか届きません2023年9月時点でFedibird、kmyblueなど一部のみです)",
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.lock_disclaimer.lock": "承認制",
"compose_form.markdown.marked": "Markdown有効",

View file

@ -25,6 +25,7 @@
# stl :boolean default(FALSE), not null
# ignore_reblog :boolean default(FALSE), not null
# insert_feeds :boolean default(FALSE), not null
# ltl :boolean default(FALSE), not null
#
class Antenna < ApplicationRecord
include Expireable
@ -45,16 +46,19 @@ class Antenna < ApplicationRecord
belongs_to :list, optional: true
scope :stls, -> { where(stl: true) }
scope :ltls, -> { where(ltl: true) }
scope :all_keywords, -> { where(any_keywords: true) }
scope :all_domains, -> { where(any_domains: true) }
scope :all_accounts, -> { where(any_accounts: true) }
scope :all_tags, -> { where(any_tags: true) }
scope :availables, -> { where(available: true).where(Arel.sql('any_keywords = FALSE OR any_domains = FALSE OR any_accounts = FALSE OR any_tags = FALSE')) }
scope :available_stls, -> { where(available: true, stl: true) }
scope :available_ltls, -> { where(available: true, stl: false, ltl: true) }
validate :list_owner
validate :validate_limit
validate :validate_stl_limit
validate :validate_ltl_limit
def list_owner
raise Mastodon::ValidationError, I18n.t('antennas.errors.invalid_list_owner') if !list_id.zero? && list.present? && list.account != account
@ -235,6 +239,22 @@ class Antenna < ApplicationRecord
stls = account.antennas.where(stl: true).where.not(id: id)
errors.add(:base, I18n.t('antennas.errors.over_stl_limit', limit: 1)) if list_id.zero? ? stls.any? { |tl| tl.list_id.zero? } : stls.any? { |tl| tl.list_id != 0 }
errors.add(:base, I18n.t('antennas.errors.over_stl_limit', limit: 1)) if if insert_feeds
list_id.zero? ? stls.any? { |tl| tl.list_id.zero? } : stls.any? { |tl| tl.list_id != 0 }
else
stls.any? { |tl| !tl.insert_feeds }
end
end
def validate_ltl_limit
return unless ltl
ltls = account.antennas.where(ltl: true).where.not(id: id)
errors.add(:base, I18n.t('antennas.errors.over_ltl_limit', limit: 1)) if if insert_feeds
list_id.zero? ? ltls.any? { |tl| tl.list_id.zero? } : ltls.any? { |tl| tl.list_id != 0 }
else
ltls.any? { |tl| !tl.insert_feeds }
end
end
end

View file

@ -43,6 +43,7 @@ class Status < ApplicationRecord
include RateLimitable
include StatusSafeReblogInsert
include StatusSearchConcern
include DtlHelper
rate_limit by: :account, family: :statuses
@ -291,7 +292,7 @@ class Status < ApplicationRecord
end
def dtl?
tags.where(name: 'kmyblue').exists?
tags.where(name: DTL_TAG).exists?
end
def emojis

View file

@ -2,6 +2,7 @@
class InitialStateSerializer < ActiveModel::Serializer
include RoutingHelper
include DtlHelper
attributes :meta, :compose, :accounts,
:media_attachments, :settings,
@ -35,6 +36,7 @@ class InitialStateSerializer < ActiveModel::Serializer
trends_as_landing_page: Setting.trends_as_landing_page,
status_page_url: Setting.status_page_url,
sso_redirect: sso_redirect,
dtl_tag: DTL_ENABLED ? DTL_TAG : nil,
}
if object.current_account

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class REST::AntennaSerializer < ActiveModel::Serializer
attributes :id, :title, :stl, :insert_feeds, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
attributes :id, :title, :stl, :ltl, :insert_feeds, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
class ListSerializer < ActiveModel::Serializer
attributes :id, :title

View file

@ -2,16 +2,21 @@
class DeliveryAntennaService
include FormattingHelper
include DtlHelper
def call(status, update, stl_home)
def call(status, update, **options)
@status = status
@account = @status.account
@update = update
if stl_home
delivery_stl!
else
mode = options[:mode] || :home
case mode
when :home
delivery!
when :stl
delivery_stl!
when :ltl
delivery_ltl!
end
end
@ -19,6 +24,8 @@ class DeliveryAntennaService
def delivery!
must_dtl_tag = @account.dissubscribable
return if must_dtl_tag && !DTL_ENABLED
tag_ids = @status.tags.pluck(:id)
domain = @account.domain || Rails.configuration.x.local_domain
follower_ids = @status.unlisted_visibility? ? @status.account.followers.pluck(:id) : []
@ -31,7 +38,7 @@ class DeliveryAntennaService
antennas = Antenna.where(id: antennas.select(:id))
if must_dtl_tag
dtl_tag = Tag.find_or_create_by_names('kmyblue').first
dtl_tag = Tag.find_or_create_by_names(DTL_TAG).first
return if !dtl_tag || tag_ids.exclude?(dtl_tag.id)
antennas = antennas.left_joins(:antenna_tags).where(antenna_tags: { tag_id: dtl_tag.id })
@ -44,7 +51,7 @@ class DeliveryAntennaService
antennas = antennas.where(account: @status.mentioned_accounts) if @status.visibility.to_sym == :limited
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)
antennas = antennas.where(stl: false, ltl: false)
collection = AntennaCollection.new(@status, @update, false)
content = extract_status_plain_text_with_spoiler_text(@status)
@ -72,7 +79,8 @@ class DeliveryAntennaService
antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
home_post = !@account.domain.nil? || @status.reblog? || [:public, :public_unlisted, :login].exclude?(@status.visibility.to_sym)
antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where.not(list_id: 0) if home_post
antennas = antennas.where(account: @account.followers).or(antennas.where(account: @account)).where('insert_feeds IS FALSE OR list_id > 0') if home_post && !@status.limited_visibility?
antennas = antennas.where(account: @status.mentioned_accounts).or(antennas.where(account: @account)).where('insert_feeds IS FALSE OR list_id > 0') if @status.limited_visibility?
collection = AntennaCollection.new(@status, @update, home_post)
@ -87,6 +95,27 @@ class DeliveryAntennaService
collection.deliver!
end
def delivery_ltl!
return if %i(public public_unlisted login).exclude?(@status.visibility.to_sym)
return unless @account.local?
return if @status.reblog?
antennas = Antenna.available_ltls
antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
collection = AntennaCollection.new(@status, @update, false)
antennas.in_batches do |ans|
ans.each do |antenna|
next if antenna.expired?
collection.push(antenna)
end
end
collection.deliver!
end
class AntennaCollection
def initialize(status, update, stl_home = false) # rubocop:disable Style/OptionalBooleanParameter
@status = status

View file

@ -2,6 +2,7 @@
class FanOutOnWriteService < BaseService
include Redisable
include DtlHelper
# Push a status into home and mentions feeds
# @param [Status] status
@ -51,11 +52,13 @@ class FanOutOnWriteService < BaseService
when :public, :unlisted, :public_unlisted, :login, :private
deliver_to_all_followers!
deliver_to_lists!
deliver_to_antennas! if !@account.dissubscribable || (@status.dtl? && @account.user&.setting_dtl_force_subscribable && @status.tags.exists?(name: 'kmyblue'))
deliver_to_antennas! if !@account.dissubscribable || (@status.dtl? && DTL_ENABLED && @account.user&.setting_dtl_force_subscribable && @status.tags.exists?(name: DTL_TAG))
deliver_to_stl_antennas!
deliver_to_ltl_antennas!
when :limited
deliver_to_lists_mentioned_accounts_only!
deliver_to_antennas! unless @account.dissubscribable
deliver_to_stl_antennas!
deliver_to_mentioned_followers!
else
deliver_to_mentioned_followers!
@ -135,11 +138,15 @@ class FanOutOnWriteService < BaseService
end
def deliver_to_stl_antennas!
DeliveryAntennaService.new.call(@status, @options[:update], true)
DeliveryAntennaService.new.call(@status, @options[:update], mode: :stl)
end
def deliver_to_ltl_antennas!
DeliveryAntennaService.new.call(@status, @options[:update], mode: :ltl)
end
def deliver_to_antennas!
DeliveryAntennaService.new.call(@status, @options[:update], false)
DeliveryAntennaService.new.call(@status, @options[:update], mode: :home)
end
def deliver_to_mentioned_followers!

View file

@ -3,6 +3,7 @@
class PostStatusService < BaseService
include Redisable
include LanguagesHelper
include DtlHelper
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
@ -101,8 +102,10 @@ class PostStatusService < BaseService
end
def overwrite_dtl_post
return unless DTL_ENABLED
raw_tags = Extractor.extract_hashtags(@text)
return if raw_tags.exclude?('kmyblue')
return if raw_tags.exclude?(DTL_TAG)
return unless %i(public public_unlisted unlisted).include?(@visibility)
@visibility = :unlisted if @account.user&.setting_dtl_force_with_tag == :full

View file

@ -8,7 +8,7 @@
= render 'shared/error_messages', object: @admin_settings
.fields-group
= f.input :ng_words, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.ng_words.keywords')
= f.input :ng_words, wrapper: :with_label, as: :text, input_html: { rows: 12 }, label: t('admin.ng_words.keywords'), hint: t('admin.ng_words.keywords_hint')
.fields-group
= f.input :post_hash_tags_max, wrapper: :with_label, as: :integer, label: t('admin.ng_words.post_hash_tags_max')

View file

@ -42,18 +42,20 @@
.fields-group
= ff.input :'web.enable_login_privacy', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_enable_login_privacy'), hint: false
%h4= t 'preferences.dtl'
- if @dtl_enabled
%p.hint= t 'preferences.dtl_hint'
%h4= t 'preferences.dtl'
.fields-group
= ff.input :'web.enable_dtl_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_dtl_menu'), hint: I18n.t('simple_form.hints.defaults.setting_dtl_menu')
%p.hint= t 'preferences.dtl_hint', tag: @dtl_tag
.fields-group
= ff.input :dtl_force_with_tag, kmyblue: true, collection: ['full', 'searchability', 'none'], label_method: lambda { |item| safe_join([t("simple_form.labels.dtl_force_with_tag.#{item}")]) }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: I18n.t('simple_form.labels.defaults.setting_dtl_force_with_tag'), hint: I18n.t('simple_form.hints.defaults.setting_dtl_force_with_tag')
.fields-group
= ff.input :'web.enable_dtl_menu', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_dtl_menu')
.fields-group
= ff.input :dtl_force_subscribable, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_dtl_force_subscribable'), hint: I18n.t('simple_form.hints.defaults.setting_dtl_force_subscribable')
.fields-group
= ff.input :dtl_force_with_tag, kmyblue: true, collection: ['full', 'searchability', 'none'], label_method: lambda { |item| safe_join([t("simple_form.labels.dtl_force_with_tag.#{item}")]) }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: I18n.t('simple_form.labels.defaults.setting_dtl_force_with_tag'), hint: I18n.t('simple_form.hints.defaults.setting_dtl_force_with_tag', tag: @dtl_tag)
.fields-group
= ff.input :dtl_force_subscribable, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_dtl_force_subscribable'), hint: I18n.t('simple_form.hints.defaults.setting_dtl_force_subscribable')
%h4= t 'preferences.public_timelines'

View file

@ -600,6 +600,7 @@ en:
enable_block_emoji_reaction_settings: Enable block emoji reactions settings for users
hide_local_users_for_anonymous: Hide timeline local user posts from anonymous
keywords: Reject keywords
keywords_hint: The first character of the line is "?". to use regular expressions
post_hash_tags_max: Hash tags max for posts
test_error: Testing is returned any errors
title: NG words and against spams
@ -780,6 +781,7 @@ en:
hint: This keywords is applied to public posts only..
keywords: Sensitive keywords
keywords_for_all: Sensitive keywords (Contains CW alert)
keywords_for_all_hint: The first character of the line is "?". to use regular expressions
title: Sensitive words and moderation options
settings:
about:
@ -1111,6 +1113,7 @@ en:
invalid_context: None or invalid context supplied
invalid_list_owner: This list is not yours
over_limit: You have exceeded the limit of %{limit} antennas
over_ltl_limit: You have exceeded the limit of %{limit} ltl antennas
over_stl_limit: You have exceeded the limit of %{limit} stl antennas
index:
contexts: Antennas in %{contexts}
@ -1642,7 +1645,7 @@ en:
too_many_options: can't contain more than %{max} items
preferences:
dtl: Deep timeline
dtl_hint: "You can join deep timeline with #kmyblue tag. Following settings make convenient to use deep timeline."
dtl_hint: "You can join deep timeline with #%{tag} tag. Following settings make convenient to use deep timeline."
other: Other
posting_defaults: Posting defaults
public_timelines: Public timelines

View file

@ -598,6 +598,7 @@ ja:
enable_block_emoji_reaction_settings: 各ユーザーにスタンプ機能のブロック設定項目を解放する
hide_local_users_for_anonymous: ログインしていない状態でローカルユーザーの投稿をタイムラインから取得できないようにする
keywords: 投稿できないキーワード
keywords_hint: 行を「?」で始めると、正規表現が使えます
post_hash_tags_max: 投稿に設定可能なハッシュタグの最大数
test_error: NGワードのテストに失敗しました。正規表現のミスが含まれているかもしれません
title: NGワードとスパム
@ -775,7 +776,7 @@ ja:
hint: センシティブなキーワードの設定は、当サーバーのローカルユーザーによる公開範囲「公開」「ローカル公開」「ログインユーザーのみ」に対して適用されます。
keywords: センシティブなキーワード(警告文は除外)
keywords_for_all: センシティブなキーワード(警告文にも適用)
keywords_for_all_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。警告文にも含まれていればCWになります
keywords_for_all_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。警告文にも含まれていればCWになります。行が「?」で始まっていれば正規表現が使えます
keywords_hint: ここで指定したキーワードを含む投稿は強制的にCWになります。ただし警告文に使用していた場合は無視されます
title: センシティブ単語と設定
settings:
@ -1031,6 +1032,7 @@ ja:
keywords: 登録できるキーワード数の上限に達しています
tags: 登録できるタグ数の上限に達しています
over_limit: 所持できるアンテナ数 %{limit}を超えています
over_ltl_limit: 所持できるLTLモード付きアンテナ数 (ホーム/リストそれぞれにつき%{limit}) を超えています
over_stl_limit: 所持できるSTLモード付きアンテナ数 (ホーム/リストそれぞれにつき%{limit}) を超えています
too_short_keyword: キーワードが短すぎます
edit:
@ -1108,7 +1110,7 @@ ja:
help_html: CAPTCHAの解決に問題がある場合は、 %{email} までお問い合わせください。お手伝いいたします。
hint_html: もう一つだけ!あなたが人間であることを確認する必要があります(スパムを防ぐためです!)。 以下のCAPTCHAを解き、「続ける」をクリックします。
title: セキュリティチェック
cloudflare_with_registering: 登録時にCloudflareの画面が表示されます。登録できないときは tt@kmycode.net までご連絡ください
cloudflare_with_registering: 登録時にCloudflareの画面が表示されます。登録できないときは管理者へご連絡ください
confirmations:
wrong_email_hint: メールアドレスが正しくない場合は、アカウント設定で変更できます。
delete_account: アカウントの削除
@ -1583,7 +1585,7 @@ ja:
too_many_options: は%{max}個までです
preferences:
dtl: ディープタイムライン
dtl_hint: "#kmyblue ハッシュタグに参加することで、ディープタイムラインに投稿できます。ここではディープタイムラインを利用しやすくするための設定ができます。"
dtl_hint: "#%{tag} ハッシュタグに参加することで、ディープタイムラインに投稿できます。ここではディープタイムラインを利用しやすくするための設定ができます。"
other: その他
posting_defaults: デフォルトの投稿設定
public_timelines: 公開タイムライン

View file

@ -67,7 +67,7 @@ en:
setting_display_media_hide_all: Always hide media
setting_display_media_show_all: Always show media
setting_dtl_force_subscribable: Your post can be detected local user's antenna to subscribe deep timeline
setting_dtl_force_with_tag: "With using #kmyblue tag, your post settings will be changed forcibly"
setting_dtl_force_with_tag: "With using #%{tag} tag, your post settings will be changed forcibly"
setting_dtl_menu: Show DTL menu on web
setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details
setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed

View file

@ -69,7 +69,7 @@ ja:
setting_display_media_hide_all: メディアを常に隠す
setting_display_media_show_all: メディアを常に表示する
setting_dtl_force_subscribable: 購読拒否設定に関係なく、ディープタイムラインに向けた投稿はアンテナに掲載されます。ディープタイムラインをアンテナ経由で閲覧している人にあなたの発言が届きます
setting_dtl_force_with_tag: "ハッシュタグ #kmyblue をつけて投稿するとき、公開範囲と検索許可を強制的に置き換えるかを設定します"
setting_dtl_force_with_tag: "ハッシュタグ #%{tag} をつけて投稿するとき、公開範囲と検索許可を強制的に置き換えるかを設定します"
setting_emoji_reaction_streaming_notify_impl2: 当該サーバーの独自機能に対応したアプリを利用時に、スタンプ機能を利用できます。動作確認していないため(そもそもそのようなアプリ自体を確認できていないため)正しく動かない場合があります
setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします
setting_link_preview: プレビュー生成を停止することは、センシティブなサイトへのリンクを頻繁に投稿する人にも有効かもしれません

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddLtlToAntennas < ActiveRecord::Migration[7.0]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def change
safety_assured do
add_column_with_default :antennas, :ltl, :boolean, default: false, allow_null: false
end
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
ActiveRecord::Schema[7.0].define(version: 2023_09_11_022527) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -50,6 +50,15 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true
end
create_table "account_groups", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "group_account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_groups_on_account_id"
t.index ["group_account_id"], name: "index_account_groups_on_group_account_id"
end
create_table "account_migrations", force: :cascade do |t|
t.bigint "account_id"
t.string "acct", default: "", null: false
@ -313,6 +322,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
t.boolean "stl", default: false, null: false
t.boolean "ignore_reblog", default: false, null: false
t.boolean "insert_feeds", default: false, null: false
t.boolean "ltl", default: false, null: false
t.index ["account_id"], name: "index_antennas_on_account_id"
t.index ["any_accounts"], name: "index_antennas_on_any_accounts"
t.index ["any_domains"], name: "index_antennas_on_any_domains"
@ -1357,6 +1367,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_07_150100) do
add_foreign_key "account_conversations", "conversations", on_delete: :cascade
add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
add_foreign_key "account_groups", "accounts", on_delete: :cascade
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
add_foreign_key "account_migrations", "accounts", on_delete: :cascade
add_foreign_key "account_moderation_notes", "accounts"

View file

@ -4,6 +4,14 @@ module Mastodon
module Version
module_function
def kmyblue_major
3
end
def kmyblue_minor
0
end
def major
4
end
@ -24,8 +32,17 @@ module Mastodon
ENV['MASTODON_VERSION_PRERELEASE'].presence || default_prerelease
end
def to_a_of_kmyblue
[kmyblue_major, kmyblue_minor].compact
end
def to_s_of_kmyblue
components = [to_a_of_kmyblue.join('.')]
components.join
end
def build_metadata
ENV.fetch('MASTODON_VERSION_METADATA', nil)
['kmyblue', to_s_of_kmyblue, ENV.fetch('MASTODON_VERSION_METADATA', nil)].compact.join('.')
end
def to_a

BIN
public/favicon.ico Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

@ -9,6 +9,8 @@ describe AccountInteractions do
let(:target_account) { Fabricate(:account, username: 'target') }
let(:target_account_id) { target_account.id }
let(:target_account_ids) { [target_account_id] }
let(:follower_account) { Fabricate(:account, username: 'follower') }
let(:followee_account) { Fabricate(:account, username: 'followee') }
describe '.following_map' do
subject { Account.following_map(target_account_ids, account_id) }
@ -711,4 +713,20 @@ describe AccountInteractions do
expect(account.lists_for_local_distribution.to_a).to contain_exactly(follower_list, self_list)
end
end
describe '#mutuals' do
subject { account.mutuals }
context 'when following target_account' do
it 'mutual one' do
account.follow!(target_account)
target_account.follow!(account)
follower_account.follow!(account)
account.follow!(followee_account)
expect(subject.count).to eq 1
expect(subject.first.id).to eq target_account.id
end
end
end
end

View file

@ -27,7 +27,7 @@ RSpec.describe DeliveryAntennaService, type: :service do
let!(:antenna) { nil }
let!(:empty_antenna) { nil }
let(:stl_home) { false }
let(:mode) { :home }
before do
bob.follow!(alice)
@ -35,7 +35,7 @@ RSpec.describe DeliveryAntennaService, type: :service do
allow(redis).to receive(:publish)
subject.call(status, false, stl_home)
subject.call(status, false, mode: mode)
end
def home_feed_of(account)
@ -323,4 +323,40 @@ RSpec.describe DeliveryAntennaService, type: :service do
end
end
end
context 'when stl mode keyword is not working' do
let(:mode) { :stl }
let!(:antenna) { antenna_with_keyword(bob, 'anime', stl: true) }
it 'detecting antenna' do
expect(antenna_feed_of(antenna)).to include status.id
end
end
context 'when ltl mode keyword is not working' do
let(:mode) { :ltl }
let!(:antenna) { antenna_with_keyword(bob, 'anime', ltl: true) }
it 'detecting antenna' do
expect(antenna_feed_of(antenna)).to include status.id
end
end
context 'when stl mode exclude_keyword is not working' do
let(:mode) { :stl }
let!(:antenna) { antenna_with_keyword(bob, 'anime', exclude_keywords: ['body'], stl: true) }
it 'detecting antenna' do
expect(antenna_feed_of(antenna)).to include status.id
end
end
context 'when ltl mode exclude_keyword is not working' do
let(:mode) { :ltl }
let!(:antenna) { antenna_with_keyword(bob, 'anime', exclude_keywords: ['body'], ltl: true) }
it 'detecting antenna' do
expect(antenna_feed_of(antenna)).to include status.id
end
end
end

View file

@ -56,6 +56,10 @@ RSpec.describe FanOutOnWriteService, type: :service do
antenna
end
def antenna_with_options(owner, **options)
Fabricate(:antenna, account: owner, **options)
end
context 'when status is public' do
let(:visibility) { 'public' }
@ -97,6 +101,26 @@ RSpec.describe FanOutOnWriteService, type: :service do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with STL antenna' do
let!(:antenna) { antenna_with_options(bob, stl: true) }
let!(:empty_antenna) { antenna_with_options(tom) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(antenna)).to include status.id
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with LTL antenna' do
let!(:antenna) { antenna_with_options(bob, ltl: true) }
let!(:empty_antenna) { antenna_with_options(tom) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(antenna)).to include status.id
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
end
context 'when status is limited' do
@ -138,6 +162,24 @@ RSpec.describe FanOutOnWriteService, type: :service do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with STL antenna' do
let!(:antenna) { antenna_with_options(bob, stl: true) }
let!(:empty_antenna) { antenna_with_options(tom, stl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(antenna)).to include status.id
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with LTL antenna' do
let!(:empty_antenna) { antenna_with_options(bob, ltl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
end
context 'when status is private' do
@ -176,6 +218,24 @@ RSpec.describe FanOutOnWriteService, type: :service do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with STL antenna' do
let!(:antenna) { antenna_with_options(bob, stl: true) }
let!(:empty_antenna) { antenna_with_options(ohagi, stl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(antenna)).to include status.id
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with LTL antenna' do
let!(:empty_antenna) { antenna_with_options(bob, ltl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
end
context 'when status is unlisted' do
@ -215,6 +275,24 @@ RSpec.describe FanOutOnWriteService, type: :service do
end
end
context 'with STL antenna' do
let!(:antenna) { antenna_with_options(bob, stl: true) }
let!(:empty_antenna) { antenna_with_options(ohagi, stl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(antenna)).to include status.id
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with LTL antenna' do
let!(:empty_antenna) { antenna_with_options(bob, ltl: true) }
it 'is added to the antenna feed of antenna follower' do
expect(antenna_feed_of(empty_antenna)).to_not include status.id
end
end
context 'with non-public searchability' do
let(:searchability) { 'direct' }

View file

@ -173,12 +173,37 @@ RSpec.describe PostStatusService, type: :service do
it 'mutual visibility' do
account = Fabricate(:account)
mutual_account = Fabricate(:account)
other_account = Fabricate(:account)
text = 'This is an English text.'
mutual_account.follow!(account)
account.follow!(mutual_account)
other_account.follow!(account)
status = subject.call(account, text: text, visibility: 'mutual')
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'mutual'
expect(status.mentioned_accounts.count).to eq 1
expect(status.mentioned_accounts.first.id).to eq mutual_account.id
end
it 'circle visibility' do
account = Fabricate(:account)
circle_account = Fabricate(:account)
other_account = Fabricate(:account)
circle = Fabricate(:circle, account: account)
text = 'This is an English text.'
circle_account.follow!(account)
other_account.follow!(account)
circle.accounts << circle_account
status = subject.call(account, text: text, visibility: 'circle', circle_id: circle.id)
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'circle'
expect(status.mentioned_accounts.count).to eq 1
expect(status.mentioned_accounts.first.id).to eq circle_account.id
end
it 'safeguards mentions' do