Merge branch 'kb_development' into kb_migration
|
@ -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
|
||||
|
|
2
.github/workflows/test-ruby.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
プルリクエストのタイトルには、プルリクエストの内容が明確になるようなものを設定してください。
|
||||
|
||||
[](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
|
@ -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
|
||||
```
|
12
README.md
|
@ -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への積極的追従
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
6
app/helpers/dtl_helper.rb
Normal 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
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
app/javascript/icons/android-chrome-192x192.png
Normal file → Executable file
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
BIN
app/javascript/icons/android-chrome-256x256.png
Normal file → Executable file
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
BIN
app/javascript/icons/android-chrome-36x36.png
Normal file → Executable file
Before Width: | Height: | Size: 950 B After Width: | Height: | Size: 1.8 KiB |
BIN
app/javascript/icons/android-chrome-384x384.png
Normal file → Executable file
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
BIN
app/javascript/icons/android-chrome-48x48.png
Normal file → Executable file
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
app/javascript/icons/android-chrome-512x512.png
Normal file → Executable file
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 37 KiB |
BIN
app/javascript/icons/android-chrome-72x72.png
Normal file → Executable file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
app/javascript/icons/android-chrome-96x96.png
Normal file → Executable file
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6 KiB |
BIN
app/javascript/icons/apple-touch-icon-1024x1024.png
Normal file → Executable file
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 112 KiB |
BIN
app/javascript/icons/apple-touch-icon-114x114.png
Normal file → Executable file
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 6.9 KiB |
BIN
app/javascript/icons/apple-touch-icon-120x120.png
Normal file → Executable file
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 7.3 KiB |
BIN
app/javascript/icons/apple-touch-icon-144x144.png
Normal file → Executable file
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 8.6 KiB |
BIN
app/javascript/icons/apple-touch-icon-152x152.png
Normal file → Executable file
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 8.9 KiB |
BIN
app/javascript/icons/apple-touch-icon-167x167.png
Normal file → Executable file
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 9.8 KiB |
BIN
app/javascript/icons/apple-touch-icon-180x180.png
Normal file → Executable file
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/javascript/icons/apple-touch-icon-57x57.png
Normal file → Executable file
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.7 KiB |
BIN
app/javascript/icons/apple-touch-icon-60x60.png
Normal file → Executable file
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
app/javascript/icons/apple-touch-icon-72x72.png
Normal file → Executable file
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
app/javascript/icons/apple-touch-icon-76x76.png
Normal file → Executable file
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
app/javascript/icons/favicon-16x16.png
Normal file → Executable file
Before Width: | Height: | Size: 588 B After Width: | Height: | Size: 986 B |
BIN
app/javascript/icons/favicon-32x32.png
Normal file → Executable file
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
app/javascript/icons/favicon-48x48.png
Normal file → Executable file
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.1 KiB |
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
{!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} />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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有効",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -42,15 +42,17 @@
|
|||
.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
|
||||
|
||||
- if @dtl_enabled
|
||||
|
||||
%h4= t 'preferences.dtl'
|
||||
|
||||
%p.hint= t 'preferences.dtl_hint'
|
||||
%p.hint= t 'preferences.dtl_hint', tag: @dtl_tag
|
||||
|
||||
.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')
|
||||
= 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_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')
|
||||
= 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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: 公開タイムライン
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: プレビュー生成を停止することは、センシティブなサイトへのリンクを頻繁に投稿する人にも有効かもしれません
|
||||
|
|
15
db/migrate/20230911022527_add_ltl_to_antennas.rb
Normal 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
|
13
db/schema.rb
|
@ -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"
|
||||
|
|
|
@ -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
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|